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

Spring Sleuth:如何为StreamingResponseBody回调日志添加Trace ID

解决StreamingResponseBody回调中日志丢失Trace ID的问题

这个问题我之前也踩过坑,根源在于**StreamingResponseBody的回调逻辑是在异步线程中执行的**——Spring Sleuth的Trace/Span上下文默认绑定在处理请求的主线程上,当回调切换到其他线程时,上下文没有被自动传递,导致日志里看不到Trace ID。

下面给你两种最可靠的解决方法:


方法一:手动传递并激活Span上下文

这是最直接可控的方案,通过Sleuth提供的Tracer工具,手动捕获请求主线程的Span,然后在回调中激活它。

步骤:

  1. 注入Tracer实例:在你的Controller类中注入Sleuth的Tracer:
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RestController
public class FileStreamingController {

    @Autowired
    private Tracer tracer;
    
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(FileStreamingController.class);
  1. 捕获当前Span并在回调中激活:在返回StreamingResponseBody前,先获取当前请求的Span,然后在回调逻辑中用withSpanInScope包裹需要打日志的代码:
@GetMapping("/stream/{id}")
    public StreamingResponseBody streamFile(@PathVariable String id) {
        // 捕获当前请求的Span(来自主线程的上下文)
        Span currentSpan = tracer.currentSpan();
        
        return outputStream -> {
            // 激活Span上下文,让当前异步线程继承Trace ID
            try (Tracer.SpanInScope spanScope = tracer.withSpanInScope(currentSpan)) {
                // 这里的日志会自动带上Trace ID
                log.info("开始流式输出文件,ID: {}", id);
                
                // 你的文件流式输出逻辑:读取文件、写入outputStream
                // 示例:
                // Files.copy(Paths.get("/path/to/your/file"), outputStream);
                
                log.info("文件流式输出完成,ID: {}", id);
            } catch (Exception e) {
                log.error("流式输出文件失败,ID: {}", id, e);
            }
        };
    }
}

用try-with-resources语法会自动关闭SpanInScope,避免资源泄漏,非常推荐。


方法二:让异步线程池支持Sleuth上下文传递

如果你的项目中用了自定义线程池来处理流式任务,可以让Sleuth包装这个线程池,这样所有提交到池中的任务都会自动继承请求的Trace ID。

配置自定义线程池:

import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class AsyncConfig {

    @Bean
    public Executor streamingTaskExecutor(Tracer tracer) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("streaming-worker-");
        executor.initialize();
        
        // 用Sleuth的LazyTraceExecutor包装线程池,实现上下文传递
        return new LazyTraceExecutor(tracer, executor);
    }
}

在Controller中使用该线程池:

如果你的流式逻辑需要异步执行,可以把任务提交到这个线程池,日志就会自动带上Trace ID:

@Autowired
private Executor streamingTaskExecutor;

@GetMapping("/stream/{id}")
public StreamingResponseBody streamFile(@PathVariable String id) {
    return outputStream -> {
        streamingTaskExecutor.execute(() -> {
            try {
                log.info("开始异步流式输出文件,ID: {}", id);
                // 你的文件输出逻辑
            } catch (Exception e) {
                log.error("异步流式输出失败,ID: {}", id, e);
            }
        });
    };
}

关键注意事项

  • 确保Spring Sleuth版本和Spring Boot版本兼容,避免依赖冲突。
  • 如果用方法一,不要在回调中随意修改Span(比如创建子Span),除非你明确需要,否则可能影响主线程的上下文。
  • 如果你用Spring Boot默认的异步线程池,spring.sleuth.async.enabled=true(默认开启)会让Sleuth自动包装它,但手动传递Span的方式更可控。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 08:11:05