Qyanger 4 месяцев назад
Родитель
Сommit
63af429f04
19 измененных файлов с 1522 добавлено и 20 удалено
  1. 243 0
      d4-metrics-90284c04.md
  2. 11 6
      pom.xml
  3. 18 0
      src/main/java/com/demo/seecanalysisbackend/config/WebConfig.java
  4. 66 0
      src/main/java/com/demo/seecanalysisbackend/controller/D4MetricsController.java
  5. 70 0
      src/main/java/com/demo/seecanalysisbackend/controller/LogAnalysisController.java
  6. 63 0
      src/main/java/com/demo/seecanalysisbackend/controller/LogSearchController.java
  7. 41 0
      src/main/java/com/demo/seecanalysisbackend/dto/LogSearchRequest.java
  8. 43 0
      src/main/java/com/demo/seecanalysisbackend/dto/LogSearchResponse.java
  9. 65 0
      src/main/java/com/demo/seecanalysisbackend/exception/GlobalExceptionHandler.java
  10. 91 0
      src/main/java/com/demo/seecanalysisbackend/model/D4Metrics.java
  11. 57 0
      src/main/java/com/demo/seecanalysisbackend/model/LogEntry.java
  12. 62 0
      src/main/java/com/demo/seecanalysisbackend/model/LogStatistics.java
  13. 52 0
      src/main/java/com/demo/seecanalysisbackend/repository/LogEntryRepository.java
  14. 298 0
      src/main/java/com/demo/seecanalysisbackend/service/D4MetricsService.java
  15. 198 0
      src/main/java/com/demo/seecanalysisbackend/service/LogAnalysisService.java
  16. 115 0
      src/main/java/com/demo/seecanalysisbackend/service/LogSearchService.java
  17. 0 1
      src/main/resources/application.properties
  18. 29 0
      src/main/resources/application.yml
  19. 0 13
      src/test/java/com/demo/seecanalysisbackend/SeecAnalysisBackendApplicationTests.java

+ 243 - 0
d4-metrics-90284c04.md

