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,112 @@
<?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-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-application</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-domain</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<!-- 打包配置必须在对应目录下创建repack.xml的打包配置文件 -->
<descriptor>src/main/resources/assemblies/repack.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<!-- install插件定制在mvn install过程使用哪个jar包安装到本地 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
<executions>
<execution>
<id>install-file</id>
<goals>
<goal>install-file</goal>
</goals>
<phase>install</phase>
</execution>
</executions>
<configuration>
<!-- install配置指定将哪个目录下的jar包install为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!--
install配置指定将install的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
<!-- deploy插件定制在mvn deploy过程使用哪个jar包发布到仓库 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<executions>
<execution>
<id>deploy-file</id>
<goals>
<goal>deploy-file</goal>
</goals>
<!-- 跳过默认的发布包 -->
<phase>none</phase>
</execution>
</executions>
<configuration>
<!-- deploy配置指定将哪个目录下的jar包发布为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!-- 发布地址 -->
<url>http://local-maven.space.com/nexus/content/repositories/snapshots/</url>
<repositoryId>nexus-snapshots</repositoryId>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<packaging>jar</packaging>
<!--
deploy配置指定将deploy的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,94 @@
package com.xspaceagi.log.api.convert;
import com.xspaceagi.domain.model.AgentLogModel;
import com.xspaceagi.domain.model.valueobj.AgentLogEntry;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.log.sdk.reponse.AgentLogModelResponse;
import com.xspaceagi.log.sdk.request.AgentLogEntryRequest;
import com.xspaceagi.log.sdk.request.AgentLogSearchParamsRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Slf4j
@Service
public class AgentLogEntryConvert {
public AgentLogEntry convertFrom(AgentLogEntryRequest request) {
AgentLogEntry agentLogEntry = AgentLogEntry.builder()
.requestId(request.getRequestId())
.messageId(request.getMessageId())
.conversationId(request.getConversationId())
.agentId(request.getAgentId())
.userUid(request.getUserUid())
.tenantId(request.getTenantId())
.spaceId(request.getSpaceId())
.userInput(request.getUserInput())
.output(request.getOutput())
.executeResult(request.getExecuteResult())
.inputToken(request.getInputToken())
.outputToken(request.getOutputToken())
.requestStartTime(request.getRequestStartTime())
.requestEndTime(request.getRequestEndTime())
.elapsedTimeMs(request.getElapsedTimeMs())
.nodeType(request.getNodeType())
.status(request.getStatus())
.nodeName(request.getNodeName())
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.userId(request.getUserId())
.userName(request.getUserName())
.build();
return agentLogEntry;
}
public AgentLogSearchParams convertFrom(AgentLogSearchParamsRequest request) {
AgentLogSearchParams agentLogSearchParams = AgentLogSearchParams.builder()
.requestId(request.getRequestId())
.conversationId(request.getConversationId())
.messageId(request.getMessageId())
.agentId(request.getAgentId())
.userUid(request.getUserUid())
.userInput(request.getUserInput())
.output(request.getOutput())
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.tenantId(request.getTenantId())
.spaceId(request.getSpaceId())
.build();
return agentLogSearchParams;
}
public AgentLogModelResponse convertTo(AgentLogModel agentLogModel) {
AgentLogModelResponse agentLogModelResponse = AgentLogModelResponse.builder()
.requestId(agentLogModel.getRequestId())
.messageId(agentLogModel.getMessageId())
.agentId(agentLogModel.getAgentId())
.conversationId(agentLogModel.getConversationId())
.userUid(agentLogModel.getUserUid())
.tenantId(agentLogModel.getTenantId())
.spaceId(agentLogModel.getSpaceId())
.userInput(agentLogModel.getUserInput())
.output(agentLogModel.getOutput())
.executeResult(agentLogModel.getExecuteResult())
.inputToken(agentLogModel.getInputToken())
.outputToken(agentLogModel.getOutputToken())
.requestStartTime(agentLogModel.getRequestStartTime())
.requestEndTime(agentLogModel.getRequestEndTime())
.elapsedTimeMs(agentLogModel.getElapsedTimeMs())
.nodeType(agentLogModel.getNodeType())
.status(agentLogModel.getStatus())
.nodeName(agentLogModel.getNodeName())
.createdAt(agentLogModel.getCreatedAt())
.updatedAt(agentLogModel.getUpdatedAt())
.userId(agentLogModel.getUserId())
.userName(agentLogModel.getUserName())
.build();
return agentLogModelResponse;
}
}

View File

@@ -0,0 +1,54 @@
package com.xspaceagi.log.api.service;
import com.xspaceagi.log.api.convert.AgentLogEntryConvert;
import com.xspaceagi.system.domain.log.LogRecordPrint;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.domain.adaptor.impl.LogHttpClientAdapter;
import com.xspaceagi.domain.model.AgentLogModel;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.domain.service.ILogPlatformDomainService;
import com.xspaceagi.log.sdk.reponse.AgentLogModelResponse;
import com.xspaceagi.log.sdk.request.AgentLogEntryRequest;
import com.xspaceagi.log.sdk.request.AgentLogSearchParamsRequest;
import com.xspaceagi.log.sdk.service.LogPlatformRpcService;
import com.xspaceagi.system.spec.utils.ValidateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class LogPlatformRpcServiceImpl implements LogPlatformRpcService {
@Resource
private AgentLogEntryConvert agentLogEntryConvert;
@Resource
private LogHttpClientAdapter logHttpClientAdapter;
@Resource
private ILogPlatformDomainService logPlatformDomainService;
@LogRecordPrint(content = "[智能体日志]-新增日志")
@Override
public boolean addLog(AgentLogEntryRequest logEntryRequest) {
log.info("Adding agent log, requestId: {}", logEntryRequest.getRequestId());
// 校验必填参数
ValidateUtil.validate(logEntryRequest);
var agentLogEntry = agentLogEntryConvert.convertFrom(logEntryRequest);
return logHttpClientAdapter.addAgentLog(agentLogEntry);
}
@LogRecordPrint(content = "[智能体日志]-搜索日志")
@Override
public IPage<AgentLogModelResponse> searchAgentLogs(AgentLogSearchParamsRequest searchParams, Long current,
Long pageSize) {
AgentLogSearchParams agentLogSearchParams = agentLogEntryConvert.convertFrom(searchParams);
IPage<AgentLogModel> result = logPlatformDomainService.searchAgentLogs(agentLogSearchParams, current, pageSize);
return result.convert(agentLogEntryConvert::convertTo);
}
}

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:app-platform-log-api</include>
<include>com.xspaceagi:app-platform-log-application</include>
<include>com.xspaceagi:app-platform-log-domain</include>
<include>com.xspaceagi:app-platform-log-infra</include>
<include>com.xspaceagi:app-platform-log-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,20 @@
<?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-api</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-log-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
<?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-application</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-infra</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,102 @@
package com.xspaceagi.log.app.service;
import com.alibaba.fastjson2.JSON;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.service.ILogRpcService;
import com.xspaceagi.log.sdk.service.ISearchRpcService;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.log.sdk.vo.SearchDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import com.xspaceagi.system.sdk.common.TraceContext;
import com.xspaceagi.system.sdk.service.AbstractTaskExecuteService;
import com.xspaceagi.system.sdk.service.ScheduleTaskApiService;
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service("logApplicationService")
public class LogApplicationServiceImpl extends AbstractTaskExecuteService implements ILogRpcService {
@Resource
private ScheduleTaskApiService scheduleTaskApiService;
@Resource
private RedisUtil redisUtil;
@Resource
private ISearchRpcService iSearchRpcService;
@PostConstruct
public void init() {
scheduleTaskApiService.start(ScheduleTaskDto.builder()
.taskId("logApplicationService")
.beanId("logApplicationService")
.maxExecTimes(Long.MAX_VALUE)
.cron(ScheduleTaskDto.Cron.EVERY_2_SECOND.getCron())
.params(Map.of())
.build());
}
@Override
protected boolean execute(ScheduleTaskDto scheduleTaskDto) {
try {
checkAndPushLogDocument0();
} catch (Exception e) {
log.error("checkAndPushLogDocument0 error", e);
}
return false;
}
private void checkAndPushLogDocument0() {
List<LogDocument> logDocumentList = new ArrayList<>();
Object val = redisUtil.rightPop("log:queue");
while (val != null) {
LogDocument logDocument = JSON.parseObject(val.toString(), LogDocument.class);
if (logDocument != null) {
logDocumentList.add(logDocument);
}
val = redisUtil.rightPop("log:queue");
}
if (CollectionUtils.isNotEmpty(logDocumentList)) {
log.info("checkAndPushLogDocument0 logDocumentList size: {}", logDocumentList.size());
iSearchRpcService.bulkIndex(logDocumentList.stream().map(logDocument -> (SearchDocument) logDocument).collect(Collectors.toList()));
}
}
@Override
public void deleteLogDocument(String id) {
iSearchRpcService.deleteDocument(LogDocument.class, id);
}
public void bulkIndex(List<LogDocument> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
list.forEach(item -> redisUtil.rightPush("log:queue", JSON.toJSONString(item)));
}
@Override
public void pushTraceLog(Object traceContext0) {
if (traceContext0 == null) {
return;
}
redisUtil.rightPush("log:queue", JSON.toJSONString(((TraceContext) traceContext0).getLog()));
((TraceContext) traceContext0).setLog(null);
redisUtil.rightPush("bill:queue", JSON.toJSONString(traceContext0));
}
public SearchResult search(DocumentSearchRequest documentSearchRequest) {
return iSearchRpcService.search(documentSearchRequest);
}
}

