Play框架中OpenTracing分布式追踪上下文传播与生命周期管理
实现OpenTracing分布式追踪全局可用的实践方案
我来给你梳理一套落地性强的实现思路,核心是靠请求拦截/中间件把追踪Span绑定到整个请求生命周期里,既能解析上游传递的头信息,又能让业务代码随时拿到trace-id打日志、传给下游,最后还能自动清理。
核心逻辑拆解
不管你用什么后端框架,这套逻辑都通用:
- 请求入口解析头:从HTTP请求头里提取OpenTracing标准的追踪字段(比如
X-B3-TraceId、X-B3-SpanId,或者Jaeger的uber-trace-id),用Tracer创建对应的SpanContext,再启动当前请求的Span。 - 绑定到请求上下文:把Span存到和当前请求强绑定的上下文容器里(Java用ThreadLocal/AsyncLocal,Node.js用AsyncLocalStorage,Python用contextvars),这样控制器、服务层、工具类都能直接拿。
- 日志自动带trace-id:把trace-id放到日志的MDC(映射诊断上下文)里,日志配置里直接引用,不用业务代码每次手动传。
- 下游请求自动传追踪头:发起下游HTTP请求时,从上下文取出Span,把追踪头注入到请求头里,无缝传给下游服务。
- 请求收尾清理:响应返回后,Finish Span,同时清理上下文和MDC里的信息,避免线程复用导致的串数据问题。
以Spring Boot为例的具体实现
1. 写个拦截器处理Span的创建与清理
先整个拦截器,在请求进来时创建Span并绑定,请求结束时收尾:
@Component public class OpenTracingInterceptor implements HandlerInterceptor { private final Tracer tracer; // 构造注入Tracer(比如JaegerTracer) public OpenTracingInterceptor(Tracer tracer) { this.tracer = tracer; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从请求头提取上游的追踪上下文 SpanContext parentContext = tracer.extract( Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(request) ); // 创建当前请求的Span,把上游上下文作为父Span Span currentSpan = tracer.buildSpan(request.getRequestURI()) .asChildOf(parentContext) .withTag(Tags.HTTP_METHOD.getKey(), request.getMethod()) .withTag(Tags.HTTP_URL.getKey(), request.getRequestURL().toString()) .start(); // 把Span绑定到当前线程,方便后续获取 tracer.activateSpan(currentSpan); // 同时把trace-id放到MDC,日志直接用 MDC.put("trace-id", currentSpan.context().toTraceId()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Span activeSpan = tracer.activeSpan(); if (activeSpan != null) { // 如果请求出错,给Span打错误标签 if (ex != null || response.getStatus() >= 400) { activeSpan.setTag(Tags.ERROR, true); if (ex != null) { activeSpan.log(Map.of("event", "error", "error.stack", ex.getMessage())); } } // 结束Span activeSpan.finish(); // 关闭Scope,清理线程绑定 tracer.scopeManager().active().close(); // 清理MDC里的trace-id MDC.remove("trace-id"); } } }
然后注册这个拦截器到Spring MVC:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final OpenTracingInterceptor tracingInterceptor; public WebMvcConfig(OpenTracingInterceptor tracingInterceptor) { this.tracingInterceptor = tracingInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { // 对所有请求生效 registry.addInterceptor(tracingInterceptor).addPathPatterns("/**"); } }
2. 日志配置自动带上trace-id
比如用Logback的话,修改logback-spring.xml的日志格式,加上%X{trace-id}:
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X{trace-id}%n</pattern> </encoder> </appender>
这样业务代码打日志的时候,不用手动传trace-id,日志里自动就带上了。
3. 下游请求自动传递追踪头
用RestTemplate调用下游服务时,把当前Span的追踪头注入到请求头里:
@Service public class DownstreamService { private final RestTemplate restTemplate; private final Tracer tracer; public DownstreamService(RestTemplate restTemplate, Tracer tracer) { this.restTemplate = restTemplate; this.tracer = tracer; } public String callOtherService(String url) { HttpHeaders headers = new HttpHeaders(); // 把当前Span的追踪信息注入到请求头 tracer.inject( tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersInjectAdapter(headers) ); HttpEntity<Void> entity = new HttpEntity<>(headers); return restTemplate.exchange(url, HttpMethod.GET, entity, String.class).getBody(); } }
踩过的坑要注意
- 异步处理的Span传递:如果用了线程池做异步任务,ThreadLocal里的Span会丢失。这时候可以用Spring的
TaskDecorator把Span传递到异步线程里,或者在异步任务里手动重新激活Span。 - 上下文一定要清理:如果请求结束后没清理Span和MDC,线程池复用的时候会把上个请求的trace-id带到新请求里,日志就串了。
- 兼容不同追踪系统的头格式:比如Zipkin用
X-B3-*系列头,Jaeger用uber-trace-id,要确保你的Tracer配置了正确的格式,不然解析和注入都会出错。
内容的提问来源于stack exchange,提问作者Thayne
相关产品推荐
相关产品推荐

