chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
# LogHttpClientAdapter 使用说明
## 概述
`LogHttpClientAdapter` 是一个用于与 Rust 日志服务进行 HTTP 通信的客户端适配器。它提供了与 Rust 工程接口一一对应的 HTTP 请求方法,支持智能体日志的新增、批量新增和搜索功能。
## 功能特性
- **健康检查**:检查 Rust 服务的健康状态
- **就绪检查**:检查 Rust 服务是否就绪
- **日志新增**:支持单个和批量新增智能体日志
- **日志搜索**:支持条件查询、分页和排序
- **异步支持**:所有方法都提供异步和同步两个版本
- **配置化**:通过配置文件管理连接参数
- **配置验证**:启动时验证必需配置,缺失时抛出异常
## 配置说明
在主配置文件(如 `application-dev.yml`)中添加以下配置:
```yaml
log-module:
log:
rust-service:
# Rust 日志服务的基础 URL必填
base-url: http://localhost:8098
# 连接超时时间(秒),默认 10
connect-timeout-seconds: 10
# 写入超时时间(秒),默认 30
write-timeout-seconds: 30
# 读取超时时间(秒),默认 30
read-timeout-seconds: 30
# 是否启用连接池,默认 true
enable-connection-pool: true
# 最大空闲连接数,默认 5
max-idle-connections: 5
# 连接保持存活时间(分钟),默认 5
keep-alive-duration-minutes: 5
# 日志级别配置(复用现有的 log.level 配置)
log:
level:
com.xspaceagi.domain.service.LogHttpClientAdapter: DEBUG
```
**注意**`base-url` 是必填配置项,如果未配置或为空,应用启动时会抛出异常。
## 使用示例
### 1. 依赖注入
```java
@Service
public class LogService {
@Autowired
private LogHttpClientAdapter logHttpClientAdapter;
// ... 业务方法
}
```
### 2. 健康检查
```java
// 异步方式
CompletableFuture<Boolean> healthFuture = logHttpClientAdapter.healthCheck();
Boolean isHealthy = healthFuture.get();
// 同步方式(不推荐在高并发场景使用)
Boolean isHealthy = logHttpClientAdapter.healthCheck().join();
```
### 3. 新增单个日志
```java
AgentLogEntry logEntry = AgentLogEntry.builder()
.requestId("req_001")
.conversationId("session_001")
.userUid("user_123")
.tenantId("tenant_001")
.userInput("用户输入内容")
.output("系统输出内容")
.inputToken(50)
.outputToken(100)
.requestStartTime(Instant.now().minusSeconds(5))
.requestEndTime(Instant.now())
.elapsedTimeMs(5000L)
.status("success")
.build();
// 异步方式
CompletableFuture<Boolean> result = logHttpClientAdapter.addAgentLog(logEntry);
// 同步方式
boolean success = logHttpClientAdapter.addAgentLogSync(logEntry);
```
### 4. 批量新增日志
```java
List<AgentLogEntry> logEntries = Arrays.asList(
AgentLogEntry.builder()...build(),
AgentLogEntry.builder()...build()
);
// 异步方式
CompletableFuture<Boolean> result = logHttpClientAdapter.batchAddAgentLogs(logEntries);
// 同步方式
boolean success = logHttpClientAdapter.batchAddAgentLogsSync(logEntries);
```
### 5. 搜索日志
```java
// 构建搜索条件
AgentLogSearchParams searchParams = AgentLogSearchParams.builder()
.tenantId("tenant_001")
.userUid("user_123")
.userInput("搜索关键词")
.startTime(Instant.now().minus(Duration.ofDays(1)))
.endTime(Instant.now())
.build();
// 构建搜索请求
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.queryFilter(searchParams)
.current(1L)
.pageSize(20L)
.orders(Arrays.asList(
AgentLogSearchRequest.OrderBy.builder()
.column("request_start_time")
.asc(false)
.build()
))
.build();
// 异步搜索
CompletableFuture<AgentLogSearchResult> future = logHttpClientAdapter.searchAgentLogs(searchRequest);
AgentLogSearchResult result = future.get();
// 同步搜索
AgentLogSearchResult result = logHttpClientAdapter.searchAgentLogsSync(searchRequest);
// 处理搜索结果
System.out.println("总记录数: " + result.getTotal());
System.out.println("当前页: " + result.getCurrent());
System.out.println("每页大小: " + result.getSize());
result.getRecords().forEach(log -> {
System.out.println("日志ID: " + log.getRequestId());
System.out.println("用户输入: " + log.getUserInput());
});
```
## 模型类说明
### AgentLogEntry
智能体日志条目,包含以下主要字段:
- `requestId`: 请求ID必填
- `conversationId`: 会话ID必填
- `userUid`: 用户UID必填
- `tenantId`: 租户ID必填
- `spaceId`: 空间ID可选
- `userInput`: 用户输入内容
- `output`: 系统输出内容
- `inputToken/outputToken`: Token 数量
- `requestStartTime/requestEndTime`: 请求时间
- `elapsedTimeMs`: 耗时(毫秒)
- `nodeType/nodeName/status`: 节点信息
### AgentLogSearchParams
搜索参数,支持多条件组合查询:
- 基础过滤:`requestId`, `conversationId`, `userUid`, `tenantId`, `spaceId`
- 全文搜索:`userInput`, `output`
- 时间范围:`startTime`, `endTime`
### AgentLogSearchRequest
搜索请求包装类:
- `queryFilter`: 查询条件
- `current`: 当前页码
- `pageSize`: 每页大小
- `orders`: 排序条件列表
### AgentLogSearchResult
搜索结果:
- `records`: 日志记录列表
- `total`: 总记录数
- `current`: 当前页
- `size`: 每页大小
- `elapsedTimeMs`: 查询耗时
## 错误处理
### 配置验证
客户端适配器在启动时会验证配置:
- 如果 `base-url` 未配置或为空,会抛出 `IllegalArgumentException`
- 错误信息会记录到日志中
### 运行时异常
客户端适配器已经内置了基本的错误处理:
- 网络异常会被记录并返回 false 或抛出异常
- HTTP 错误状态码会被记录并处理
- JSON 序列化/反序列化错误会被捕获
在业务代码中建议使用 try-catch 块处理异常:
```java
try {
AgentLogSearchResult result = logHttpClientAdapter.searchAgentLogsSync(searchRequest);
// 处理搜索结果
} catch (Exception e) {
log.error("搜索日志失败", e);
// 处理错误情况
}
```
## 注意事项
1. **必填配置**`log-module.log.rust-service.base-url` 是必填配置,不能为空
2. **服务依赖**:确保 Rust 日志服务正在运行并可访问
3. **线程安全**`LogHttpClientAdapter` 是线程安全的,可以在多线程环境中使用
4. **资源管理**:应用关闭时会自动清理 HTTP 客户端资源
5. **配置优化**:根据实际网络环境调整超时时间和连接池参数
6. **日志级别**:通过 `log.level` 配置控制日志输出级别
## 测试
运行单元测试前确保:
1. Rust 服务正在运行
2. 配置正确的服务地址
```bash
# 运行特定测试类
mvn test -Dtest=LogHttpClientAdapterTest
# 运行所有测试
mvn test
```
## 配置示例
### 开发环境配置
```yaml
log-module:
log:
rust-service:
base-url: http://localhost:8098
log:
level:
com.xspaceagi.domain.service.LogHttpClientAdapter: DEBUG
```
### 生产环境配置
```yaml
log-module:
log:
rust-service:
base-url: http://rust-log-service:8098
connect-timeout-seconds: 5
write-timeout-seconds: 60
read-timeout-seconds: 60
max-idle-connections: 10
log:
level:
com.xspaceagi.domain.service.LogHttpClientAdapter: INFO
```

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-log</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-log-domain</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-spec</artifactId>
</dependency>
<!-- 其他原有依赖保持不变 -->
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-sdk</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,268 @@
package com.xspaceagi.domain.adaptor.impl;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.exception.LogPlatformException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xspaceagi.domain.model.valueobj.AgentLogEntry;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchRequest;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchResult;
import com.xspaceagi.domain.model.valueobj.RustServiceResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
@Slf4j
@Service
public class LogHttpClientAdapter {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private final OkHttpClient httpClient;
private final String baseUrl;
@Resource
private ObjectMapper objectMapper;
@Autowired
public LogHttpClientAdapter(LogHttpClientConfiguration config) {
// 验证配置
if (!StringUtils.hasText(config.getBaseUrl())) {
String errorMsg = "日志服务配置错误log-module.log.rust-service.base-url 不能为空,请检查配置文件";
log.error(errorMsg);
throw new IllegalArgumentException(errorMsg);
}
this.baseUrl = config.getBaseUrl();
log.info("初始化日志 HTTP 客户端,服务地址: {}", this.baseUrl);
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(config.getConnectTimeoutSeconds()))
.writeTimeout(Duration.ofSeconds(config.getWriteTimeoutSeconds()))
.readTimeout(Duration.ofSeconds(config.getReadTimeoutSeconds()))
.build();
}
/**
* 健康检查 - 直接使用同步API
* GET /health
*/
public boolean healthCheck() {
Request request = new Request.Builder()
.url(baseUrl + "/health")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
return response.isSuccessful();
} catch (IOException e) {
log.error("健康检查失败", e);
return false;
}
}
/**
* 就绪检查 - 直接使用同步API
* GET /ready
*/
public boolean readyCheck() {
Request request = new Request.Builder()
.url(baseUrl + "/ready")
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
return response.isSuccessful();
} catch (IOException e) {
log.error("就绪检查失败", e);
return false;
}
}
/**
* 新增单个智能体日志 - 直接使用同步API
* POST /api/agent/log/add
*/
public boolean addAgentLog(AgentLogEntry logEntry) {
try {
String json = objectMapper.writeValueAsString(logEntry);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(baseUrl + "/api/agent/log/add")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
boolean success = response.isSuccessful();
if (!success) {
log.warn("新增单个智能体日志失败,状态码: {}, 响应: {}",
response.code(), response.body().string());
}
return success;
}
} catch (Exception e) {
log.error("新增单个智能体日志失败", e);
throw LogPlatformException.build(BizExceptionCodeEnum.logPlatformInsertFailed, e.getMessage());
}
}
/**
* 批量新增智能体日志 - 直接使用同步API
* POST /api/agent/log/batch
*/
public boolean batchAddAgentLogs(List<AgentLogEntry> logEntries) {
try {
String json = objectMapper.writeValueAsString(logEntries);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(baseUrl + "/api/agent/log/batch")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
boolean success = response.isSuccessful();
if (!success) {
log.warn("批量新增智能体日志失败,状态码: {}, 响应: {}",
response.code(), response.body().string());
}
return success;
}
} catch (Exception e) {
log.error("批量新增智能体日志失败", e);
throw LogPlatformException.build(BizExceptionCodeEnum.logPlatformBatchInsertFailed, e.getMessage());
}
}
/**
* 搜索智能体日志 - 直接使用同步API
* POST /api/agent/log/search
*/
public AgentLogSearchResult searchAgentLogs(AgentLogSearchRequest searchRequest) {
try {
// 确保 queryFilter 不为空Rust 服务要求必须有这个字段
if (searchRequest.getQueryFilter() == null) {
searchRequest.setQueryFilter(new AgentLogSearchParams());
}
String json = objectMapper.writeValueAsString(searchRequest);
log.debug("Sending log search request: {}", json);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(baseUrl + "/api/agent/log/search")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
log.debug("Search response: {}", responseBody);
// 解析包装的响应
RustServiceResponse<AgentLogSearchResult> rustResponse = objectMapper.readValue(responseBody,
objectMapper.getTypeFactory().constructParametricType(
RustServiceResponse.class, AgentLogSearchResult.class));
if (rustResponse.isSuccess()) {
return rustResponse.getData();
} else {
String errorMsg = String.format("服务返回错误code=%s, message=%s",
rustResponse.getCode(), rustResponse.getMessage());
log.warn(errorMsg);
throw new LogPlatformException(BizExceptionCodeEnum.logPlatformSearchFailed.getCode(),
errorMsg);
}
} else {
String errorBody = response.body().string();
log.warn("Agent log search failed, status: {}, body: {}", response.code(), errorBody);
throw new LogPlatformException(BizExceptionCodeEnum.logPlatformSearchFailed.getCode(),
"搜索失败HTTP状态码: " + response.code() + ", 响应: " + errorBody);
}
}
} catch (LogPlatformException e) {
throw e;
} catch (Exception e) {
log.error("Agent log search failed", e);
throw LogPlatformException.build(BizExceptionCodeEnum.logPlatformSearchFailed, e.getMessage());
}
}
/**
* 搜索智能体日志 - 直接使用同步API
* POST /api/agent/log/search
*/
public AgentLogSearchResult queryOneAgentLog(AgentLogSearchRequest searchRequest) {
try {
// 确保 queryFilter 不为空Rust 服务要求必须有这个字段
if (searchRequest.getQueryFilter() == null) {
searchRequest.setQueryFilter(new AgentLogSearchParams());
}
String json = objectMapper.writeValueAsString(searchRequest);
log.debug("Sending log search request: {}", json);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(baseUrl + "/api/agent/log/detail")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
log.debug("Search response: {}", responseBody);
// 解析包装的响应
RustServiceResponse<AgentLogSearchResult> rustResponse = objectMapper.readValue(responseBody,
objectMapper.getTypeFactory().constructParametricType(
RustServiceResponse.class, AgentLogSearchResult.class));
if (rustResponse.isSuccess()) {
return rustResponse.getData();
} else {
String errorMsg = String.format("服务返回错误code=%s, message=%s",
rustResponse.getCode(), rustResponse.getMessage());
log.warn(errorMsg);
throw new LogPlatformException(BizExceptionCodeEnum.logPlatformSearchFailed.getCode(),
errorMsg);
}
} else {
String errorBody = response.body().string();
log.warn("Agent log search failed, status: {}, body: {}", response.code(), errorBody);
throw new LogPlatformException(BizExceptionCodeEnum.logPlatformSearchFailed.getCode(),
"搜索失败HTTP状态码: " + response.code() + ", 响应: " + errorBody);
}
}
} catch (LogPlatformException e) {
throw e;
} catch (Exception e) {
log.error("Agent log search failed", e);
throw LogPlatformException.build(BizExceptionCodeEnum.logPlatformSearchFailed, e.getMessage());
}
}
/**
* 关闭资源
*/
public void close() {
if (httpClient != null) {
httpClient.dispatcher().executorService().shutdown();
httpClient.connectionPool().evictAll();
}
}
}