View File

@@ -0,0 +1,63 @@
package com.xspaceagi.log.app.service;
import java.util.List;
import org.springframework.stereotype.Service;
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;
import com.xspaceagi.domain.service.ILogPlatformDomainService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* 日志平台应用服务
*/
@Slf4j
@Service
public class LogPlatformApplicationService {
@Resource
private ILogPlatformDomainService logPlatformDomainService;
/**
* 搜索智能体日志
*/
public IPage<AgentLogModel> searchAgentLogs(AgentLogSearchParams searchParams,
Long current,
Long pageSize) {
// 调用领域服务
return logPlatformDomainService.searchAgentLogs(searchParams, current, pageSize);
}
/**
* 详情
*/
public AgentLogModel queryOneAgentLog(AgentLogSearchParams searchParams) {
return logPlatformDomainService.queryOneAgentLog(searchParams);
}
/**
* 新增单个智能体日志
*/
public boolean addAgentLog(AgentLogEntry logEntry) {
return logPlatformDomainService.addAgentLog(logEntry);
}
/**
* 批量新增智能体日志
*/
public boolean batchAddAgentLogs(List<AgentLogEntry> logEntries) {
return logPlatformDomainService.batchAddAgentLogs(logEntries);
}
/**
* 健康检查
*/
public boolean healthCheck() {
return logPlatformDomainService.healthCheck();
}
}

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();
}
}
}

View File

@@ -0,0 +1,29 @@
<?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>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-infra</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-domain</artifactId>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.2.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,4 @@
package com.xspaceagi.search.infra;
public interface SearchService {
}

View File

