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.
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;
}
}Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.