Implementing MDC TraceId Across HTTP, MQ, Thread Pools & Jobs in Spring Boot
This guide explains how to use Spring's MDC to generate and propagate a traceId through HTTP requests, RabbitMQ messages, thread‑pool tasks, and XXL‑Job scheduled jobs, enabling unified log tracing without adding extra dependencies.
Introduction
When troubleshooting a Spring Boot project, logs are essential but hard to correlate with specific requests under high traffic or long call chains. Existing solutions like Zipkin, OTel, Jaeger, etc., use traceId. This article shows how to use Spring’s MDC to set a traceId without extra dependencies.
MDC Implementation Principle
MDC (Mapped Diagnostic Context) stores thread‑local context via ThreadLocal<Map<String, String>>. When MDC.put("traceId", "xxx") is called, the value is stored in the current thread and automatically injected into log patterns.
Put traceId into ThreadLocal.
Log framework reads traceId from MDC and inserts it into the log template.
Each thread has an independent MDC, avoiding interference.
Logback Configuration
Add a placeholder for traceId 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 output, add "traceId": "%X{traceId}" to the pattern.
HTTP Request Handling
Define a filter that generates a UUID traceId, puts it into MDC, and clears it after the request:
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 Handling
Wrap Runnable and Callable to copy MDC context before execution and restore it afterwards:
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, obtain the current traceId from MDC, add it to the message header, and on the consumer side use an Advice to restore the traceId in MDC:
public <T> 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
Use an AOP aspect to generate a traceId before each XXL‑Job method and clear it afterwards:
@Aspect
@Component
public class XxlJobTraceAspect {
@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);
}
}
private static final String TRACE_ID = "traceId";
}Demo Endpoints
Two test controllers illustrate that the same traceId appears in the main request, asynchronous execution, MQ handling, and scheduled jobs.
@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();
}Result
Log screenshots show that the traceId is consistent across HTTP request, RabbitMQ producer/consumer, thread‑pool tasks, and XXL‑Job executions.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
