You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    }
}

四、关键问题解答

  1. TraceId 与 SpanId 的复用规则

    • TraceId:必须全程复用,它是整个追踪链路的唯一标识,确保所有相关请求都归属于同一条链路。
    • SpanId:绝对不能复用,每个独立的调用/处理步骤都需要生成新的 SpanId。客户端的 SpanId 作为服务端 Span 的 ParentSpanId,以此构建清晰的父子层级关系。
  2. ParentId 的处理方式

    • 客户端:在发起请求时,将当前 Span 的 SpanId 放入 X-B3-ParentSpanId 请求头中传递给服务端。
    • 服务端:解析该请求头,将其作为新创建 Span 的 ParentId,这样 Sleuth 就能正确识别链路的调用层级。

五、额外注意事项

  • 调整服务端过滤器的优先级,确保它在 Jersey 的过滤器之前执行,保证追踪上下文在 Jersey 处理请求前已初始化。
  • 如果使用异步请求,需配置 CurrentTraceContext 支持异步上下文传递(例如使用 ThreadLocalCurrentTraceContext 或适配线程池的实现)。

内容的提问来源于stack exchange,提问作者Klapsa2503

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 06:35:14