How to Propagate TraceId with Spring MDC Across HTTP, MQ, Thread Pools, and Jobs
This guide explains how to use Spring's built‑in Mapped Diagnostic Context (MDC) to generate a traceId for each request, configure Logback to include it in logs, and propagate the traceId through HTTP filters, RabbitMQ messages, thread‑pool tasks, and XXL‑Job scheduled jobs, complete with code examples and configuration snippets.
Overview
In high‑traffic Spring Boot services it is difficult to correlate log entries with the originating HTTP request. A lightweight solution is to generate a unique traceId for each request, store it in the SLF4J/Logback MDC (Mapped Diagnostic Context), and let the logging pattern inject the value into every log line. The same traceId is propagated to asynchronous thread‑pool tasks, RabbitMQ messages, and scheduled jobs (XXL‑Job).
MDC Mechanism
MDC is a ThreadLocal<Map<String, String>> maintained by Logback. MDC.put("traceId", value) stores the identifier in the current thread; Logback resolves %X{traceId} in the log pattern to the stored value. Because each thread has its own MDC, contexts are isolated unless explicitly copied.
Logback Configuration
Add a traceId placeholder to the console and file patterns in logback-spring.xml:
<property name="CONSOLE_LOG_PATTERN" value="${DEFAULT_CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} [traceId:%X{traceId}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>For file appenders add "traceId": "%X{traceId}" to the pattern map.
HTTP Request Filter
Implement a OncePerRequestFilter that creates a UUID‑based traceId, puts it into MDC, and removes it after the request finishes.
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
filterChain.doFilter(request, response);
return;
}
try {
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}Thread‑Pool Context Propagation
Wrap Runnable and Callable tasks so that the MDC map of the submitting thread is copied before execution and restored afterwards. This prevents traceId leakage between reused pool threads.
public void execute(Runnable task) {
defaultThreadPoolExecutor.execute(wrap(task, MDC.getCopyOfContextMap()));
}
public <T> Future<T> submit(Callable<T> task) {
return defaultThreadPoolExecutor.submit(wrap(task, MDC.getCopyOfContextMap()));
}
private Runnable wrap(Runnable task, Map<String, String> contextMap) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
MDC.clear();
}
try {
task.run();
} finally {
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}
private <T> Callable<T> wrap(Callable<T> task, Map<String, String> contextMap) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
MDC.clear();
}
try {
return task.call();
} finally {
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}RabbitMQ Integration
When sending a message, read the current traceId (or generate one) and store it in the AMQP header. On the consumer side, define an Advice bean that extracts the header and puts it into MDC before the listener method runs.
public void sendMq(MqEnum.TypeEnum typeEnum, MqMessage<T> message) {
rabbitTemplate.convertAndSend(MqEnum.Exchange.EXCHANGE_NAME, typeEnum.getRoutingKey(), message, msg -> {
String traceId = MDC.get(TRACE_ID);
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
}
msg.getMessageProperties().getHeaders().put(TRACE_ID, traceId);
return msg;
});
}
@Bean
public Advice traceIdAdvice() {
return (MethodInterceptor) invocation -> {
Object[] args = invocation.getArguments();
String traceId = null;
for (Object arg : args) {
if (arg instanceof Message) {
traceId = (String) ((Message) arg).getMessageProperties().getHeaders().get(TRACE_ID);
break;
}
}
if (traceId != null) {
MDC.put(TRACE_ID, traceId);
}
try {
return invocation.proceed();
} finally {
MDC.remove(TRACE_ID);
}
};
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory, Advice traceIdAdvice) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAdviceChain(traceIdAdvice);
return factory;
}XXL‑Job Scheduled Tasks
Define a global AOP aspect that intercepts all methods annotated with @XxlJob. The aspect generates a traceId, puts it into MDC for the job execution, and clears it afterwards.
@Aspect
@Component
public class XxlJobTraceAspect {
private static final String TRACE_ID = "traceId";
@Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
public void xxlJobMethods() {}
@Around("xxlJobMethods()")
public Object aroundXxlJob(ProceedingJoinPoint joinPoint) throws Throwable {
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
try {
return joinPoint.proceed();
} finally {
MDC.remove(TRACE_ID);
}
}
}Verification Endpoints
Two controller methods demonstrate that the same traceId appears in the main thread, async tasks, thread‑pool executions, and RabbitMQ consumer logs.
@GetMapping("/test/traceId/async")
public Result<NullResult> traceId() {
log.info("main traceId");
asyncExecutors.execute(() -> log.info("execute traceId"));
asyncExecutors.submit(() -> { log.info("submit traceId"); return "ok"; });
List<Runnable> list = new ArrayList<>();
list.add(() -> log.info("execute list traceId"));
asyncExecutors.execute(list);
return Result.buildSuccess();
}
@GetMapping("/test/traceId/mq")
public Result<NullResult> mq() {
log.info("main mq traceId");
MqMessage<String> message = new MqMessage<>();
message.setData(JSON.toJSONString(Collections.emptyList()));
mqSender.sendMq(MqEnum.TypeEnum.PROP_SEND, message);
return Result.buildSuccess();
}Calling the endpoints produces logs similar to the screenshots below, showing identical traceId values across all involved components.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