View File

@@ -0,0 +1,50 @@
package com.xspaceagi.domain.adaptor.impl;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
/**
* 日志 HTTP 客户端配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "log-module.log.rust-service")
public class LogHttpClientConfiguration {
/**
* Rust 服务的基础 URL必填
*/
private String baseUrl;
/**
* 连接超时时间(秒)
*/
private Integer connectTimeoutSeconds = 10;
/**
* 写入超时时间(秒)
*/
private Integer writeTimeoutSeconds = 30;
/**
* 读取超时时间(秒)
*/
private Integer readTimeoutSeconds = 30;
/**
* 是否启用连接池
*/
private Boolean enableConnectionPool = true;
/**
* 最大空闲连接数
*/
private Integer maxIdleConnections = 5;
/**
* 连接保持存活时间(分钟)
*/
private Integer keepAliveDurationMinutes = 5;
}

View File

@@ -0,0 +1,152 @@
package com.xspaceagi.domain.model;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 智能体调试大模型的日志结构定义
* 对应 Rust 的 AgentLogEntry 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentLogModel {
/**
* 请求ID唯一标识一次请求必填
*/
@Schema(description = "请求ID唯一标识一次请求必填")
private String requestId;
/**
* 消息ID必填
*/
@Schema(description = "消息ID必填")
private String messageId;
/**
* 智能体ID
*/
@Schema(description = "智能体ID")
private String agentId;
/**
* 会话ID必填
*/
@Schema(description = "会话ID必填")
private String conversationId;
/**
* 用户UID必填
*/
@Schema(description = "用户UID必填")
private String userUid;
/**
* 租户ID用于租户隔离必填
*/
@Schema(description = "租户ID用于租户隔离必填")
private String tenantId;
/**
* 空间ID用户可以有多个空间
*/
@Schema(description = "空间ID用户可以有多个空间")
private String spaceId;
/**
* 用户输入的内容
*/
@Schema(description = "用户输入的内容")
private String userInput;
/**
* 系统输出的内容
*/
@Schema(description = "系统输出的内容")
private String output;
/**
* 执行结果,扩展字段,字段类型存储的是json文本
*/
@Schema(description = "执行结果,扩展字段,字段类型存储的是json文本")
private String executeResult;
/**
* 输入token数量
*/
@Schema(description = "输入token数量")
private Integer inputToken;
/**
* 输出token数量
*/
@Schema(description = "输出token数量")
private Integer outputToken;
/**
* 请求开始时间
*/
@Schema(description = "请求开始时间")
private LocalDateTime requestStartTime;
/**
* 请求结束时间
*/
@Schema(description = "请求结束时间")
private LocalDateTime requestEndTime;
/**
* 耗时(毫秒)
*/
@Schema(description = "耗时(毫秒)")
private Long elapsedTimeMs;
/**
* 节点类型
*/
@Schema(description = "节点类型")
private String nodeType;
/**
* 节点状态
*/
@Schema(description = "节点状态")
private String status;
/**
* 节点名称
*/
@Schema(description = "节点名称")
private String nodeName;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
/**
* 用户ID
*/
@Schema(description = "用户ID")
private Long userId;
/**
* 用户名称
*/
@Schema(description = "用户名称")
private String userName;
}

