欢迎你来读这篇博客,这篇博客主要是关于 Spring Boot 日志链路追踪 的落地方案。
本文会从 MDC + Logback + TTL 的自研方案讲起,再分析 TLog、SkyWalking、OpenTelemetry、Micrometer Tracing、Zipkin、Jaeger、Grafana Tempo、Elastic APM、Pinpoint、SigNoz、Datadog、New Relic 等常见链路追踪与 APM 方案。
核心目标不是堆工具名,而是给出一套在 Spring Boot 项目里真正能落地的方案:日志里能看到链路标识,异步线程里不丢 traceId,服务间调用能透传,未来也能平滑升级到完整 APM。
序言 在单体系统里,我们通常靠日志定位问题:看异常堆栈、看业务参数、看调用顺序。
到了微服务系统,问题会变得麻烦很多:
一个请求会经过网关、订单服务、库存服务、财务服务、支付服务、消息队列、异步线程池。
每个服务都有自己的日志文件。
同一时间可能有几百个请求并发执行。
线程池会复用线程,如果上下文清理不干净,日志 traceId 还可能串。
所以,链路追踪至少要解决三个问题:
能串起来 :同一个请求在多个服务、多个线程、多个日志文件中的日志,可以通过同一个 traceId 找到。
能看清楚 :不仅知道是同一个请求,还能看到它经过了哪些服务、哪些方法、哪些 SQL、哪些 RPC。
能定位慢点和错点 :当接口慢、超时、报错时,可以快速定位瓶颈发生在哪个服务、哪个组件。
MDC + Logback + TTL 主要解决第一个问题,也就是 日志关联 。
SkyWalking、OpenTelemetry、Elastic APM、Pinpoint、Datadog、New Relic 这类方案解决的是更完整的 APM/可观测性 问题。
这两者不是互斥关系。更合理的路线是:
先把日志 traceId 标准化,再根据系统规模接入链路追踪平台,最后把日志、指标、调用链、告警打通。
正文 一、先说结论 如果你现在是 Spring Boot 项目,并且想快速拥有日志链路追踪能力,我建议这样选:
场景
推荐方案
原因
单体应用,主要想让日志按请求串起来
MDC + Logback
成本最低,不需要引入 APM 平台
Spring Boot 微服务,存在异步线程池
MDC + Logback + TaskDecorator/TTL
解决线程池上下文丢失与串号问题
Spring Cloud/Dubbo 项目,想快速接入日志 traceId
TLog
接入轻,自动打标签,适合轻量级日志追踪
Spring Boot 3 新项目,想走官方生态
Micrometer Tracing + OpenTelemetry
Spring Boot 3 官方观测路线,更标准
需要服务拓扑、慢 SQL、RPC 调用链、告警
SkyWalking
Java 微服务生态成熟,零侵入 Java Agent 体验好
已有 ELK/Elastic Stack
Elastic APM
日志、指标、APM、检索体验统一
已有 Grafana/Prometheus/Loki
OpenTelemetry + Tempo
云原生可观测性组合,成本可控
想要开源一体化 APM 平台
SigNoz
OpenTelemetry 原生,日志/指标/Trace 一体化
企业已有商业监控预算
Datadog/New Relic
功能完整,成本高,但省运维
我的建议是:
基础能力统一做 :所有服务都要统一 traceId、spanId、requestId、userId、tenantId 等日志字段。
不要只做 Controller 层 :异步线程池、Feign、RestTemplate、WebClient、Dubbo、MQ、定时任务都要考虑。
不要把 MDC 当业务上下文存储 :MDC 是日志输出上下文,不是业务上下文容器。
不要把日志 traceId 等同于分布式追踪 :日志有 traceId 只能检索日志;完整 APM 还需要 Span、拓扑、耗时、错误、采样、存储和 UI。
Spring Boot 3 项目优先考虑 Micrometer Tracing/OpenTelemetry :如果只是短期自研日志追踪,可以先做 MDC;如果要长期演进,尽量靠近 OTel 标准。
二、什么是 MDC、Logback、TTL 1. MDC 是什么 MDC 全称是 Mapped Diagnostic Context,可以理解为日志框架提供的“线程级上下文 Map”。
在代码里放入:
1 MDC.put("traceId" , "abc123" );
在 Logback 输出格式里使用:
日志里就能打印出:
1 2026-06-11 17:30:00.123 INFO [traceId=abc123] c.demo.OrderService - create order success
它的本质通常是 ThreadLocal。这也是 MDC 的核心优点和核心坑点。
优点是:当前请求线程内,任何地方打印日志都能带上上下文。
坑点是:换线程之后,MDC 默认不会自动过去。
2. Logback 是什么 Logback 是 Spring Boot 默认使用的日志实现之一。
Spring Boot 内部使用 Commons Logging,但默认日志实现通常是 Logback。我们在 Spring Boot 里写的:
1 private static final Logger log = LoggerFactory.getLogger(OrderService.class);
一般会通过 SLF4J 门面输出到 Logback。
Logback 负责:
控制日志格式。
控制日志级别。
控制日志输出位置,比如控制台、文件、JSON、异步 Appender。
从 MDC 中取值并打印到日志。
3. TTL 是什么 TTL 是 Alibaba 开源的 TransmittableThreadLocal。
JDK 自带的 ThreadLocal 只能保证当前线程可见。
InheritableThreadLocal 可以把父线程的值传给新创建的子线程,但在线程池里不可靠。因为线程池里的线程不是每次提交任务时重新创建的,而是提前创建并反复复用。
TTL 解决的是:
在线程池这类线程复用场景中,把任务提交时的上下文,传递到任务执行时的线程里。
但是这里有个非常重要的边界:
TTL 不是自动把所有 MDC 内容天然传播到所有线程的魔法。MDC 是日志框架自己的 ThreadLocal。生产上更稳的方式是:用 TTL 管自己的 TraceContext,用 TaskDecorator/包装器在执行线程里恢复 MDC。
也就是说,推荐架构是:
1 2 3 4 5 6 7 请求入口 -> 解析/生成 traceId -> 写入 TraceContext(TTL) -> 同步写入 MDC -> 输出日志 | v 异步任务提交 | v 捕获 TraceContext -> 执行线程恢复 -> 同步 MDC -> 输出日志
三、日志链路追踪字段规范 不要一上来就只搞一个 traceId。生产系统里建议至少区分这些字段:
字段
含义
是否建议打印
是否建议透传
traceId
一次完整链路的全局唯一 ID
是
是
spanId
当前调用片段 ID
是
是
parentSpanId
上游调用片段 ID
可选
是
requestId
当前入口请求 ID,常用于网关/接口层
是
可选
userId
当前用户 ID
可选
不一定
tenantId
当前租户 ID
可选
不一定
bizType
业务类型,比如 settlement、order
是
不一定
bizId
业务主键,比如订单 ID、结算单 ID
是
不一定
clientIp
客户端 IP
可选
否
appName
当前服务名
是
否
这里要注意:
traceId 是技术链路维度,不应该携带敏感信息。
requestId 可以和 traceId 相同,也可以只表示当前入口请求。
spanId 如果只是 MDC 自研方案,可以先简单生成;如果接入 OpenTelemetry/SkyWalking,应以后者生成的 Span 为准。
userId、tenantId 属于业务上下文,是否跨服务透传要看安全策略。
bizId 适合用于日志检索,但不要滥放大对象和敏感字段。
推荐请求头规范:
Header
说明
traceparent
W3C Trace Context 标准头,OpenTelemetry 推荐
X-Trace-Id
自定义 traceId,兼容内部系统
X-Request-Id
请求 ID
X-B3-TraceId
Zipkin/Brave B3 传播兼容
X-B3-SpanId
Zipkin/Brave B3 传播兼容
X-B3-ParentSpanId
Zipkin/Brave B3 传播兼容
如果公司没有历史包袱,建议优先兼容 traceparent,同时保留 X-Trace-Id 作为内部检索友好字段。
四、MDC + Logback + TTL 整体架构 flowchart LR
A[HTTP 请求进入网关/服务] --> B[TraceFilter 解析请求头]
B --> C{是否存在 traceId}
C -- 是 --> D[复用上游 traceId]
C -- 否 --> E[生成新的 traceId]
D --> F[写入 TraceContext]
E --> F
F --> G[同步到 MDC]
G --> H[业务代码打印日志]
H --> I[Logback 输出 traceId/spanId/requestId]
H --> J[调用下游服务]
J --> K[Feign/RestTemplate/WebClient/Dubbo 透传 Header]
H --> L[提交异步任务]
L --> M[TaskDecorator/TTL 捕获上下文]
M --> N[执行线程恢复 TraceContext 与 MDC]
N --> O[异步日志继续带 traceId]
I --> P[日志采集到 ELK/Loki/文件]
这套方案的关键点:
请求入口统一生成或读取 traceId。
TraceContext 是上下文事实来源。
MDC 只负责日志输出。
异步线程要捕获、恢复、清理上下文。
服务间调用要把 traceId 放进请求头。
请求结束必须清理,避免线程复用导致串号。
五、Maven 依赖 Spring Boot 默认已经带了 spring-boot-starter-logging,通常不需要额外引入 Logback。
如果你要使用 TTL:
1 2 3 4 5 6 <dependency > <groupId > com.alibaba</groupId > <artifactId > transmittable-thread-local</artifactId > <version > 2.14.5</version > </dependency >
如果你希望输出 JSON 日志,方便 ELK/Loki 等采集,可以引入:
1 2 3 4 5 6 <dependency > <groupId > net.logstash.logback</groupId > <artifactId > logstash-logback-encoder</artifactId > <version > 8.0</version > </dependency >
版本建议跟随公司 BOM 统一管理。不要每个项目自己散着配,否则排查日志问题时,第一步就会变成“猜版本”。
六、Trace 常量定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.example.common.trace;public final class TraceConstants { private TraceConstants () { } public static final String TRACE_ID = "traceId" ; public static final String SPAN_ID = "spanId" ; public static final String PARENT_SPAN_ID = "parentSpanId" ; public static final String REQUEST_ID = "requestId" ; public static final String USER_ID = "userId" ; public static final String TENANT_ID = "tenantId" ; public static final String APP_NAME = "appName" ; public static final String HEADER_TRACE_PARENT = "traceparent" ; public static final String HEADER_TRACE_ID = "X-Trace-Id" ; public static final String HEADER_REQUEST_ID = "X-Request-Id" ; public static final String HEADER_SPAN_ID = "X-Span-Id" ; public static final String HEADER_PARENT_SPAN_ID = "X-Parent-Span-Id" ; public static final String HEADER_B3_TRACE_ID = "X-B3-TraceId" ; public static final String HEADER_B3_SPAN_ID = "X-B3-SpanId" ; public static final String HEADER_B3_PARENT_SPAN_ID = "X-B3-ParentSpanId" ; }
七、TraceId 生成器 生产环境里,traceId 建议满足:
全局唯一。
不包含业务敏感信息。
便于检索。
长度不要太夸张。
如果要兼容 W3C Trace Context,可以使用 32 位十六进制字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.example.common.trace;import java.security.SecureRandom;public final class TraceIdGenerator { private static final SecureRandom RANDOM = new SecureRandom (); private static final char [] HEX = "0123456789abcdef" .toCharArray(); private TraceIdGenerator () { } public static String traceId () { return randomHex(16 ); } public static String spanId () { return randomHex(8 ); } private static String randomHex (int byteLength) { byte [] bytes = new byte [byteLength]; RANDOM.nextBytes(bytes); char [] chars = new char [byteLength * 2 ]; for (int i = 0 ; i < bytes.length; i++) { int value = bytes[i] & 0xFF ; chars[i * 2 ] = HEX[value >>> 4 ]; chars[i * 2 + 1 ] = HEX[value & 0x0F ]; } return new String (chars); } }
八、TraceContext:用 TTL 管上下文,用 MDC 做输出 这是自研方案里最关键的一层。
不要在业务代码里到处 MDC.put(),否则后面你会很难治理。
推荐做法是:
TraceContext 维护当前链路上下文。
TraceContext 内部使用 TransmittableThreadLocal。
每次设置上下文时,同步到 MDC。
每次清理上下文时,也清理 MDC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package com.example.common.trace;import com.alibaba.ttl.TransmittableThreadLocal;import java.util.Collections;import java.util.HashMap;import java.util.Map;import org.slf4j.MDC;public final class TraceContext { private static final TransmittableThreadLocal<Map<String, String>> LOCAL = new TransmittableThreadLocal <>(); private TraceContext () { } public static void put (String key, String value) { if (key == null || value == null || value.isBlank()) { return ; } Map<String, String> context = new HashMap <>(current()); context.put(key, value); set(context); } public static String get (String key) { return current().get(key); } public static Map<String, String> copy () { Map<String, String> context = LOCAL.get(); if (context == null || context.isEmpty()) { return Collections.emptyMap(); } return new HashMap <>(context); } public static void set (Map<String, String> context) { if (context == null || context.isEmpty()) { clear(); return ; } Map<String, String> copied = new HashMap <>(context); LOCAL.set(copied); syncMdc(copied); } public static void clear () { LOCAL.remove(); MDC.clear(); } public static void syncMdc () { syncMdc(copy()); } private static Map<String, String> current () { Map<String, String> context = LOCAL.get(); if (context == null ) { return Collections.emptyMap(); } return context; } private static void syncMdc (Map<String, String> context) { MDC.clear(); for (Map.Entry<String, String> entry : context.entrySet()) { if (entry.getKey() != null && entry.getValue() != null ) { MDC.put(entry.getKey(), entry.getValue()); } } } }
为什么不直接把 MDC 当唯一上下文?
因为 MDC 的职责是日志输出,不应该承载业务上下文传播。你可以把 MDC 理解成“打印机墨盒”,TraceContext 才是“原始文档”。
九、HTTP 入口 TraceFilter 推荐使用 OncePerRequestFilter,在每个 HTTP 请求入口统一处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 package com.example.common.trace;import jakarta.servlet.FilterChain;import jakarta.servlet.ServletException;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.HashMap;import java.util.Map;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.filter.OncePerRequestFilter;@Component public class TraceFilter extends OncePerRequestFilter { @Value("${spring.application.name:unknown-service}") private String appName; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { Map<String, String> context = new HashMap <>(); String traceId = firstNotBlank( parseTraceIdFromTraceParent(request.getHeader(TraceConstants.HEADER_TRACE_PARENT)), request.getHeader(TraceConstants.HEADER_TRACE_ID), request.getHeader(TraceConstants.HEADER_B3_TRACE_ID), TraceIdGenerator.traceId() ); String parentSpanId = firstNotBlank( request.getHeader(TraceConstants.HEADER_SPAN_ID), request.getHeader(TraceConstants.HEADER_B3_SPAN_ID) ); String spanId = TraceIdGenerator.spanId(); String requestId = firstNotBlank( request.getHeader(TraceConstants.HEADER_REQUEST_ID), traceId ); context.put(TraceConstants.TRACE_ID, traceId); context.put(TraceConstants.SPAN_ID, spanId); context.put(TraceConstants.REQUEST_ID, requestId); context.put(TraceConstants.APP_NAME, appName); if (StringUtils.hasText(parentSpanId)) { context.put(TraceConstants.PARENT_SPAN_ID, parentSpanId); } try { TraceContext.set(context); response.setHeader(TraceConstants.HEADER_TRACE_ID, traceId); response.setHeader(TraceConstants.HEADER_REQUEST_ID, requestId); response.setHeader(TraceConstants.HEADER_SPAN_ID, spanId); filterChain.doFilter(request, response); } finally { TraceContext.clear(); } } private static String firstNotBlank (String... values) { if (values == null ) { return null ; } for (String value : values) { if (StringUtils.hasText(value)) { return value; } } return null ; } private static String parseTraceIdFromTraceParent (String traceParent) { if (!StringUtils.hasText(traceParent)) { return null ; } String[] parts = traceParent.split("-" ); if (parts.length >= 4 && parts[1 ].length() == 32 ) { return parts[1 ]; } return null ; } }
如果你还在使用 Spring Boot 2,jakarta.servlet 要替换为 javax.servlet。
十、Logback 输出 traceId 1. 普通文本日志 logback-spring.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <springProperty scope ="context" name ="APP_NAME" source ="spring.application.name" defaultValue ="unknown-service" /> <property name ="LOG_PATTERN" value ="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{appName:-${APP_NAME}}] [traceId=%X{traceId:-}] [spanId=%X{spanId:-}] [requestId=%X{requestId:-}] %logger{36} - %msg%n" /> <appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > ${LOG_PATTERN}</pattern > <charset > UTF-8</charset > </encoder > </appender > <root level ="INFO" > <appender-ref ref ="CONSOLE" /> </root > </configuration >
输出效果:
1 2026-06-11 17:30:00.123 INFO [http-nio-8080-exec-1] [order-service] [traceId=3f5a...] [spanId=9a1c...] [requestId=3f5a...] c.e.OrderService - create order success
2. JSON 日志 如果你要接入 ELK、OpenSearch、Loki,建议输出 JSON。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <appender name ="JSON_CONSOLE" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder" > <providers > <timestamp > <timeZone > Asia/Shanghai</timeZone > </timestamp > <logLevel /> <threadName /> <loggerName /> <message /> <arguments /> <stackTrace /> <mdc /> </providers > </encoder > </appender >
JSON 日志的好处是:
traceId 可以作为独立字段检索。
不依赖正则切割文本日志。
对接 Kibana、Grafana Loki、OpenSearch Dashboard 更舒服。
十一、异步线程池上下文传播 这是 MDC 方案里最容易出问题的地方。
常见问题:
Controller 里有 traceId,@Async 里没有。
主线程是 A 请求,线程池日志里却打印了 B 请求的 traceId。
CompletableFuture 里 traceId 丢了。
定时任务执行后,没有清理 MDC,污染后续任务。
根因是:线程池复用线程,而 MDC 通常基于 ThreadLocal。
1. Spring ThreadPoolTaskExecutor 推荐使用 TaskDecorator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.example.common.trace;import java.util.Map;import org.springframework.core.task.TaskDecorator;public class TraceTaskDecorator implements TaskDecorator { @Override public Runnable decorate (Runnable runnable) { Map<String, String> captured = TraceContext.copy(); return () -> { Map<String, String> backup = TraceContext.copy(); try { TraceContext.set(captured); runnable.run(); } finally { TraceContext.set(backup); } }; } }
线程池配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.example.common.config;import com.example.common.trace.TraceTaskDecorator;import java.util.concurrent.Executor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;@EnableAsync @Configuration public class AsyncExecutorConfig { @Bean("bizExecutor") public Executor bizExecutor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(8 ); executor.setMaxPoolSize(32 ); executor.setQueueCapacity(500 ); executor.setThreadNamePrefix("biz-executor-" ); executor.setTaskDecorator(new TraceTaskDecorator ()); executor.initialize(); return executor; } }
使用:
1 2 3 4 @Async("bizExecutor") public void asyncCreateBill (Long billId) { log.info("async create bill, billId={}" , billId); }
2. 原生 ExecutorService 使用包装器 如果项目中手写了 ExecutorService:
1 ExecutorService executorService = Executors.newFixedThreadPool(8 );
建议统一包装:
1 2 3 4 import com.alibaba.ttl.threadpool.TtlExecutors;ExecutorService rawExecutor = Executors.newFixedThreadPool(8 );ExecutorService executorService = TtlExecutors.getTtlExecutorService(rawExecutor);
然后再配合 TraceContext 在任务执行前同步 MDC。
更稳的方式是封装一个工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.example.common.trace;import java.util.Map;import java.util.concurrent.Callable;public final class TraceWrappers { private TraceWrappers () { } public static Runnable wrap (Runnable runnable) { Map<String, String> captured = TraceContext.copy(); return () -> { Map<String, String> backup = TraceContext.copy(); try { TraceContext.set(captured); runnable.run(); } finally { TraceContext.set(backup); } }; } public static <T> Callable<T> wrap (Callable<T> callable) { Map<String, String> captured = TraceContext.copy(); return () -> { Map<String, String> backup = TraceContext.copy(); try { TraceContext.set(captured); return callable.call(); } finally { TraceContext.set(backup); } }; } }
使用:
1 2 3 executorService.submit(TraceWrappers.wrap(() -> { log.info("run async task" ); }));
3. CompletableFuture 要指定线程池 不要随手写:
1 CompletableFuture.runAsync(() -> doSomething());
它会使用公共 ForkJoinPool,不方便统一治理上下文。
推荐:
1 2 3 4 CompletableFuture.runAsync( TraceWrappers.wrap(() -> doSomething()), bizExecutor );
或者统一封装:
1 2 3 4 5 6 7 8 9 public final class TraceFutures { private TraceFutures () { } public static CompletableFuture<Void> runAsync (Runnable runnable, Executor executor) { return CompletableFuture.runAsync(TraceWrappers.wrap(runnable), executor); } }
十二、下游 HTTP 调用透传 traceId 只在本服务日志里有 traceId 不够。服务间调用要把 traceId 透传下去。
1. RestTemplate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Bean public RestTemplate restTemplate (RestTemplateBuilder builder) { return builder .additionalInterceptors((request, body, execution) -> { putTraceHeaders(request.getHeaders()); return execution.execute(request, body); }) .build(); }private void putTraceHeaders (HttpHeaders headers) { putIfPresent(headers, TraceConstants.HEADER_TRACE_ID, TraceContext.get(TraceConstants.TRACE_ID)); putIfPresent(headers, TraceConstants.HEADER_REQUEST_ID, TraceContext.get(TraceConstants.REQUEST_ID)); putIfPresent(headers, TraceConstants.HEADER_SPAN_ID, TraceContext.get(TraceConstants.SPAN_ID)); }private void putIfPresent (HttpHeaders headers, String name, String value) { if (value != null && !value.isBlank()) { headers.set(name, value); } }
2. OpenFeign 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean public RequestInterceptor traceFeignInterceptor () { return template -> { putIfPresent(template, TraceConstants.HEADER_TRACE_ID, TraceContext.get(TraceConstants.TRACE_ID)); putIfPresent(template, TraceConstants.HEADER_REQUEST_ID, TraceContext.get(TraceConstants.REQUEST_ID)); putIfPresent(template, TraceConstants.HEADER_SPAN_ID, TraceContext.get(TraceConstants.SPAN_ID)); }; }private void putIfPresent (RequestTemplate template, String name, String value) { if (value != null && !value.isBlank()) { template.header(name, value); } }
3. WebClient 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public WebClient webClient (WebClient.Builder builder) { return builder .filter((request, next) -> { ClientRequest.Builder requestBuilder = ClientRequest.from(request); putIfPresent(requestBuilder, TraceConstants.HEADER_TRACE_ID, TraceContext.get(TraceConstants.TRACE_ID)); putIfPresent(requestBuilder, TraceConstants.HEADER_REQUEST_ID, TraceContext.get(TraceConstants.REQUEST_ID)); putIfPresent(requestBuilder, TraceConstants.HEADER_SPAN_ID, TraceContext.get(TraceConstants.SPAN_ID)); ClientRequest tracedRequest = requestBuilder.build(); return next.exchange(tracedRequest); }) .build(); }private void putIfPresent (ClientRequest.Builder builder, String name, String value) { if (value != null && !value.isBlank()) { builder.header(name, value); } }
注意:WebFlux/Reactor 是响应式上下文模型,不能简单照搬 Servlet + ThreadLocal 思路。WebFlux 项目建议优先使用 Micrometer Tracing/OpenTelemetry 的上下文传播能力。
十三、Dubbo、MQ、定时任务怎么处理 1. Dubbo Dubbo 调用需要在 Consumer 侧把 trace 信息放入 attachment,在 Provider 侧取出来。
思路如下:
1 2 3 4 5 RpcContext.getClientAttachment().setAttachment( TraceConstants.HEADER_TRACE_ID, TraceContext.get(TraceConstants.TRACE_ID) );
1 2 String traceId = RpcContext.getServerAttachment().getAttachment(TraceConstants.HEADER_TRACE_ID);
如果已经接入 SkyWalking、TLog、OpenTelemetry Java Agent,通常不建议自己重复写一套 Dubbo Filter。否则容易出现两个 traceId 并存。
2. Kafka/RabbitMQ 生产消息时,把 trace 信息放到消息 header:
1 record.headers().add(TraceConstants.HEADER_TRACE_ID, traceId.getBytes(StandardCharsets.UTF_8));
消费消息时,从 header 取出 traceId,恢复 TraceContext,再执行业务逻辑。
关键点依旧是:
1 2 3 4 5 6 try { TraceContext.set(context); handleMessage(message); } finally { TraceContext.clear(); }
3. 定时任务 定时任务没有上游请求,所以应该自己生成 traceId:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Scheduled(cron = "0 */5 * * * ?") public void syncSettlementStatus () { Map<String, String> context = Map.of( TraceConstants.TRACE_ID, TraceIdGenerator.traceId(), TraceConstants.SPAN_ID, TraceIdGenerator.spanId(), TraceConstants.REQUEST_ID, "schedule-" + TraceIdGenerator.spanId(), TraceConstants.APP_NAME, "finance-service" ); try { TraceContext.set(context); log.info("start sync settlement status" ); doSync(); } finally { TraceContext.clear(); } }
十四、业务日志字段建议 链路追踪不是让你少写业务日志,而是让业务日志更容易被串起来。
以财务结算类业务为例,建议统一日志字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 traceId requestId biz scene step phase billType billId billNo bizId bizUk status expectedStatus fromStatus toStatus decision reason result errorCode errorMsg changed costMs msg
日志示例:
1 2 3 4 5 6 7 log.info( "biz=settlement scene=statement step=preprocess phase=start billType={} billId={} billNo={} msg={}" , billType, billId, billNo, "start preprocess statement" );
异常日志:
1 2 3 4 5 6 7 8 log.error( "biz=settlement scene=statement step=preprocess phase=failed billType={} billId={} errorCode={} errorMsg={}" , billType, billId, "PREPROCESS_FAILED" , ex.getMessage(), ex );
注意:
查询接口不要打太多 INFO 日志,容易刷屏。
写操作、状态变更、异步任务、补偿任务要重点打。
大对象不要直接打日志。
手机号、身份证、银行卡、token、密码、密钥不要明文打。
错误日志一定要带异常对象,不要只打 ex.getMessage()。
十五、MDC 方案的完整落地清单
模块
是否必须
说明
TraceConstants
是
统一字段名和 Header 名
TraceIdGenerator
是
生成 traceId/spanId
TraceContext
是
统一上下文读写
TraceFilter
是
HTTP 入口生成/恢复上下文
Logback Pattern
是
打印 MDC 字段
TaskDecorator
是
解决 @Async 上下文丢失
Executor 包装器
建议
解决手写线程池/CompletableFuture
Feign Interceptor
微服务必须
服务间透传
RestTemplate/WebClient Interceptor
按需
HTTP 调用透传
MQ Header 透传
按需
异步消息链路
定时任务 Trace 包装
建议
定时任务日志可检索
JSON 日志
建议
方便日志平台检索
日志脱敏
必须
避免敏感信息泄漏
十六、测试验证方案 1. HTTP 请求验证 1 curl -H "X-Trace-Id: test-trace-001" http://localhost:8080/orders/1
期望日志:
1 [traceId=test-trace-001] get order detail, orderId=1
2. 异步线程验证 Controller:
1 2 3 4 5 6 @GetMapping("/async") public String async () { log.info("main thread" ); asyncService.run(); return "ok" ; }
Async Service:
1 2 3 4 @Async("bizExecutor") public void run () { log.info("async thread" ); }
期望两条日志 traceId 一致。
3. 并发串号验证 1 2 3 4 for i in $(seq 1 100); do curl -s -H "X-Trace-Id: trace-$i " "http://localhost:8080/async" &done wait
检查日志:
trace-1 的异步日志不能出现在 trace-2 的上下文中。
请求结束后,线程池下一次任务不能残留上一次 traceId。
4. 下游透传验证 服务 A 调服务 B,服务 A 请求头传:
期望:
服务 A 日志有 trace-a-001。
服务 B 日志也有 trace-a-001。
服务 B response header 返回同一个 X-Trace-Id。
十七、MDC + Logback + TTL 的优缺点 优点
成本低,不需要部署链路追踪平台。
对老项目友好。
日志排查体验提升明显。
字段可控,方便结合公司业务规范。
和 ELK、Loki、OpenSearch 等日志平台容易结合。
缺点
只能做日志关联,不是完整调用链。
看不到完整服务拓扑。
看不到自动生成的 Span 耗时树。
SQL、Redis、MQ、RPC 等调用耗时需要自己埋点。
WebFlux/Reactor 场景处理复杂。
需要严格治理线程池,否则容易丢 traceId 或串 traceId。
适合场景:
单体项目。
微服务规模不大。
团队暂时不想部署 APM。
主要诉求是日志检索和问题定位。
不适合场景:
服务数量多。
跨语言系统多。
需要拓扑图、慢 SQL、调用树、错误聚合。
已经有可观测性平台建设诉求。
十八、TLog 分析 TLog 是 Dromara 社区下的轻量级分布式日志标记追踪框架。
它的定位不是完整 APM,而是:
通过对日志打标签,完成轻量级微服务日志追踪。
TLog 的特点:
支持 Log4j、Log4j2、Logback。
支持 Dubbo、Dubbox、Spring Cloud。
支持 Spring Cloud Gateway、Soul 网关。
支持 HttpClient、OkHttp 等 HTTP 调用透传。
支持线程池、多级异步线程。
支持 TimerTask、Quartz、XXL-JOB。
提供 Java Agent、字节码、一行代码、配置文件等接入方式。
不负责日志收集和存储。
引入方式示例:
1 2 3 4 5 6 <dependency > <groupId > com.yomahub</groupId > <artifactId > tlog-all-spring-boot-starter</artifactId > <version > 1.5.2</version > </dependency >
TLog 适合:
想快速让日志带 traceId。
项目使用 Spring Cloud/Dubbo。
不想自己写 Filter、Interceptor、线程池包装。
不需要完整 APM 平台。
团队能接受引入一个日志链路追踪框架。
TLog 不适合:
已经准备统一上 OpenTelemetry。
已经接入 SkyWalking/Elastic APM/Datadog/New Relic。
需要完整服务拓扑、Span 树、慢 SQL 分析。
公司要求所有链路数据走标准 OTLP。
我的评价:
TLog 很适合解决“日志 traceId 快速落地”问题,但不要把它当成 SkyWalking/OpenTelemetry 的替代品。它是轻量链路日志方案,不是完整可观测性平台。
如果你当前项目还没有任何链路追踪能力,TLog 是一个很务实的选择。
如果你正在规划长期可观测性平台,建议直接评估 OpenTelemetry 或 SkyWalking。
十九、SkyWalking 分析 Apache SkyWalking 是一个面向微服务、云原生、容器化架构的 APM 系统。
它的 Java Agent 能提供:
分布式追踪。
指标采集。
日志关联。
事件。
Profiling。
服务拓扑。
慢接口、慢 SQL、异常分析。
典型架构:
flowchart LR
A[Java 服务 + SkyWalking Agent] --> B[SkyWalking OAP]
B --> C[(Storage: Elasticsearch / BanyanDB / MySQL 等)]
B --> D[SkyWalking UI]
A --> E[应用日志]
E --> F[日志平台]
D --> F
接入方式通常是在 JVM 启动参数中增加 Java Agent:
1 2 3 4 5 java \ -javaagent:/opt/skywalking/agent/skywalking-agent.jar \ -Dskywalking.agent.service_name=order-service \ -Dskywalking.collector.backend_service=127.0.0.1:11800 \ -jar order-service.jar
SkyWalking 适合:
Java 微服务为主。
Spring Cloud、Dubbo、MyBatis、Redis、MQ 等组件较多。
希望低侵入接入。
需要服务拓扑和调用链 UI。
希望自建开源 APM。
SkyWalking 的优点:
Java 生态成熟。
Agent 自动增强,业务代码侵入少。
UI 对 Java 微服务排障友好。
对 Dubbo、Spring Cloud 等国内常见技术栈支持较好。
可以同时看 Trace、Metrics、Log、Profiling。
SkyWalking 的缺点:
需要部署 OAP、UI、存储。
Agent 升级要治理。
跨语言标准化程度不如 OpenTelemetry。
如果公司未来全部走 OTel,需要考虑数据模型和协议兼容。
我的评价:
如果公司是 Java 微服务为主,想快速搭一套自建 APM,SkyWalking 仍然是非常强的选择。它比纯 MDC 方案强太多,也比从零组 OpenTelemetry + Collector + Backend 省心。
二十、OpenTelemetry 与 Micrometer Tracing 分析 OpenTelemetry 是当前云原生可观测性领域非常重要的标准。
它提供:
Trace 标准。
Metrics 标准。
Logs 标准。
SDK。
Java Agent。
Collector。
OTLP 协议。
Spring Boot 3 以后,Spring 官方路线更偏向:
1 2 3 4 5 6 7 8 9 Spring Boot Actuator | Micrometer Observation | Micrometer Tracing | OpenTelemetry / Brave | Zipkin / Tempo / Jaeger / OTLP Backend
1. Spring Boot 3 接入 Micrometer Tracing + OpenTelemetry Maven 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-tracing-bridge-otel</artifactId > </dependency > <dependency > <groupId > io.opentelemetry</groupId > <artifactId > opentelemetry-exporter-otlp</artifactId > </dependency >
配置示例:
1 2 3 4 5 6 7 management: tracing: sampling: probability: 0.1 otlp: tracing: endpoint: http://localhost:4318/v1/traces
生产环境不要轻易 probability: 1.0。全量采样在高并发系统里会带来存储和网络压力。
2. 使用 OpenTelemetry Java Agent 1 2 3 4 5 6 7 java \ -javaagent:/opt/opentelemetry/opentelemetry-javaagent.jar \ -Dotel.service.name=order-service \ -Dotel.exporter.otlp.endpoint=http://otel-collector:4318 \ -Dotel.traces.exporter=otlp \ -Dotel.metrics.exporter=otlp \ -jar order-service.jar
OpenTelemetry Java Agent 的优势是:
自动增强常见框架。
代码侵入低。
跨语言统一。
后端可替换。
可以导出到 Tempo、Jaeger、Zipkin、SigNoz、Elastic、Datadog 等。
3. OpenTelemetry Collector 生产上不建议每个应用直接连最终后端。
更推荐:
1 应用 -> OpenTelemetry Collector -> 后端存储/UI
Collector 负责:
接收数据。
处理数据。
采样。
过滤。
脱敏。
转发到多个后端。
典型配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 receivers: otlp: protocols: grpc: endpoint: 0.0 .0 .0 :4317 http: endpoint: 0.0 .0 .0 :4318 processors: batch: memory_limiter: check_interval: 1s limit_mib: 512 exporters: otlp/tempo: endpoint: tempo:4317 tls: insecure: true service: pipelines: traces: receivers: [ otlp ] processors: [ memory_limiter , batch ] exporters: [ otlp/tempo ]
4. Micrometer Tracing 和 OpenTelemetry 的关系 Micrometer Tracing 是 Spring 生态下的 tracing 门面。
它可以桥接:
OpenTelemetry。
Brave。
你可以把它理解为日志里的 SLF4J:
1 2 SLF4J 是日志门面,后面可以接 Logback/Log4j2。 Micrometer Tracing 是 Trace 门面,后面可以接 OpenTelemetry/Brave。
Spring Boot 3 项目里,如果你想走 Spring 官方生态,不建议再上 Spring Cloud Sleuth。Sleuth 在旧 Spring Cloud 体系里很常见,但新项目应优先看 Micrometer Tracing。
二十一、Zipkin、Jaeger、Tempo 怎么选 这三个更偏 Trace 后端,不是完整 APM 平台。
方案
定位
优点
注意点
Zipkin
分布式追踪系统
简单、轻量、Brave 生态成熟
UI 和分析能力相对基础
Jaeger
CNCF 分布式追踪平台
云原生常见,Trace 查询能力较成熟
新项目通常通过 OTel 接入
Grafana Tempo
高规模 Trace 后端
与 Grafana/Prometheus/Loki 组合好,成本较低
强依赖 Grafana 生态,通常要配合日志和指标
如果你已经有 Grafana + Prometheus + Loki:
1 2 3 4 5 6 7 OpenTelemetry Java Agent | OpenTelemetry Collector | Grafana Tempo | Grafana UI
这是一个很自然的组合。
如果你想先最小化体验分布式追踪,可以用 Zipkin。
如果公司已有 CNCF 可观测性平台规划,可以评估 Jaeger 或 Tempo。
二十二、Elastic APM 分析 如果公司已经在用 Elasticsearch、Kibana、Logstash/Filebeat,那么 Elastic APM 是一个自然选项。
Elastic APM Java Agent 支持通过 -javaagent 接入:
1 2 3 4 5 6 java \ -javaagent:/opt/elastic/elastic-apm-agent.jar \ -Delastic.apm.service_name=order-service \ -Delastic.apm.server_url=http://apm-server:8200 \ -Delastic.apm.environment=prod \ -jar order-service.jar
优点:
和 Elasticsearch/Kibana 结合紧密。
日志检索、APM、指标可以统一看。
Java Agent 接入简单。
商业生态成熟。
缺点:
存储成本要认真评估。
Elastic Stack 版本和 License 策略要提前确认。
如果未来想完全厂商无关,还是要考虑 OpenTelemetry。
适合场景:
公司已有 ELK。
日志检索是核心排障入口。
希望 APM 和日志统一在 Kibana 里看。
二十三、Pinpoint 分析 Pinpoint 是面向大规模分布式系统的 APM 工具,早期在 Java APM 领域很有影响力。
它的特点:
Java Agent 接入。
能看应用拓扑。
能追踪事务调用。
对 Java Web 应用排障友好。
优点:
对 Java 事务链路分析较细。
UI 对调用关系展示直观。
适合传统 Java Web/微服务系统。
缺点:
部署组件相对重。
生态热度和标准化程度不如 OpenTelemetry。
国内 Java 微服务新项目通常会更多考虑 SkyWalking 或 OpenTelemetry。
适合:
已经在公司内部使用 Pinpoint。
传统 Java 系统较多。
团队熟悉 Pinpoint 运维。
新项目如果没有历史原因,我会优先评估 SkyWalking 或 OpenTelemetry。
二十四、SigNoz、Datadog、New Relic、Sentry 1. SigNoz SigNoz 是开源可观测性平台,主打 OpenTelemetry 原生,一体化提供:
Logs。
Metrics。
Traces。
APM。
Dashboard。
Alerts。
适合想要 OpenTelemetry 标准,又想要一体化 UI 的团队。
2. Datadog Datadog 是商业可观测性平台。
优点:
功能完整。
APM、日志、指标、基础设施监控、告警、安全能力一体化。
Agent 和云平台集成成熟。
缺点:
成本高。
数据出境、合规、预算要提前确认。
深度使用后迁移成本较高。
3. New Relic New Relic 也是成熟商业 APM 平台。
优点:
Java Agent 成熟。
APM 分析能力强。
对性能问题排查友好。
缺点类似 Datadog:成本、合规、平台绑定。
4. Sentry Sentry 更偏错误监控和性能追踪。
它适合:
前端错误监控。
后端异常聚合。
Release 维度问题定位。
用户影响范围分析。
它不是传统意义上最完整的 Java APM 替代品,但在错误治理上非常强。
二十五、方案选型矩阵
方案
日志 traceId
异步传播
服务拓扑
慢 SQL/RPC
指标
日志收集
接入成本
适合团队
MDC + Logback
是
需要自研
否
否
否
否
低
小团队/老项目
MDC + Logback + TTL
是
是
否
否
否
否
中
有异步线程池的项目
TLog
是
是
否
否
否
否
低
想快速日志追踪
SkyWalking
是
是
是
是
是
可集成
中
Java 微服务
Micrometer + OTel
是
是
取决于后端
取决于后端
是
取决于后端
中
Spring Boot 3 新项目
Zipkin
是
是
基础
基础
否
否
低
简单 Trace
Jaeger
是
是
是
是
否
否
中
云原生 Trace
Tempo
是
是
依赖 Grafana
依赖 Grafana
否
否
中
Grafana 技术栈
Elastic APM
是
是
是
是
是
是
中高
ELK 用户
Pinpoint
是
是
是
是
是
否
中高
传统 Java APM
SigNoz
是
是
是
是
是
是
中
OTel 一体化
Datadog/New Relic
是
是
是
是
是
是
高
有商业预算
二十六、推荐落地路线 阶段一:先统一日志 traceId 目标:
所有服务日志都输出 traceId、spanId、requestId。
HTTP/RPC 调用透传 traceId。
异步线程池不丢 traceId。
请求结束清理上下文。
技术选择:
1 MDC + Logback + TraceFilter + TaskDecorator + Feign/RestTemplate Interceptor
这个阶段最适合立刻做。
阶段二:接入日志平台 目标:
日志结构化。
traceId 可检索。
错误日志可聚合。
业务关键字段可筛选。
技术选择:
1 JSON Log + Filebeat/Fluent Bit/Vector + Elasticsearch/OpenSearch/Loki
阶段三:接入完整 Trace/APM 如果是 Java 微服务为主:
1 SkyWalking Java Agent + SkyWalking OAP + UI
如果希望长期标准化、跨语言:
1 OpenTelemetry Java Agent + OpenTelemetry Collector + Tempo/Jaeger/SigNoz/Elastic
阶段四:打通日志、指标、调用链、告警 目标:
从告警跳到 Trace。
从 Trace 跳到日志。
从日志反查 traceId。
从慢接口定位 SQL/RPC。
从错误聚合定位影响范围。
最终形态:
flowchart TD
A[用户请求] --> B[服务 Trace]
B --> C[Metrics 指标]
B --> D[Logs 日志]
B --> E[APM 调用链]
C --> F[Alert 告警]
D --> G[日志检索]
E --> H[拓扑与瓶颈分析]
F --> E
E --> D
D --> E
二十七、生产注意事项 1. 必须清理 MDC 每个入口都要:
1 2 3 4 5 6 try { TraceContext.set(context); doBusiness(); } finally { TraceContext.clear(); }
不清理就会出现线程复用污染。
2. traceId 不要放敏感信息 不要这样:
1 traceId=user_13800138000_order_123
这会把隐私信息扩散到所有日志、请求头、监控平台。
3. 不要日志爆炸 链路追踪不是让每行代码都打日志。
建议:
入口日志。
关键状态变更日志。
外部调用日志。
异常日志。
长耗时日志。
补偿/重试日志。
4. 采样要谨慎 日志 traceId 通常全量打印。
APM Trace 不一定全量采样。
生产建议:
1 2 3 4 management: tracing: sampling: probability: 0.05
高价值接口可以用规则提高采样率,比如支付、结算、下单。
5. 统一字段命名 不要 A 服务叫 traceId,B 服务叫 trace_id,C 服务叫 tid。
建议统一:
1 2 3 4 5 6 7 8 9 traceId spanId requestId userId tenantId biz scene step costMs
6. 兼容标准协议 自定义 X-Trace-Id 很好用,但不要完全封闭。
建议同时兼容:
W3C traceparent。
B3 Header。
内部 X-Trace-Id。
这样以后接入 OpenTelemetry、Zipkin、SkyWalking、网关、Service Mesh 都更轻松。
二十八、一个推荐的公司级方案 如果是普通 Java 微服务团队,我推荐这样落:
1. 短期方案 1 MDC + Logback + TraceFilter + TaskDecorator + Feign Interceptor
必须完成:
HTTP 入口 traceId。
日志打印 traceId。
异步线程池传播。
Feign/RestTemplate 透传。
统一日志字段规范。
2. 中期方案 1 JSON Log + 日志平台 + traceId 检索
必须完成:
JSON 日志。
日志采集。
traceId 索引。
errorCode、bizId、costMs 索引。
异常日志聚合。
3. 长期方案 二选一:
适合 Java 微服务为主,想快速自建 APM。
或者:
1 OpenTelemetry + Collector + Tempo/SigNoz/Elastic
适合长期标准化、跨语言、云原生。
二十九、常见错误 1. 只在 Controller 里 MDC.put 问题:
异步线程丢。
下游调用不透传。
定时任务没有。
MQ 消费没有。
正确做法:
入口统一 Filter,异步统一包装,下游统一 Interceptor。
2. 忘记 finally 清理 错误:
1 2 MDC.put("traceId" , traceId); filterChain.doFilter(request, response);
正确:
1 2 3 4 5 6 try { MDC.put("traceId" , traceId); filterChain.doFilter(request, response); } finally { MDC.clear(); }
3. 线程池不加 TaskDecorator 错误:
1 executor.setThreadNamePrefix("biz-" );
正确:
1 executor.setTaskDecorator(new TraceTaskDecorator ());
4. 同时接入多套 Trace,字段打架 比如:
自研 MDC 生成 traceId。
TLog 又生成一个 traceId。
SkyWalking 又生成一个 traceId。
OpenTelemetry 又生成一个 traceId。
最后日志里出现多个链路 ID,排查问题更乱。
建议:
短期自研时,自研 traceId 是主。
接入 APM 后,以 APM traceId 为主。
自定义 requestId 可以保留。
不要让多个框架重复生成主 traceId。
三十、最终建议 如果只问一句话:Spring Boot 日志链路追踪应该怎么做?
我的答案是:
先用 MDC + Logback 统一日志 traceId,用 TaskDecorator/TTL 解决异步传播,用 Interceptor 解决服务间透传;如果系统已经是微服务规模,就不要停在日志层,继续接入 SkyWalking 或 OpenTelemetry。
更具体一点:
老项目:MDC + Logback + TTL 先救火。
Spring Cloud/Dubbo 快速方案:评估 TLog。
Java 微服务 APM:优先评估 SkyWalking。
Spring Boot 3 长期方案:优先评估 Micrometer Tracing + OpenTelemetry。
已有 ELK:评估 Elastic APM。
已有 Grafana:评估 OpenTelemetry + Tempo。
预算充足且追求省心:评估 Datadog/New Relic。
参考资料
Logback MDC Manual: https://logback.qos.ch/manual/mdc.html
SLF4J MDC API: https://www.slf4j.org/api/org/slf4j/MDC.html
Spring Boot Logging: https://docs.spring.io/spring-boot/reference/features/logging.html
Spring Boot Observability: https://docs.spring.io/spring-boot/reference/actuator/observability.html
Spring Boot Tracing: https://docs.spring.io/spring-boot/reference/actuator/tracing.html
Micrometer Tracing Reference: https://docs.micrometer.io/tracing/reference/
OpenTelemetry Java Instrumentation: https://opentelemetry.io/docs/languages/java/instrumentation/
OpenTelemetry Spring Boot Starter: https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/
Alibaba TransmittableThreadLocal: https://github.com/alibaba/transmittable-thread-local
TLog 官方文档: https://tlog.yomahub.com/
Dromara TLog GitHub: https://github.com/dromara/TLog
Apache SkyWalking Documentation: https://skywalking.apache.org/docs/
Apache SkyWalking Java Agent: https://skywalking.apache.org/docs/skywalking-java/
Zipkin: https://zipkin.io/
Jaeger Documentation: https://www.jaegertracing.io/docs/
Grafana Tempo Documentation: https://grafana.com/docs/tempo/latest/
Elastic APM Java Agent: https://www.elastic.co/docs/reference/apm/agents/java
Pinpoint APM: https://pinpoint-apm.github.io/pinpoint/
SigNoz Documentation: https://signoz.io/docs/
Datadog Java APM: https://docs.datadoghq.com/tracing/trace_collection/dd_libraries/java/
New Relic Java Agent: https://docs.newrelic.com/docs/apm/agents/java-agent/
Sentry Spring Boot: https://docs.sentry.io/platforms/java/guides/spring-boot/
启示录 日志链路追踪这件事,最怕的是“看起来有 traceId,实际上到关键地方就断了”。
真正可用的链路追踪,不是某一行配置,也不是某一个 starter,而是一套上下文规范:入口生成、过程传播、异步恢复、出口透传、最终清理。
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。