Spring Boot 4+Logbook 4异步响应日志缺失trace-id求助
问题背景
将应用从Spring Boot 3.5/Logbook 3.12迁移至Spring Boot 4/Logbook 4.0.4后,Spring Web MVC控制器的异步HTTP响应日志中,Logbook生成的日志缺失trace-id。异步响应通过控制器返回Mono类型实现(符合Spring官方规范),后续将返回类型改为CompletableFuture<T>后问题仍存在,排除Reactor依赖影响。
已尝试无效操作
- 添加
ContextPropagatingTaskDecoratorBean - 在单元测试前调用
Hooks.enableAutomaticContextPropagation()
使用依赖
org.springframework.boot:spring-boot-starter-actuatororg.springframework.boot:spring-boot-micrometer-tracing-opentelemetryio.micrometer:micrometer-tracing-bridge-otel
根因推测
javax.servlet.AsyncListener::onComplete方法未在Micrometer的context-snapshot中被调用。LogbookFilter会注册实现javax.servlet.AsyncListener的LogbookAsyncListener,当集成Micrometer Tracing时,Spring Boot需正确对Servlet栈埋点,确保该方法在重建的上下文快照中执行,否则Logbook无法获取trace-id。
可行修复方案
方案1:包装AsyncListener实现上下文恢复
自定义AsyncListener包装类,在onComplete方法中手动恢复trace上下文,确保Logbook执行时能获取到trace-id:
@Component public class TracingAsyncListenerWrapper implements AsyncListener { private final AsyncListener delegate; private final ContextSnapshot snapshot; public TracingAsyncListenerWrapper(AsyncListener delegate) { this.delegate = delegate; this.snapshot = ContextSnapshot.captureAll(); } @Override public void onComplete(AsyncEvent event) throws IOException { try (ContextSnapshot.Scope scope = snapshot.restore()) { delegate.onComplete(event); } } @Override public void onTimeout(AsyncEvent event) throws IOException { delegate.onTimeout(event); } @Override public void onError(AsyncEvent event) throws IOException { delegate.onError(event); } @Override public void onStartAsync(AsyncEvent event) throws IOException { delegate.onStartAsync(event); } @Bean public Filter tracingAsyncListenerFilter() { return (request, response, chain) -> { if (request instanceof HttpServletRequest httpServletRequest) { AsyncContext asyncContext = httpServletRequest.startAsync(); // 替换原有AsyncListener为包装后的实例 Collection<AsyncListener> listeners = new ArrayList<>(asyncContext.getListeners()); listeners.forEach(asyncContext::removeListener); listeners.forEach(listener -> asyncContext.addListener(new TracingAsyncListenerWrapper(listener))); } chain.doFilter(request, response); }; } }
方案2:调整Filter执行顺序
确保Micrometer的TracingFilter在LogbookFilter之前执行,保证上下文先被初始化:
# application.properties spring.servlet.filter.order.micrometer-tracing=5 spring.servlet.filter.order.logbook=10
方案3:升级依赖版本
检查Micrometer Tracing或Spring Boot的最新补丁版本,确认是否有针对异步Servlet上下文传播的修复,尝试升级至对应版本。
方案4:自定义Logbook异步监听器
继承LogbookAsyncListener,在构造时保存当前trace-id,执行onComplete时手动恢复上下文:
public class TracingLogbookAsyncListener extends LogbookAsyncListener { private final String traceId; public TracingLogbookAsyncListener(Logbook logbook, HttpServletRequest request, HttpServletResponse response) { super(logbook, request, response); this.traceId = TraceContext.current().traceId(); } @Override public void onComplete(AsyncEvent event) throws IOException { try (Scope scope = TraceContext.newBuilder().traceId(traceId).build().makeCurrent()) { super.onComplete(event); } } }
同时自定义LogbookFilter,替换默认的LogbookAsyncListener为上述自定义实现。
内容的提问来源于stack exchange,提问作者Krzysztof Tomaszewski

