Implement Distributed Log Tracing in Spring Boot with MDC and traceId

This guide explains how to add a unique traceId to each request in a Spring Boot application, configure Logback/MDC to record it, propagate the ID across microservice calls and asynchronous threads using OpenFeign, RestTemplate, and TransmittableThreadLocal, enabling complete distributed log tracing.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Implement Distributed Log Tracing in Spring Boot with MDC and traceId

1. Overview

Backend developers often rely on system logs to troubleshoot issues. In a distributed cluster, logs from concurrent requests interleave, making it difficult to filter logs belonging to a single request, especially when asynchronous processing or downstream micro‑service calls are involved.

2. Solution Overview

The proposed solution assigns a unique traceId to every incoming request and stores it in the logging context using Logback's MDC. The traceId is added to the log pattern, allowing each log line to be correlated. No changes to existing log statements are required, keeping the code non‑intrusive.

2.1 Configure Log Pattern

<property name="CONSOLE_LOG_PATTERN" value="[%X{traceId}] [%-5p] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t@${PID}]  %c %M : %m%n"/>

Including %X{traceId} in the pattern makes the traceId appear in every log line.

2.2 Request‑Level traceId Generation and MDC Population

A servlet filter creates or extracts the traceId at the beginning of each request and puts it into MDC. The filter is registered early in the Spring container so that all subsequent processing inherits the context.

public class WebTraceFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws IOException, ServletException {
        try {
            String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER);
            if (StrUtil.isEmpty(traceId)) {
                MDCTraceUtils.addTrace();
            } else {
                MDCTraceUtils.putTrace(traceId);
            }
            filterChain.doFilter(request, response);
        } finally {
            MDCTraceUtils.removeTrace();
        }
    }
}

The filter uses the helper class MDCTraceUtils to manage the traceId lifecycle.

2.3 MDCTraceUtils Helper

public class MDCTraceUtils {
    public static final String KEY_TRACE_ID = "traceId";
    public static final String TRACE_ID_HEADER = "x-traceId-header";

    public static void addTrace() {
        String traceId = createTraceId();
        MDC.put(KEY_TRACE_ID, traceId);
    }

    public static void putTrace(String traceId) {
        MDC.put(KEY_TRACE_ID, traceId);
    }

    public static String getTraceId() {
        return MDC.get(KEY_TRACE_ID);
    }

    public static void removeTrace() {
        MDC.remove(KEY_TRACE_ID);
    }

    public static String createTraceId() {
        return IdUtil.getSnowflake().nextIdStr(); // uses Hutool Snowflake algorithm
    }
}

The IdUtil from Hutool generates a globally unique ID (UUID or Snowflake) that serves as the traceId.

2.4 Propagating traceId Across Services

When a request calls another micro‑service, the traceId must be sent in the HTTP header. The article demonstrates using Spring Cloud OpenFeign:

@FeignClient(name = "workshopService", url = "http://127.0.0.1:16688/textile", path = "/workshop")
public interface WorkshopService {
    @GetMapping("/list/temp")
    ResponseVO<List<WorkshopDTO>> getList();
}

A custom FeignInterceptor copies all incoming headers and adds the traceId header:

public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            HttpServletRequest req = attrs.getRequest();
            Enumeration<String> names = req.getHeaderNames();
            if (names != null) {
                while (names.hasMoreElements()) {
                    String name = names.nextElement();
                    if ("content-length".equals(name)) continue;
                    requestTemplate.header(name, req.getHeader(name));
                }
            }
        }
        String traceId = MDCTraceUtils.getTraceId();
        if (StringUtils.isNotBlank(traceId)) {
            requestTemplate.header(MDCTraceUtils.TRACE_ID_HEADER, traceId);
        }
    }
}

The same principle applies to RestTemplate or other HTTP clients by implementing a ClientHttpRequestInterceptor that adds the traceId header.

2.5 Asynchronous Execution and traceId Propagation

Logback’s default MDC implementation uses ThreadLocal, which does not transmit values to child threads in a thread pool. To solve this, the article replaces Logback’s LogbackMDCAdapter with a custom adapter that uses Alibaba’s TransmittableThreadLocal:

public class TtlMDCAdapter implements MDCAdapter {
    private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();
    // ... implementation of put, get, remove, clear, getCopyOfContextMap, setContextMap ...
    static {
        MDC.mdcAdapter = new TtlMDCAdapter();
    }
}

An initializer registers the adapter at application startup:

public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        TtlMDCAdapter.getInstance();
    }
}

With this adapter, the traceId stored in MDC is automatically transmitted to worker threads created by a thread pool.

2.6 Async Test Example

private ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("letter-pool-%d").build();
private ExecutorService fixedThreadPool = new ThreadPoolExecutor(
        Runtime.getRuntime().availableProcessors()*2,
        Runtime.getRuntime().availableProcessors()*40,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(Runtime.getRuntime().availableProcessors()*20),
        namedThreadFactory);

@GetMapping("/async")
public void testAsync() {
    log.info("打印日志了");
    fixedThreadPool.execute(() -> {
        log.info("异步执行了");
        try {
            Student s = null;
            s.getName();
        } catch (Exception e) {
            log.error("异步报错了:", e);
        }
    });
}

The resulting logs show the same traceId in both the main request thread and the asynchronous worker thread, confirming successful propagation.

3. Summary

By generating a unique traceId per request, inserting it into Logback’s MDC, configuring the log pattern, and ensuring the ID is passed through HTTP headers and asynchronous contexts (via OpenFeign, RestTemplate interceptors, and a TransmittableThreadLocal‑based MDC adapter), developers can efficiently locate all logs belonging to a single request across a distributed Spring Boot system.

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.

Spring BootDistributed TracingLogbackOpenFeignTransmittableThreadLocaltraceIdMDC
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

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.