
在微服务架构中,一个业务请求往往跨越多个服务节点,传统的日志查看方式难以将分散在各处的日志串联起来形成完整的调用链。本文介绍一套基于 SLF4J MDC(Mapped Diagnostic Context)实现的轻量级分布式追踪框架,通过 Servlet 过滤器、HTTP 客户端拦截器、WebFlux 过滤器以及任务装饰器,实现 TraceId 在全链路中的自动传递与日志关联。该框架无需引入重量级 APM 系统,即可快速为 Spring Boot 应用提供基础的全链路追踪能力。
分布式追踪是微服务可观测性的重要组成部分。常见的解决方案如 Zipkin、Jaeger、SkyWalking 等,虽然功能强大,但对于中小型项目而言可能存在接入成本高、资源占用大等问题。一种更轻量的替代方案是利用 SLF4J MDC 机制:将 TraceId 存储在线程上下文中,日志模板自动输出该 ID,并通过 HTTP 请求头在服务间传递,从而实现调用链的标识与串联。
本文介绍的框架正是基于上述思想,为 Spring Boot 应用提供了一套开箱即用的全链路追踪能力,同时支持传统的 Servlet 容器和响应式 WebFlux 环境,并能处理异步任务场景下的上下文传递。
TraceFilter 实现了 jakarta.servlet.Filter,是整个追踪链路的入口(对外接收请求)。
String traceId = httpRequest.getHeader(DefaultConsts.HTTP_TRACE_ID);
if (traceId == null || traceId.isEmpty()) {
traceId = RestUtils.generateTraceId();
}
MDC.put(DefaultConsts.HTTP_TRACE_ID, traceId);
httpResponse.setHeader(DefaultConsts.HTTP_TRACE_ID, traceId);核心职责:
traceId、spanId、parentSpanId,若缺失则自动生成traceId 和 spanId 回写到响应头,方便客户端或网关获取该过滤器通过配置类 TraceConfiguration 注册为 FilterRegistrationBean,并设置为最高优先级(Ordered.HIGHEST_PRECEDENCE),确保在最早阶段完成追踪 ID 的初始化。
用于 Spring 的 RestTemplate 同步 HTTP 客户端,在发出请求前自动注入追踪头信息。
String traceId = MDC.get(DefaultConsts.HTTP_TRACE_ID);
if (traceId != null) {
request.getHeaders().add(DefaultConsts.HTTP_TRACE_ID, traceId);
}
String spanId = MDC.get(DefaultConsts.HTTP_TRACE_SPAN_ID);
if (spanId != null) {
String childSpanId = RestUtils.generateSpanId();
request.getHeaders().add(DefaultConsts.HTTP_TRACE_PARENT_SPAN_ID, spanId);
request.getHeaders().add(DefaultConsts.HTTP_TRACE_SPAN_ID, childSpanId);
}设计要点:
spanId,生成子 spanId 并放入请求头parentSpanId),供下游服务还原调用树RestClient(通过 RestClientCustomizer)针对 Spring WebFlux 的 WebClient 设计的 ExchangeFilterFunction,除了添加请求头外,还需处理 Reactor 上下文中的 MDC 传递问题。
Map<String, String> currentMdc = MDC.getCopyOfContextMap();
Map<String, String> toPropagate = currentMdc.entrySet().stream()
.filter(entry -> DefaultConsts.HTTP_MDC_KEYS.contains(entry.getKey()))
.collect(...);
ClientRequest filteredRequest = ClientRequest.from(request)
.headers(headers -> toPropagate.forEach(headers::set))
.build();
return next.exchange(filteredRequest)
.contextWrite(Context.of(DefaultConsts.MDC_CONTEXT_KEY, toPropagate))
.doOnEach(signal -> { /* 从响应头更新 MDC */ });关键点:
contextWrite 将 MDC 快照存入 Reactor 上下文,而非依赖线程局部变量DefaultConsts.HTTP_MDC_KEYS 中的特定键值对,避免过度传递当应用使用 @Async 或手动提交 Runnable 到线程池时,子线程默认无法继承父线程的 MDC。TraceTaskDecorator 通过装饰 Runnable 解决了这一痛点。
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};该装饰器通过 CompositeTaskDecorator 机制集成到 Spring 的 TaskDecorator 体系中,可与其他装饰器组合使用。
@Configuration 类,条件化注册所有追踪组件:
Bean | 作用 |
|---|---|
| 注册 Servlet 过滤器,支持通过 |
| 为所有 |
| 为 |
| 为 |
| 为异步任务提供 MDC 传递能力 |
框架预留了 ServletFilterOptions 配置类,用户可通过 application.yml 灵活调整过滤器行为,例如仅对特定路径启用追踪。
TraceFilter 拦截进入的 HTTP 请求 X-Trace-Id 等字段,若无则生成 TraceRestTemplateInterceptor 从 MDC 取出当前 TraceId 和 SpanId,自动添加到请求头中,并生成子 SpanId TraceExchangeFilterFunction 从 MDC 取快照,存入 Reactor 上下文并发起请求,响应后可能从响应头更新 MDCTraceTaskDecorator 捕获当前 MDC 快照 服务 A(入口)收到请求 → 生成 TraceId=abc,SpanId=1 → 调用服务 B → 请求头携带 X-Trace-Id: abc, X-Parent-Span-Id: 1, X-Span-Id: 2 → 服务 B 日志输出 [trace=abc, span=2, parent=1] → 服务 B 异步处理 → 异步线程日志输出相同的 TraceId。
框架假设 tutorials4j.framework.common.core.DefaultConsts 中定义了常量键名(如 HTTP_TRACE_ID、HTTP_MDC_KEYS 等),实际集成时需确保类路径包含这些常量。
tutorials4j:
web:
http:
trace:
name: customTraceFilter
url-patterns:
- "/api/*"
- "/inner/*"
order: -100
dispatcher-types:
- REQUEST
- FORWARD为了在日志中输出追踪 ID,需要在日志配置文件(如 logback-spring.xml)中引用 MDC 变量:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [trace=%X{X-Trace-Id},span=%X{X-Span-Id}] %-5level %logger{36} - %msg%n</pattern>MDC.put 可能失效。本框架采用 contextWrite 将 MDC 快照存入 Reactor 上下文,并在操作符链中恢复,确保了响应式链路的正确性。但需注意不要在 subscribe 之后的异步边界中使用 MDC.get,应通过上下文传播。TraceTaskDecorator 仅对通过 Spring TaskExecutor 执行的任务生效。如果直接使用 new Thread() 或未配置装饰器的线程池,MDC 将无法传递。推荐统一使用 Spring 的 @Async 或 ThreadPoolTaskExecutor 并设置 taskDecorator。DefaultConsts.HTTP_MDC_KEYS 限制了传播的键名,避免将整个 MDC 内容放入请求头,从而防止内部敏感变量泄露。ArrayMap 等轻量结构或在常量层面硬编码传播字段。RestUtils.generateSpanId(),实现应保证全局唯一性且尽量简短(如 16 位十六进制)。建议与 TraceId 生成算法保持一致。本文介绍的追踪框架基于 MDC 实现,具有以下优点:
CompositeTaskDecorator 可组合其他上下文传递逻辑(如安全性、租户 ID)通过该框架,开发者可以在不改造业务代码的前提下,快速获得全链路日志关联能力,显著提升微服务架构下问题定位的效率。对于需要更完整调用链可视化、性能分析等高级功能的场景,建议在此基础上接入 Zipkin 或 Jaeger,本框架的 TraceId 生成与传递机制可以平滑迁移。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。