chore: initialize qiming workspace repository
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain.adaptor.impl;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,可选,用于查询特定空间的日志,支持多个ID(OR关系)
|
||||
*/
|
||||
private List<String> spaceId;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain.model.valueobj;
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain;
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain.repository;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain.service;
|
||||
@@ -0,0 +1 @@
|
||||
package com.xspaceagi.domain.specification;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user