上篇我们解决了"大模型调用慢在哪一段"的问题。但慢只是表象,真正排障时你更常遇到的是:"这条慢链路对应的业务上下文是什么?" —— 比如用户问了什么、命中了哪个知识库、模型返回了什么敏感内容、当时的租户 ID 是多少。这些信息不可能全塞进 SkyWalking 的 Tag(Tag 要求低基数),它们天然属于日志。

本篇讲两件联动的事:第一,如何用自定义标签把"业务维度"打在 Trace 上,让链路可按租户、按模型、按意图筛选;第二,如何把 traceId / spanId 注入到应用日志,并在 SkyWalking UI 里实现"从一条 Trace 直接跳到它产生的日志"。掌握这两点,你就能把"链路"和"上下文"拼成完整证据链。

  1. 为什么要把 Trace 和 Log 关联起来
  2. SkyWalking 的日志插件与 TraceContext
  3. 自定义标签:从业务维度给链路打标
  4. 代码实战:在日志中注入 traceId / spanId
  5. 通过 gRPC 上报日志到 OAP
  6. UI 中联动排查:从 Trace 跳日志
  7. 最佳实践与踩坑

1. 为什么要把 Trace 和 Log 关联起来

在微服务架构里,日志和链路长期是两套体系:日志管"发生了什么",链路管"花了多久、调了谁"。但线上排障需要的是二者的交集。

举个大模型场景的真实案例:某个租户反馈"回答里出现了不该出现的价格"。你用 SkyWalking 找到那条慢 Trace,看到 `llm.inference` 正常,但仅凭链路你不知道它问了什么、RAG 召回了哪段知识。此时若日志里带着同一个 `traceId`,你就能用 traceId 搜到这次请求打印的 `userQuery=...`、`kbHit=...`、`modelOutput=...`,三秒钟还原现场。

没有关联时,排查路径是:先看监控发现慢 → 再去日志系统按时间戳 + 关键字盲搜 → 还要对齐线程和时间窗口,极易找错日志。有了关联,路径变成:点开 Trace → 一键跳到关联日志,效率提升一个数量级。

SkyWalking 提供了两条关联路径:日志里带 traceId(被动关联,靠搜索),以及日志直接上报到 OAP 并和 Trace 绑定(主动关联,可点击)。

2. SkyWalking 的日志插件与 TraceContext

SkyWalking 的日志关联依赖 `TraceContext`。只要在挂载了 Agent 的 JVM 内,任何地方都能拿到当前链路的 ID:

String traceId = TraceContext.traceId();
String segmentId = TraceContext.segmentId();
String spanId = String.valueOf(TraceContext.spanId());

 

SkyWalking 提供了多种日志框架的桥接插件,位于 `agent/optional-plugins`:

  • `apm-toolkit-logback-1.x-activation`:桥接 Logback,自动把 traceId 写进 MDC。
  • `apm-toolkit-log4j-2.x-activation`:桥接 Log4j2。
  • `apm-toolkit-log4j-1.x-activation`:桥接 Log4j 1.x。

要使用,把对应 jar 从 `optional-plugins` 复制到 `plugins` 目录,然后在日志 Pattern 里引用 `%tid` 即可输出 traceId。注意 `TraceContext.traceId()` 返回空字符串时代表当前不在链路上下文中(如后台定时线程),需要做判空兜底。

除日志桥接外,SkyWalking 还支持把日志通过 gRPC 直接上报 OAP(见第 5 节),这样日志会作为 Trace 的附属数据存储在 OAP 中,实现 UI 内联动。

3. 自定义标签:从业务维度给链路打标

Tag 是 SkyWalking 里给 Span 附加键值对的能力。和日志不同,Tag 会被索引、可用于查询和拓扑筛选,因此必须低基数。

大模型业务里,推荐把以下"业务维度"做成 Tag,而非写日志:

Tag 名

取值示例

用途

---

---

---

llm.tenant

t_1001 / t_1002

按租户隔离分析性能

llm.model

gpt-4o / qwen-max

对比不同模型耗时

llm.intent

问答 / 摘要 / 翻译

按意图聚合

llm.isStream

true / false

区分流式与非流式

llm.errorType

timeout / 429 / null

错误归类

注意:不要把 `userQuery` 全文、`userId` 原文(高基数)作为 Tag。它们应当进日志。Tag 是"分桶"用的,日志才是"明细"用的。

`ActiveSpan.tag(key, value)` 即可打标,value 必须是字符串。若 value 为 null 会抛异常,需判空。

4. 代码实战:在日志中注入 traceId / spanId

以 Logback 为例。首先激活插件(复制 jar 到 plugins 目录),然后修改 `logback-spring.xml` 的 Pattern,加入 `%tid`:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
  <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} %tid - %msg%n</pattern>
  </encoder>
</appender>

 

`%tid` 会渲染成 `[TRACEID,SEGMENTID,SPANID]` 形式(无链路时为空)。这样每条业务日志都自带链路坐标。

