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.
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;
}
}
}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.
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.