|
|
@@ -8,10 +8,11 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.data.domain.Page;
|
|
|
import org.springframework.data.domain.PageRequest;
|
|
|
import org.springframework.data.domain.Pageable;
|
|
|
+import org.springframework.data.domain.Sort;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
+import java.time.Duration;
|
|
|
import java.time.LocalDateTime;
|
|
|
-import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -27,6 +28,8 @@ public class D4MetricsService {
|
|
|
"D4_COLLAB_CONFLICT_CREATED", "D4_COLLAB_CONFLICT_RESOLVED",
|
|
|
"D4_COMM_MESSAGE_SENT", "D4_COMM_MESSAGE_RESPONSE"
|
|
|
);
|
|
|
+ private static final int D4_QUERY_PAGE_SIZE = 1000;
|
|
|
+ private static final long DEFAULT_D4_SLA_THRESHOLD_MS = 24L * 60L * 60L * 1000L;
|
|
|
|
|
|
public D4Metrics calculateD4Metrics(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
log.info("Calculating D4 metrics from {} to {}", startTime, endTime);
|
|
|
@@ -43,14 +46,13 @@ public class D4MetricsService {
|
|
|
}
|
|
|
|
|
|
private D4Metrics.D1Metrics calculateD1Metrics(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
- Pageable pageable = PageRequest.of(0, 10000);
|
|
|
- Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
|
|
|
+ List<LogEntry> allLogs = loadAllD4Logs(startTime, endTime);
|
|
|
|
|
|
- List<LogEntry> taskAssignedLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> taskAssignedLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_TASK_ASSIGNED".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
- List<LogEntry> taskCompletedLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> taskCompletedLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_TASK_COMPLETED".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
@@ -82,33 +84,34 @@ public class D4MetricsService {
|
|
|
}
|
|
|
|
|
|
private D4Metrics.D2Metrics calculateD2Metrics(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
- Pageable pageable = PageRequest.of(0, 10000);
|
|
|
- Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
|
|
|
+ List<LogEntry> allLogs = loadAllD4Logs(startTime, endTime);
|
|
|
|
|
|
- List<LogEntry> messageResponseLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> messageResponseLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_COMM_MESSAGE_RESPONSE".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
- List<LogEntry> collabEffectiveLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> collabEffectiveLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_COLLAB_EFFECTIVE".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
- List<LogEntry> conflictCreatedLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> conflictCreatedLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_COLLAB_CONFLICT_CREATED".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
- List<LogEntry> conflictResolvedLogs = allLogs.getContent().stream()
|
|
|
+ List<LogEntry> conflictResolvedLogs = allLogs.stream()
|
|
|
.filter(log -> "D4_COLLAB_CONFLICT_RESOLVED".equals(log.getBizName()))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
double avgResponseTimeMs = calculateAvgResponseTime(messageResponseLogs);
|
|
|
long totalEffectiveCollaborations = collabEffectiveLogs.size();
|
|
|
long totalConflictResolutions = conflictResolvedLogs.size();
|
|
|
- double conflictResolutionRate = conflictCreatedLogs.size() > 0 ?
|
|
|
- (double) totalConflictResolutions / conflictCreatedLogs.size() : 0.0;
|
|
|
+ long totalCreatedConflicts = countDistinctConflicts(conflictCreatedLogs);
|
|
|
+ double conflictResolutionRate = totalCreatedConflicts > 0 ?
|
|
|
+ (double) totalConflictResolutions / totalCreatedConflicts : 0.0;
|
|
|
|
|
|
Map<String, Long> collaborationsBySource = calculateCollaborationsBySource(collabEffectiveLogs);
|
|
|
Map<String, Long> conflictsByType = calculateConflictsByType(conflictCreatedLogs);
|
|
|
+ Map<String, LocalDateTime> conflictCreatedTimeByKey = buildConflictCreatedTimeIndex(conflictCreatedLogs);
|
|
|
|
|
|
List<D4Metrics.ResponseTimeDetail> recentResponseTimes = messageResponseLogs.stream()
|
|
|
.limit(10)
|
|
|
@@ -117,7 +120,7 @@ public class D4MetricsService {
|
|
|
|
|
|
List<D4Metrics.ConflictResolutionDetail> recentResolutions = conflictResolvedLogs.stream()
|
|
|
.limit(10)
|
|
|
- .map(this::convertToConflictResolutionDetail)
|
|
|
+ .map(log -> convertToConflictResolutionDetail(log, conflictCreatedTimeByKey))
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
return D4Metrics.D2Metrics.builder()
|
|
|
@@ -131,6 +134,25 @@ public class D4MetricsService {
|
|
|
.recentResolutions(recentResolutions)
|
|
|
.build();
|
|
|
}
|
|
|
+
|
|
|
+ private List<LogEntry> loadAllD4Logs(LocalDateTime startTime, LocalDateTime endTime) {
|
|
|
+ List<LogEntry> allLogs = new ArrayList<>();
|
|
|
+ List<String> d4Events = new ArrayList<>(D4_EVENTS);
|
|
|
+ int page = 0;
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ Pageable pageable = PageRequest.of(page, D4_QUERY_PAGE_SIZE, Sort.by(Sort.Direction.DESC, "timestamp"));
|
|
|
+ Page<LogEntry> pageResult = logEntryRepository.findByTimestampBetweenAndBizNameIn(startTime, endTime, d4Events, pageable);
|
|
|
+ allLogs.addAll(pageResult.getContent());
|
|
|
+
|
|
|
+ if (!pageResult.hasNext()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ page++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return allLogs;
|
|
|
+ }
|
|
|
|
|
|
private Map<String, Double> calculateCompletionRateByTaskType(List<LogEntry> assignedLogs, List<LogEntry> completedLogs) {
|
|
|
Map<String, Long> assignedByType = assignedLogs.stream()
|
|
|
@@ -188,21 +210,18 @@ public class D4MetricsService {
|
|
|
|
|
|
private Map<String, Double> calculateCompletionRateByUser(List<LogEntry> assignedLogs, List<LogEntry> completedLogs) {
|
|
|
Map<String, Long> assignedByUser = assignedLogs.stream()
|
|
|
- .filter(log -> log.getContext() != null && log.getContext().containsKey("creatorId"))
|
|
|
+ .map(this::extractAssignedUserId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
.collect(Collectors.groupingBy(
|
|
|
- log -> String.valueOf(log.getContext().get("creatorId")),
|
|
|
+ userId -> userId,
|
|
|
Collectors.counting()
|
|
|
));
|
|
|
|
|
|
Map<String, Long> completedByUser = completedLogs.stream()
|
|
|
- .filter(log -> log.getContext() != null &&
|
|
|
- (log.getContext().containsKey("reviewerId") || log.getContext().containsKey("operatorId")))
|
|
|
+ .map(this::extractExecutorUserId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
.collect(Collectors.groupingBy(
|
|
|
- log -> {
|
|
|
- String userId = (String) log.getContext().getOrDefault("reviewerId",
|
|
|
- log.getContext().get("operatorId"));
|
|
|
- return String.valueOf(userId);
|
|
|
- },
|
|
|
+ userId -> userId,
|
|
|
Collectors.counting()
|
|
|
));
|
|
|
|
|
|
@@ -219,14 +238,10 @@ public class D4MetricsService {
|
|
|
}
|
|
|
|
|
|
private double calculateOnTimeCompletionRate(List<LogEntry> completedLogs) {
|
|
|
- final long SLA_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24小时
|
|
|
-
|
|
|
long onTimeCount = completedLogs.stream()
|
|
|
- .filter(log -> log.getContext() != null && log.getContext().containsKey("taskDurationMs"))
|
|
|
- .mapToLong(log -> {
|
|
|
- Long duration = (Long) log.getContext().get("taskDurationMs");
|
|
|
- return duration != null && duration <= SLA_THRESHOLD_MS ? 1L : 0L;
|
|
|
- })
|
|
|
+ .map(log -> getContextLong(log, "taskDurationMs"))
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .mapToLong(duration -> duration <= DEFAULT_D4_SLA_THRESHOLD_MS ? 1L : 0L)
|
|
|
.sum();
|
|
|
|
|
|
return completedLogs.size() > 0 ? (double) onTimeCount / completedLogs.size() : 0.0;
|
|
|
@@ -234,11 +249,9 @@ public class D4MetricsService {
|
|
|
|
|
|
private double calculateAvgResponseTime(List<LogEntry> messageResponseLogs) {
|
|
|
return messageResponseLogs.stream()
|
|
|
- .filter(log -> log.getContext() != null && log.getContext().containsKey("responseMs"))
|
|
|
- .mapToLong(log -> {
|
|
|
- Long responseMs = (Long) log.getContext().get("responseMs");
|
|
|
- return responseMs != null ? responseMs : 0L;
|
|
|
- })
|
|
|
+ .map(log -> getContextLong(log, "responseMs"))
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .mapToLong(Long::longValue)
|
|
|
.average()
|
|
|
.orElse(0.0);
|
|
|
}
|
|
|
@@ -256,43 +269,139 @@ public class D4MetricsService {
|
|
|
return conflictLogs.stream()
|
|
|
.filter(log -> log.getContext() != null && log.getContext().containsKey("conflictType"))
|
|
|
.collect(Collectors.groupingBy(
|
|
|
- log -> (String) log.getContext().get("conflictType"),
|
|
|
+ log -> String.valueOf(log.getContext().get("conflictType")),
|
|
|
Collectors.counting()
|
|
|
));
|
|
|
}
|
|
|
|
|
|
private D4Metrics.TaskCompletionDetail convertToTaskCompletionDetail(LogEntry log) {
|
|
|
+ Long durationMs = getContextLong(log, "taskDurationMs");
|
|
|
return D4Metrics.TaskCompletionDetail.builder()
|
|
|
- .taskId(log.getContext() != null ? String.valueOf(log.getContext().get("taskId")) : null)
|
|
|
- .taskType(log.getContext() != null ? (String) log.getContext().get("taskType") : null)
|
|
|
- .projectId(log.getContext() != null ? String.valueOf(log.getContext().get("projectId")) : null)
|
|
|
- .userId(log.getContext() != null ? String.valueOf(log.getContext().getOrDefault("reviewerId",
|
|
|
- log.getContext().get("operatorId"))) : null)
|
|
|
+ .taskId(getContextString(log, "taskId"))
|
|
|
+ .taskType(getContextString(log, "taskType"))
|
|
|
+ .projectId(getContextString(log, "projectId"))
|
|
|
+ .userId(extractExecutorUserId(log))
|
|
|
.completedTime(log.getTimestamp())
|
|
|
- .durationMs(log.getContext() != null ? (Long) log.getContext().get("taskDurationMs") : null)
|
|
|
- .onTime(log.getContext() != null && log.getContext().containsKey("taskDurationMs") ?
|
|
|
- ((Long) log.getContext().get("taskDurationMs")) <= 24 * 60 * 60 * 1000 : null)
|
|
|
+ .durationMs(durationMs)
|
|
|
+ .onTime(durationMs != null ? durationMs <= DEFAULT_D4_SLA_THRESHOLD_MS : null)
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
private D4Metrics.ResponseTimeDetail convertToResponseTimeDetail(LogEntry log) {
|
|
|
return D4Metrics.ResponseTimeDetail.builder()
|
|
|
- .messageId(log.getContext() != null ? String.valueOf(log.getContext().get("messageId")) : null)
|
|
|
- .creatorId(log.getContext() != null ? String.valueOf(log.getContext().get("creatorId")) : null)
|
|
|
- .receiverId(log.getContext() != null ? String.valueOf(log.getContext().get("receiverId")) : null)
|
|
|
+ .messageId(getContextString(log, "messageId"))
|
|
|
+ .creatorId(getContextString(log, "creatorId"))
|
|
|
+ .receiverId(getContextString(log, "receiverId"))
|
|
|
.responseTime(log.getTimestamp())
|
|
|
- .responseMs(log.getContext() != null ? (Long) log.getContext().get("responseMs") : null)
|
|
|
+ .responseMs(getContextLong(log, "responseMs"))
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
- private D4Metrics.ConflictResolutionDetail convertToConflictResolutionDetail(LogEntry log) {
|
|
|
+ private D4Metrics.ConflictResolutionDetail convertToConflictResolutionDetail(
|
|
|
+ LogEntry log,
|
|
|
+ Map<String, LocalDateTime> conflictCreatedTimeByKey) {
|
|
|
+ String conflictKey = buildConflictKey(log);
|
|
|
+ LocalDateTime createdTime = conflictCreatedTimeByKey.get(conflictKey);
|
|
|
+ Long resolutionTimeMs = null;
|
|
|
+ if (createdTime != null && log.getTimestamp() != null && !log.getTimestamp().isBefore(createdTime)) {
|
|
|
+ resolutionTimeMs = Duration.between(createdTime, log.getTimestamp()).toMillis();
|
|
|
+ }
|
|
|
+
|
|
|
return D4Metrics.ConflictResolutionDetail.builder()
|
|
|
- .conflictId(log.getContext() != null ? String.valueOf(log.getContext().get("taskId")) : null)
|
|
|
- .conflictType(log.getContext() != null ? (String) log.getContext().get("conflictType") : null)
|
|
|
- .projectId(log.getContext() != null ? String.valueOf(log.getContext().get("projectId")) : null)
|
|
|
- .resolverId(log.getContext() != null ? String.valueOf(log.getContext().get("resolverId")) : null)
|
|
|
+ .conflictId(getContextString(log, "taskId"))
|
|
|
+ .conflictType(getContextString(log, "conflictType"))
|
|
|
+ .projectId(getContextString(log, "projectId"))
|
|
|
+ .resolverId(getContextString(log, "resolverId"))
|
|
|
.resolvedTime(log.getTimestamp())
|
|
|
- .resolutionTimeMs(null)
|
|
|
+ .resolutionTimeMs(resolutionTimeMs)
|
|
|
.build();
|
|
|
}
|
|
|
+
|
|
|
+ private long countDistinctConflicts(List<LogEntry> conflictCreatedLogs) {
|
|
|
+ return conflictCreatedLogs.stream()
|
|
|
+ .map(this::buildConflictKey)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .distinct()
|
|
|
+ .count();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, LocalDateTime> buildConflictCreatedTimeIndex(List<LogEntry> conflictCreatedLogs) {
|
|
|
+ Map<String, LocalDateTime> result = new HashMap<>();
|
|
|
+ for (LogEntry log : conflictCreatedLogs) {
|
|
|
+ String key = buildConflictKey(log);
|
|
|
+ if (key == null || log.getTimestamp() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ result.merge(key, log.getTimestamp(), (existing, current) -> current.isBefore(existing) ? current : existing);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildConflictKey(LogEntry log) {
|
|
|
+ String taskId = getContextString(log, "taskId");
|
|
|
+ String projectId = getContextString(log, "projectId");
|
|
|
+ String conflictType = getContextString(log, "conflictType");
|
|
|
+ if (taskId == null && projectId == null && conflictType == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return String.join("|",
|
|
|
+ conflictType == null ? "" : conflictType,
|
|
|
+ projectId == null ? "" : projectId,
|
|
|
+ taskId == null ? "" : taskId);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractAssignedUserId(LogEntry log) {
|
|
|
+ String reviewerId = getContextString(log, "reviewerId");
|
|
|
+ if (reviewerId != null) {
|
|
|
+ return reviewerId;
|
|
|
+ }
|
|
|
+ String operatorId = getContextString(log, "operatorId");
|
|
|
+ if (operatorId != null) {
|
|
|
+ return operatorId;
|
|
|
+ }
|
|
|
+ return getContextString(log, "creatorId");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractExecutorUserId(LogEntry log) {
|
|
|
+ String reviewerId = getContextString(log, "reviewerId");
|
|
|
+ if (reviewerId != null) {
|
|
|
+ return reviewerId;
|
|
|
+ }
|
|
|
+ return getContextString(log, "operatorId");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getContextString(LogEntry logEntry, String key) {
|
|
|
+ if (logEntry == null || logEntry.getContext() == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Object value = logEntry.getContext().get(key);
|
|
|
+ return value != null ? String.valueOf(value) : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long getContextLong(LogEntry logEntry, String key) {
|
|
|
+ if (logEntry == null || logEntry.getContext() == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Object value = logEntry.getContext().get(key);
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value instanceof Number number) {
|
|
|
+ return number.longValue();
|
|
|
+ }
|
|
|
+ String text = String.valueOf(value).trim();
|
|
|
+ if (text.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Long.parseLong(text);
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
+ try {
|
|
|
+ return (long) Double.parseDouble(text);
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ this.log.debug("Cannot parse context key {} value {} to long", key, text);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|