View File

@@ -0,0 +1,139 @@
package com.xspaceagi.domain.model.valueobj;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.xspaceagi.log.spec.FlexibleDateTimeDeserializer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 智能体调试大模型的日志结构定义
* 对应 Rust 的 AgentLogEntry 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AgentLogEntry {
/**
* 请求ID唯一标识一次请求必填
*/
private String requestId;
/**
* 消息ID必填
*/
private String messageId;
/**
* 会话ID必填
*/
private String conversationId;
/**
* 智能体ID
*/
private String agentId;
/**
* 用户UID必填
*/
private String userUid;
/**
* 租户ID用于租户隔离必填
*/
private String tenantId;
/**
* 空间ID用户可以有多个空间
*/
private String spaceId;
/**
* 用户输入的内容
*/
private String userInput;
/**
* 系统输出的内容
*/
private String output;
/**
* 执行结果,扩展字段,字段类型存储的是json文本
*/
private String executeResult;
/**
* 输入token数量
*/
private Integer inputToken;
/**
* 输出token数量
*/
private Integer outputToken;
/**
* 请求开始时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
private LocalDateTime requestStartTime;
/**
* 请求结束时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
private LocalDateTime requestEndTime;
/**
* 耗时(毫秒)
*/
private Long elapsedTimeMs;
/**
* 节点类型
*/
private String nodeType;
/**
* 节点状态
*/
private String status;
/**
* 节点名称
*/
private String nodeName;
/**
* 创建时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
private LocalDateTime createdAt;
/**
* 更新时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
private LocalDateTime updatedAt;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名称
*/
private String userName;
}

