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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing MDC TraceId Across HTTP, MQ, Thread Pools & Jobs in Spring Boot

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPloggingSpring BootRabbitMQThread PoolXXL-JobTraceIdmdc
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.