@@ -0,0 +1,282 @@
package com.xspaceagi.search.infra.elasticsearch;
import cn.hutool.core.collection.ConcurrentHashSet;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.*;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.xspaceagi.log.sdk.annotation.SearchField;
import com.xspaceagi.log.sdk.annotation.SearchIndex;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.service.ISearchRpcService;
import com.xspaceagi.log.sdk.vo.SearchDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import com.xspaceagi.search.infra.SearchService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ElasticsearchServiceImpl implements SearchService, ISearchRpcService {
private final Set<String> indexNameSet = new ConcurrentHashSet<>();
@Value("${search.elasticsearch.url}")
private String esUrl;
@Value("${search.elasticsearch.api_key:}")
private String apiKey;
@Value("${search.elasticsearch.username:}")
private String username;
@Value("${search.elasticsearch.password:}")
private String password;
private ElasticsearchClient client;
@PostConstruct
private void init() {
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
client = ElasticsearchClient.of(b -> b
.host(esUrl)
.usernameAndPassword(username, password)
);
} else {
client = ElasticsearchClient.of(b -> b
.host(esUrl)
.apiKey(apiKey)
);
}
}
@PreDestroy
private void destroy() {
client.shutdown();
}
@Override
public void bulkIndex(List<SearchDocument> list) {
Assert.noNullElements(list, "list cannot be left blank.");
String finalIndexName = createIndexIfNotExists(list.get(0).getClass());
BulkRequest.Builder br = new BulkRequest.Builder();
for (SearchDocument object : list) {
log.info("构建文档索引, id {}, finalIndexName {}", object.getId(), finalIndexName);
br.operations(op -> op.index(idx -> idx.index(finalIndexName).id(object.getId()).document(object)));
}
BulkResponse result;
try {
result = client.bulk(br.build());
} catch (IOException e) {
throw new RuntimeException(e);
}
if (result.errors()) {
throw new RuntimeException("Failed to batch build indexes.");
}
}
private String createIndexIfNotExists(Class<? extends SearchDocument> searchDocumentClazz) {
Assert.notNull(searchDocumentClazz, "searchDocumentClazz cannot be left blank.");
SearchIndex annotation = searchDocumentClazz.getAnnotation(SearchIndex.class);
String indexName = searchDocumentClazz.getSimpleName();
int shards = 3;
int replicas = 1;
if (annotation != null) {
indexName = annotation.indexName();
shards = annotation.shards();
replicas = annotation.replicas();
}
if (indexNameSet.contains(indexName)) {
return indexName;
}
final String finalIndexName = indexName;
final int finalShards = shards;
final int finalReplicas = replicas;
synchronized (indexNameSet) {
BooleanResponse exists;
try {
exists = client.indices().exists(request -> request.index(finalIndexName));
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!exists.value()) {
try {
log.info("创建索引, indexName {}", finalIndexName);
client.indices().create(request -> request.index(finalIndexName)
.settings(builder -> builder.numberOfShards(String.valueOf(finalShards)).numberOfReplicas(String.valueOf(finalReplicas)))
.mappings(builder -> {
Field[] declaredFields = searchDocumentClazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
AtomicBoolean index = new AtomicBoolean(true);
AtomicBoolean keyword = new AtomicBoolean(false);
AtomicBoolean store = new AtomicBoolean(false);
AtomicReference<String> name = new AtomicReference<>(declaredField.getName());
SearchField fieldAnnotation = declaredField.getAnnotation(SearchField.class);
if (fieldAnnotation != null) {
index.set(fieldAnnotation.index());
keyword.set(fieldAnnotation.keyword());
store.set(fieldAnnotation.store());
}
JsonProperty annotation1 = declaredField.getAnnotation(JsonProperty.class);
if (annotation1 != null) {
name.set(annotation1.value());
}
if (declaredField.getType() == String.class) {
if (keyword.get()) {
builder.properties(name.get(), property -> property.keyword(kw -> kw.index(index.get()).store(store.get())));
} else {
builder.properties(name.get(), property -> property.text(text -> text.analyzer("ik_max_word")
.searchAnalyzer("ik_smart").store(store.get()).index(index.get())
));
}
} else if (declaredField.getType() == Integer.class) {
builder.properties(name.get(), property -> property.integer(integer -> integer.index(index.get()).store(store.get())));
} else if (declaredField.getType() == Long.class) {
builder.properties(name.get(), property -> property.long_(long_ -> long_.index(index.get()).store(store.get())));
} else if (declaredField.getType() == Double.class || declaredField.getType() == BigDecimal.class) {
builder.properties(name.get(), property -> property.double_(double_ -> double_.index(index.get()).store(store.get())));
} else if (declaredField.getType() == Float.class) {
builder.properties(name.get(), property -> property.float_(float_ -> float_.index(index.get()).store(store.get())));
} else if (declaredField.getType() == Boolean.class) {
builder.properties(name.get(), property -> property.boolean_(boolean_ -> boolean_.index(index.get()).store(store.get())));
} else if (declaredField.getType() == Date.class) {
builder.properties(name.get(), property -> property.date(date -> date.index(index.get()).store(store.get())));
} else {
builder.properties(name.get(), property -> property.text(text -> text.analyzer("ik_max_word")
.searchAnalyzer("ik_smart").store(store.get()).index(index.get())
));
}
}
return builder;
})
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
indexNameSet.add(finalIndexName);
}
return finalIndexName;
}
@Override
public void deleteDocument(Class<? extends SearchDocument> searchDocumentClazz, String id) {
String indexName = createIndexIfNotExists(searchDocumentClazz);
log.info("删除文档, indexName {}, id {}", indexName, id);
DeleteRequest request = new DeleteRequest.Builder().index(indexName).id(id).build();
try {
client.delete(request);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public SearchResult search(DocumentSearchRequest documentSearchRequest) {
if (documentSearchRequest == null) {
throw new IllegalArgumentException("Search parameters cannot be empty");
}
log.info("搜索文档, documentSearchRequest {}", documentSearchRequest);
Integer from = documentSearchRequest.getFrom();
Integer size = documentSearchRequest.getSize();
if (from == null || from < 0) {
from = 0;
}
if (size == null || size <= 0) {
size = 10;
}
String indexName = createIndexIfNotExists(documentSearchRequest.getSearchDocumentClazz());
SearchRequest.Builder requestBuilder = new SearchRequest.Builder().index(indexName).query(q -> {
q.bool(bool -> {
if (CollectionUtils.isNotEmpty(documentSearchRequest.getFilterFieldsAndValues())) {
for (Map<String, Object> filterFieldAndValue : documentSearchRequest.getFilterFieldsAndValues()) {
bool.filter(f -> {
for (Map.Entry<String, Object> entry : filterFieldAndValue.entrySet()) {
if (entry.getValue() instanceof List<?>) {
f.terms(TermsQuery.of(t -> t.field(entry.getKey()).terms(
TermsQueryField.of(t1 -> t1.value(((List<?>) entry.getValue()).stream().map(m -> FieldValue.of(m)).collect(Collectors.toList())))
)));
} else if (entry.getValue() instanceof Map<?, ?> && ((Map<?, ?>) entry.getValue()).containsKey("express")) {
Object express = ((Map<?, ?>) entry.getValue()).get("express");
if (express != null) {
if (express.equals("range")) {
f.range(RangeQuery.of(r -> r.term(TermRangeQuery.of(t -> {
t.field(entry.getKey());
Object gt = ((Map<?, ?>) entry.getValue()).get("gt");
if (gt != null) {
t.gt(gt.toString());
}
Object lt = ((Map<?, ?>) entry.getValue()).get("lt");
if (lt != null) {
t.lt(lt.toString());
}
return t;
}))));
}
if (express.equals("match")) {
f.match(MatchQuery.of(m -> m.field(entry.getKey()).query(((Map<?, ?>) entry.getValue()).get("query").toString())));
}
}
} else {
f.term(TermQuery.of(t -> t.field(entry.getKey()).value(FieldValue.of(entry.getValue()))));
}
}
return f;
});
}
}
if (CollectionUtils.isNotEmpty(documentSearchRequest.getSearchFields()) && StringUtils.isNotBlank(documentSearchRequest.getKeyword())) {
bool.must(m1 -> m1.multiMatch(m -> m.query(documentSearchRequest.getKeyword()).fields(documentSearchRequest.getSearchFields())));
}
return bool;
});
return q;
})
.from(from)
.size(size);
if (documentSearchRequest.getSortFieldsAndValues() != null) {
requestBuilder.sort(s -> {
for (Map.Entry<String, Object> entry : documentSearchRequest.getSortFieldsAndValues().entrySet()) {
s.field(f -> f.field(entry.getKey()).order(SortOrder.valueOf(entry.getValue().toString())));
}
return s;
});
}
try {
SearchResponse<?> response = client.search(requestBuilder.build(), documentSearchRequest.getSearchDocumentClazz());
List<SearchResult.SearchResultItem> collect = response.hits().hits().stream().map(hit -> SearchResult.SearchResultItem.builder()
.score(hit.score())
.document((SearchDocument) hit.source())
.build()).collect(Collectors.toList());
log.info("搜索结果, total {}, items {}", response.hits().total().value(), collect);
return SearchResult.builder()
.total(response.hits().total().value())
.items(collect)
.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,106 @@
<?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-job</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-application</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<!-- 打包配置必须在对应目录下创建repack.xml的打包配置文件 -->
<descriptor>src/main/resources/assemblies/repack.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<!-- install插件定制在mvn install过程使用哪个jar包安装到本地 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
<executions>
<execution>
<id>install-file</id>
<goals>
<goal>install-file</goal>
</goals>
<phase>install</phase>
</execution>
</executions>
<configuration>
<!-- install配置指定将哪个目录下的jar包install为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!--
install配置指定将install的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
<!-- deploy插件定制在mvn deploy过程使用哪个jar包发布到仓库 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<executions>
<execution>
<id>deploy-file</id>
<goals>
<goal>deploy-file</goal>
</goals>
<!-- 跳过默认的发布包 -->
<phase>none</phase>
</execution>
</executions>
<configuration>
<!-- deploy配置指定将哪个目录下的jar包发布为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!-- 发布地址 -->
<url>http://local-maven.space.com/nexus/content/repositories/snapshots/</url>
<repositoryId>nexus-snapshots</repositoryId>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<packaging>jar</packaging>
<!--
deploy配置指定将deploy的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:app-platform-log-job</include>
<include>com.xspaceagi:app-platform-log-application</include>
<include>com.xspaceagi:app-platform-log-domain</include>
<include>com.xspaceagi:app-platform-log-infra</include>
<include>com.xspaceagi:app-platform-log-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,21 @@
<?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-job</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-log-job</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,20 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>app-platform-log-sdk</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.log.sdk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SearchField {
/**
* 是否索引字段
*
* @return
*/
boolean index() default true;
/**
* 是否为keyword字段
*
* @return
*/
boolean keyword() default false;
/**
* 是否存储字段
*
* @return
*/
boolean store() default false;
}

View File

@@ -0,0 +1,27 @@
package com.xspaceagi.log.sdk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SearchIndex {
/**
* 索引名称,默认类名
*/
String indexName() default "";
/**
* 分片数量
*/
int shards() default 3;
/**
* 副本数量
*/
int replicas() default 1;
}

View File

@@ -0,0 +1,152 @@
package com.xspaceagi.log.sdk.reponse;
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 AgentLogModelResponse {
/**
* 请求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,29 @@
package com.xspaceagi.log.sdk.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 日志搜索请求参数
* 对应 Rust 的 AgentLogSearchParams 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentLogDetailParamsRequest {
/**
* 请求ID
*/
private String requestId;
/**
* 智能体ID
*/
private String agentId;
}

View File

@@ -0,0 +1,120 @@
package com.xspaceagi.log.sdk.request;
import java.time.LocalDateTime;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 智能体调试大模型的日志结构定义
* 对应 Rust 的 AgentLogEntry 结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentLogEntryRequest {
/**
* 请求ID唯一标识一次请求必填
*/
@Schema(description = "请求ID唯一标识一次请求必填", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "requestId is required")
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;
/**
* 请求开始时间
*/
private LocalDateTime requestStartTime;
/**
* 请求结束时间
*/
private LocalDateTime requestEndTime;
/**
* 耗时(毫秒)
*/
private Long elapsedTimeMs;
/**
* 节点类型
*/
private String nodeType;
/**
* 节点状态
*/
private String status;
/**
* 节点名称
*/
private String nodeName;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名称
*/
private String userName;
}

View File

@@ -0,0 +1,76 @@
package com.xspaceagi.log.sdk.request;
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
public class AgentLogSearchParamsRequest {
/**
* 请求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;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 租户ID用于租户隔离确保只查询特定租户的日志
*/
private String tenantId;
/**
* 空间ID可选用于查询特定空间的日志支持多个IDOR关系
*/
private List<String> spaceId;
}

View File

@@ -0,0 +1,41 @@
package com.xspaceagi.log.sdk.request;
import com.xspaceagi.log.sdk.vo.SearchDocument;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DocumentSearchRequest implements Serializable {
@Schema(description = "搜索文档类", requiredMode = Schema.RequiredMode.REQUIRED)
private Class<? extends SearchDocument> searchDocumentClazz;
@Schema(description = "搜索字段", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private List<String> searchFields;
@Schema(description = "搜索关键字", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String keyword;
@Schema(description = "过滤字段,全匹配", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private List<Map<String, Object>> filterFieldsAndValues;
//排序
@Schema(description = "排序字段", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private Map<String, Object> sortFieldsAndValues;
@Schema(description = "分页起始位置", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private Integer from;
@Schema(description = "分页大小", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private Integer size;
}

View File

@@ -0,0 +1,18 @@
package com.xspaceagi.log.sdk.service;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import java.util.List;
public interface ILogRpcService {
void bulkIndex(List<LogDocument> list);
void pushTraceLog(Object traceContext);
void deleteLogDocument(String id);
SearchResult search(DocumentSearchRequest documentSearchRequest);
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.log.sdk.service;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.vo.SearchDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import java.util.List;
public interface ISearchRpcService {
void bulkIndex(List<SearchDocument> list);
void deleteDocument(Class<? extends SearchDocument> searchDocumentClazz, String id);
SearchResult search(DocumentSearchRequest documentSearchRequest);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.log.sdk.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.log.sdk.reponse.AgentLogModelResponse;
import com.xspaceagi.log.sdk.request.AgentLogEntryRequest;
import com.xspaceagi.log.sdk.request.AgentLogSearchParamsRequest;
/**
* 日志平台的 RPC 服务,用于新增日志
*/
public interface LogPlatformRpcService {
/**
* 新增日志
*
* @param logEntryRequest
* @return
*/
boolean addLog(AgentLogEntryRequest logEntryRequest);
/**
* 搜索日志
*
* @param searchParams 搜索参数
* @param current 页码
* @param pageSize 页大小
* @return
*/
IPage<AgentLogModelResponse> searchAgentLogs(AgentLogSearchParamsRequest searchParams,
Long current,
Long pageSize);
}

View File

@@ -0,0 +1,98 @@
package com.xspaceagi.log.sdk.vo;
import com.xspaceagi.log.sdk.annotation.SearchField;
import com.xspaceagi.log.sdk.annotation.SearchIndex;
import com.xspaceagi.log.sdk.vo.SearchDocument;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@SearchIndex(indexName = "req_log")
public class LogDocument extends SearchDocument {
@Schema(description = "租户ID", hidden = true)
private Long tenantId;
@Schema(description = "日志ID, 不用展示,用于查询详情")
private String id;
@Schema(description = "请求唯一标识可以用于关联一次请求中所有相关的操作")
@SearchField(keyword = true)
private String requestId;
@Schema(description = "日志产生对象所在的空间ID")
private Long spaceId;
@Schema(description = "请求发起的用户ID")
private Long userId;
@Schema(description = "用户名")
@SearchField(keyword = true)
private String userName;
@Schema(description = "日志对象类型")
@SearchField(keyword = true)
private String targetType;
@Schema(description = "日志对象名称")
@SearchField(keyword = true)
private String targetName;
@Schema(description = "日志对象ID")
@SearchField(keyword = true)
private String targetId;
@Schema(description = "会话ID")
@SearchField(keyword = true)
private String conversationId;
@Schema(description = "输入参数")
@SearchField(store = true)
private String input;
@Schema(description = "执行结果")
@SearchField(store = true)
private String output;
@Schema(description = "执行过程数据")
@SearchField(store = true)
private String processData;
@Schema(description = "缓存输入token数量")
private Integer cacheInputToken;
@Schema(description = "输入token数量")
private Integer inputToken;
@Schema(description = "输出token数量")
private Integer outputToken;
@Schema(description = "请求开始时间")
private Long requestStartTime;
@Schema(description = "请求结束时间")
private Long requestEndTime;
@Schema(description = "执行结果码 0000为成功")
@SearchField(keyword = true)
private String resultCode;
@Schema(description = "执行结果描述")
@SearchField(store = true)
private String resultMsg;
@Schema(description = "日志产生时间")
private Long createTime;
@Schema(description = "日志产生来源", hidden = true)
@SearchField(keyword = true)
private String from;
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.log.sdk.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class SearchDocument implements Serializable {
private String id;
}

View File

@@ -0,0 +1,28 @@
package com.xspaceagi.log.sdk.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SearchResult implements Serializable {
private Long total;
private List<SearchResultItem> items;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class SearchResultItem {
private Double score;
private SearchDocument document;
}
}

View File

@@ -0,0 +1,23 @@
<?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-spec</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.log.spec;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/**
* 自定义 LocalDateTime 的序列化器,按照UTC时间格式输出
*/
public class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
private static final String UTC_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private static final ZoneId BEIJING_ZONE_ID = ZoneId.of("Asia/Shanghai");
private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC");
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(UTC_DATE_TIME_PATTERN);
public CustomLocalDateTimeSerializer() {
super(LocalDateTime.class);
}
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value != null) {
// LocalDateTime 是东八区的本地时间,将其转换为 UTC 时间
ZonedDateTime beijingDateTime = value.atZone(BEIJING_ZONE_ID);
ZonedDateTime utcDateTime = beijingDateTime.withZoneSameInstant(UTC_ZONE_ID);
// 使用指定的格式输出 UTC 时间
gen.writeString(utcDateTime.format(formatter));
} else {
gen.writeNull();
}
}
}

View File

@@ -0,0 +1,100 @@
package com.xspaceagi.log.spec;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;
/**
* 反序列化 {@link LocalDateTime}:支持 UTC/带 Z 的字符串、ISO 日期时间、仅日期 {@code yyyy-MM-dd}、以及 Unix 时间戳。
*/
@Slf4j
public class FlexibleDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER_WITH_NANOS =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'").withZone(ZoneId.of("UTC"));
private static final DateTimeFormatter FORMATTER_WITH_MILLIS =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneId.of("UTC"));
private static final DateTimeFormatter FORMATTER_WITHOUT_FRACTION =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("UTC"));
private static final DateTimeFormatter SPACE_SEPARATED =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT);
private static final ZoneId BEIJING_ZONE = ZoneId.of("Asia/Shanghai");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonToken token = p.currentToken();
if (token == JsonToken.VALUE_NULL) {
return null;
}
if (token == JsonToken.VALUE_NUMBER_INT || token == JsonToken.VALUE_NUMBER_FLOAT) {
return fromEpoch(p.getLongValue());
}
if (token == JsonToken.VALUE_STRING) {
String dateString = p.getText().trim();
if (StringUtils.isBlank(dateString)) {
return null;
}
return parseString(dateString, p);
}
throw new JsonParseException(p, "无法解析 LocalDateTime需要字符串或数字实际为 " + token);
}
private static LocalDateTime fromEpoch(long raw) {
Instant instant = raw > 10_000_000_000L ? Instant.ofEpochMilli(raw) : Instant.ofEpochSecond(raw);
return LocalDateTime.ofInstant(instant, BEIJING_ZONE);
}
private LocalDateTime parseString(String dateString, JsonParser p) throws IOException {
try {
if (dateString.matches(".*\\.\\d{6}Z$")) {
Instant instant = Instant.from(FORMATTER_WITH_NANOS.parse(dateString));
return LocalDateTime.ofInstant(instant, BEIJING_ZONE);
}
if (dateString.matches(".*\\.\\d{3}Z$")) {
Instant instant = Instant.from(FORMATTER_WITH_MILLIS.parse(dateString));
return LocalDateTime.ofInstant(instant, BEIJING_ZONE);
}
if (dateString.endsWith("Z") && dateString.contains("T")) {
Instant instant = Instant.from(FORMATTER_WITHOUT_FRACTION.parse(dateString));
return LocalDateTime.ofInstant(instant, BEIJING_ZONE);
}
} catch (DateTimeParseException ignored) {
// 走下方通用解析
}
try {
return LocalDateTime.ofInstant(OffsetDateTime.parse(dateString).toInstant(), BEIJING_ZONE);
} catch (DateTimeParseException ignored) {
}
try {
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} catch (DateTimeParseException ignored) {
}
try {
return LocalDateTime.parse(dateString, SPACE_SEPARATED);
} catch (DateTimeParseException ignored) {
}
try {
// 纯日期 yyyy-MM-dd → 当天 00:00:00东八区墙钟
return LocalDate.parse(dateString, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(BEIJING_ZONE).toLocalDateTime();
} catch (DateTimeParseException ignored) {
}
log.error("无法解析日期时间: {}", dateString);
throw new JsonParseException(p, "无法解析日期时间: " + dateString);
}
}

View File

@@ -0,0 +1,141 @@
<?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-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-application</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-application</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<!-- 打包配置必须在对应目录下创建repack.xml的打包配置文件 -->
<descriptor>src/main/resources/assemblies/repack.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<!-- install插件定制在mvn install过程使用哪个jar包安装到本地 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
<executions>
<execution>
<id>install-file</id>
<goals>
<goal>install-file</goal>
</goals>
<phase>install</phase>
</execution>
</executions>
<configuration>
<!-- install配置指定将哪个目录下的jar包install为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!--
install配置指定将install的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
<!-- deploy插件定制在mvn deploy过程使用哪个jar包发布到仓库 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
<executions>
<execution>
<id>deploy-file</id>
<goals>
<goal>deploy-file</goal>
</goals>
<!-- 跳过默认的发布包 -->
<phase>none</phase>
</execution>
</executions>
<configuration>
<!-- deploy配置指定将哪个目录下的jar包发布为这个工程的jar -->
<file>${project.build.directory}/${project.build.finalName}-repack.jar</file>
<!-- 发布地址 -->
<url>http://local-maven.space.com/nexus/content/repositories/snapshots/</url>
<repositoryId>nexus-snapshots</repositoryId>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<packaging>jar</packaging>
<!--
deploy配置指定将deploy的jar打包使用的pom.xml文件
即其他依赖这个jar包的工程按照哪个pom.xml文件来解析依赖
-->
<pomFile>src/main/resources/install-pom.xml</pomFile>
</configuration>
</plugin>
<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.3.6</version>
<configuration>
<!--指定生成文档的使用的配置文件,配置文件放在自己的项目中-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>${project.artifactId}</projectName>
<!--smart-doc实现自动分析依赖树加载第三方依赖的源码如果一些框架依赖库加载不到导致报错这时请使用excludes排除掉-->
<excludes>
<!--格式为groupId:artifactId;参考如下-->
<!--也可以支持正则式如com.alibaba:.* -->
<exclude>com.alibaba:fastjson</exclude>
</excludes>
<!--includes配置用于配置加载外部依赖源码,配置后插件会按照配置项加载外部源代码而不是自动加载所有,因此使用时需要注意-->
<!--smart-doc能自动分析依赖树加载所有依赖源码原则上会影响文档构建效率因此你可以使用includes来让插件加载你配置的组件-->
<includes></includes>
</configuration>
<executions>
<execution>
<goals>
<!--smart-doc提供了html、openapi、markdown等goal可按需配置-->
<goal>rpc-html</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,99 @@
package com.xspaceagi.log.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.service.ISearchRpcService;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import com.xspaceagi.log.web.controller.base.BaseController;
import com.xspaceagi.log.web.controller.dto.LogDetailQueryDto;
import com.xspaceagi.log.web.controller.dto.LogQueryDto;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.PageQueryVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.xspaceagi.system.spec.enums.ResourceEnum.SPACE_LOG_QUERY_DETAIL;
import static com.xspaceagi.system.spec.enums.ResourceEnum.SPACE_LOG_QUERY_LIST;
@Tag(name = "日志平台-搜索接口")
@RestController
@RequestMapping("/api/requestLogs")
@Slf4j
public class LogQueryController extends BaseController {
@Resource
private ISearchRpcService iSearchRpcService;
@Resource
private SpacePermissionService spacePermissionService;
@RequireResource(SPACE_LOG_QUERY_LIST)
@Operation(summary = "统一日志查询(工作空间)")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<LogDocument>> search(@RequestBody PageQueryVo<LogQueryDto> pageQueryVo) {
var filter = pageQueryVo.getQueryFilter();
if (filter == null) {
return ReqResult.error("查询条件不能为空");
}
if (filter.getSpaceId() == null) {
return ReqResult.error("空间ID不能为空");
}
spacePermissionService.checkSpaceUserPermission(filter.getSpaceId());
return search0(pageQueryVo);
}
@Operation(summary = "统一日志查询API调用")
@RequestMapping(path = "/apikey/list", method = RequestMethod.POST)
public ReqResult<IPage<LogDocument>> apiInvokeSearch(@RequestBody PageQueryVo<LogQueryDto> pageQueryVo) {
var filter = pageQueryVo.getQueryFilter();
if (filter == null) {
pageQueryVo.setQueryFilter(new LogQueryDto());
filter = pageQueryVo.getQueryFilter();
}
filter.setTargetType("ApiKey");
filter.setUserId(RequestContext.get().getUserId());
return search0(pageQueryVo);
}
@RequireResource(SPACE_LOG_QUERY_DETAIL)
@Operation(summary = "日志详情")
@RequestMapping(path = "/detail", method = RequestMethod.POST)
public ReqResult<LogDocument> detail(@RequestBody LogDetailQueryDto logDetailQueryDto) {
DocumentSearchRequest.DocumentSearchRequestBuilder builder = DocumentSearchRequest.builder();
builder.searchDocumentClazz(LogDocument.class);
List<Map<String, Object>> filterFieldsAndValues = new ArrayList<>();
builder.filterFieldsAndValues(filterFieldsAndValues);
if (StringUtils.isNoneBlank(logDetailQueryDto.getId())) {
filterFieldsAndValues.add(Map.of("id", logDetailQueryDto.getId()));
}
SearchResult search = iSearchRpcService.search(builder.build());
LogDocument logDocument = search.getItems().stream().map(item -> (LogDocument) item.getDocument()).findFirst().orElse(null);
if (logDocument == null) {
return ReqResult.error("日志不存在");
}
if (!RequestContext.get().getUserId().equals(logDocument.getUserId())) {
spacePermissionService.checkSpaceUserPermission(logDocument.getSpaceId());
}
return ReqResult.success(logDocument);
}
}

View File

@@ -0,0 +1,73 @@
package com.xspaceagi.log.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.service.ISearchRpcService;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.log.sdk.vo.SearchResult;
import com.xspaceagi.log.web.controller.base.BaseController;
import com.xspaceagi.log.web.controller.dto.LogDetailQueryDto;
import com.xspaceagi.log.web.controller.dto.LogQueryDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.exception.LogPlatformException;
import com.xspaceagi.system.spec.page.PageQueryVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.SYSTEM_RUNNING_LOG_QUERY_DETAIL;
import static com.xspaceagi.system.spec.enums.ResourceEnum.SYSTEM_RUNNING_LOG_QUERY_LIST;
@Tag(name = "日志平台管理端-搜索接口")
@RestController
@RequestMapping("/api/system/requestLogs")
@Slf4j
public class LogQueryManController extends BaseController {
@Resource
private ISearchRpcService iSearchRpcService;
@RequireResource({SYSTEM_RUNNING_LOG_QUERY_LIST})
@Operation(summary = "统一日志查询")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<LogDocument>> search(@RequestBody PageQueryVo<LogQueryDto> pageQueryVo) {
return search0(pageQueryVo);
}
@RequireResource({SYSTEM_RUNNING_LOG_QUERY_DETAIL})
@Operation(summary = "日志详情")
@RequestMapping(path = "/detail", method = RequestMethod.POST)
public ReqResult<LogDocument> detail(@RequestBody LogDetailQueryDto logDetailQueryDto) {
DocumentSearchRequest.DocumentSearchRequestBuilder builder = DocumentSearchRequest.builder();
builder.searchDocumentClazz(LogDocument.class);
List<Map<String, Object>> filterFieldsAndValues = new ArrayList<>();
builder.filterFieldsAndValues(filterFieldsAndValues);
if (StringUtils.isNoneBlank(logDetailQueryDto.getId())) {
filterFieldsAndValues.add(Map.of("id", logDetailQueryDto.getId()));
}
SearchResult search = iSearchRpcService.search(builder.build());
LogDocument logDocument = search.getItems().stream().map(item -> (LogDocument) item.getDocument()).findFirst().orElse(null);
if (logDocument == null) {
return ReqResult.error("日志不存在");
}
return ReqResult.success(logDocument);
}
}

View File

@@ -0,0 +1,116 @@
package com.xspaceagi.log.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.domain.model.AgentLogModel;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.log.app.service.LogPlatformApplicationService;
import com.xspaceagi.log.sdk.request.AgentLogDetailParamsRequest;
import com.xspaceagi.log.web.controller.base.BaseController;
import com.xspaceagi.log.web.controller.dto.AgentLogSearchParamsRequest;
import com.xspaceagi.system.sdk.operate.ActionType;
import com.xspaceagi.system.sdk.operate.OperationLogReporter;
import com.xspaceagi.system.sdk.operate.SystemEnum;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.exception.LogPlatformException;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "日志平台-搜索接口")
@RestController
@RequestMapping("/api/logPlatform/agent")
@Slf4j
public class LogSearchController extends BaseController {
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private LogPlatformApplicationService logPlatformApplicationService;
@OperationLogReporter(actionType = ActionType.QUERY, action = "数据列表查询", objectName = "智能体日志", systemCode = SystemEnum.LOG_PLATFORM)
@Operation(summary = "日志查询")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<AgentLogModel>> search(
@RequestBody PageQueryVo<AgentLogSearchParamsRequest> pageQueryVo) {
var userContext = this.getUser();
var userId = userContext.getUserId();
var filter = pageQueryVo.getQueryFilter();
if (filter == null) {
return ReqResult.error("查询条件不能为空");
}
// 查询用户有权限的空间,限制访问空间
var spaceList = this.spaceApplicationService.queryListByUserId(userId);
var authSpaceIds = spaceList.stream().map(space -> String.valueOf(space.getId())).toList();
var tenantId = userContext.getTenantId();
// 转换前端请求为领域模型
AgentLogSearchParams searchParams = AgentLogSearchParamsRequest.convertFrom(filter);
// 设置租户ID从用户上下文获取
searchParams.setTenantId(tenantId.toString());
searchParams.setSpaceId(authSpaceIds);
try {
// 使用 logPlatformApplicationService 接口查询日志
IPage<AgentLogModel> result = logPlatformApplicationService.searchAgentLogs(
searchParams,
pageQueryVo.getCurrent(),
pageQueryVo.getPageSize());
return ReqResult.success(result);
} catch (LogPlatformException e) {
log.error("Log search failed: {}", e.getMessage(), e);
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("Unknown error during log search", e);
return ReqResult.error("日志搜索发生未知错误: " + e.getMessage());
}
}
@OperationLogReporter(actionType = ActionType.QUERY, action = "数据列表查询", objectName = "智能体日志", systemCode = SystemEnum.LOG_PLATFORM)
@Operation(summary = "日志详情")
@RequestMapping(path = "/detail", method = RequestMethod.POST)
public ReqResult<AgentLogModel> detail(
@RequestBody AgentLogDetailParamsRequest request) {
var userContext = this.getUser();
var userId = userContext.getUserId();
// 查询用户有权限的空间,限制访问空间
var spaceList = this.spaceApplicationService.queryListByUserId(userId);
var authSpaceIds = spaceList.stream().map(space -> String.valueOf(space.getId())).toList();
var tenantId = userContext.getTenantId();
// 转换前端请求为领域模型
AgentLogSearchParams searchParams = AgentLogSearchParamsRequest.convertFrom(request);
// 设置租户ID从用户上下文获取
searchParams.setTenantId(tenantId.toString());
searchParams.setSpaceId(authSpaceIds);
try {
// 使用 logPlatformApplicationService 接口查询日志
AgentLogModel result = logPlatformApplicationService.queryOneAgentLog(searchParams);
return ReqResult.success(result);
} catch (LogPlatformException e) {
log.error("日志详情失败: {}", e.getMessage(), e);
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("Unknown error while loading log detail", e);
return ReqResult.error("日志搜索发生未知错误: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,149 @@
package com.xspaceagi.log.web.controller.base;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.log.sdk.request.DocumentSearchRequest;
import com.xspaceagi.log.sdk.service.ISearchRpcService;
import com.xspaceagi.log.sdk.vo.LogDocument;
import com.xspaceagi.log.web.controller.dto.LogQueryDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.exception.LogPlatformException;
import com.xspaceagi.system.spec.page.PageQueryVo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public abstract class BaseController {
@Resource
private ISearchRpcService iSearchRpcService;
/**
* 获取当前登录用户信息
*
* @return
*/
public UserContext getUser() {
var userDto = (UserDto) RequestContext.get().getUser();
return UserContext.builder()
.userId(userDto.getId())
.userName(userDto.getUserName())
.nickName(userDto.getNickName())
.email(userDto.getEmail())
.phone(userDto.getPhone())
.status(userDto.getStatus() == User.Status.Enabled ? 1 : -1)
.tenantId(userDto.getTenantId())
.tenantName(null)
.orgId(null)
.orgName(null)
.roleType(null)
.build();
}
protected ReqResult<IPage<LogDocument>> search0(@RequestBody PageQueryVo<LogQueryDto> pageQueryVo) {
var filter = pageQueryVo.getQueryFilter();
if (filter == null) {
filter = new LogQueryDto();
}
if (pageQueryVo.getPageSize() == null || pageQueryVo.getPageSize() <= 0) {
pageQueryVo.setPageSize(10L);
}
if (pageQueryVo.getCurrent() == null || pageQueryVo.getCurrent() <= 0) {
pageQueryVo.setCurrent(1L);
}
// 避免到最后一页报错
if (pageQueryVo.getCurrent() * pageQueryVo.getPageSize() > 10000) {
pageQueryVo.setCurrent(pageQueryVo.getCurrent() - 1);
}
DocumentSearchRequest.DocumentSearchRequestBuilder builder = DocumentSearchRequest.builder();
builder.searchDocumentClazz(LogDocument.class);
builder.from((int) (pageQueryVo.getPageSize() * (pageQueryVo.getCurrent() - 1)));
builder.size(pageQueryVo.getPageSize().intValue());
List<Map<String, Object>> filterFieldsAndValues = new ArrayList<>();
builder.filterFieldsAndValues(filterFieldsAndValues);
filterFieldsAndValues.add(Map.of("tenantId", RequestContext.get().getTenantId()));
if (StringUtils.isNoneBlank(filter.getInput())) {
filterFieldsAndValues.add(Map.of("input", Map.of("express", "match", "query", filter.getInput())));
}
if (StringUtils.isNoneBlank(filter.getOutput())) {
filterFieldsAndValues.add(Map.of("output", Map.of("express", "match", "query", filter.getOutput())));
}
if (StringUtils.isNoneBlank(filter.getProcessData())) {
filterFieldsAndValues.add(Map.of("processData", filter.getProcessData()));
}
if (StringUtils.isNoneBlank(filter.getRequestId())) {
filterFieldsAndValues.add(Map.of("requestId", filter.getRequestId()));
}
if (StringUtils.isNoneBlank(filter.getResultCode())) {
filterFieldsAndValues.add(Map.of("resultCode", filter.getResultCode()));
}
if (StringUtils.isNoneBlank(filter.getConversationId())) {
filterFieldsAndValues.add(Map.of("conversationId", filter.getConversationId()));
}
if (StringUtils.isNoneBlank(filter.getTargetId())) {
filterFieldsAndValues.add(Map.of("targetId", filter.getTargetId()));
}
if (StringUtils.isNoneBlank(filter.getTargetName())) {
filterFieldsAndValues.add(Map.of("targetName", filter.getTargetName()));
}
if (StringUtils.isNoneBlank(filter.getTargetType())) {
filterFieldsAndValues.add(Map.of("targetType", filter.getTargetType()));
}
if (StringUtils.isNoneBlank(filter.getUserName())) {
filterFieldsAndValues.add(Map.of("userName", filter.getUserName()));
}
if (StringUtils.isNoneBlank(filter.getFrom())) {
filterFieldsAndValues.add(Map.of("from", filter.getFrom()));
}
if (filter.getUserId() != null) {
filterFieldsAndValues.add(Map.of("userId", filter.getUserId()));
}
if (filter.getSpaceId() != null) {
filterFieldsAndValues.add(Map.of("spaceId", filter.getSpaceId()));
}
Map<String, Object> createTimeRange = new HashMap<>();
if (filter.getCreateTimeGt() != null) {
createTimeRange.put("gt", filter.getCreateTimeGt());
}
if (filter.getCreateTimeLt() != null) {
createTimeRange.put("lt", filter.getCreateTimeLt());
}
if (!createTimeRange.isEmpty()) {
createTimeRange.put("express", "range");
filterFieldsAndValues.add(Map.of("createTime", createTimeRange));
}
builder.sortFieldsAndValues(Map.of("createTime", "Desc"));
var result = iSearchRpcService.search(builder.build());
try {
IPage<LogDocument> page = new Page<>(pageQueryVo.getCurrent().intValue(), pageQueryVo.getPageSize().intValue());
page.setTotal(result.getTotal());
page.setRecords(result.getItems().stream().map(item -> {
LogDocument document = (LogDocument) item.getDocument();
document.setProcessData(null);
return document;
}).collect(Collectors.toList()));
return ReqResult.success(page);
} catch (LogPlatformException e) {
log.error("Log search failed: {}", e.getMessage(), e);
return ReqResult.error(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("Unknown error during log search", e);
return ReqResult.error("日志搜索发生未知错误: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,173 @@
package com.xspaceagi.log.web.controller.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.xspaceagi.domain.model.valueobj.AgentLogSearchParams;
import com.xspaceagi.log.sdk.request.AgentLogDetailParamsRequest;
import com.xspaceagi.log.spec.FlexibleDateTimeDeserializer;
import io.swagger.v3.oas.annotations.media.Schema;
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
public class AgentLogSearchParamsRequest {
/**
* 请求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;
/**
* 用户输入,需要支持全文检索支持多个关键字AND关系
*/
@Schema(description = "用户输入,需要支持全文检索支持多个关键字AND关系")
private List<String> userInput;
/**
* 系统输出,需要支持全文检索支持多个关键字AND关系
*/
@Schema(description = "系统输出,需要支持全文检索支持多个关键字AND关系")
private List<String> output;
/**
* 开始时间
*/
@Schema(description = "开始时间支持RFC 3339格式例如2024-05-15T10:30:00Z")
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", shape = JsonFormat.Shape.STRING)
private LocalDateTime startTime;
/**
* 结束时间
*/
@Schema(description = "结束时间支持RFC 3339格式例如2024-05-15T10:30:00Z")
@JsonDeserialize(using = FlexibleDateTimeDeserializer.class)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", shape = JsonFormat.Shape.STRING)
private LocalDateTime endTime;
/**
* 租户ID用于租户隔离确保只查询特定租户的日志
*/
@Schema(description = "租户ID用于租户隔离确保只查询特定租户的日志")
private String tenantId;
/**
* 空间ID可选用于查询特定空间的日志支持多个IDOR关系
*/
@Schema(description = "空间ID可选用于查询特定空间的日志支持多个IDOR关系")
private List<String> spaceId;
public static AgentLogSearchParams convertFrom(AgentLogSearchParamsRequest request) {
if (request == null) {
return null;
}
// request 处理下面的String字段的2边的空白,使用trim(),如果不为空的话
if (request.getRequestId() != null) {
request.setRequestId(request.getRequestId().trim());
}
if (request.getMessageId() != null) {
request.setMessageId(request.getMessageId().trim());
}
if (request.getAgentId() != null) {
request.setAgentId(request.getAgentId().trim());
}
if (request.getConversationId() != null) {
request.setConversationId(request.getConversationId().trim());
}
if (request.getUserUid() != null) {
request.setUserUid(request.getUserUid().trim());
}
if (request.getTenantId() != null) {
request.setTenantId(request.getTenantId().trim());
}
if (request.getSpaceId() != null) {
var spaceId = request.getSpaceId().stream().map(item -> {
if (item != null) {
item = item.trim();
}
return item;
}).toList();
request.setSpaceId(spaceId);
}
if (request.getUserInput() != null) {
var userInput = request.getUserInput().stream().map(item -> {
if (item != null) {
item = item.trim();
}
return item;
}).toList();
request.setUserInput(userInput);
}
if (request.getOutput() != null) {
var output = request.getOutput().stream().map(item -> {
if (item != null) {
item = item.trim();
}
return item;
}).toList();
request.setOutput(output);
}
return AgentLogSearchParams.builder()
.requestId(request.getRequestId())
.messageId(request.getMessageId())
.agentId(request.getAgentId())
.conversationId(request.getConversationId())
.userUid(request.getUserUid())
.tenantId(request.getTenantId())
.spaceId(request.getSpaceId())
.userInput(request.getUserInput())
.output(request.getOutput())
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.build();
}
public static AgentLogSearchParams convertFrom(AgentLogDetailParamsRequest request) {
return AgentLogSearchParams.builder()
.requestId(request.getRequestId())
.messageId(null)
.agentId(request.getAgentId())
.conversationId(null)
.userUid(null)
.tenantId(null)
.spaceId(null)
.build();
}
}

View File

@@ -0,0 +1,8 @@
package com.xspaceagi.log.web.controller.dto;
import lombok.Data;
@Data
public class LogDetailQueryDto {
private String id;
}

View File

@@ -0,0 +1,63 @@
package com.xspaceagi.log.web.controller.dto;
import com.xspaceagi.log.sdk.annotation.SearchField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class LogQueryDto {
@Schema(description = "请求唯一标识可以用于关联一次请求中所有相关的操作")
@SearchField(keyword = true)
private String requestId;
@Schema(description = "日志产生对象所在的空间ID")
private Long spaceId;
@Schema(description = "请求发起的用户ID")
private Long userId;
@Schema(description = "用户名")
@SearchField(keyword = true)
private String userName;
@Schema(description = "日志对象类型Agent、Plugin、Workflow、Mcp")
@SearchField(keyword = true)
private String targetType;
@Schema(description = "日志对象名称")
@SearchField(keyword = true)
private String targetName;
@Schema(description = "日志对象ID")
@SearchField(keyword = true)
private String targetId;
@Schema(description = "会话ID")
@SearchField(keyword = true)
private String conversationId;
@Schema(description = "输入参数")
@SearchField(store = true)
private String input;
@Schema(description = "执行结果")
@SearchField(store = true)
private String output;
@Schema(description = "执行过程数据")
@SearchField(store = true)
private String processData;
@Schema(description = "执行结果码 0000为成功")
@SearchField(keyword = true)
private String resultCode;
@Schema(description = "日志产生时间(大于)")
private Long createTimeGt;
@Schema(description = "日志产生时间(小于)")
private Long createTimeLt;
@Schema(description = "日志产生来源task_center 任务中心(从任务中心点击查看详情时带上该参数)")
private String from;
}

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:app-platform-log-web</include>
<include>com.xspaceagi:app-platform-log-application</include>
<include>com.xspaceagi:app-platform-log-domain</include>
<include>com.xspaceagi:app-platform-log-infra</include>
<include>com.xspaceagi:app-platform-log-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,21 @@
<?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-web</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-log-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,91 @@
<?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>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-modules</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-log</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties/>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-application</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-infra</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-domain</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-spec</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-web</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-log-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>app-platform-log-application</module>
<module>app-platform-log-domain</module>
<module>app-platform-log-infra</module>
<module>app-platform-log-spec</module>
<module>app-platform-log-api</module>
<module>app-platform-log-web</module>
<module>app-platform-log-job</module>
<module>app-platform-log-sdk</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version> <!-- 使用最新版本 -->
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>