Backend Development 10 min read

Using SLF4J MDC to Correlate Logs Across Threads in Java

This article explains how to use SLF4J's MDC feature to attach a request ID to log entries, demonstrates the limitation of MDC in asynchronous threads, and provides a decorator‑based solution (MDCRunnable) that propagates the context to child threads and thread pools.

Top Architect
Top Architect
Top Architect
Using SLF4J MDC to Correlate Logs Across Threads in Java

Preface – When troubleshooting a production issue, we often need to collect all logs related to a single request. In a single‑threaded request this can be done by filtering on the thread ID, but asynchronous processing makes this approach insufficient.

Background – Huawei IoT platform processes incoming device data asynchronously (storage, rule engine, push, command issuance, etc.). To quickly filter all logs belonging to one data upload request, we use SLF4J's MDC (Mapped Diagnostic Context) to store a requestId.

Basic MDC usage

public class Main {
    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        // Set request ID at entry
        MDC.put(KEY, UUID.randomUUID().toString());

        // Log statements
        logger.debug("log in main thread 1");
        logger.debug("log in main thread 2");
        logger.debug("log in main thread 3");

        // Remove request ID at exit
        MDC.remove(KEY);
    }
}

Running this program after configuring log4j2.xml prints logs that contain the requestId, allowing us to filter with grep requestId=xxx *.log .

Problem in asynchronous threads – When a new thread is started, the MDC context is not automatically propagated because MDC relies on ThreadLocal . The child thread logs without the requestId.

public class Main {
    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        MDC.put(KEY, UUID.randomUUID().toString());
        logger.debug("log in main thread");

        new Thread(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread");
            }
        }).start();

        MDC.remove(KEY);
    }
}

The output shows the child thread log missing the requestId.

Solution – Decorator pattern (MDCRunnable) – We create a wrapper that captures the current MDC map and restores it in the child thread before execution, then clears it afterwards.

public class MDCRunnable implements Runnable {
    private final Runnable runnable;
    private final Map
map;

    public MDCRunnable(Runnable runnable) {
        this.runnable = runnable;
        this.map = MDC.getCopyOfContextMap(); // capture current MDC
    }

    @Override
    public void run() {
        // Restore MDC in child thread
        for (Map.Entry
entry : map.entrySet()) {
            MDC.put(entry.getKey(), entry.getValue());
        }
        runnable.run();
        // Clean up
        for (Map.Entry
entry : map.entrySet()) {
            MDC.remove(entry.getKey());
        }
    }
}

We then use MDCRunnable when creating new threads or submitting tasks to an executor:

public class Main {
    private static final String KEY = "requestId";
    private static final Logger logger = LoggerFactory.getLogger(Main.class);
    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {
        MDC.put(KEY, UUID.randomUUID().toString());
        logger.debug("log in main thread");

        // Asynchronous thread with MDC propagation
        new Thread(new MDCRunnable(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread");
            }
        })).start();

        // Thread pool task with MDC propagation
        EXECUTOR.execute(new MDCRunnable(new Runnable() {
            @Override
            public void run() {
                logger.debug("log in other thread pool");
            }
        }));
        EXECUTOR.shutdown();

        MDC.remove(KEY);
    }
}

Running the program now produces logs where the requestId appears in the main thread, the new thread, and the thread‑pool task, confirming that MDC works across asynchronous boundaries.

Conclusion – By using MDC together with a decorator (MDCRunnable) or AOP to inject the requestId into all relevant execution points, developers can quickly filter logs for any request, greatly improving debugging efficiency in both development and operations environments.

LoggingmultithreadingthreadlocalSlf4jDecorator PatternMDC
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.