I'm using Spring Boot 3.5.7 with Micrometer Tracing (OpenTelemetry bridge) to add a traceId to my logs. I have a REST endpoint and a custom filter like this:
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthRestController {
private final PaymentService paymentService;
@PostMapping("/token")
public String generateToken(@RequestBody TokenRequest request) throws Exception {
return paymentService.createToken(request);
}
}
@Slf4j
@RequiredArgsConstructor
public class RedisJwtAuthenticationFilter extends OncePerRequestFilter {
private final PaymentSessionService paymentSessionService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String txn = request.getHeader("X-Transaction-Id");
log.info("FILTER THREAD = {}", Thread.currentThread().getName());
if (StringUtils.isNotBlank(txn)) {
if (!paymentSessionService.sessionExists(txn)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
filterChain.doFilter(request, response);
}
}
public boolean sessionExists(final String txn) {
log.info("REDIS THREAD = {}", Thread.currentThread().getName());
final String key = ENDA_TXN + txn;
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (DataAccessException e) {
throw new RedisUnavailableException("Redis is unavailable while checking the session", e);
}
}
@Bean
@Order(2)
public SecurityFilterChain filterChain(HttpSecurity http,
ApplicationProperties applicationProperties,
AesGcmJwtDecoder aesGcmJwtDecoder,
JwtAuthenticationConverter jwtAuthenticationConverter,
RedisJwtAuthenticationFilter redisJwtAuthenticationFilter) throws Exception {
http
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.addFilterAfter(redisJwtAuthenticationFilter, SecurityContextHolderFilter.class)
.headers(headers ->
headers
.contentSecurityPolicy(csp -> csp.policyDirectives(applicationProperties.getSecurity().getContentSecurityPolicy()))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
.permissionsPolicyHeader(permissions ->
permissions.policy("camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()")
)
)
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.sessionManagement(mgmt -> mgmt.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(aesGcmJwtDecoder)
.jwtAuthenticationConverter(jwtAuthenticationConverter)
)
);
return http.build();
}
Logs example:
2025-11-19 09:47:31.611 [traceId: a5dba50bef617c87a] [http-nio-9008-exec-3] AuthRestController - Finish execution
2025-11-19 09:47:31.629 [traceId: c2aa664e25e67da2] [http-nio-9008-exec-4] RedisJwtAuthenticationFilter - FILTER THREAD
As you can see:
The controller executes on http-nio-9008-exec-3
The filter executes on http-nio-9008-exec-4
Because of this thread change, Micrometer generates a new traceId, and I can't track the transaction across logs.
Notes:
I'm using Jedis / StringRedisTemplate in the filter (synchronous, blocking)
I do not use @Async or CompletableFuture
WebClient calls are .block()ed
I also have @EnableAspectJAutoProxy(proxyTargetClass = true) but I don't think it causes this
Question:
Why does the thread change between the controller and my filter in Tomcat?
How can I keep the same thread (or propagate the same traceId) across the filter and the controller for correct tracing?