@@ -0,0 +1,243 @@
+# 90284c04 日志埋点说明与 D4 指标实现
+
+## 变更背景
+
+- 提交号:`90284c04369fa9c0e3da56c3bae0cd05e9ee6747`
+- 提交信息:`feat: 添加日志记录功能以跟踪分支合并和文件评审任务`
+- 目标:通过统一日志埋点,支持以下指标统计:
+  - D4-1 个人分工任务按时完成能力:协作任务完成率
+  - D4-2 团队内部沟通能力:
+    - 团队沟通平均响应耗时
+    - 有效协作次数
+    - 协作冲突解决次数
+
+## 本次改动文件
+
+- `pom.xml`
+- `web/src/main/java/cn/seecoder/web/service/impl/code_review/BranchReviewServiceImpl.java`
+- `web/src/main/java/cn/seecoder/web/service/impl/code_review/FileReviewServiceImpl.java`
+- `web/src/main/java/cn/seecoder/web/service/impl/message/MessageServiceImpl.java`
+
+## 依赖与接入
+
+在 `pom.xml` 中新增依赖:
+
+```xml
+<dependency>
+    <groupId>cn.seecoder</groupId>
+    <artifactId>SeecoderLog</artifactId>
+    <version>0.2.0-jdk8-SNAPSHOT</version>
+</dependency>
+```
+
+在上述 3 个业务服务类中引入日志工具:
+
+```java
+import cn.seecoder.seecoderlog.utils.SeecoderLogUtils;
+```
+
+## 新增日志事件总览
+
+| 事件(bizName) | 级别 | 触发位置 | 含义 |
+|---|---|---|---|
+| `D4_TASK_ASSIGNED` | info | 文件评审创建后 | 记录协作任务分配 |
+| `D4_TASK_COMPLETED` | info | 分支合并成功 / 文件评审通过 | 记录协作任务完成 |
+| `D4_COLLAB_EFFECTIVE` | info | 分支合并成功 / 文件评审通过 | 记录一次有效协作 |
+| `D4_COLLAB_CONFLICT_CREATED` | warn / error | 分支合并异常 / 文件评审返工 | 记录协作冲突或失败 |
+| `D4_COLLAB_CONFLICT_RESOLVED` | info | 文件评审从返工状态转为通过 | 记录冲突被解决 |
+| `D4_COMM_MESSAGE_SENT` | info | 站内消息创建后 | 记录沟通发起 |
+| `D4_COMM_MESSAGE_RESPONSE` | info | 站内消息已读后 | 记录沟通响应与耗时 |
+
+---
+
+## 逐文件说明:加了哪些日志
+
+## 1. `BranchReviewServiceImpl`
+
+### 1.1 合并成功路径新增
+- `D4_TASK_COMPLETED`
+  - `taskType=branchReview`
+  - `taskId`
+  - `projectId`
+  - `operatorId`
+  - `taskDurationMs=当前时间-任务创建时间`
+  - `completedTs`
+- `D4_COLLAB_EFFECTIVE`
+  - `source=branchReviewMerge`
+  - `taskId`
+  - `projectId`
+  - `operatorId`
+  - `ts`
+
+### 1.2 合并异常路径新增
+在捕获 `SeecoderGitlabException` 时:
+- `D4_COLLAB_CONFLICT_CREATED`(warn)
+  - `conflictType=branchMerge`
+  - `taskId`
+  - `projectId`
+  - `operatorId`
+  - `reason=异常类名`
+  - `ts`
+- `D4_COLLAB_CONFLICT_CREATED`(error,带异常堆栈)
+  - `conflictType=branchMerge`
+  - `taskId`
+  - `projectId`
+  - `operatorId`
+
+> 说明:同一个冲突事件使用 warn + error 记录,warn 用于指标统计,error 用于排障追踪。
+
+---
+
+## 2. `FileReviewServiceImpl`
+
+### 2.1 创建评审任务后新增
+- `D4_TASK_ASSIGNED`
+  - `taskType=fileReview`
+  - `taskId`
+  - `projectId`
+  - `creatorId`
+  - `reviewerCount`
+  - `createdTs`
+
+### 2.2 返工路径新增
+当有评审人选择返工时:
+- `D4_COLLAB_CONFLICT_CREATED`(warn)
+  - `conflictType=fileReviewRework`
+  - `taskId`
+  - `projectId`
+  - `reviewerId`
+  - `ts`
+
+### 2.3 评审通过路径新增
+- `D4_TASK_COMPLETED`
+  - `taskType=fileReview`
+  - `taskId`
+  - `projectId`
+  - `reviewerId`
+  - `taskDurationMs=当前时间-任务创建时间`
+  - `completedTs`
+- `D4_COLLAB_EFFECTIVE`
+  - `source=fileReviewPass`
+  - `taskId`
+  - `projectId`
+  - `ts`
+- `D4_COLLAB_CONFLICT_RESOLVED`(仅当此前状态为 `REWORK`)
+  - `conflictType=fileReviewRework`
+  - `taskId`
+  - `projectId`
+  - `resolverId`
+  - `ts`
+
+---
+
+## 3. `MessageServiceImpl`
+
+### 3.1 消息发送新增
+- `D4_COMM_MESSAGE_SENT`
+  - `messageId`
+  - `creatorId`
+  - `receiverId`
+  - `linkTo`
+  - `ts`
+
+### 3.2 消息已读新增
+- `D4_COMM_MESSAGE_RESPONSE`
+  - `messageId`
+  - `creatorId`
+  - `receiverId`
+  - `responseMs=readTs-消息创建时间`
+  - `readTs`
+
+---
+
+## D4 指标如何落在日志上
+
+## D4-1 个人分工任务按时完成能力
+
+### 指标:协作任务完成率
+
+可由以下事件构成统计闭环:
+- 分配:`D4_TASK_ASSIGNED`
+- 完成:`D4_TASK_COMPLETED`
+
+建议口径:
+- `协作任务完成率 = 完成任务数 / 分配任务数`
+- 支持维度:`taskType`、`projectId`、`creatorId`、`reviewerId`、`operatorId`
+
+按时能力建议扩展:
+- 结合 `taskDurationMs` 与 SLA 阈值统计按时完成率
+- 示例:`按时完成率 = taskDurationMs <= SLA 的完成任务数 / 完成任务总数`
+
+> 当前提交中,`D4_TASK_ASSIGNED` 已覆盖 fileReview;branchReview 若需独立完成率闭环,后续可补充 branchReview 的分配/创建埋点。
+
+## D4-2 团队内部沟通能力
+
+### 指标1:团队沟通平均响应耗时
+- 事件来源:`D4_COMM_MESSAGE_RESPONSE`
+- 字段来源:`responseMs`
+- 统计口径:`avg(responseMs)`
+
+### 指标2:有效协作次数
+- 事件来源:`D4_COLLAB_EFFECTIVE`
+- 当前来源标签:
+  - `source=branchReviewMerge`
+  - `source=fileReviewPass`
+- 统计口径:`count(*)`(可按 `source/projectId/taskId` 分维)
+
+### 指标3:协作冲突解决次数
+- 冲突创建:`D4_COLLAB_CONFLICT_CREATED`
+- 冲突解决:`D4_COLLAB_CONFLICT_RESOLVED`
+- 统计口径:
+  - `协作冲突解决次数 = count(D4_COLLAB_CONFLICT_RESOLVED)`
+  - 可扩展:`冲突解决率 = resolved / created`
+
+---
+
+## 可统计参数设计(字段字典)
+
+| 参数 | 类型建议 | 含义 | 常见事件 |
+|---|---|---|---|
+| `taskId` | long | 协作任务 ID | TASK_ASSIGNED / TASK_COMPLETED / COLLAB_* |
+| `taskType` | enum/string | 任务类型(`fileReview`/`branchReview`) | TASK_ASSIGNED / TASK_COMPLETED |
+| `projectId` | long | 项目 ID | 大多数任务事件 |
+| `creatorId` | long | 发起人/创建人 | TASK_ASSIGNED / COMM_* |
+| `reviewerId` | long | 评审人 | TASK_COMPLETED / CONFLICT_CREATED |
+| `operatorId` | long | 操作执行人 | 分支合并相关 |
+| `resolverId` | long | 冲突解决人 | CONFLICT_RESOLVED |
+| `reviewerCount` | int | 评审人数 | TASK_ASSIGNED |
+| `messageId` | long | 消息 ID | COMM_* |
+| `linkTo` | string | 消息跳转链接 | COMM_MESSAGE_SENT |
+| `createdTs` | long(ms) | 任务创建时间 | TASK_ASSIGNED |
+| `completedTs` | long(ms) | 任务完成时间 | TASK_COMPLETED |
+| `readTs` | long(ms) | 消息读取时间 | COMM_MESSAGE_RESPONSE |
+| `ts` | long(ms) | 事件发生时间 | 多数事件 |
+| `taskDurationMs` | long(ms) | 任务耗时 | TASK_COMPLETED |
+| `responseMs` | long(ms) | 消息响应耗时 | COMM_MESSAGE_RESPONSE |
+| `source` | string | 有效协作来源 | COLLAB_EFFECTIVE |
+| `conflictType` | string | 冲突类型(`branchMerge`/`fileReviewRework`) | CONFLICT_CREATED/RESOLVED |
+| `reason` | string | 冲突/异常原因 | CONFLICT_CREATED |
+
+---
+
+## 建议的数据看板指标(可直接配置)
+
+1. `D4_协作任务完成率`
+   - 分子:`count(D4_TASK_COMPLETED)`
+   - 分母:`count(D4_TASK_ASSIGNED)`
+2. `D4_沟通平均响应耗时`
+   - `avg(responseMs)` from `D4_COMM_MESSAGE_RESPONSE`
+3. `D4_有效协作次数`
+   - `count(D4_COLLAB_EFFECTIVE)`
+4. `D4_冲突解决次数`
+   - `count(D4_COLLAB_CONFLICT_RESOLVED)`
+5. `D4_冲突解决率`(建议)
+   - `count(RESOLVED) / count(CREATED)`
+
+---
+
+## 备注
+
+- `bizName` 已按 D4 统计语义区分,支持按事件直接过滤聚合。
+- 业务上下文通过 `context` 透传,便于后续扩展统计维度。
+- 若后续希望覆盖更完整“个人按时完成”口径,建议在 branchReview 的创建/分配入口补充 `D4_TASK_ASSIGNED`。
+

+ 11 - 6
pom.xml

@@ -32,7 +32,17 @@
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-webmvc</artifactId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
         </dependency>
 
         <dependency>
@@ -40,11 +50,6 @@
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-webmvc-test</artifactId>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 
     <build>

+ 18 - 0
src/main/java/com/demo/seecanalysisbackend/config/WebConfig.java

