Spring集成Jersey环境下如何使用Sleuth实现链路追踪?
解答
一、核心思路与当前实现的问题
你的方向是对的:通过 Jersey 客户端过滤器传递 B3 追踪头,服务端用 Servlet 过滤器解析上下文,但当前实现不符合 Sleuth 的追踪链路规范,存在链路断裂、Span 关系错误的问题,需要针对性调整。
二、客户端过滤器优化
当前代码仅传递了 TraceId 和 SpanId,但未创建子 Span,无法体现调用的父子关系。正确做法是:
- 基于当前 Span 创建子 Span,将子 Span 的 SpanId 作为请求的 SpanId,父 Span 的 SpanId 作为 ParentSpanId 传递。
- 在请求结束后结束子 Span,避免资源泄漏。
优化后的客户端过滤器:
@Component @RequiredArgsConstructor public class TracingClientRequestFilter implements ClientRequestFilter, ClientResponseFilter { public static final String TRACE_ID_HEADER = "X-B3-TraceId"; public static final String SPAN_ID_HEADER = "X-B3-SpanId"; public static final String PARENT_SPAN_ID_HEADER = "X-B3-ParentSpanId"; private static final String SPAN_NAME = "jersey-client-invoke"; private final Tracer tracer; private final CurrentTraceContext currentTraceContext; @Override public void filter(ClientRequestContext requestContext) throws IOException { // 创建子 Span Span clientSpan = tracer.nextSpan().name(SPAN_NAME); try (CurrentTraceContext.Scope scope = currentTraceContext.newScope(clientSpan.context())) { // 传递 B3 规范头 requestContext.getHeaders().add(TRACE_ID_HEADER, clientSpan.context().traceId()); requestContext.getHeaders().add(SPAN_ID_HEADER, clientSpan.context().spanId()); // 传递父 SpanId(如果存在) Optional.ofNullable(tracer.currentSpan()) .map(Span::context) .ifPresent(parentCtx -> requestContext.getHeaders().add(PARENT_SPAN_ID_HEADER, parentCtx.spanId()) ); // 存储 Span 到请求上下文,供响应过滤器结束使用 requestContext.setProperty("sleuth-client-span", clientSpan); } } @Override public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException { // 结束子 Span Span clientSpan = (Span) requestContext.getProperty("sleuth-client-span"); if (clientSpan != null) { clientSpan.end(); } } }
三、服务端过滤器优化
当前代码直接复用客户端的 SpanId,这会导致链路中无法区分客户端调用和服务端处理两个步骤。正确做法是:
- 解析请求头中的 TraceId 和 ParentSpanId(即客户端的 SpanId)。
- 创建服务端自己的 Span,复用 TraceId,将 ParentSpanId 设置为客户端的 SpanId,生成新的 SpanId。
- 请求结束后正确结束 Span。
优化后的服务端过滤器:
@Component @RequiredArgsConstructor public class TracingServerFilter extends OncePerRequestFilter { public static final String TRACE_ID_HEADER = "X-B3-TraceId"; public static final String PARENT_SPAN_ID_HEADER = "X-B3-ParentSpanId"; private static final String SPAN_NAME = "jersey-server-handle"; private final Tracer tracer; private final CurrentTraceContext currentTraceContext; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { String traceId = req.getHeader(TRACE_ID_HEADER); String parentSpanId = req.getHeader(PARENT_SPAN_ID_HEADER); Span serverSpan; if (StringUtils.hasText(traceId)) { // 基于客户端传递的追踪信息创建子 Span TraceContext.Builder ctxBuilder = tracer.traceContextBuilder().traceId(traceId); if (StringUtils.hasText(parentSpanId)) { ctxBuilder.parentId(parentSpanId); } serverSpan = tracer.nextSpan(ctxBuilder.build()).name(SPAN_NAME); } else { // 无追踪头时创建根 Span serverSpan = tracer.nextSpan().name(SPAN_NAME); } try (CurrentTraceContext.Scope scope = currentTraceContext.newScope(serverSpan.context())) { chain.doFilter(req, res); } finally { // 结束服务端 Span serverSpan.end(); } } }
四、关键问题解答
TraceId 与 SpanId 的复用规则
- TraceId:必须全程复用,它是整个追踪链路的唯一标识,确保所有相关请求都归属于同一条链路。
- SpanId:绝对不能复用,每个独立的调用/处理步骤都需要生成新的 SpanId。客户端的 SpanId 作为服务端 Span 的 ParentSpanId,以此构建清晰的父子层级关系。
ParentId 的处理方式
- 客户端:在发起请求时,将当前 Span 的 SpanId 放入
X-B3-ParentSpanId请求头中传递给服务端。 - 服务端:解析该请求头,将其作为新创建 Span 的 ParentId,这样 Sleuth 就能正确识别链路的调用层级。
- 客户端:在发起请求时,将当前 Span 的 SpanId 放入
五、额外注意事项
- 调整服务端过滤器的优先级,确保它在 Jersey 的过滤器之前执行,保证追踪上下文在 Jersey 处理请求前已初始化。
- 如果使用异步请求,需配置
CurrentTraceContext支持异步上下文传递(例如使用ThreadLocalCurrentTraceContext或适配线程池的实现)。
内容的提问来源于stack exchange,提问作者Klapsa2503
相关产品推荐
相关产品推荐

