Java 全链路日志追踪:MDC 与 TraceId 的完整实现

yipeng
yipeng 正式会员认证极客
发布于 2026-07-19 02:06 ·6 浏览 ·0 回复

学完这篇你能拿到一套可以直接抄进项目的方案:一次 HTTP 请求从进门到落库、从同步到异步、从本机到下游服务,日志里都带着同一个 TraceId,出问题按一个 ID 就能捞出全链路日志。

第一步:先搞清楚 MDC 和 TraceId 各干什么

TraceId 是"一次请求的身份证",一个随机字符串,全链路共享。MDC(Mapped Diagnostic Context)是 SLF4J/Logback 提供的线程级诊断上下文,底层就是一个 ThreadLocal 的 Map。把 TraceId 塞进 MDC 后,日志框架在输出每行日志时会自动把这个值填到 pattern 里,你的业务代码一个参数都不用传。

所以整体链路是:**Filter 生成/读取 TraceId → 存入 MDC → 日志 pattern 自动带上 → 请求结束时清理**。

第二步:配置 logback 输出 TraceId

在 `logback-spring.xml` 的 pattern 里加上 `%X{traceId}`:

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

`%X{traceId:-}` 里的 `:-` 是 Logback 的默认值语法,冒号后面为空表示"取不到就输出空字符串"。这样异步线程、定时任务等没有 TraceId 的场景不会打出 `traceId_IS_UNDEFINED` 这种噪音。

第三步:写入口 Filter,生成并清理 TraceId

public class TraceIdFilter extends OncePerRequestFilter {
    public static final String TRACE_ID = "traceId";
    public static final String HEADER = "X-Trace-Id";

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
                                    FilterChain chain) throws ServletException, IOException {
        String traceId = req.getHeader(HEADER);
        if (traceId == null || traceId.isEmpty()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        MDC.put(TRACE_ID, traceId);
        resp.setHeader(HEADER, traceId);   // 方便前端/网关对账
        try {
            chain.doFilter(req, resp);
        } finally {
            MDC.remove(TRACE_ID);          // 必须清理,见下方注意
        }
    }
}

注册时把顺序调到最靠前,保证它包住后面所有 Filter 和 Controller:

@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilter() {
    FilterRegistrationBean<TraceIdFilter> bean = new FilterRegistrationBean<>(new TraceIdFilter());
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

`finally` 里用 `MDC.remove()` 而不是 `MDC.clear()`:clear 会把当前线程里别的业务埋点(比如 userId、租户号)一起清掉。

注意:Tomcat 的请求线程是复用的。如果漏掉 finally 里的清理,下一个请求进来会继承上一个请求的 TraceId,日志串线,排查时会被带到沟里。

第四步:让异步线程也带上 TraceId

MDC 是 ThreadLocal,线程池里的线程拿不到父线程的上下文,这是最常见的翻车点。Spring 的 `ThreadPoolTaskExecutor` 有现成的钩子:

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> ctx = MDC.getCopyOfContextMap();
        return () -> {
            if (ctx != null) MDC.setContextMap(ctx);
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

// 线程池上挂一下
executor.setTaskDecorator(new MdcTaskDecorator());

注意:`@Async` 如果不指定执行器,走的是 Spring 默认的 `SimpleAsyncTaskExecutor`,TaskDecorator 不生效。要么显式 `@Async("myExecutor")`,要么在容器里定义一个 `ThreadPoolTaskExecutor` 类型的 Bean 让 Spring Boot 自动接管。

第五步:跨服务传递,把 Header 带出去

只要下游也装了同一套 Filter,它就会优先读 `X-Trace-Id` 而不是自己新生成,链路自动接上。

RestTemplate 加拦截器:

restTemplate.getInterceptors().add((request, body, execution) -> {
    String traceId = MDC.get("traceId");
    if (traceId != null) request.getHeaders().add("X-Trace-Id", traceId);
    return execution.execute(request, body);
});

Feign 更简单,加个配置类:

@Bean
public RequestInterceptor traceInterceptor() {
    return template -> {
        String traceId = MDC.get("traceId");
        if (traceId != null) template.header("X-Trace-Id", traceId);
    };
}

发 MQ 消息时,把 TraceId 写进消息 Header(比如 RocketMQ 的 userProperty、Kafka 的 header),消费端取出来再 `MDC.put` 回去。网关(Spring Cloud Gateway 或 Nginx)最好统一生成一次 TraceId 并透传,避免各服务各生成一个。

注意:TraceId 别用完整 UUID 带横杠的 36 位,全链路日志量大的时候很占体积。用 32 位无横杠,或者雪花算法、日期前缀 + 短随机串,都能兼顾可读性和长度。

第六步:日志聚合,按 ID 检索

本地只做到"日志里有 TraceId",真正好用的是集中检索。把日志通过 Filebeat / Fluent Bit 送到 ELK 或 Loki,索引里 `traceId` 设为 keyword 字段,排查时直接 `traceId: "xxxx"` 一次查出跨服务的所有日志。这时建议顺带把 `spanId`、`userId`、请求 URI 也塞进 MDC,检索维度会更丰富。

注意:如果项目已经引入了 Micrometer Tracing 或老的 Spring Cloud Sleuth,MDC 里的 `traceId` 和 `spanId` 由它维护,不要自己再生成覆盖,直接复用 `MDC.get("traceId")` 即可。自己造一套会和框架的采样、上报打架。

小结

  • TraceId 是请求身份证,MDC 是把它自动写进每行日志的载体,两者配合才不用手写参数。
  • 入口 Filter 负责生成、写响应头、`finally` 里 `MDC.remove` 清理,顺序调到最高优先级。
  • 线程池必须用 `TaskDecorator` 或手动 `MDC.setContextMap` 传递,`@Async` 要显式指定执行器。
  • 跨服务靠 `X-Trace-Id` 请求头透传,RestTemplate/Feign/MQ 三处都别漏。
  • 想真正好用,最后的落点是日志聚合平台的 keyword 检索,不是本地 grep。
  • 已有 Sleuth / Micrometer Tracing 的项目,复用不要重造。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-545.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~