View File

@@ -0,0 +1,85 @@
package com.xspaceagi.domain.model.valueobj;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.xspaceagi.log.spec.FlexibleDateTimeDeserializer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 日志搜索请求参数
* 对应 Rust 的 AgentLogSearchParams 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AgentLogSearchParams {
/**
* 请求ID
*/
private String requestId;
/**
* 会话ID
*/
private String conversationId;
/**
* 消息ID
*/
private String messageId;
/**
* 智能体ID
*/
private String agentId;
/**
* 用户UID
*/
private String userUid;
/**
* 用户输入,需要支持全文检索支持多个关键字AND关系
*/
private List<String> userInput;
/**
* 系统输出,需要支持全文检索支持多个关键字AND关系
*/
private List<String> output;
/**
* 开始时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", shape = JsonFormat.Shape.STRING)
private LocalDateTime startTime;
/**
* 结束时间
*/
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", shape = JsonFormat.Shape.STRING)
private LocalDateTime endTime;
/**
* 租户ID用于租户隔离确保只查询特定租户的日志
*/
private String tenantId;
/**
* 空间ID可选用于查询特定空间的日志支持多个IDOR关系
*/
private List<String> spaceId;
}

View File

@@ -0,0 +1,60 @@
package com.xspaceagi.domain.model.valueobj;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 日志搜索请求包装类
* 用于包装搜索参数、分页和排序信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentLogSearchRequest {
/**
* 查询过滤条件
*/
private AgentLogSearchParams queryFilter;
/**
* 当前页码
*/
@Builder.Default
private Long current = 1L;
/**
* 每页大小
*/
@Builder.Default
private Long pageSize = 10L;
/**
* 排序条件
*/
private List<OrderBy> orders;
/**
* 排序条件
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OrderBy {
/**
* 排序字段
*/
private String column;
/**
* 是否升序
*/
private Boolean asc;
}
}

