Using SLF4J MDC to Correlate Logs Across Threads in Java Applications
This article explains how to employ SLF4J's MDC feature to attach a request identifier to log entries, demonstrates why it fails in asynchronous threads, and shows how to fix the issue with a decorator‑style MDCRunnable that propagates the MDC context to child threads and thread pools.
When a request fails in production, developers often need to collect all logs generated during that request to locate the problem. In a single‑threaded scenario, filtering logs by thread ID works, but in asynchronous or multi‑threaded processing the thread ID is insufficient.
The article introduces SLF4J's MDC (Mapped Diagnostic Context) as a way to embed a request identifier (e.g., requestId ) into each log entry. A simple example shows how to put the ID at the entry point with MDC.put(KEY, UUID.randomUUID().toString()) , log messages, and then remove the ID with MDC.remove(KEY) . The resulting log lines contain the request ID inside curly braces, enabling quick grep filtering.
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 1");
logger.debug("log in main thread 2");
logger.debug("log in main thread 3");
MDC.remove(KEY);
}
}Running this code prints logs that include the request ID, making it easy to filter with grep requestId=xxx *.log . However, when the same request spawns an asynchronous thread, the MDC context is not automatically propagated because MDC relies on ThreadLocal , which is confined to the originating thread.
To solve this, the article proposes a decorator pattern: a custom MDCRunnable that captures the current MDC map, copies it into the new thread before execution, and clears it afterward.
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() {
for (Map.Entry
entry : map.entrySet()) {
MDC.put(entry.getKey(), entry.getValue());
}
runnable.run();
for (Map.Entry
entry : map.entrySet()) {
MDC.remove(entry.getKey());
}
}
}Using MDCRunnable to wrap both a raw Thread and an ExecutorService ensures the request ID is present in logs from the main thread, the spawned thread, and the thread pool.
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");
new Thread(new MDCRunnable(() -> logger.debug("log in other thread"))).start();
EXECUTOR.execute(new MDCRunnable(() -> logger.debug("log in other thread pool")));
EXECUTOR.shutdown();
MDC.remove(KEY);
}
}The output now shows the same request ID in all three log lines, confirming that MDC works across asynchronous boundaries when wrapped with the decorator.
In summary, by leveraging MDC together with a simple decorator that propagates the MDC context, developers can reliably trace a request through multi‑threaded Java back‑end services, greatly reducing debugging time and improving operational efficiency.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.