@@ -0,0 +1,18 @@
+package com.demo.seecanalysisbackend.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+    
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/api/**")
+                .allowedOrigins("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+                .allowedHeaders("*")
+                .maxAge(3600);
+    }
+}

+ 66 - 0
src/main/java/com/demo/seecanalysisbackend/controller/D4MetricsController.java

@@ -0,0 +1,66 @@
+package com.demo.seecanalysisbackend.controller;
+
+import com.demo.seecanalysisbackend.model.D4Metrics;
+import com.demo.seecanalysisbackend.service.D4MetricsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+
+@RestController
+@RequestMapping("/api/d4-metrics")
+@RequiredArgsConstructor
+@Slf4j
+@CrossOrigin(origins = "*")
+public class D4MetricsController {
+    
+    private final D4MetricsService d4MetricsService;
+    
+    @GetMapping("/summary")
+    public ResponseEntity<D4Metrics> getD4Metrics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for D4 metrics from {} to {}", startTime, endTime);
+        
+        if (startTime.isAfter(endTime)) {
+            throw new IllegalArgumentException("Start time must be before end time");
+        }
+        
+        D4Metrics metrics = d4MetricsService.calculateD4Metrics(startTime, endTime);
+        return ResponseEntity.ok(metrics);
+    }
+    
+    @GetMapping("/d1-task-completion")
+    public ResponseEntity<D4Metrics.D1Metrics> getD1Metrics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for D1 metrics from {} to {}", startTime, endTime);
+        
+        if (startTime.isAfter(endTime)) {
+            throw new IllegalArgumentException("Start time must be before end time");
+        }
+        
+        D4Metrics metrics = d4MetricsService.calculateD4Metrics(startTime, endTime);
+        return ResponseEntity.ok(metrics.getD1Metrics());
+    }
+    
+    @GetMapping("/d2-communication")
+    public ResponseEntity<D4Metrics.D2Metrics> getD2Metrics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for D2 metrics from {} to {}", startTime, endTime);
+        
+        if (startTime.isAfter(endTime)) {
+            throw new IllegalArgumentException("Start time must be before end time");
+        }
+        
+        D4Metrics metrics = d4MetricsService.calculateD4Metrics(startTime, endTime);
+        return ResponseEntity.ok(metrics.getD2Metrics());
+    }
+}

+ 70 - 0
src/main/java/com/demo/seecanalysisbackend/controller/LogAnalysisController.java

@@ -0,0 +1,70 @@
+package com.demo.seecanalysisbackend.controller;
+
+import com.demo.seecanalysisbackend.model.LogStatistics;
+import com.demo.seecanalysisbackend.service.LogAnalysisService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.Valid;
+import java.time.LocalDateTime;
+
+@RestController
+@RequestMapping("/api/logs/statistics")
+@RequiredArgsConstructor
+@Slf4j
+@CrossOrigin(origins = "*")
+public class LogAnalysisController {
+    
+    private final LogAnalysisService logAnalysisService;
+    
+    @GetMapping("/summary")
+    public ResponseEntity<LogStatistics> getLogStatistics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for log statistics from {} to {}", startTime, endTime);
+        
+        if (startTime.isAfter(endTime)) {
+            throw new IllegalArgumentException("Start time must be before end time");
+        }
+        
+        LogStatistics statistics = logAnalysisService.getLogStatistics(startTime, endTime);
+        return ResponseEntity.ok(statistics);
+    }
+    
+    @GetMapping("/trends")
+    public ResponseEntity<LogStatistics> getLogTrends(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for log trends from {} to {}", startTime, endTime);
+        
+        LogStatistics statistics = logAnalysisService.getLogStatistics(startTime, endTime);
+        return ResponseEntity.ok(statistics);
+    }
+    
+    @GetMapping("/errors")
+    public ResponseEntity<LogStatistics> getErrorStatistics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for error statistics from {} to {}", startTime, endTime);
+        
+        LogStatistics statistics = logAnalysisService.getLogStatistics(startTime, endTime);
+        return ResponseEntity.ok(statistics);
+    }
+    
+    @GetMapping("/performance")
+    public ResponseEntity<LogStatistics> getPerformanceStatistics(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
+        
+        log.info("Received request for performance statistics from {} to {}", startTime, endTime);
+        
+        LogStatistics statistics = logAnalysisService.getLogStatistics(startTime, endTime);
+        return ResponseEntity.ok(statistics);
+    }
+}

+ 63 - 0
src/main/java/com/demo/seecanalysisbackend/controller/LogSearchController.java

@@ -0,0 +1,63 @@
+package com.demo.seecanalysisbackend.controller;
+
+import com.demo.seecanalysisbackend.dto.LogSearchRequest;
+import com.demo.seecanalysisbackend.dto.LogSearchResponse;
+import com.demo.seecanalysisbackend.service.LogSearchService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.Valid;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/logs")
+@RequiredArgsConstructor
+@Slf4j
+@CrossOrigin(origins = "*")
+public class LogSearchController {
+    
+    private final LogSearchService logSearchService;
+    
+    @PostMapping("/search")
+    public ResponseEntity<LogSearchResponse> searchLogs(@Valid @RequestBody LogSearchRequest request) {
+        log.info("Received log search request: {}", request);
+        
+        if (request.getStartTime().isAfter(request.getEndTime())) {
+            throw new IllegalArgumentException("Start time must be before end time");
+        }
+        
+        LogSearchResponse response = logSearchService.searchLogs(request);
+        return ResponseEntity.ok(response);
+    }
+    
+    @GetMapping("/search")
+    public ResponseEntity<LogSearchResponse> searchLogsWithParams(
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
+            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime,
+            @RequestParam(required = false) String level,
+            @RequestParam(required = false) String application,
+            @RequestParam(required = false) String searchText,
+            @RequestParam(defaultValue = "0") Integer page,
+            @RequestParam(defaultValue = "20") Integer size,
+            @RequestParam(defaultValue = "timestamp") String sortBy,
+            @RequestParam(defaultValue = "desc") String sortDirection) {
+        
+        LogSearchRequest request = LogSearchRequest.builder()
+            .startTime(startTime)
+            .endTime(endTime)
+            .levels(level != null ? List.of(level) : null)
+            .applications(application != null ? List.of(application) : null)
+            .searchText(searchText)
+            .page(page)
+            .size(size)
+            .sortBy(sortBy)
+            .sortDirection(sortDirection)
+            .build();
+        
+        return searchLogs(request);
+    }
+}

+ 41 - 0
src/main/java/com/demo/seecanalysisbackend/dto/LogSearchRequest.java

@@ -0,0 +1,41 @@
+package com.demo.seecanalysisbackend.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LogSearchRequest {
+    
+    @NotNull(message = "Start time is required")
+    private LocalDateTime startTime;
+    
+    @NotNull(message = "End time is required")
+    private LocalDateTime endTime;
+    
+    private List<String> levels;
+    private List<String> applications;
+    private List<String> services;
+    private List<String> bizNames;
+    private String searchText;
+    private String traceId;
+    private String spanId;
+    
+    @Min(value = 0, message = "Page must be non-negative")
+    private Integer page = 0;
+    
+    @Min(value = 1, message = "Size must be positive")
+    private Integer size = 20;
+    
+    private String sortBy = "timestamp";
+    private String sortDirection = "desc";
+}

+ 43 - 0
src/main/java/com/demo/seecanalysisbackend/dto/LogSearchResponse.java

@@ -0,0 +1,43 @@
+package com.demo.seecanalysisbackend.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LogSearchResponse {
+    
+    private List<LogEntryDto> logs;
+    private Long totalCount;
+    private Integer currentPage;
+    private Integer pageSize;
+    private Integer totalPages;
+    private Boolean hasNext;
+    private Boolean hasPrevious;
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class LogEntryDto {
+        private String id;
+        private String timestamp;
+        private String rawMessage;  //原始日志完整内容
+        private String serviceName;
+        private String thread;
+        private String level;
+        private String className;
+        private String methodName;
+        private String traceId;
+        private String spanId;
+        private String bizName;
+        private Map<String, Object> context;
+    }
+}

+ 65 - 0
src/main/java/com/demo/seecanalysisbackend/exception/GlobalExceptionHandler.java

@@ -0,0 +1,65 @@
+package com.demo.seecanalysisbackend.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+    
+    @ExceptionHandler(IllegalArgumentException.class)
+    public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex) {
+        log.error("Illegal argument exception: {}", ex.getMessage());
+        
+        Map<String, Object> response = new HashMap<>();
+        response.put("timestamp", LocalDateTime.now());
+        response.put("status", HttpStatus.BAD_REQUEST.value());
+        response.put("error", "Bad Request");
+        response.put("message", ex.getMessage());
+        
+        return ResponseEntity.badRequest().body(response);
+    }
+    
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
+        log.error("Validation exception: {}", ex.getMessage());
+        
+        Map<String, String> errors = new HashMap<>();
+        ex.getBindingResult().getAllErrors().forEach((error) -> {
+            String fieldName = ((FieldError) error).getField();
+            String errorMessage = error.getDefaultMessage();
+            errors.put(fieldName, errorMessage);
+        });
+        
+        Map<String, Object> response = new HashMap<>();
+        response.put("timestamp", LocalDateTime.now());
+        response.put("status", HttpStatus.BAD_REQUEST.value());
+        response.put("error", "Validation Error");
+        response.put("message", "Request validation failed");
+        response.put("errors", errors);
+        
+        return ResponseEntity.badRequest().body(response);
+    }
+    
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
+        log.error("Unexpected error occurred", ex);
+        
+        Map<String, Object> response = new HashMap<>();
+        response.put("timestamp", LocalDateTime.now());
+        response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
+        response.put("error", "Internal Server Error");
+        response.put("message", "An unexpected error occurred");
+        
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+    }
+}

+ 91 - 0
src/main/java/com/demo/seecanalysisbackend/model/D4Metrics.java

@@ -0,0 +1,91 @@
+package com.demo.seecanalysisbackend.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class D4Metrics {
+    
+    private LocalDateTime startTime;
+    private LocalDateTime endTime;
+    private D1Metrics d1Metrics;
+    private D2Metrics d2Metrics;
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class D1Metrics {
+        private Double taskCompletionRate;
+        private Long totalAssignedTasks;
+        private Long totalCompletedTasks;
+        private Double onTimeCompletionRate;
+        private Map<String, Double> completionRateByTaskType;
+        private Map<String, Double> completionRateByProject;
+        private Map<String, Double> completionRateByUser;
+        private List<TaskCompletionDetail> recentCompletions;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class D2Metrics {
+        private Double avgResponseTimeMs;
+        private Long totalEffectiveCollaborations;
+        private Long totalConflictResolutions;
+        private Double conflictResolutionRate;
+        private Map<String, Long> collaborationsBySource;
+        private Map<String, Long> conflictsByType;
+        private List<ResponseTimeDetail> recentResponseTimes;
+        private List<ConflictResolutionDetail> recentResolutions;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class TaskCompletionDetail {
+        private String taskId;
+        private String taskType;
+        private String projectId;
+        private String userId;
+        private LocalDateTime completedTime;
+        private Long durationMs;
+        private Boolean onTime;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class ResponseTimeDetail {
+        private String messageId;
+        private String creatorId;
+        private String receiverId;
+        private LocalDateTime responseTime;
+        private Long responseMs;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class ConflictResolutionDetail {
+        private String conflictId;
+        private String conflictType;
+        private String projectId;
+        private String resolverId;
+        private LocalDateTime resolvedTime;
+        private Long resolutionTimeMs;
+    }
+}

+ 57 - 0
src/main/java/com/demo/seecanalysisbackend/model/LogEntry.java

@@ -0,0 +1,57 @@
+package com.demo.seecanalysisbackend.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.elasticsearch.annotations.Document;
+import org.springframework.data.elasticsearch.annotations.Field;
+import org.springframework.data.elasticsearch.annotations.FieldType;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Document(indexName = "logs")
+public class LogEntry {
+    
+    @Id
+    private String id;
+    
+    @Field(type = FieldType.Text, name = "timestamp")
+    private LocalDateTime timestamp;
+    
+    @Field(type = FieldType.Text, name = "raw_message")
+    private String rawMessage;
+    
+    @Field(type = FieldType.Keyword, name = "service_name")
+    private String serviceName;
+    
+    @Field(type = FieldType.Keyword, name = "thread")
+    private String thread;
+    
+    @Field(type = FieldType.Keyword, name = "level")
+    private String level;
+    
+    @Field(type = FieldType.Keyword, name = "class_name")
+    private String className;
+    
+    @Field(type = FieldType.Keyword, name = "method_name")
+    private String methodName;
+    
+    @Field(type = FieldType.Keyword, name = "trace_id")
+    private String traceId;
+    
+    @Field(type = FieldType.Keyword, name = "span_id")
+    private String spanId;
+    
+    @Field(type = FieldType.Keyword, name = "biz_name")
+    private String bizName;
+    
+    @Field(type = FieldType.Object, name = "context")
+    private Map<String, Object> context;
+}

+ 62 - 0
src/main/java/com/demo/seecanalysisbackend/model/LogStatistics.java

@@ -0,0 +1,62 @@
+package com.demo.seecanalysisbackend.model;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LogStatistics {
+    
+    private LocalDateTime startTime;
+    private LocalDateTime endTime;
+    private Long totalLogs;
+    private Map<String, Long> logsByLevel;
+    private Map<String, Long> logsByService;
+    private List<ErrorStatistics> errorStats;
+    private List<PerformanceStatistics> performanceStats;
+    private List<TrendData> trends;
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class ErrorStatistics {
+        private String errorType;
+        private Long count;
+        private String lastOccurrence;
+        private List<String> sampleMessages;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class PerformanceStatistics {
+        private String endpoint;
+        private Double avgResponseTime;
+        private Double maxResponseTime;
+        private Double minResponseTime;
+        private Long requestCount;
+        private Double errorRate;
+    }
+    
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @Builder
+    public static class TrendData {
+        private String timestamp;
+        private Long totalCount;
+        private Long errorCount;
+        private Long warningCount;
+        private Double avgResponseTime;
+    }
+}

+ 52 - 0
src/main/java/com/demo/seecanalysisbackend/repository/LogEntryRepository.java

@@ -0,0 +1,52 @@
+package com.demo.seecanalysisbackend.repository;
+
+import com.demo.seecanalysisbackend.model.LogEntry;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.elasticsearch.annotations.Query;
+import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+public interface LogEntryRepository extends ElasticsearchRepository<LogEntry, String> {
+    
+    Page<LogEntry> findByTimestampBetween(LocalDateTime startTime, LocalDateTime endTime, Pageable pageable);
+    
+    Page<LogEntry> findByTimestampBetweenAndLevelIn(LocalDateTime startTime, LocalDateTime endTime, 
+                                                   List<String> levels, Pageable pageable);
+    
+    Page<LogEntry> findByTimestampBetweenAndApplicationIn(LocalDateTime startTime, LocalDateTime endTime, 
+                                                         List<String> applications, Pageable pageable);
+    
+    List<LogEntry> findByTimestampBetweenAndLevel(LocalDateTime startTime, LocalDateTime endTime, String level);
+    
+    Page<LogEntry> findByTimestampBetweenAndBizName(LocalDateTime startTime, LocalDateTime endTime, 
+                                                    String bizName, Pageable pageable);
+    
+    Page<LogEntry> findByTimestampBetweenAndBizNameIn(LocalDateTime startTime, LocalDateTime endTime, 
+                                                     List<String> bizNames, Pageable pageable);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}, {\"match\": {\"message\": \"?2\"}}]}}")
+    Page<LogEntry> findByTimestampBetweenAndMessage(LocalDateTime startTime, LocalDateTime endTime, 
+                                                   String message, Pageable pageable);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}, {\"term\": {\"level\": \"?2\"}}]}}")
+    long countByTimestampBetweenAndLevel(LocalDateTime startTime, LocalDateTime endTime, String level);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}]}}")
+    long countByTimestampBetween(LocalDateTime startTime, LocalDateTime endTime);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}, {\"term\": {\"biz_name\": \"?2\"}}]}}")
+    long countByTimestampBetweenAndBizName(LocalDateTime startTime, LocalDateTime endTime, String bizName);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}, {\"term\": {\"service_name\": \"?2\"}}]}}")
+    Page<LogEntry> findByTimestampBetweenAndServiceName(LocalDateTime startTime, LocalDateTime endTime, 
+                                                       String serviceName, Pageable pageable);
+    
+    @Query("{\"bool\": {\"must\": [{\"range\": {\"timestamp\": {\"gte\": \"?0\", \"lte\": \"?1\"}}}, {\"term\": {\"trace_id\": \"?2\"}}]}}")
+    Page<LogEntry> findByTimestampBetweenAndTraceId(LocalDateTime startTime, LocalDateTime endTime, 
+                                                   String traceId, Pageable pageable);
+}

+ 298 - 0
src/main/java/com/demo/seecanalysisbackend/service/D4MetricsService.java

@@ -0,0 +1,298 @@
+package com.demo.seecanalysisbackend.service;
+
+import com.demo.seecanalysisbackend.model.D4Metrics;
+import com.demo.seecanalysisbackend.model.LogEntry;
+import com.demo.seecanalysisbackend.repository.LogEntryRepository;
+import lombok.RequiredArgsConstructor;
+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.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class D4MetricsService {
+    
+    private final LogEntryRepository logEntryRepository;
+    
+    private static final Set<String> D4_EVENTS = Set.of(
+        "D4_TASK_ASSIGNED", "D4_TASK_COMPLETED", "D4_COLLAB_EFFECTIVE",
+        "D4_COLLAB_CONFLICT_CREATED", "D4_COLLAB_CONFLICT_RESOLVED",
+        "D4_COMM_MESSAGE_SENT", "D4_COMM_MESSAGE_RESPONSE"
+    );
+    
+    public D4Metrics calculateD4Metrics(LocalDateTime startTime, LocalDateTime endTime) {
+        log.info("Calculating D4 metrics from {} to {}", startTime, endTime);
+        
+        D4Metrics.D1Metrics d1Metrics = calculateD1Metrics(startTime, endTime);
+        D4Metrics.D2Metrics d2Metrics = calculateD2Metrics(startTime, endTime);
+        
+        return D4Metrics.builder()
+            .startTime(startTime)
+            .endTime(endTime)
+            .d1Metrics(d1Metrics)
+            .d2Metrics(d2Metrics)
+            .build();
+    }
+    
+    private D4Metrics.D1Metrics calculateD1Metrics(LocalDateTime startTime, LocalDateTime endTime) {
+        Pageable pageable = PageRequest.of(0, 10000);
+        Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
+        
+        List<LogEntry> taskAssignedLogs = allLogs.getContent().stream()
+            .filter(log -> "D4_TASK_ASSIGNED".equals(log.getBizName()))
+            .collect(Collectors.toList());
+        
+        List<LogEntry> taskCompletedLogs = allLogs.getContent().stream()
+            .filter(log -> "D4_TASK_COMPLETED".equals(log.getBizName()))
+            .collect(Collectors.toList());
+        
+        long totalAssigned = taskAssignedLogs.size();
+        long totalCompleted = taskCompletedLogs.size();
+        double completionRate = totalAssigned > 0 ? (double) totalCompleted / totalAssigned : 0.0;
+        
+        Map<String, Double> completionRateByTaskType = calculateCompletionRateByTaskType(taskAssignedLogs, taskCompletedLogs);
+        Map<String, Double> completionRateByProject = calculateCompletionRateByProject(taskAssignedLogs, taskCompletedLogs);
+        Map<String, Double> completionRateByUser = calculateCompletionRateByUser(taskAssignedLogs, taskCompletedLogs);
+        
+        List<D4Metrics.TaskCompletionDetail> recentCompletions = taskCompletedLogs.stream()
+            .limit(10)
+            .map(this::convertToTaskCompletionDetail)
+            .collect(Collectors.toList());
+        
+        double onTimeCompletionRate = calculateOnTimeCompletionRate(taskCompletedLogs);
+        
+        return D4Metrics.D1Metrics.builder()
+            .taskCompletionRate(completionRate)
+            .totalAssignedTasks(totalAssigned)
+            .totalCompletedTasks(totalCompleted)
+            .onTimeCompletionRate(onTimeCompletionRate)
+            .completionRateByTaskType(completionRateByTaskType)
+            .completionRateByProject(completionRateByProject)
+            .completionRateByUser(completionRateByUser)
+            .recentCompletions(recentCompletions)
+            .build();
+    }
+    
+    private D4Metrics.D2Metrics calculateD2Metrics(LocalDateTime startTime, LocalDateTime endTime) {
+        Pageable pageable = PageRequest.of(0, 10000);
+        Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
+        
+        List<LogEntry> messageResponseLogs = allLogs.getContent().stream()
+            .filter(log -> "D4_COMM_MESSAGE_RESPONSE".equals(log.getBizName()))
+            .collect(Collectors.toList());
+        
+        List<LogEntry> collabEffectiveLogs = allLogs.getContent().stream()
+            .filter(log -> "D4_COLLAB_EFFECTIVE".equals(log.getBizName()))
+            .collect(Collectors.toList());
+        
+        List<LogEntry> conflictCreatedLogs = allLogs.getContent().stream()
+            .filter(log -> "D4_COLLAB_CONFLICT_CREATED".equals(log.getBizName()))
+            .collect(Collectors.toList());
+        
+        List<LogEntry> conflictResolvedLogs = allLogs.getContent().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;
+        
+        Map<String, Long> collaborationsBySource = calculateCollaborationsBySource(collabEffectiveLogs);
+        Map<String, Long> conflictsByType = calculateConflictsByType(conflictCreatedLogs);
+        
+        List<D4Metrics.ResponseTimeDetail> recentResponseTimes = messageResponseLogs.stream()
+            .limit(10)
+            .map(this::convertToResponseTimeDetail)
+            .collect(Collectors.toList());
+        
+        List<D4Metrics.ConflictResolutionDetail> recentResolutions = conflictResolvedLogs.stream()
+            .limit(10)
+            .map(this::convertToConflictResolutionDetail)
+            .collect(Collectors.toList());
+        
+        return D4Metrics.D2Metrics.builder()
+            .avgResponseTimeMs(avgResponseTimeMs)
+            .totalEffectiveCollaborations(totalEffectiveCollaborations)
+            .totalConflictResolutions(totalConflictResolutions)
+            .conflictResolutionRate(conflictResolutionRate)
+            .collaborationsBySource(collaborationsBySource)
+            .conflictsByType(conflictsByType)
+            .recentResponseTimes(recentResponseTimes)
+            .recentResolutions(recentResolutions)
+            .build();
+    }
+    
+    private Map<String, Double> calculateCompletionRateByTaskType(List<LogEntry> assignedLogs, List<LogEntry> completedLogs) {
+        Map<String, Long> assignedByType = assignedLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("taskType"))
+            .collect(Collectors.groupingBy(
+                log -> (String) log.getContext().get("taskType"),
+                Collectors.counting()
+            ));
+        
+        Map<String, Long> completedByType = completedLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("taskType"))
+            .collect(Collectors.groupingBy(
+                log -> (String) log.getContext().get("taskType"),
+                Collectors.counting()
+            ));
+        
+        return assignedByType.entrySet().stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                entry -> {
+                    String taskType = entry.getKey();
+                    long assigned = entry.getValue();
+                    long completed = completedByType.getOrDefault(taskType, 0L);
+                    return assigned > 0 ? (double) completed / assigned : 0.0;
+                }
+            ));
+    }
+    
+    private Map<String, Double> calculateCompletionRateByProject(List<LogEntry> assignedLogs, List<LogEntry> completedLogs) {
+        Map<String, Long> assignedByProject = assignedLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("projectId"))
+            .collect(Collectors.groupingBy(
+                log -> String.valueOf(log.getContext().get("projectId")),
+                Collectors.counting()
+            ));
+        
+        Map<String, Long> completedByProject = completedLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("projectId"))
+            .collect(Collectors.groupingBy(
+                log -> String.valueOf(log.getContext().get("projectId")),
+                Collectors.counting()
+            ));
+        
+        return assignedByProject.entrySet().stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                entry -> {
+                    String projectId = entry.getKey();
+                    long assigned = entry.getValue();
+                    long completed = completedByProject.getOrDefault(projectId, 0L);
+                    return assigned > 0 ? (double) completed / assigned : 0.0;
+                }
+            ));
+    }
+    
+    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"))
+            .collect(Collectors.groupingBy(
+                log -> String.valueOf(log.getContext().get("creatorId")),
+                Collectors.counting()
+            ));
+        
+        Map<String, Long> completedByUser = completedLogs.stream()
+            .filter(log -> log.getContext() != null && 
+                (log.getContext().containsKey("reviewerId") || log.getContext().containsKey("operatorId")))
+            .collect(Collectors.groupingBy(
+                log -> {
+                    String userId = (String) log.getContext().getOrDefault("reviewerId", 
+                        log.getContext().get("operatorId"));
+                    return String.valueOf(userId);
+                },
+                Collectors.counting()
+            ));
+        
+        return assignedByUser.entrySet().stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                entry -> {
+                    String userId = entry.getKey();
+                    long assigned = entry.getValue();
+                    long completed = completedByUser.getOrDefault(userId, 0L);
+                    return assigned > 0 ? (double) completed / assigned : 0.0;
+                }
+            ));
+    }
+    
+    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;
+            })
+            .sum();
+        
+        return completedLogs.size() > 0 ? (double) onTimeCount / completedLogs.size() : 0.0;
+    }
+    
+    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;
+            })
+            .average()
+            .orElse(0.0);
+    }
+    
+    private Map<String, Long> calculateCollaborationsBySource(List<LogEntry> collabLogs) {
+        return collabLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("source"))
+            .collect(Collectors.groupingBy(
+                log -> (String) log.getContext().get("source"),
+                Collectors.counting()
+            ));
+    }
+    
+    private Map<String, Long> calculateConflictsByType(List<LogEntry> conflictLogs) {
+        return conflictLogs.stream()
+            .filter(log -> log.getContext() != null && log.getContext().containsKey("conflictType"))
+            .collect(Collectors.groupingBy(
+                log -> (String) log.getContext().get("conflictType"),
+                Collectors.counting()
+            ));
+    }
+    
+    private D4Metrics.TaskCompletionDetail convertToTaskCompletionDetail(LogEntry log) {
+        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)
+            .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)
+            .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)
+            .responseTime(log.getTimestamp())
+            .responseMs(log.getContext() != null ? (Long) log.getContext().get("responseMs") : null)
+            .build();
+    }
+    
+    private D4Metrics.ConflictResolutionDetail convertToConflictResolutionDetail(LogEntry log) {
+        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)
+            .resolvedTime(log.getTimestamp())
+            .resolutionTimeMs(null)
+            .build();
+    }
+}

+ 198 - 0
src/main/java/com/demo/seecanalysisbackend/service/LogAnalysisService.java

@@ -0,0 +1,198 @@
+package com.demo.seecanalysisbackend.service;
+
+import com.demo.seecanalysisbackend.model.LogEntry;
+import com.demo.seecanalysisbackend.model.LogStatistics;
+import com.demo.seecanalysisbackend.repository.LogEntryRepository;
+import lombok.RequiredArgsConstructor;
+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.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class LogAnalysisService {
+    
+    private final LogEntryRepository logEntryRepository;
+    
+    public LogStatistics getLogStatistics(LocalDateTime startTime, LocalDateTime endTime) {
+        log.info("Calculating log statistics from {} to {}", startTime, endTime);
+        
+        long totalLogs = logEntryRepository.countByTimestampBetween(startTime, endTime);
+        
+        Map<String, Long> logsByLevel = new HashMap<>();
+        for (String level : Arrays.asList("ERROR", "WARN", "INFO", "DEBUG")) {
+            long count = logEntryRepository.countByTimestampBetweenAndLevel(startTime, endTime, level);
+            if (count > 0) {
+                logsByLevel.put(level, count);
+            }
+        }
+        
+        Pageable pageable = PageRequest.of(0, 1000);
+        Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
+        
+        Map<String, Long> logsByService = allLogs.getContent().stream()
+            .filter(log -> log.getServiceName() != null)
+            .collect(Collectors.groupingBy(LogEntry::getServiceName, Collectors.counting()));
+        
+        List<LogStatistics.ErrorStatistics> errorStats = calculateErrorStatistics(startTime, endTime);
+        
+        List<LogStatistics.PerformanceStatistics> performanceStats = calculatePerformanceStatistics(startTime, endTime);
+        
+        List<LogStatistics.TrendData> trends = calculateTrendData(startTime, endTime);
+        
+        return LogStatistics.builder()
+            .startTime(startTime)
+            .endTime(endTime)
+            .totalLogs(totalLogs)
+            .logsByLevel(logsByLevel)
+            .logsByService(logsByService)
+            .errorStats(errorStats)
+            .performanceStats(performanceStats)
+            .trends(trends)
+            .build();
+    }
+    
+    private List<LogStatistics.ErrorStatistics> calculateErrorStatistics(LocalDateTime startTime, LocalDateTime endTime) {
+        List<LogEntry> errorLogs = logEntryRepository.findByTimestampBetweenAndLevel(startTime, endTime, "ERROR");
+        
+        Map<String, List<LogEntry>> errorsByType = errorLogs.stream()
+            .collect(Collectors.groupingBy(log -> {
+                String message = log.getRawMessage();
+                if (message != null && message.length() > 50) {
+                    return message.substring(0, 50);
+                }
+                return message != null ? message : "Unknown Error";
+            }));
+        
+        return errorsByType.entrySet().stream()
+            .map(entry -> {
+                String errorType = entry.getKey();
+                List<LogEntry> logs = entry.getValue();
+                
+                String lastOccurrence = logs.stream()
+                    .max(Comparator.comparing(LogEntry::getTimestamp))
+                    .map(log -> log.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
+                    .orElse("");
+                
+                List<String> sampleMessages = logs.stream()
+                    .limit(3)
+                    .map(LogEntry::getRawMessage)
+                    .collect(Collectors.toList());
+                
+                return LogStatistics.ErrorStatistics.builder()
+                    .errorType(errorType)
+                    .count((long) logs.size())
+                    .lastOccurrence(lastOccurrence)
+                    .sampleMessages(sampleMessages)
+                    .build();
+            })
+            .sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
+            .limit(10)
+            .collect(Collectors.toList());
+    }
+    
+    private List<LogStatistics.PerformanceStatistics> calculatePerformanceStatistics(LocalDateTime startTime, LocalDateTime endTime) {
+        Pageable pageable = PageRequest.of(0, 1000);
+        Page<LogEntry> allLogs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
+        
+        Map<String, List<LogEntry>> logsByEndpoint = allLogs.getContent().stream()
+            .filter(log -> log.getContext() != null && log.getContext().get("responseTime") != null)
+            .collect(Collectors.groupingBy(log -> log.getContext().get("requestId") != null ? 
+                log.getContext().get("requestId").toString() : "unknown"));
+        
+        return logsByEndpoint.entrySet().stream()
+            .map(entry -> {
+                String endpoint = entry.getKey();
+                List<LogEntry> logs = entry.getValue();
+                
+                List<Long> responseTimes = logs.stream()
+                    .map(log -> log.getContext() != null && log.getContext().get("responseTime") != null ? 
+                        Long.parseLong(log.getContext().get("responseTime").toString()) : 0L)
+                    .toList();
+                
+                double avgResponseTime = responseTimes.stream()
+                    .mapToLong(Long::longValue)
+                    .average()
+                    .orElse(0.0);
+                
+                long requestCount = logs.size();
+                
+                long errorCount = logs.stream()
+                    .mapToLong(log -> "ERROR".equals(log.getLevel()) ? 1 : 0)
+                    .sum();
+                
+                double errorRate = requestCount > 0 ? (double) errorCount / requestCount * 100 : 0.0;
+                
+                return LogStatistics.PerformanceStatistics.builder()
+                    .endpoint(endpoint)
+                    .avgResponseTime(avgResponseTime)
+                    .maxResponseTime((double) responseTimes.stream().mapToLong(Long::longValue).max().orElse(0L))
+                    .minResponseTime((double) responseTimes.stream().mapToLong(Long::longValue).min().orElse(0L))
+                    .requestCount(requestCount)
+                    .errorRate(errorRate)
+                    .build();
+            })
+            .sorted((a, b) -> Double.compare(b.getAvgResponseTime(), a.getAvgResponseTime()))
+            .limit(10)
+            .collect(Collectors.toList());
+    }
+    
+    private List<LogStatistics.TrendData> calculateTrendData(LocalDateTime startTime, LocalDateTime endTime) {
+        List<LogStatistics.TrendData> trends = new ArrayList<>();
+        
+        long hoursBetween = java.time.Duration.between(startTime, endTime).toHours();
+        if (hoursBetween > 24) {
+            long interval = hoursBetween / 24;
+            for (int i = 0; i < 24; i++) {
+                LocalDateTime intervalStart = startTime.plusHours(i * interval);
+                LocalDateTime intervalEnd = startTime.plusHours((i + 1) * interval);
+                
+                trends.add(calculateTrendForInterval(intervalStart, intervalEnd));
+            }
+        } else {
+            LocalDateTime current = startTime;
+            while (current.isBefore(endTime)) {
+                LocalDateTime intervalEnd = current.plusHours(1);
+                if (intervalEnd.isAfter(endTime)) {
+                    intervalEnd = endTime;
+                }
+                trends.add(calculateTrendForInterval(current, intervalEnd));
+                current = intervalEnd;
+            }
+        }
+        
+        return trends;
+    }
+    
+    private LogStatistics.TrendData calculateTrendForInterval(LocalDateTime startTime, LocalDateTime endTime) {
+        long totalCount = logEntryRepository.countByTimestampBetween(startTime, endTime);
+        long errorCount = logEntryRepository.countByTimestampBetweenAndLevel(startTime, endTime, "ERROR");
+        long warningCount = logEntryRepository.countByTimestampBetweenAndLevel(startTime, endTime, "WARN");
+        
+        Pageable pageable = PageRequest.of(0, 1000);
+        Page<LogEntry> logs = logEntryRepository.findByTimestampBetween(startTime, endTime, pageable);
+        
+        double avgResponseTime = logs.getContent().stream()
+            .filter(log -> log.getContext() != null && log.getContext().get("responseTime") != null)
+            .mapToLong(log -> Long.parseLong(log.getContext().get("responseTime").toString()))
+            .filter(responseTime -> responseTime != 0L)
+            .average()
+            .orElse(0.0);
+        
+        return LogStatistics.TrendData.builder()
+            .timestamp(startTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
+            .totalCount(totalCount)
+            .errorCount(errorCount)
+            .warningCount(warningCount)
+            .avgResponseTime(avgResponseTime)
+            .build();
+    }
+}

+ 115 - 0
src/main/java/com/demo/seecanalysisbackend/service/LogSearchService.java

@@ -0,0 +1,115 @@
+package com.demo.seecanalysisbackend.service;
+
+import com.demo.seecanalysisbackend.dto.LogSearchRequest;
+import com.demo.seecanalysisbackend.dto.LogSearchResponse;
+import com.demo.seecanalysisbackend.model.LogEntry;
+import com.demo.seecanalysisbackend.repository.LogEntryRepository;
+import lombok.RequiredArgsConstructor;
+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.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class LogSearchService {
+    
+    private final LogEntryRepository logEntryRepository;
+    
+    public LogSearchResponse searchLogs(LogSearchRequest request) {
+        log.info("Searching logs with criteria: {}", request);
+        
+        Pageable pageable = createPageable(request);
+        Page<LogEntry> logPage = performSearch(request, pageable);
+        
+        List<LogSearchResponse.LogEntryDto> logDtos = logPage.getContent().stream()
+            .map(this::convertToDto)
+            .collect(Collectors.toList());
+        
+        return LogSearchResponse.builder()
+            .logs(logDtos)
+            .totalCount(logPage.getTotalElements())
+            .currentPage(logPage.getNumber())
+            .pageSize(logPage.getSize())
+            .totalPages(logPage.getTotalPages())
+            .hasNext(logPage.hasNext())
+            .hasPrevious(logPage.hasPrevious())
+            .build();
+    }
+    
+    private Page<LogEntry> performSearch(LogSearchRequest request, Pageable pageable) {
+        if (request.getSearchText() != null && !request.getSearchText().trim().isEmpty()) {
+            return logEntryRepository.findByTimestampBetweenAndMessage(
+                request.getStartTime(), request.getEndTime(), 
+                request.getSearchText(), pageable);
+        }
+        
+        if (request.getBizNames() != null && !request.getBizNames().isEmpty()) {
+            return logEntryRepository.findByTimestampBetweenAndBizNameIn(
+                request.getStartTime(), request.getEndTime(), 
+                request.getBizNames(), pageable);
+        }
+        
+        if (request.getServices() != null && !request.getServices().isEmpty()) {
+            String serviceName = request.getServices().getFirst();
+            return logEntryRepository.findByTimestampBetweenAndServiceName(
+                request.getStartTime(), request.getEndTime(), 
+                serviceName, pageable);
+        }
+        
+        if (request.getTraceId() != null && !request.getTraceId().trim().isEmpty()) {
+            return logEntryRepository.findByTimestampBetweenAndTraceId(
+                request.getStartTime(), request.getEndTime(), 
+                request.getTraceId(), pageable);
+        }
+        
+        if (request.getLevels() != null && !request.getLevels().isEmpty()) {
+            return logEntryRepository.findByTimestampBetweenAndLevelIn(
+                request.getStartTime(), request.getEndTime(), 
+                request.getLevels(), pageable);
+        }
+        
+        if (request.getApplications() != null && !request.getApplications().isEmpty()) {
+            return logEntryRepository.findByTimestampBetweenAndApplicationIn(
+                request.getStartTime(), request.getEndTime(), 
+                request.getApplications(), pageable);
+        }
+        
+        return logEntryRepository.findByTimestampBetween(
+            request.getStartTime(), request.getEndTime(), pageable);
+    }
+    
+    private Pageable createPageable(LogSearchRequest request) {
+        Sort.Direction direction = "asc".equalsIgnoreCase(request.getSortDirection()) ? 
+            Sort.Direction.ASC : Sort.Direction.DESC;
+        
+        return PageRequest.of(request.getPage(), request.getSize(), 
+            Sort.by(direction, request.getSortBy()));
+    }
+    
+    private LogSearchResponse.LogEntryDto convertToDto(LogEntry logEntry) {
+        return LogSearchResponse.LogEntryDto.builder()
+            .id(logEntry.getId())
+            .timestamp(logEntry.getTimestamp() != null ? 
+                logEntry.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null)
+            .rawMessage(logEntry.getRawMessage())
+            .serviceName(logEntry.getServiceName())
+            .thread(logEntry.getThread())
+            .level(logEntry.getLevel())
+            .className(logEntry.getClassName())
+            .methodName(logEntry.getMethodName())
+            .traceId(logEntry.getTraceId())
+            .spanId(logEntry.getSpanId())
+            .bizName(logEntry.getBizName())
+            .context(logEntry.getContext())
+            .build();
+    }
+}

+ 0 - 1
src/main/resources/application.properties

@@ -1 +0,0 @@
-spring.application.name=seec-analysis-backend

+ 29 - 0
src/main/resources/application.yml

@@ -0,0 +1,29 @@
+spring:
+  application:
+    name: seec-analysis-backend
+
+  elasticsearch:
+    uris: http://environment-27-45.seec.svc.cluster.local:9200
+    username: "elastic"
+    password: "NJU67es"
+    connection-timeout: 5s
+    socket-timeout: 10s
+
+  jackson:
+    datatype:
+      datetime:
+        WRITE_DATES_AS_TIMESTAMPS: false
+    date-format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+    time-zone: UTC
+
+server:
+  port: 8080
+  servlet:
+    context-path: /api
+
+logging:
+  level:
+    com.demo.seecanalysisbackend: INFO
+    org.springframework.data.elasticsearch: DEBUG
+  pattern:
+    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

+ 0 - 13
src/test/java/com/demo/seecanalysisbackend/SeecAnalysisBackendApplicationTests.java

@@ -1,13 +0,0 @@
-package com.demo.seecanalysisbackend;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class SeecAnalysisBackendApplicationTests {
-
-    @Test
-    void contextLoads() {
-    }
-
-}