View File

@@ -0,0 +1,52 @@
package com.xspaceagi.domain.model.valueobj;
import java.util.List;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 日志分页结果
* 对应 Rust 的 AgentLogSearchResult 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class AgentLogSearchResult {
/**
* 处理耗时(毫秒)
*/
private Long elapsedTimeMs;
/**
* 查询数据列表
*/
private List<AgentLogEntry> records;
/**
* 总数
*/
private Long total;
/**
* 每页显示条数默认10
*/
@Builder.Default
private Long size = 10L;
/**
* 当前页
*/
@Builder.Default
private Long current = 1L;
}

View File

@@ -0,0 +1,44 @@
package com.xspaceagi.domain.model.valueobj;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Rust 服务响应包装类
* 用于包装 Rust 服务返回的标准响应格式
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RustServiceResponse<T> {
/**
* 响应代码
*/
private String code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 事务ID
*/
private String tid;
/**
* 判断响应是否成功
*/
public boolean isSuccess() {
return "0000".equals(code);
}
}

View File

@@ -0,0 +1,58 @@
package com.xspaceagi.domain.service;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.domain.model.AgentLogModel;
import com.xspaceagi.domain.model.valueobj.AgentLogEntry;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
/**
* 日志平台领域服务接口
*/
public interface ILogPlatformDomainService {
/**
* 搜索智能体日志
*
* @param searchParams 搜索参数
* @param current 当前页
* @param pageSize 每页大小
* @return 分页日志结果
*/
IPage<AgentLogModel> searchAgentLogs(AgentLogSearchParams searchParams, Long current, Long pageSize);
/**
* 详情
*
* @param searchParams 搜索参数
* @param current 当前页
* @param pageSize 每页大小
* @return 日志详情
*/
AgentLogModel queryOneAgentLog(AgentLogSearchParams searchParams);
/**
* 新增单个智能体日志
*
* @param logEntry 日志条目
* @return 是否成功
*/
boolean addAgentLog(AgentLogEntry logEntry);
/**
* 批量新增智能体日志
*
* @param logEntries 日志条目列表
* @return 是否成功
*/
boolean batchAddAgentLogs(List<AgentLogEntry> logEntries);
/**
* 健康检查
*
* @return Rust 服务是否健康
*/
boolean healthCheck();
}

