Mastering MDC with Logback: Traceable Logging for Distributed Systems

This article explains how to use SLF4J's MDC with Logback to assign a unique trace ID to each request, propagate it across threads and services, and configure log patterns so that logs become fully traceable for easier debugging in distributed systems.

Lin is Dream
Lin is Dream
Lin is Dream
Mastering MDC with Logback: Traceable Logging for Distributed Systems

In distributed systems, service calls often span multiple applications and threads. Because requests frequently switch across threads and asynchronous processing, ordinary logs cannot effectively link a complete call, making troubleshooting difficult. To better trace the full execution path, we need to assign a unique identifier to each request and carry it throughout the call chain, printing it in logs so the whole process can be retrieved.

SLF4J provides MDC (Mapped Diagnostic Context) which stores and propagates context information within the same thread, commonly used for log tracing. This article demonstrates MDC with Logback, its usage, and source code analysis.

Function Overview

MDC's main functions include:

Call chain tracing Propagate request ID across the entire call chain for easier log analysis.

Log enrichment Automatically attach context information such as user ID, order number, etc., to logs.

Thread isolation MDC variables are effective only for the current thread and do not affect other requests.

Cross‑process calls Pass the request ID from the previous service to the next service.

How to Use

Below is a simple demonstration of using MDC; in real projects it should be applied in a logging aspect for uniform handling.

Single‑thread environment

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class MDCDemo {
    private static final Logger logger = LoggerFactory.getLogger(MDCDemo.class);

    public static void main(String[] args) {
        String traceId = request.getHeader(TRACE_ID);
        if (traceId == null || traceId.isEmpty()) {
            traceId = IdUtil.simpleUUID();
        }
        MDC.put("TRACE_ID", traceId);
        logger.info("This is a test log");
        // todo business logic
        // clear MDC to avoid thread‑pool contamination
        MDC.clear();
    }
}

To print the call chain, the Logback configuration must include the trace ID pattern.

<property scope="context" name="APP_PATTERN" value='%-5p %d [%t] [%X{TRACE_ID}] %c{50}:%L> %m%n'/>

Multi‑thread environment

MDC is effective only for the current thread; in multi‑thread scenarios you need to transfer it manually.

public static void main(String[] args) {
    MDC.put("traceId", "67890");
    executor.submit(() -> {
        // manually transfer MDC
        MDC.put("TRACE_ID", MDC.get("traceId"));
        logger.info("Child thread log");
        MDC.clear();
    });
    MDC.clear(); // main thread cleanup
    executor.shutdown();
}

In Spring Boot, a custom decorator can automatically propagate MDC.

public class MDCTaskDecorator {
    public static Runnable wrap(Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                runnable.run();
            } finally {
                MDC.clear(); // cleanup
            }
        };
    }
}
public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    MDC.put("traceId", "99999");
    executor.submit(wrap(() -> {
        System.out.println(Thread.currentThread().getName() + " traceId: " + MDC.get("traceId"));
    }));
    MDC.clear();
    executor.shutdown();
}

Source Code Analysis

MDC is based on a ThreadLocal variable, so data is isolated per thread. MDC maintains an MDCAdapter; Logback provides LogbackMDCAdapter implementation.

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> oldMap = (Map) this.copyOnThreadLocal.get();
            Integer lastOp = this.getAndSetLastOperation(1);
            if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
                oldMap.put(key, val);
            } else {
                Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
                newMap.put(key, val);
            }
        }
    }
    // other methods omitted for brevity
}

copyOnThreadLocal is a ThreadLocal object; each thread maintains its own MDC data, and get() retrieves values directly from the ThreadLocal variable, ensuring thread safety.

Conclusion

For software development, using MDC with Logback and a traceId is recommended to achieve log tracing and full‑chain analysis.

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.

distributed systemsJavaloggingThreadLocalTraceabilitymdc
Lin is Dream
Written by

Lin is Dream

Sharing Java developer knowledge, practical articles, and continuous insights into computer engineering.

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.