使用logstash和elasticsearch实现日志链路(二)

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.测试

返回结果:

相关推荐
小邓睡不饱耶2 小时前
Hadoop 进阶:企业级项目实战、生态深度整合与故障排查
大数据·hadoop·分布式
esmap2 小时前
技术深析:ESMAP智慧医院解决方案——基于AOA蓝牙定位的全场景精准感知实现
大数据·网络·人工智能
小邓睡不饱耶2 小时前
深耕 Hadoop:内核优化、分布式一致性与大规模集群实战
大数据·hadoop·分布式
海兰2 小时前
win11下本地部署单节点Elasticsearch9.0+开发
大数据·elasticsearch·jenkins
Elastic 中国社区官方博客10 小时前
使用 Discord 和 Elastic Agent Builder A2A 构建游戏社区支持机器人
人工智能·elasticsearch·游戏·搜索引擎·ai·机器人·全文检索
琅琊榜首202012 小时前
AI生成脑洞付费短篇小说:从灵感触发到内容落地
大数据·人工智能
TTBIGDATA13 小时前
【knox】User: knox is not allowed to impersonate admin
大数据·运维·ambari·hdp·trino·knox·bigtop
紧固视界14 小时前
了解常见紧固件分类标准
大数据·制造·紧固件·上海紧固件展
无忧智库14 小时前
跨国制造企业全球供应链协同平台(SRM+WMS+TMS)数字化转型方案深度解析:打造端到端可视化的“数字供应链“(WORD)
大数据