View File

@@ -0,0 +1,164 @@
package com.xspaceagi.domain.service;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.domain.adaptor.impl.LogHttpClientAdapter;
import com.xspaceagi.domain.model.AgentLogModel;
import com.xspaceagi.domain.model.valueobj.AgentLogEntry;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchRequest;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class LogPlatformDomainService implements ILogPlatformDomainService {
@Resource
private LogHttpClientAdapter logHttpClientAdapter;
@Override
public IPage<AgentLogModel> searchAgentLogs(AgentLogSearchParams searchParams, Long current, Long pageSize) {
log.info("开始搜索智能体日志,参数: {}, 当前页: {}, 每页大小: {}", searchParams, current, pageSize);
// 构建搜索请求
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.queryFilter(searchParams)
.current(current != null ? current : 1L)
.pageSize(pageSize != null ? pageSize : 10L)
.build();
// 调用 Rust 服务搜索
AgentLogSearchResult searchResult = logHttpClientAdapter.searchAgentLogs(searchRequest);
// 转换结果
List<AgentLogModel> records = convertToAgentLogModels(searchResult.getRecords());
// 构建分页结果
Page<AgentLogModel> page = new Page<>(
searchResult.getCurrent(),
searchResult.getSize());
page.setRecords(records);
page.setTotal(searchResult.getTotal());
log.info("Search done, total: {}, page: {}, elapsed: {}ms",
searchResult.getTotal(), searchResult.getCurrent(), searchResult.getElapsedTimeMs());
return page;
}
@Override
public boolean addAgentLog(AgentLogEntry logEntry) {
log.info("Adding agent log, requestId: {}", logEntry.getRequestId());
boolean result = logHttpClientAdapter.addAgentLog(logEntry);
if (result) {
log.info("新增智能体日志成功请求ID: {}", logEntry.getRequestId());
} else {
log.warn("新增智能体日志失败请求ID: {}", logEntry.getRequestId());
}
return result;
}
@Override
public boolean batchAddAgentLogs(List<AgentLogEntry> logEntries) {
if (CollectionUtils.isEmpty(logEntries)) {
log.warn("批量新增智能体日志失败:日志列表为空");
return false;
}
log.info("开始批量新增智能体日志,数量: {}", logEntries.size());
boolean result = logHttpClientAdapter.batchAddAgentLogs(logEntries);
if (result) {
log.info("批量新增智能体日志成功,数量: {}", logEntries.size());
} else {
log.warn("批量新增智能体日志失败,数量: {}", logEntries.size());
}
return result;
}
@Override
public boolean healthCheck() {
log.debug("开始检查 Rust 日志服务健康状态");
boolean isHealthy = logHttpClientAdapter.healthCheck();
log.debug("Rust 日志服务健康状态: {}", isHealthy ? "健康" : "不健康");
return isHealthy;
}
/**
* 将 AgentLogEntry 转换为 AgentLogModel
*/
private List<AgentLogModel> convertToAgentLogModels(List<AgentLogEntry> logEntries) {
if (CollectionUtils.isEmpty(logEntries)) {
return List.of();
}
return logEntries.stream()
.map(this::convertToAgentLogModel)
.collect(Collectors.toList());
}
/**
* 将单个 AgentLogEntry 转换为 AgentLogModel
*/
private AgentLogModel convertToAgentLogModel(AgentLogEntry logEntry) {
return AgentLogModel.builder()
.requestId(logEntry.getRequestId())
.messageId(logEntry.getMessageId())
.conversationId(logEntry.getConversationId())
.agentId(logEntry.getAgentId())
.userUid(logEntry.getUserUid())
.tenantId(logEntry.getTenantId())
.spaceId(logEntry.getSpaceId())
.userInput(logEntry.getUserInput())
.output(logEntry.getOutput())
.inputToken(logEntry.getInputToken())
.outputToken(logEntry.getOutputToken())
.executeResult(logEntry.getExecuteResult())
.requestStartTime(logEntry.getRequestStartTime())
.requestEndTime(logEntry.getRequestEndTime())
.elapsedTimeMs(logEntry.getElapsedTimeMs())
.nodeType(logEntry.getNodeType())
.status(logEntry.getStatus())
.nodeName(logEntry.getNodeName())
.createdAt(logEntry.getCreatedAt())
.updatedAt(logEntry.getUpdatedAt())
.userId(logEntry.getUserId())
.userName(logEntry.getUserName())
.build();
}
@Override
public AgentLogModel queryOneAgentLog(AgentLogSearchParams searchParams) {
log.info("开始搜索智能体日志,参数: {}", searchParams);
// 构建搜索请求
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.queryFilter(searchParams)
.current(1L)
.pageSize(1L)
.build();
// 调用 Rust 服务搜索
AgentLogSearchResult searchResult = logHttpClientAdapter.queryOneAgentLog(searchRequest);
// 转换结果
List<AgentLogModel> records = convertToAgentLogModels(searchResult.getRecords());
log.info("Search done, total: {}, page: {}, elapsed: {}ms",
searchResult.getTotal(), searchResult.getCurrent(), searchResult.getElapsedTimeMs());
return records.stream().findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,228 @@
package com.xspaceagi.domain.service;
import com.xspaceagi.domain.adaptor.impl.LogHttpClientAdapter;
import com.xspaceagi.domain.adaptor.impl.LogHttpClientConfiguration;
import com.xspaceagi.domain.model.valueobj.AgentLogEntry;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchRequest;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchResult;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* LogHttpClientAdapter 单元测试
* 注意:这些测试需要 Rust 服务正在运行
*/
@Slf4j
public class LogHttpClientAdapterTest {
private LogHttpClientAdapter logHttpClientAdapter;
@BeforeEach
void setUp() {
// 创建配置对象
LogHttpClientConfiguration config = new LogHttpClientConfiguration();
config.setBaseUrl("http://localhost:8098");
config.setConnectTimeoutSeconds(10);
config.setWriteTimeoutSeconds(30);
config.setReadTimeoutSeconds(30);
config.setEnableConnectionPool(true);
config.setMaxIdleConnections(5);
config.setKeepAliveDurationMinutes(5);
logHttpClientAdapter = new LogHttpClientAdapter(config);
}
@Test
void testHealthCheck() throws Exception {
Boolean result = logHttpClientAdapter.healthCheck();
log.info("健康检查结果: {}", result);
// 注意:如果 Rust 服务未运行,这个测试会失败
}
@Test
void testReadyCheck() throws Exception {
Boolean result = logHttpClientAdapter.readyCheck();
log.info("就绪检查结果: {}", result);
}
@Test
void testAddAgentLog() throws Exception {
AgentLogEntry logEntry = AgentLogEntry.builder()
.requestId("req_test_001")
.conversationId("session_test_001")
.userUid("user_test_123")
.tenantId("tenant_test_001")
.spaceId("space_test_001")
.userInput("测试用户输入")
.output("测试系统输出")
.inputToken(50)
.outputToken(100)
.requestStartTime(LocalDateTime.now().minusSeconds(5))
.requestEndTime(LocalDateTime.now())
.elapsedTimeMs(5000L)
.nodeType("test_node")
.status("success")
.nodeName("测试节点")
.build();
Boolean result = logHttpClientAdapter.addAgentLog(logEntry);
log.info("新增单个日志结果: {}", result);
}
@Test
void testBatchAddAgentLogs() throws Exception {
List<AgentLogEntry> logEntries = Arrays.asList(
AgentLogEntry.builder()
.requestId("req_batch_001")
.conversationId("session_batch_001")
.userUid("user_batch_123")
.tenantId("tenant_batch_001")
.spaceId("space_batch_001")
.userInput("批量测试输入1")
.output("批量测试输出1")
.inputToken(30)
.outputToken(60)
.requestStartTime(LocalDateTime.now().minusSeconds(10))
.requestEndTime(LocalDateTime.now().minusSeconds(7))
.elapsedTimeMs(3000L)
.status("success")
.build(),
AgentLogEntry.builder()
.requestId("req_batch_002")
.conversationId("session_batch_001")
.userUid("user_batch_123")
.tenantId("tenant_batch_001")
.spaceId("space_batch_001")
.userInput("批量测试输入2")
.output("批量测试输出2")
.inputToken(40)
.outputToken(80)
.requestStartTime(LocalDateTime.now().minusSeconds(7))
.requestEndTime(LocalDateTime.now().minusSeconds(4))
.elapsedTimeMs(3000L)
.status("success")
.build()
);
Boolean result = logHttpClientAdapter.batchAddAgentLogs(logEntries);
log.info("批量新增日志结果: {}", result);
}
@Test
void testSearchAgentLogs() throws Exception {
AgentLogSearchParams searchParams = AgentLogSearchParams.builder()
.tenantId("tenant_test_001")
.userUid("user_test_123")
.build();
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.queryFilter(searchParams)
.current(1L)
.pageSize(10L)
.build();
AgentLogSearchResult result = logHttpClientAdapter.searchAgentLogs(searchRequest);
log.info("搜索结果: 总数={}, 当前页={}, 每页大小={}, 记录数={}",
result.getTotal(), result.getCurrent(), result.getSize(),
result.getRecords() != null ? result.getRecords().size() : 0);
}
@Test
void testSyncMethods() {
// 测试同步方法
AgentLogEntry logEntry = AgentLogEntry.builder()
.requestId("req_sync_001")
.conversationId("session_sync_001")
.userUid("user_sync_123")
.tenantId("tenant_sync_001")
.userInput("同步测试输入")
.output("同步测试输出")
.build();
boolean addResult = logHttpClientAdapter.addAgentLog(logEntry);
log.info("同步新增日志结果: {}", addResult);
// 搜索测试
AgentLogSearchParams searchParams = AgentLogSearchParams.builder()
.tenantId("tenant_sync_001")
.build();
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.queryFilter(searchParams)
.current(1L)
.pageSize(5L)
.build();
try {
AgentLogSearchResult searchResult = logHttpClientAdapter.searchAgentLogs(searchRequest);
log.info("同步搜索结果: 总数={}", searchResult.getTotal());
} catch (Exception e) {
log.error("同步搜索失败", e);
}
}
@Test
void testConfigurationValidation() {
// 测试配置验证 - 空 URL
LogHttpClientConfiguration emptyConfig = new LogHttpClientConfiguration();
emptyConfig.setBaseUrl("");
try {
new LogHttpClientAdapter(emptyConfig);
// 如果没有抛出异常,测试失败
log.error("Expected exception but none was thrown");
} catch (IllegalArgumentException e) {
log.info("Caught expected config exception: {}", e.getMessage());
}
// 测试配置验证 - null URL
LogHttpClientConfiguration nullConfig = new LogHttpClientConfiguration();
nullConfig.setBaseUrl(null);
try {
new LogHttpClientAdapter(nullConfig);
// 如果没有抛出异常,测试失败
log.error("Expected exception but none was thrown");
} catch (IllegalArgumentException e) {
log.info("Caught expected config exception: {}", e.getMessage());
}
}
@Test
public void testSearchWithNullQueryFilter() {
// 创建配置
LogHttpClientConfiguration config = new LogHttpClientConfiguration();
config.setBaseUrl("http://localhost:8097");
// 创建适配器
LogHttpClientAdapter adapter = new LogHttpClientAdapter(config);
try {
// 创建搜索请求queryFilter 为 null
AgentLogSearchRequest searchRequest = AgentLogSearchRequest.builder()
.current(1L)
.pageSize(10L)
.queryFilter(null) // 故意设置为 null
.build();
log.info("测试 queryFilter 为 null 的情况");
log.info("原始 searchRequest: {}", searchRequest);
// 执行搜索
AgentLogSearchResult result = adapter.searchAgentLogs(searchRequest);
log.info("搜索成功,结果: {}", result);
} catch (Exception e) {
log.error("测试失败", e);
} finally {
adapter.close();
}
}
}