Backend Development 17 min read

Implementing TraceId and MDC for Log Correlation in Java Backend Services

This article explains how to generate a unique TraceId, propagate it via HTTP headers and SLF4J MDC, integrate the mechanism with Logback, Feign clients, thread pools, and SkyWalking, and details the underlying MDC and Logback placeholder implementations for reliable log tracing in Java backend applications.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing TraceId and MDC for Log Correlation in Java Backend Services

Pain Points

When viewing logs from a single pod, interleaved multi‑thread logs make it difficult to trace which log belongs to which request.

Collecting logs from multiple pods into a single database further mixes the logs, worsening the problem.

Solution

TraceId + MDC

Frontend adds an X-App-Trace-Id request header. The header value can be generated as timestamp + UUID to guarantee uniqueness.

Backend extracts the header in a TraceIdFilter : String traceId = httpServletRequest.getHeader(TRACE_ID_HEADER_KEY); If the header is missing, generate a UUID or Snowflake ID.

Put the traceId into SLF4J MDC: MDC.put(MDC_TRACE_ID_KEY, traceId) and use the %X{traceId} placeholder in the Logback pattern to print it.

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);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <property name="pattern" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%thread] %logger %line [%X{traceId}] [%tid] - %msg%n"/>
</configuration>

Integrate Feign

When making inter‑service calls, propagate the MDC traceId via Feign RequestInterceptor:

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

Multi‑thread Adaptation

Please note that MDC as implemented by logback‑classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.

Before executing a task in a child thread, copy the parent thread's MDC context to the child and clear it after execution.

Adapt ThreadPoolExecutor :

public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor {
    @Override
    public void execute(Runnable command) {
        Map
parentThreadContextMap = MDC.getCopyOfContextMap();
        super.execute(MdcTaskUtils.adaptMdcRunnable(command, parentThreadContextMap));
    }
}

Adapt Spring TaskDecorator :

@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:

@Slf4j
public abstract class MdcTaskUtils {
    public static Runnable adaptMdcRunnable(Runnable runnable, Map
parentThreadContextMap) {
        return () -> {
            log.debug("parentThreadContextMap: {}, currentThreadContextMap: {}", parentThreadContextMap, MDC.getCopyOfContextMap());
            if (MapUtils.isEmpty(parentThreadContextMap) || !parentThreadContextMap.containsKey(MDC_TRACE_ID_KEY)) {
                log.debug("can not find a parentThreadContextMap, maybe task is fired using async or schedule task.");
                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);
            }
        };
    }
}

Integrate SkyWalking

SkyWalking provides the apm-toolkit-logback-1.x module to print SkyWalking traceId in Logback. Combine X-App-Trace-Id and SkyWalking traceId for easier troubleshooting.

Select the layout implementation class TraceIdPatternLogbackLayout .

In the Logback pattern, use %tid to print the SkyWalking traceId.

<?xml version="1.0" encoding="UTF-8"?>
<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>

The layout registers two converters: tid maps to LogbackPatternConverter (which prints the SkyWalking traceId) and sw_ctx maps to LogbackSkyWalkingContextPatternConverter (which prints the SkyWalking context).

public class TraceIdPatternLogbackLayout extends PatternLayout {
    static {
        defaultConverterMap.put("tid", LogbackPatternConverter.class.getName());
        defaultConverterMap.put("sw_ctx", LogbackSkyWalkingContextPatternConverter.class.getName());
    }
}

SkyWalking agents instrument the original LogbackPatternConverter class, replacing its convert() method (which originally returned "TID: N/A") with logic that extracts the real traceId.

public class LogbackPatternConverter extends ClassicConverter implements EnhancedInstance {
    public String convert(ILoggingEvent iLoggingEvent) {
        return (String) delegate$mo3but1.intercept(this, new Object[]{iLoggingEvent}, new LogbackPatternConverter$auxiliary$pJ6Zrqzi(this, iLoggingEvent), cachedValue$oeLgRjrq);
    }
    // original method kept as convert$original$T8InTdln()
}

MDC Principle

MDC is defined in the SLF4J API. All MDC operations delegate to an MDCAdapter implementation.

public class MDC {
    static MDCAdapter mdcAdapter;
    public static void put(String key, String val) {
        if (key == null) throw new IllegalArgumentException("key parameter cannot be null");
        if (mdcAdapter == null) throw new IllegalStateException("MDCAdapter cannot be null");
        mdcAdapter.put(key, val);
    }
}

Logback provides LogbackMDCAdapter which stores the context in a ThreadLocal map.

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal
> copyOnThreadLocal = new ThreadLocal<>();
    // put, get, etc.
}

Logback Placeholder

During Logback initialization, PatternLayout registers converters for common placeholders (date, level, thread, logger, message, MDC, etc.). The %X{key} or %mdc{key} placeholder is handled by MDCConverter , which retrieves the value from the event's MDC map.

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);
        String value = mdcPropertyMap.get(key);
        return value != null ? value : defaultValue;
    }
}

The LoggingEvent implementation lazily obtains the MDC map from the underlying MDCAdapter when getMDCPropertyMap() is called.

public class LoggingEvent implements ILoggingEvent {
    public Map
getMDCPropertyMap() {
        if (mdcPropertyMap == null) {
            MDCAdapter mdc = MDC.getMDCAdapter();
            if (mdc instanceof LogbackMDCAdapter)
                mdcPropertyMap = ((LogbackMDCAdapter) mdc).getPropertyMap();
            else
                mdcPropertyMap = mdc.getCopyOfContextMap();
        }
        return mdcPropertyMap == null ? Collections.emptyMap() : mdcPropertyMap;
    }
}
backendJavafeignLogbackThreadPoolExecutorTraceIdSkyWalkingMDC
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.