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.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
How to Propagate TraceId with Spring MDC Across HTTP, MQ, Thread Pools, and Jobs

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.

Logback pattern example
Logback pattern example

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.

HTTP traceId log
HTTP traceId log
MQ traceId propagation
MQ traceId propagation
Thread‑pool traceId
Thread‑pool traceId
XXL‑Job traceId
XXL‑Job traceId
JavaSpringloggingSpring BootTraceIdmdc
Java Tech Enthusiast
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.