feat: add analytics-service (Spring Cloud Gateway + Consul)
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package com.posthub.analytics;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class AnalyticsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AnalyticsApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.posthub.analytics.config;
|
||||
|
||||
import com.posthub.analytics.dto.UserEvent;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.common.serialization.StringDeserializer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
|
||||
import org.springframework.kafka.core.ConsumerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
|
||||
import org.springframework.kafka.support.serializer.JsonDeserializer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
public class KafkaConsumerConfig {
|
||||
|
||||
@Value("${spring.kafka.bootstrap-servers}")
|
||||
private String bootstrapServers;
|
||||
|
||||
@Value("${spring.kafka.consumer.group-id}")
|
||||
private String groupId;
|
||||
|
||||
@Bean
|
||||
public ConsumerFactory<String, UserEvent> consumerFactory() {
|
||||
JsonDeserializer<UserEvent> deserializer = new JsonDeserializer<>(UserEvent.class);
|
||||
deserializer.setRemoveTypeHeaders(true);
|
||||
deserializer.addTrustedPackages("*");
|
||||
deserializer.setUseTypeMapperForKey(false);
|
||||
|
||||
return new DefaultKafkaConsumerFactory<>(
|
||||
Map.of(
|
||||
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers,
|
||||
ConsumerConfig.GROUP_ID_CONFIG, groupId,
|
||||
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest",
|
||||
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class,
|
||||
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class
|
||||
),
|
||||
new StringDeserializer(),
|
||||
deserializer
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConcurrentKafkaListenerContainerFactory<String, UserEvent> kafkaListenerContainerFactory() {
|
||||
ConcurrentKafkaListenerContainerFactory<String, UserEvent> factory =
|
||||
new ConcurrentKafkaListenerContainerFactory<>();
|
||||
factory.setConsumerFactory(consumerFactory());
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.posthub.analytics.consumer;
|
||||
|
||||
import com.posthub.analytics.dto.UserEvent;
|
||||
import com.posthub.analytics.service.AnalyticsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.kafka.annotation.KafkaListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserEventConsumer {
|
||||
|
||||
private final AnalyticsService analyticsService;
|
||||
|
||||
@KafkaListener(
|
||||
topics = "${analytics.kafka.topic:user-events}",
|
||||
groupId = "${spring.kafka.consumer.group-id}"
|
||||
)
|
||||
public void consume(UserEvent event) {
|
||||
try {
|
||||
log.info("Received event: type={}, userId={}", event.getType(), event.getUserId());
|
||||
analyticsService.processEvent(event);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to process event: {}", event, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.posthub.analytics.controller;
|
||||
|
||||
import com.posthub.analytics.dto.DailyStatsResponse;
|
||||
import com.posthub.analytics.dto.DashboardResponse;
|
||||
import com.posthub.analytics.model.UserStats;
|
||||
import com.posthub.analytics.service.AnalyticsQueryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/analytics")
|
||||
@RequiredArgsConstructor
|
||||
public class AnalyticsController {
|
||||
|
||||
private final AnalyticsQueryService queryService;
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public ResponseEntity<DashboardResponse> getDashboard() {
|
||||
return ResponseEntity.ok(queryService.getDashboard());
|
||||
}
|
||||
|
||||
@GetMapping("/users/active")
|
||||
public ResponseEntity<List<UserStats>> getActiveUsers(
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
return ResponseEntity.ok(queryService.getActiveUsers(days));
|
||||
}
|
||||
|
||||
@GetMapping("/queries/daily")
|
||||
public ResponseEntity<List<DailyStatsResponse>> getDailyQueries(
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
return ResponseEntity.ok(queryService.getDailyStats(days));
|
||||
}
|
||||
|
||||
@GetMapping("/tokens/usage")
|
||||
public ResponseEntity<List<DailyStatsResponse>> getTokenUsage(
|
||||
@RequestParam(defaultValue = "30") int days) {
|
||||
return ResponseEntity.ok(queryService.getTokenUsage(days));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.posthub.analytics.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DailyStatsResponse {
|
||||
|
||||
private LocalDate date;
|
||||
private Long queries;
|
||||
private Long tokensUsed;
|
||||
private Long ragHits;
|
||||
private Long newUsers;
|
||||
private Long newChats;
|
||||
private Integer activeUsers;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.posthub.analytics.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DashboardResponse {
|
||||
|
||||
private Long totalQueries;
|
||||
private Long totalTokensUsed;
|
||||
private Long totalRagHits;
|
||||
private Long totalUsers;
|
||||
private Integer activeUsersToday;
|
||||
private Map<String, Long> eventBreakdown;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.posthub.analytics.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class UserEvent {
|
||||
|
||||
private EventType type;
|
||||
private String userId;
|
||||
private String chatId;
|
||||
private Integer tokensUsed;
|
||||
private Integer documentsFound;
|
||||
private Instant timestamp;
|
||||
|
||||
public enum EventType {
|
||||
USER_CREATED,
|
||||
CHAT_CREATED,
|
||||
CHAT_DELETED,
|
||||
QUERY_SENT,
|
||||
RAG_CONTEXT_HIT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.posthub.analytics.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "daily_stats", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = "statsDate")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DailyStats {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate statsDate;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalQueries = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalTokensUsed = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalRagHits = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalDocumentsFound = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long newUsers = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long newChats = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long deletedChats = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Integer activeUsers = 0;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.posthub.analytics.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "event_log", indexes = {
|
||||
@Index(name = "idx_event_log_type", columnList = "eventType"),
|
||||
@Index(name = "idx_event_log_user", columnList = "userId"),
|
||||
@Index(name = "idx_event_log_date", columnList = "eventDate")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class EventLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
private String eventType;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String userId;
|
||||
|
||||
private String chatId;
|
||||
|
||||
private Integer tokensUsed;
|
||||
|
||||
private Integer documentsFound;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant eventTimestamp;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate eventDate;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.posthub.analytics.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_stats", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = "userId")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class UserStats {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String userId;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalQueries = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalTokensUsed = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Long totalRagHits = 0L;
|
||||
|
||||
@Builder.Default
|
||||
private Integer totalChats = 0;
|
||||
|
||||
private Instant firstSeen;
|
||||
|
||||
private Instant lastActive;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.posthub.analytics.repository;
|
||||
|
||||
import com.posthub.analytics.model.DailyStats;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface DailyStatsRepository extends JpaRepository<DailyStats, Long> {
|
||||
|
||||
Optional<DailyStats> findByStatsDate(LocalDate statsDate);
|
||||
|
||||
List<DailyStats> findByStatsDateBetweenOrderByStatsDateAsc(LocalDate from, LocalDate to);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.posthub.analytics.repository;
|
||||
|
||||
import com.posthub.analytics.model.EventLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface EventLogRepository extends JpaRepository<EventLog, Long> {
|
||||
|
||||
@Query("SELECT COUNT(DISTINCT e.userId) FROM EventLog e " +
|
||||
"WHERE e.eventDate BETWEEN :from AND :to")
|
||||
Integer countActiveUsers(@Param("from") LocalDate from, @Param("to") LocalDate to);
|
||||
|
||||
@Query("SELECT e.eventType, COUNT(e) FROM EventLog e " +
|
||||
"WHERE e.eventDate BETWEEN :from AND :to GROUP BY e.eventType")
|
||||
List<Object[]> countEventsByType(@Param("from") LocalDate from, @Param("to") LocalDate to);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.posthub.analytics.repository;
|
||||
|
||||
import com.posthub.analytics.model.UserStats;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserStatsRepository extends JpaRepository<UserStats, Long> {
|
||||
|
||||
Optional<UserStats> findByUserId(String userId);
|
||||
|
||||
@Query("SELECT u FROM UserStats u WHERE u.lastActive >= :since ORDER BY u.lastActive DESC")
|
||||
List<UserStats> findActiveUsersSince(@Param("since") Instant since);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.posthub.analytics.service;
|
||||
|
||||
import com.posthub.analytics.dto.DailyStatsResponse;
|
||||
import com.posthub.analytics.dto.DashboardResponse;
|
||||
import com.posthub.analytics.model.DailyStats;
|
||||
import com.posthub.analytics.model.UserStats;
|
||||
import com.posthub.analytics.repository.DailyStatsRepository;
|
||||
import com.posthub.analytics.repository.EventLogRepository;
|
||||
import com.posthub.analytics.repository.UserStatsRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class AnalyticsQueryService {
|
||||
|
||||
private final EventLogRepository eventLogRepository;
|
||||
private final DailyStatsRepository dailyStatsRepository;
|
||||
private final UserStatsRepository userStatsRepository;
|
||||
|
||||
public DashboardResponse getDashboard() {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate thirtyDaysAgo = today.minusDays(30);
|
||||
|
||||
List<DailyStats> last30Days = dailyStatsRepository
|
||||
.findByStatsDateBetweenOrderByStatsDateAsc(thirtyDaysAgo, today);
|
||||
|
||||
long totalQueries = last30Days.stream().mapToLong(DailyStats::getTotalQueries).sum();
|
||||
long totalTokens = last30Days.stream().mapToLong(DailyStats::getTotalTokensUsed).sum();
|
||||
long totalRagHits = last30Days.stream().mapToLong(DailyStats::getTotalRagHits).sum();
|
||||
long totalUsers = userStatsRepository.count();
|
||||
Integer activeToday = eventLogRepository.countActiveUsers(today, today);
|
||||
|
||||
Map<String, Long> breakdown = new LinkedHashMap<>();
|
||||
List<Object[]> eventCounts = eventLogRepository.countEventsByType(thirtyDaysAgo, today);
|
||||
for (Object[] row : eventCounts) {
|
||||
breakdown.put((String) row[0], (Long) row[1]);
|
||||
}
|
||||
|
||||
return DashboardResponse.builder()
|
||||
.totalQueries(totalQueries)
|
||||
.totalTokensUsed(totalTokens)
|
||||
.totalRagHits(totalRagHits)
|
||||
.totalUsers(totalUsers)
|
||||
.activeUsersToday(activeToday != null ? activeToday : 0)
|
||||
.eventBreakdown(breakdown)
|
||||
.build();
|
||||
}
|
||||
|
||||
public List<DailyStatsResponse> getDailyStats(int days) {
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDate from = today.minusDays(days);
|
||||
|
||||
return dailyStatsRepository.findByStatsDateBetweenOrderByStatsDateAsc(from, today)
|
||||
.stream()
|
||||
.map(this::toResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<UserStats> getActiveUsers(int days) {
|
||||
Instant since = Instant.now().minus(days, ChronoUnit.DAYS);
|
||||
return userStatsRepository.findActiveUsersSince(since);
|
||||
}
|
||||
|
||||
public List<DailyStatsResponse> getTokenUsage(int days) {
|
||||
return getDailyStats(days);
|
||||
}
|
||||
|
||||
private DailyStatsResponse toResponse(DailyStats ds) {
|
||||
return DailyStatsResponse.builder()
|
||||
.date(ds.getStatsDate())
|
||||
.queries(ds.getTotalQueries())
|
||||
.tokensUsed(ds.getTotalTokensUsed())
|
||||
.ragHits(ds.getTotalRagHits())
|
||||
.newUsers(ds.getNewUsers())
|
||||
.newChats(ds.getNewChats())
|
||||
.activeUsers(ds.getActiveUsers())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.posthub.analytics.service;
|
||||
|
||||
import com.posthub.analytics.dto.UserEvent;
|
||||
import com.posthub.analytics.model.DailyStats;
|
||||
import com.posthub.analytics.model.EventLog;
|
||||
import com.posthub.analytics.model.UserStats;
|
||||
import com.posthub.analytics.repository.DailyStatsRepository;
|
||||
import com.posthub.analytics.repository.EventLogRepository;
|
||||
import com.posthub.analytics.repository.UserStatsRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AnalyticsService {
|
||||
|
||||
private final EventLogRepository eventLogRepository;
|
||||
private final DailyStatsRepository dailyStatsRepository;
|
||||
private final UserStatsRepository userStatsRepository;
|
||||
|
||||
@Transactional
|
||||
public void processEvent(UserEvent event) {
|
||||
Instant timestamp = event.getTimestamp() != null ? event.getTimestamp() : Instant.now();
|
||||
LocalDate eventDate = timestamp.atZone(ZoneOffset.UTC).toLocalDate();
|
||||
|
||||
// 1. Save raw event
|
||||
EventLog eventLog = EventLog.builder()
|
||||
.eventType(event.getType().name())
|
||||
.userId(event.getUserId())
|
||||
.chatId(event.getChatId())
|
||||
.tokensUsed(event.getTokensUsed())
|
||||
.documentsFound(event.getDocumentsFound())
|
||||
.eventTimestamp(timestamp)
|
||||
.eventDate(eventDate)
|
||||
.build();
|
||||
eventLogRepository.save(eventLog);
|
||||
|
||||
// 2. Update daily stats
|
||||
updateDailyStats(event, eventDate);
|
||||
|
||||
// 3. Update user stats
|
||||
updateUserStats(event, timestamp);
|
||||
|
||||
log.debug("Processed event: type={}, userId={}", event.getType(), event.getUserId());
|
||||
}
|
||||
|
||||
private void updateDailyStats(UserEvent event, LocalDate eventDate) {
|
||||
DailyStats daily = dailyStatsRepository.findByStatsDate(eventDate)
|
||||
.orElseGet(() -> DailyStats.builder().statsDate(eventDate).build());
|
||||
|
||||
switch (event.getType()) {
|
||||
case USER_CREATED -> daily.setNewUsers(daily.getNewUsers() + 1);
|
||||
case CHAT_CREATED -> daily.setNewChats(daily.getNewChats() + 1);
|
||||
case CHAT_DELETED -> daily.setDeletedChats(daily.getDeletedChats() + 1);
|
||||
case QUERY_SENT -> {
|
||||
daily.setTotalQueries(daily.getTotalQueries() + 1);
|
||||
if (event.getTokensUsed() != null) {
|
||||
daily.setTotalTokensUsed(daily.getTotalTokensUsed() + event.getTokensUsed());
|
||||
}
|
||||
}
|
||||
case RAG_CONTEXT_HIT -> {
|
||||
daily.setTotalRagHits(daily.getTotalRagHits() + 1);
|
||||
if (event.getDocumentsFound() != null) {
|
||||
daily.setTotalDocumentsFound(daily.getTotalDocumentsFound() + event.getDocumentsFound());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dailyStatsRepository.save(daily);
|
||||
}
|
||||
|
||||
private void updateUserStats(UserEvent event, Instant timestamp) {
|
||||
UserStats userStats = userStatsRepository.findByUserId(event.getUserId())
|
||||
.orElseGet(() -> UserStats.builder()
|
||||
.userId(event.getUserId())
|
||||
.firstSeen(timestamp)
|
||||
.build());
|
||||
|
||||
userStats.setLastActive(timestamp);
|
||||
|
||||
switch (event.getType()) {
|
||||
case CHAT_CREATED -> userStats.setTotalChats(userStats.getTotalChats() + 1);
|
||||
case QUERY_SENT -> {
|
||||
userStats.setTotalQueries(userStats.getTotalQueries() + 1);
|
||||
if (event.getTokensUsed() != null) {
|
||||
userStats.setTotalTokensUsed(userStats.getTotalTokensUsed() + event.getTokensUsed());
|
||||
}
|
||||
}
|
||||
case RAG_CONTEXT_HIT -> userStats.setTotalRagHits(userStats.getTotalRagHits() + 1);
|
||||
default -> { /* no user-level aggregation needed */ }
|
||||
}
|
||||
|
||||
userStatsRepository.save(userStats);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user