微服务日志智能诊断系统(八) 全链路追踪——request_id驱动的跨服务诊断


项目地址gitee.com/guoxuezha/e...


1. 单服务诊断的局限性

前面的博客聚焦于"单服务"的日志诊断:给定一个服务编码和发生时间,抓取该服务的日志并分析。但在真实的微服务架构中,一个用户请求往往会穿越多个服务:

scss 复制代码
用户请求 → Gateway → Doctor-Service → (Feign) Order-Service → (MQ) Nurse-Service

如果异常发生在 Order-Service,但用户只看到了 Doctor-Service 的超时错误,仅分析入口服务的日志只能看到"表象"(超时),看不到"根因"(Order-Service 的 SQL 慢查询)。

全链路诊断 Agent(FullChainDiagnosisAgent) 的目标就是:自动追踪请求在各个服务间的传递过程,把所有相关服务的日志串联起来,让 AI 进行综合分析。

2. 核心思路:request_id 是钥匙

我们的日志格式中,每行日志都携带 request_id(通过 MDC 注入):

ini 复制代码
[abc-123-def] [admin] 2026-07-07 10:30:15.123 [http-nio-8080-exec-1] ERROR ...

同一个 HTTP 请求,在入口服务和下游服务中,共享同一个 request_id。这意味着:

  1. 从入口服务的错误日志中提取 request_id
  2. request_id 为关键字,去下游服务的 INFO 级别日志中搜索。
  3. 将所有匹配结果按服务分组,就能还原完整的请求链路。

3. FullChainDiagnosisAgent 的 Action 编排

全链路 Agent 包含 7 个 Action,形成一个清晰的线性 + 分支流程:

scss 复制代码
parseRequest
     │ [hasParsedInput]
     ▼
fetchEntryServiceLog
     │ [entryLogFetched]
     ▼
assessEntryLogQuality
     ├── [chainQualityPass] ──→ queryDownstreamServices
     │                                ├── [hasDownstreamServices] ──→ extractRequestIds
     │                                │                                    ├── [hasValidRequestIds] ──→ fetchDownstreamLogs
     │                                │                                    │                                │ [downstreamLogsFetched]
     │                                │                                    │                                ▼
     │                                │                                    │                        generateChainReport (全链路报告)
     │                                │                                    │
     │                                │                                    └── [noValidRequestIds]
     │                                │                                            ▼
     │                                │                                    generateNoRequestIdReport (降级报告)
     │                                │
     │                                └── [noDownstreamServices]
     │                                        ▼
     │                                generateSingleServiceReport (单服务报告)
     │
     └── [chainQualityFail]
             ▼
       generateFailReport (失败报告)

可以看到,Agent 在不同条件下有 4 种终点

  • 全链路报告(COMPLETE):成功追踪到下游服务日志。
  • 单服务报告(COMPLETE):没有下游服务,仅分析入口服务。
  • 降级报告(PARTIAL):有下游服务但未能提取 request_id。
  • 失败报告(FAILED):入口服务日志质量不达标。

这种"渐进降级"的设计,确保系统在任何情况下都能给出尽可能有价值的诊断结果。

4. 关键 Action 详解

4.1 请求解析

java 复制代码
@Action(
    description = "处理用户输入,解析为全链路诊断请求参数",
    post = {"hasParsedInput"}
)
public FullChainDiagnosisInput parseRequest(UserInput userInput, OperationContext ctx) {
    FullChainDiagnosisInput input = ctx.ai()
            .withDefaultLlm()
            .createObject(PromptBuilder.buildFullChainInputPrompt(userInput), FullChainDiagnosisInput.class);
    input.setParsed(true);
    return input;
}

与单服务 Agent 一样,用户输入通过 LLM 解析为结构化对象。全链路版本需要额外的必填字段:url(接口路径)和 problemDescription(问题描述)。

4.2 入口服务日志抓取

java 复制代码
@Action(
    description = "通过 SSH 远程抓取入口服务日志",
    pre = {"hasParsedInput"},
    post = {"entryLogFetched"},
    canRerun = true
)
public SshLogResult fetchEntryServiceLog(FullChainDiagnosisInput input, OperationContext ctx) {
    List<ServiceResource> resources = knowledgeBaseService.findServiceResources(serviceCode);
    SshLogResult sshResult = sshService.firstLogCatch(resources, occurredTime);
    // ...
}

这里只抓取 error 级别日志,因为入口服务的目标是定位异常并提取 request_id

4.3 质量评估

全链路版本的质量评估标准与单服务版本略有不同:评分 ≥ 6 分即视为达标 (而非单服务版本的 8 分)。这是因为全链路诊断的入口日志主要用于提取 request_id,对日志完整性的要求相对宽松。

4.4 下游服务查询

java 复制代码
@Action(
    description = "根据 serviceCode 和 url 查询异常请求的下游服务信息",
    pre = {"chainQualityPass"},
    post = {"hasDownstreamServices", "noDownstreamServices"}
)
public DownstreamServiceInfo queryDownstreamServices(FullChainDiagnosisInput input) {
    HashSet<String> downstream = knowledgeBaseService.getDownStreamService(
            input.getServiceCode(), input.getUrl());
    return DownstreamServiceInfo.builder()
            .entryServiceCode(input.getServiceCode())
            .url(input.getUrl())
            .downstreamServiceCodes(downstream)
            .build();
}

下游服务查询复用了知识库的 getDownStreamService 方法,它同时考虑了 Feign 调用MQ 消费者 两种关联关系:

java 复制代码
public HashSet<String> getDownStreamService(String serviceCode, String url) {
    HashSet<String> relations = new HashSet<>();
    ApiRelationInfo relationInfo = apiRelationMap.get(serviceCode).get(url);

    // Feign 调用:直接获取目标服务编码
    if (relationInfo.getHasFeignCall()) {
        relationInfo.getFeignCalls().forEach(f -> relations.add(f.getServiceCode()));
    }

    // MQ 消费者:通过 topic#tag 查找消费方
    if (relationInfo.getHasMqSend()) {
        for (MqSendInfo mqSend : relationInfo.getMqSends()) {
            String topicTag = mqSend.getTopic() + "#" + mqSend.getTag();
            relations.addAll(topicConsumerMap.getOrDefault(topicTag, List.of()));
        }
    }
    return relations;
}

4.5 request_id 提取

这是全链路追踪最关键的一步。我们采用了双重提取策略

java 复制代码
@Action(
    description = "根据日志行格式识别关键日志的 request_id",
    pre = {"hasDownstreamServices"},
    post = {"hasValidRequestIds", "noValidRequestIds"}
)
public RequestIdExtractOutput extractRequestIds(SshLogResult logResult,
                                                 FullChainDiagnosisInput input,
                                                 OperationContext ctx) {
    // 方式一: 正则提取(快,精准)
    List<String> requestIds = extractRequestIdsFromLog(logContent);

    // 方式二: LLM 辅助提取(正则失败时的备选方案)
    if (CollUtil.isEmpty(requestIds)) {
        requestIds = extractRequestIdsByLlm(logContent, ctx);
    }

    return RequestIdExtractOutput.builder()
            .requestIds(requestIds)
            .serviceCode(input.getServiceCode())
            .build();
}

正则提取:匹配每行日志第一个方括号内的内容:

java 复制代码
private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("^\[([^\]]+)]");

private List<String> extractRequestIdsFromLog(String logContent) {
    Set<String> requestIds = new LinkedHashSet<>();
    for (String line : logContent.split("\n")) {
        Matcher matcher = REQUEST_ID_PATTERN.matcher(line.trim());
        if (matcher.find()) {
            String requestId = matcher.group(1).trim();
            if (!"EMPTY".equalsIgnoreCase(requestId) && StrUtil.isNotEmpty(requestId)) {
                requestIds.add(requestId);
            }
        }
    }
    return new ArrayList<>(requestIds);
}

LLM 辅助提取:当日志格式非标准或 request_id 位置不规则时,让 LLM 理解日志结构并提取。这种"规则优先 + AI 兜底"的策略,在准确性和鲁棒性之间取得了良好的平衡。

4.6 下游服务日志抓取

拿到 request_id 后,逐一访问每个下游服务,以 request_id 为关键字抓取 INFO 级别以上的日志:

java 复制代码
@Action(
    description = "以 request_id 为关键字,获取下游服务完整的请求链路日志",
    pre = {"hasValidRequestIds"},
    post = {"downstreamLogsFetched"}
)
public FullChainLogResult fetchDownstreamLogs(RequestIdExtractOutput requestIdOutput,
                                               DownstreamServiceInfo downstreamInfo,
                                               SshLogResult entryLogResult,
                                               FullChainDiagnosisInput input,
                                               OperationContext ctx) {
    List<ServiceLogGroup> allGroups = new ArrayList<>();

    // 首先加入入口服务日志
    allGroups.add(ServiceLogGroup.builder()
            .serviceCode(input.getServiceCode())
            .logLevel("error")
            .logContent(entryLogResult.getOutput())
            .build());

    // 选取第一个有效的 request_id
    String requestId = requestIdOutput.getRequestIds().getFirst();

    // 逐一抓取下游服务日志
    for (String downstreamCode : downstreamInfo.getDownstreamServiceCodes()) {
        List<ServiceResource> resources = knowledgeBaseService.findServiceResources(downstreamCode);

        // 用 request_id 作为关键字抓取 INFO 级别以上的日志
        SshLogResult infoResult = sshService.logSectionCatch(
                resources, input.getOccurredTime(), requestId, null, null, null);

        allGroups.add(ServiceLogGroup.builder()
                .serviceCode(downstreamCode)
                .logLevel("info")
                .logContent(infoResult.getOutput())
                .build());
    }

    return FullChainLogResult.builder()
            .serviceLogGroups(allGroups)
            .entryServiceCode(input.getServiceCode())
            .requestId(requestId)
            .build();
}

5. 全链路诊断报告

最终的报告包含所有服务的日志和 LLM 的综合分析:

java 复制代码
@AchievesGoal(description = "完成全链路日志整合分析并生成诊断报告")
@Action(
    description = "基于全链路日志生成综合诊断报告",
    pre = {"downstreamLogsFetched"}
)
public FullChainDiagnosisReport generateChainReport(FullChainLogResult chainResult,
                                                     FullChainDiagnosisInput input,
                                                     DownstreamServiceInfo downstreamInfo,
                                                     OperationContext ctx) {
    // 构建全链路日志文本(按服务分组展示)
    String chainLogsText = buildChainLogsText(chainResult.getServiceLogGroups());

    // 调用 LLM 进行综合分析
    String analysis = ctx.ai()
            .withDefaultLlm()
            .generateText(PromptBuilder.buildFullChainAnalysisPrompt(
                    input.getProblemDescription(),
                    input.getServiceCode(),
                    chainResult.getEntryLogContent(),
                    chainLogsText
            ));

    return FullChainDiagnosisReport.builder()
            .entryServiceCode(input.getServiceCode())
            .serviceLogGroups(chainResult.getServiceLogGroups())
            .requestId(chainResult.getRequestId())
            .analysisConclusion(analysis)
            .diagnosisStatus("COMPLETE")
            .build();
}

6. 全链路日志的呈现格式

buildChainLogsText 方法将各服务的日志按清晰的格式组织:

ini 复制代码
========== [his-doctor-service] ==========
日志级别: error
来源主机: 192.168.0.52
日志文件: doctor-error.log
日志内容:
[abc-123] [admin] 2026-07-07 10:30:15.123 ... ERROR ... 查询超时

========== [his-order-service] ==========
日志级别: info
来源主机: 192.168.0.53
日志文件: order-info.log
日志内容:
[abc-123] [admin] 2026-07-07 10:30:14.890 ... INFO ... 开始处理订单查询
[abc-123] [admin] 2026-07-07 10:30:14.950 ... INFO ... 执行SQL: SELECT ...
[abc-123] [admin] 2026-07-07 10:30:15.100 ... WARN ... SQL执行耗时210ms

LLM 基于这份全链路日志,能够清晰地还原事件时间线,并精确定位问题发生在哪个服务的哪个环节。

7. LlmTool:供 LLM 自主调用的诊断工具集

除了 Agent 的 Action 编排外,我们还提供了一组 @LlmTool 注解的工具方法,让 LLM 在推理过程中可以自主调用:

工具名 功能 典型使用场景
search-log-by-time 按时间搜索日志 首次抓取目标服务日志
search-log-by-keyword 按关键字搜索日志 用 request_id 追踪链路
search-log-from-file 从指定文件抓取 精确控制抓取的文件和范围
query-feign-calls 查询 Feign 调用关系 确定下游服务
query-mq-consumers 查询 MQ 消费者 确定消息消费方
query-downStream-service 查询下游服务 一站式获取所有下游
lookup-service-resource 查询服务资源 获取主机、端口等信息

