Backend Development 15 min read

Using MDC and TraceId for Log Correlation in Java Applications

This article explains how to use SLF4J MDC together with a TraceId header to correlate logs across threads, services, and distributed tracing tools like SkyWalking, providing code examples for filters, Feign interceptors, thread‑pool adapters, and Logback configuration.

Architect
Architect
Architect
Using MDC and TraceId for Log Correlation in Java Applications

Pain Points

When viewing logs of a single Pod, logs from multiple threads interleave, making it hard to trace which request generated which log line. Collecting logs from many Pods into a single database makes the problem even worse.

Solution

TraceId + MDC

MDC (Mapped Diagnostic Context) can store a trace identifier per request. Frontend adds an X-App-Trace-Id header (timestamp + UUID). Backend extracts it in a TraceIdFilter and puts it into SLF4J MDC, then prints it with the %X{traceId} placeholder in Logback.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    String traceId = httpServletRequest.getHeader(TRACE_ID_HEADER_KEY);
    if (StrUtil.isBlank(traceId)) {
        traceId = UUID.randomUUID().toString();
    }
    MDC.put(MDC_TRACE_ID_KEY, traceId);
    try {
        chain.doFilter(request, response);
    } finally {
        MDC.remove(MDC_TRACE_ID_KEY);
    }
}

Feign Integration

When a service calls another via Feign, the traceId stored in MDC is copied to the outgoing request header in a RequestInterceptor implementation.

@Override
public void apply(RequestTemplate template) {
    template.header(TRACE_ID_HEADER_KEY, MDC.get(MDC_TRACE_ID_KEY));
}

Thread Adaptation

Logback’s MDC is thread‑local, so child threads do not inherit the parent’s context. A custom MdcAwareThreadPoolExecutor or Spring TaskDecorator copies the parent MDC map to the child before execution and clears it afterwards.

public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor {
    @Override
    public void execute(Runnable command) {
        Map
parentThreadContextMap = MDC.getCopyOfContextMap();
        super.execute(MdcTaskUtils.adaptMdcRunnable(command, parentThreadContextMap));
    }
}
@Component
public class MdcAwareTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map
parentThreadContextMap = MDC.getCopyOfContextMap();
        return MdcTaskUtils.adaptMdcRunnable(runnable, parentThreadContextMap);
    }
}

The utility MdcTaskUtils.adaptMdcRunnable decorates the original Runnable , sets the parent MDC before execution, and removes it afterwards.

public static Runnable adaptMdcRunnable(Runnable runnable, Map
parentThreadContextMap) {
    return () -> {
        if (MapUtils.isEmpty(parentThreadContextMap) || !parentThreadContextMap.containsKey(MDC_TRACE_ID_KEY)) {
            MDC.put(MDC_TRACE_ID_KEY, UUID.randomUUID().toString());
        } else {
            MDC.put(MDC_TRACE_ID_KEY, parentThreadContextMap.get(MDC_TRACE_ID_KEY));
        }
        try {
            runnable.run();
        } finally {
            MDC.remove(MDC_TRACE_ID_KEY);
        }
    };
}

SkyWalking Integration

SkyWalking provides apm-toolkit-logback-1.x which adds a %tid placeholder to print the SkyWalking traceId. The layout class TraceIdPatternLogbackLayout registers converters for tid and sw_ctx .

<configuration debug="false">
    <property name="pattern" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%thread] %logger %line [%X{traceId}] [%tid] - %msg%n"/>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <pattern>${pattern}</pattern>
            </layout>
        </encoder>
    </appender>
</configuration>

SkyWalking agents instrument the original LogbackPatternConverter class, overriding its convert() method to return the actual traceId instead of the default "TID: N/A".

MDC Principle

MDC is defined in the SLF4J API. All logging frameworks implement the MDCAdapter interface; Logback provides LogbackMDCAdapter which stores the context in a ThreadLocal . The MDCConverter reads the configured key (e.g., traceId ) from the event’s MDC map and outputs its value.

public class MDCConverter extends ClassicConverter {
    private String key;
    private String defaultValue = "";
    @Override
    public void start() {
        String[] keyInfo = extractDefaultReplacement(getFirstOption());
        key = keyInfo[0];
        if (keyInfo[1] != null) {
            defaultValue = keyInfo[1];
        }
        super.start();
    }
    @Override
    public String convert(ILoggingEvent event) {
        Map
mdcPropertyMap = event.getMDCPropertyMap();
        if (mdcPropertyMap == null) {
            return defaultValue;
        }
        if (key == null) {
            return outputMDCForAllKeys(mdcPropertyMap);
        } else {
            String value = mdcPropertyMap.get(key);
            return value != null ? value : defaultValue;
        }
    }
}
JavaMicroservicesloggingLogbackTraceIdMDC
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.