接着在业务代码里,除了打印明细,也把关键上下文打上 Tag:

package com.demo.llm.service;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
    private static final Logger log = LoggerFactory.getLogger(ChatService.class);
    public String chat(String tenant, String intent, String model, String userQuery) {
        // 业务维度打标(低基数)
        ActiveSpan.tag("llm.tenant", tenant);
        ActiveSpan.tag("llm.intent", intent);
        ActiveSpan.tag("llm.model", model);
        // 明细进日志(高基数,带 traceId 自动注入)
        log.info("用户提问 tenant={} intent={} query={}", tenant, intent, userQuery);
        String output = doInference(model, userQuery);
        // 输出也可能很长,只记录长度与风险标记,正文进日志
        log.info("模型返回 tenant={} outputLen={} output={}", tenant, output.length(), output);
        return output;
    }
    private String doInference(String model, String q) { return "..."; }
}

 

运行后,控制台日志会出现类似:`... [TID: 3a9f... , 2b1c... , 0] - 用户提问 ...`,这就是关联键。

5. 通过 gRPC 上报日志到 OAP

仅把 traceId 打进本地日志,仍需去 ELK/Loki 里搜。若希望"在 SkyWalking UI 里直接看日志",需要把日志上报到 OAP。SkyWalking 提供 `apm-toolkit-logback-1.x` 的 gRPC 上报 Appender。

引入依赖(provided):

<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-logback-1.x</artifactId>
    <version>9.7.0</version>
    <scope>provided</scope>
</dependency>

 

在 `logback-spring.xml` 增加 gRPC Appender:

<appender name="SW_GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCEncoder"/>
    </encoder>
</appender>
<root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="SW_GRPC"/>
</root>

 

OAP 侧需在 `application.yml` 开启日志接收:

receiver-banyandb:
  selector: ${SW_RECEIVER_BANYANDB:-}
logging:
  selector: ${SW_LOGGING_RECEIVER_ENABLED:default}

 

日志上报走 `11800` 端口(与 Trace 同一 gRPC 通道)。上报后,每条日志会带上 traceId,并在 OAP 内部与 Trace 绑定,UI 中即可联动。

6. UI 中联动排查:从 Trace 跳日志

配置就绪后,排查流程变成:

  1. SkyWalking UI → "追踪",按 `llm.tenant = t_1001` 过滤,找到慢 Trace。
  2. 点开该 Trace,在详情面板右侧可见"关联日志"标签页,列出这次请求产生的全部日志条目。
  3. 点某条日志,能看到完整 `userQuery`、`kbHit`、`modelOutput`,无需切换系统。
  4. 反向也可:在"日志"菜单按关键字搜到异常日志,点 traceId 跳回对应 Trace,看当时的链路拓扑与耗时。

这种双向联动对大模型排障尤其重要,因为模型问题的"原因"往往在输入,"现象"在输出,二者分处链路两端,只有联动视图才能一次看清。

下表对比两种关联方式的取舍:

方式

实现成本

联动体验

适用场景

---

---

---

---

日志带 traceId(ELK/Loki)

低,仅改 Pattern

需跨系统搜

已有成熟日志平台

日志上报 OAP

中,加 Appender

UI 内一键跳转

希望统一观测面

标签筛选

低,代码打标

按维度聚合

多租户/多模型对比

7. 最佳实践与踩坑

第一,Tag 与日志职责分清。再强调一次:Tag 负责"分桶筛选"(低基数、可枚举),日志负责"明细排查"(高基数、可长文本)。把 userId 当 Tag,OAP 索引会膨胀到不可用。

第二,异步线程会丢上下文。大模型回调常切到线程池,子线程里 `TraceContext.traceId()` 可能为空。解决:用 SkyWalking 的 `@TraceCrossThread` 或 `RunnableWrapper/CallableWrapper` 包装任务,把上下文传递过去,否则日志里的 traceId 会断开。

第三,批量上报注意采样。全量日志上报 OAP 在高峰压力很大。可对 INFO 走本地日志、仅把 WARN/ERROR 上报 OAP,或用 `log4j2` 的异步 Appender 削峰。

第四,MDC 清理。Logback 的 `%tid` 基于 MDC,线程复用(如 Tomcat 线程池)时若上下文未清除,可能串号。SkyWalking 插件默认会清理,但自写跨线程包装时要手动 `MDC.clear()`。

第五,敏感信息脱敏。大模型日志常含用户隐私与模型输出,上报前务必做脱敏(如手机号、身份证掩码),避免把 PII 写进 OAP 存储。

第六,traceId 空值兜底。`TraceContext.traceId()` 在非链路线程返回空串,日志里会显示空,打印关联键前先判断,避免误导。

做好标签 + 日志联动,你手里就同时有了"按维度筛查的望远镜"(Tag)和"还原现场的显微镜"(Log),大模型线上问题不再是黑盒。

更多推荐