这些工具通过 @LlmTool 注解注册,LLM 可以在 createObject 推理过程中自主决定调用哪些工具、传什么参数。这赋予了 Agent 极大的灵活性------LLM 不再只是"被动接收输入并输出结果",而是可以"主动探索"。

8. 渐进降级:四种终点的价值

全链路 Agent 的四种终点不是"失败",而是"不同程度的成功":

场景一:完美诊断

"Doctor-Service 的 /consult/query 接口超时。追踪 request_id=abc-123 发现:Order-Service 在处理订单查询时执行了一条全表扫描 SQL,耗时 3.2 秒。建议在 order 表的 patient_id 字段上添加索引。"

场景二:无下游服务

"Doctor-Service 的 /doctor/list 接口报 NullPointerException。该接口没有调用任何下游服务。日志显示 DoctorDTO.getDepartment() 返回 null,建议添加空值检查。"

场景三:request_id 提取失败(降级)

"Doctor-Service 的日志中未找到有效的 request_id(可能日志格式不标准)。仅分析了入口服务的日志,初步判断为数据库连接池耗尽。建议进一步人工排查 Order-Service。"

场景四:入口日志质量不达标(失败)

"Doctor-Service 的日志内容为空或与描述的问题无关(评分 2/10)。可能原因:发生时间不准确、服务编码错误、或日志已被轮转清理。建议确认参数后重试。"

即使是最差的情况,系统也给出了有价值的反馈(诊断过程摘要 + 失败原因 + 下一步建议),而不是简单地报一个"500 错误"。

9. 实际诊断案例

案例:某医院 HIS 系统报告"医生工作站查询患者信息偶发超时"。

  1. 用户输入:serviceCode=his-doctor-service, occurredTime=2026-07-07 10:30:15, url=/patient/query, problemDescription=查询患者信息超时
  2. Agent 抓取入口服务 error.log,评分 8/10(发现超时异常堆栈)。
  3. 查询知识库,发现 /patient/query 通过 Feign 调用了 his-patient-service
  4. 从日志中提取 request_id=req-7f8a9b
  5. req-7f8a9b 为关键字搜索 his-patient-service 的 INFO 日志。
  6. LLM 综合分析发现:his-patient-service 在 10:30:14 执行了一条 SELECT * FROM patient_info WHERE ... 查询,耗时 4.5 秒(正常应 < 100ms)。
  7. 报告建议:检查 patient_info 表索引是否缺失,并排查是否存在锁等待。

整个诊断过程约 25 秒,而人工排查通常需要 15-30 分钟。

10. 小结

全链路追踪 Agent 通过 request_id 这一"线索",将分散在多个微服务中的日志串联起来,让 AI 能够从全局视角分析问题。结合知识库的服务关联信息(Feign + MQ),系统能自动发现下游依赖,无需用户手动指定。渐进降级机制确保了在任何场景下都能给出有价值的诊断结果。


相关推荐
爷_1 小时前
AI Coding Agent 沙箱的设计原理解析
前端·人工智能·后端
ACGkaka_1 小时前
Spring Boot 实战(四十一):集成 BPMN(Flowable+bpmn-js)
java·spring boot·后端
前端Baymax1 小时前
Agent工具调用中无限重试死循环的根因与修复
ai·agent·rag
ljs6482739511 小时前
智慧商城(Smart Mall):基于 Spring Boot + Vue 3 的现代化电商平台实战
linux·vue.js·spring boot·后端
yeffky1 小时前
Vibe Coding一个能办事的 AI Agent:Spring Boot + DeepSeek + LangGraph4j 六节点 ReAct 项目实战
人工智能·后端·agent
乒乓狂魔14786739970001 小时前
OpenClaw 终于可以读懂 OpenTelemetry APM 了
后端
不好听6131 小时前
从零构建一个 AI Coding Agent:不会写代码?让 AI 帮你写
agent
Rain的Java大神之路1 小时前
高并发下如何防止商品超卖少卖
后端
小Z加点糖1 小时前
开源 FlowScript:把 Skill 变成可执行、可检查、可回放的工作流
agent