1.开发第二个接口
1.1.接口定义
127.0.0.1:5501/api/traces/{tradeId}
127.0.0.1:5501/api/traces/695ca6b528140ca7802052bea3187457
1.2.手搓日志数据
servera.log新增:
bash
[linkservera] [695ca6b528140ca7802052bea3187457,802052bea3187457] [com.gg.core.aspect.MethodAspect] [2026-01-29 14:07:50.491] > [INFO] [http-nio-13000-exec-3] - 请求参数: {"tradeTime":"2026-01-29 14:07:50","name":"小王","patientId":"123456"}
[linkservera] [695ca6b528140ca7802052bea3187457,802052bea3187457] [com.gg.core.aspect.MethodAspect] [2026-01-29 14:07:51.562] > [INFO] [http-nio-13000-exec-3] - 返回结果: {"tradeTime":"2026-01-29 14:07:50","name":"小王","patientId":"123456"}
serverb.log新增:
bash
[linkserverb] [695ca6b528140ca7802052bea3187457,802052bea3187457] [com.gg.core.aspect.MethodAspect] [2026-01-29 14:07:51.491] > [INFO] [http-nio-14000-exec-3] - 请求参数: {"tradeTime":"2026-01-29 14:07:50","name":"小王","patientId":"123456"}
[linkserverb] [695ca6b528140ca7802052bea3187457,802052bea3187457] [com.gg.core.aspect.MethodAspect] [2026-01-29 14:07:51.541] > [INFO] [http-nio-14000-exec-3] - 返回结果: {"tradeTime":"2026-01-29 14:07:50","name":"小王","patientId":"123456"}
1.3.代码
此时的代码类:
java
package com.gg.midend.service.impl;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.gg.midend.config.GlobalConfig;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class TraceQueryService {
private final ElasticsearchClient esClient;
// 正则表达式:提取serviceName和traceId
private static final Pattern LOG_PATTERN = Pattern.compile(
"(?:\\[)?([^\\[\\]]+)(?:\\])?\\s+\\[([a-fA-F0-9]+),[a-fA-F0-9]+\\]"
);
// 需要保留的关键词
private static final List<String> KEEP_KEYWORDS = Arrays.asList(
"请求参数:", "返回结果:"
);
// 正则表达式:提取日志时间戳 [2026-01-28 14:07:54.541]
private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(
"\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\]"
);
// 正则表达式:提取"请求参数:"或"返回结果:"后面的内容
private static final Pattern CONTENT_PATTERN = Pattern.compile(
"(?:请求参数:|返回结果:)\\s*(\\{.*\\})"
);
public TraceQueryService(ElasticsearchClient esClient) {
this.esClient = esClient;
}
public List<Map<String, Object>> listLogsByTraceId(String traceId) throws IOException {
if (traceId == null || traceId.trim().isEmpty()) {
return new ArrayList<>();
}
GlobalConfig.log_api.info(">>> [Trace Detail] 开始查询traceId: " + traceId);
// 优化查询条件:使用更宽松的查询条件
SearchRequest request = SearchRequest.of(s -> s
.index("microservice-logs-*")
.query(Query.of(q -> q
.bool(b -> b
.should(sh -> sh.wildcard(w -> w
.field("content.keyword")
.value("*[" + traceId + ",*]*")
))
.should(sh -> sh.wildcard(w -> w
.field("content.keyword")
.value("*" + traceId + "*")
))
)
))
.sort(sort -> sort.field(f -> f.field("@timestamp").order(SortOrder.Asc)))
// 只查询需要的字段:@timestamp 和 content
.source(src -> src.filter(f -> f.includes(
"@timestamp", "content"
)))
.size(1000) // 增加返回数量
);
SearchResponse<Map> response = esClient.search(request, Map.class);
List<Map<String, Object>> results = new ArrayList<>();
GlobalConfig.log_api.info(">>> [Trace Detail] ES查询完成,命中条数: " + response.hits().hits().size());
for (Hit<Map> hit : response.hits().hits()) {
Map<String, Object> source = hit.source();
if (source != null) {
String content = (String) source.get("content");
if (content == null) {
continue;
}
// 过滤逻辑:只保留包含"请求参数:"或"返回结果:"的日志
boolean shouldKeep = false;
for (String keyword : KEEP_KEYWORDS) {
if (content.contains(keyword)) {
shouldKeep = true;
break;
}
}
// 如果不包含关键词,跳过此条记录
if (!shouldKeep) {
continue;
}
// 提取serviceName和traceId
String serviceName = "";
String extractedTraceId = "";
Matcher matcher = LOG_PATTERN.matcher(content);
if (matcher.find()) {
serviceName = matcher.group(1);
extractedTraceId = matcher.group(2);
}
// 如果从content中提取的traceId为空,使用传入的traceId
if (extractedTraceId.isEmpty()) {
extractedTraceId = traceId;
}
// 处理content字段,只保留"请求参数:"或"返回结果:"后面的内容
String processedContent = extractContentAfterKeywords(content);
if (processedContent != null && !processedContent.isEmpty()) {
source.put("content", processedContent);
} else {
// 如果提取失败,跳过此条记录
GlobalConfig.log_api.warn(">>> [Trace Detail] 提取content失败,跳过记录,原始content: " + content);
continue;
}
// 提取日志时间戳 [2026-01-28 14:07:54.541]
String logTimestamp = extractLogTimestamp(content);
if (logTimestamp != null && !logTimestamp.isEmpty()) {
source.put("logTime", logTimestamp);
} else {
// 如果没有提取到时间戳,跳过此条记录
GlobalConfig.log_api.warn(">>> [Trace Detail] 提取logTime失败,跳过记录,原始content: " + content);
continue;
}
// 提取日志类型(请求参数或返回结果)
String logType = extractLogType(content);
if (!logType.isEmpty()) {
source.put("logType", logType);
} else {
// 如果没有提取到日志类型,跳过此条记录
GlobalConfig.log_api.warn(">>> [Trace Detail] 提取logType失败,跳过记录,原始content: " + content);
continue;
}
// 添加 serviceName 和 traceId
source.put("serviceName", serviceName);
source.put("traceId", extractedTraceId);
// 移除不需要的字段
source.remove("tradeTime");
source.remove("patientId");
source.remove("name");
results.add(source);
}
}
// 新增:按照logTime字段进行升序排序
sortResultsByLogTime(results);
GlobalConfig.log_api.info("<<< [Trace Detail] 查询完成,返回结果条数: " + results.size());
return results;
}
/**
* 按照logTime字段进行升序排序
* @param results 待排序的结果列表
*/
private void sortResultsByLogTime(List<Map<String, Object>> results) {
if (results == null || results.size() <= 1) {
return;
}
results.sort((map1, map2) -> {
String time1 = (String) map1.get("logTime");
String time2 = (String) map2.get("logTime");
// 处理空值情况
if (time1 == null && time2 == null) {
return 0;
} else if (time1 == null) {
return 1; // time1为空放到后面
} else if (time2 == null) {
return -1; // time2为空放到后面
}
// 直接比较字符串,因为时间格式是固定的:yyyy-MM-dd HH:mm:ss.SSS
return time1.compareTo(time2);
});
// 打印排序后的时间顺序用于调试
if (GlobalConfig.log_api.isDebugEnabled()) {
for (int i = 0; i < Math.min(results.size(), 5); i++) {
GlobalConfig.log_api.debug("排序后第" + (i+1) + "条记录的logTime: " + results.get(i).get("logTime"));
}
}
}
/**
* 提取"请求参数:"或"返回结果:"后面的内容,并处理中文引号
* @param originalContent 原始日志内容
* @return 处理后的内容
*/
private String extractContentAfterKeywords(String originalContent) {
if (originalContent == null || originalContent.isEmpty()) {
return originalContent;
}
// 使用正则表达式提取
Matcher matcher = CONTENT_PATTERN.matcher(originalContent);
if (matcher.find()) {
String extracted = matcher.group(1);
// 处理中文引号
extracted = replaceChineseQuotes(extracted);
return extracted;
}
// 如果正则失败,使用字符串查找
for (String keyword : KEEP_KEYWORDS) {
int index = originalContent.indexOf(keyword);
if (index != -1) {
String extracted = originalContent.substring(index + keyword.length()).trim();
// 清理可能的空格和换行符
extracted = extracted.replaceAll("[\\r\\n\\t]", "");
// 处理中文引号
extracted = replaceChineseQuotes(extracted);
return extracted;
}
}
// 如果没有找到关键词,返回null
return null;
}
/**
* 替换中文引号为英文引号
* @param content 原始内容
* @return 替换后的内容
*/
private String replaceChineseQuotes(String content) {
if (content == null || content.isEmpty()) {
return content;
}
// 使用Unicode编码
return content
.replace("\u201c", "\"") // 左中文引号的Unicode
.replace("\u201d", "\""); // 右中文引号的Unicode
}
/**
* 提取日志时间戳 [2026-01-28 14:07:54.541]
* @param originalContent 原始日志内容
* @return 日志时间戳
*/
private String extractLogTimestamp(String originalContent) {
if (originalContent == null || originalContent.isEmpty()) {
return "";
}
// 使用正则表达式提取时间戳
Matcher matcher = TIMESTAMP_PATTERN.matcher(originalContent);
if (matcher.find()) {
return matcher.group(1);
}
return "";
}
/**
* 提取日志类型(请求参数或返回结果)
* @param originalContent 原始日志内容
* @return 日志类型
*/
private String extractLogType(String originalContent) {
if (originalContent == null || originalContent.isEmpty()) {
return "";
}
if (originalContent.contains("请求参数:")) {
return "REQUEST";
} else if (originalContent.contains("返回结果:")) {
return "RESPONSE";
}
return "";
}
}
1.4.测试
返回结果:
