How to Use TraceId and MDC for Precise Log Tracing in Java Microservices
This article explains how to solve interleaved log lines in multi‑threaded pods by propagating a unique TraceId via HTTP headers and SLF4J MDC, integrating the approach with Spring filters, Feign interceptors, thread‑pool adapters, and SkyWalking for end‑to‑end tracing.
Pain Points
When viewing online logs, logs from multiple threads within the same Pod interleave, making it difficult to trace the log information of a single request. After a log‑collection tool aggregates logs from many Pods into one database, the situation becomes even more chaotic.
Solution
TraceId + MDC
MDC (Mapped Diagnostic Context) is used to store a trace identifier that can be accessed by the logging framework.
Frontend: add an X-App-Trace-Id request header for each request. The value can be generated as timestamp+UUID to guarantee uniqueness.
Backend: in a TraceIdFilter, retrieve the header value. If the header is absent, generate a UUID or Snowflake ID.
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);
}
}Put the traceId into SLF4J MDC and use the %X{traceId} placeholder in the Logback pattern to print it.
Integrate with Feign
When making inter‑service calls, transfer the MDC traceId to the downstream service via a Feign RequestInterceptor:
@Override
public void apply(RequestTemplate template) {
template.header(TRACE_ID_HEADER_KEY, MDC.get(MDC_TRACE_ID_KEY));
}Thread‑Pool Adaptation
Before executing a task in a child thread, copy the parent thread’s MDC context; after execution, clear the child thread’s MDC.
public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor {
@Override
public void execute(Runnable command) {
Map<String, String> parentThreadContextMap = MDC.getCopyOfContextMap();
super.execute(MdcTaskUtils.adaptMdcRunnable(command, parentThreadContextMap));
}
} @Component
public class MdcAwareTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> 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.
Integrate with SkyWalking
SkyWalking provides the apm-toolkit-logback-1.x module, which adds a TraceIdPatternLogbackLayout that can print both the custom X-App-Trace-Id and the SkyWalking traceId. Use the %tid placeholder to output the SkyWalking traceId.
<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>When the Java application starts with the SkyWalking agent ( -javaagent:/opt/tools/skywalking-agent.jar), the agent instruments LogbackPatternConverter to replace its convert() method, allowing the SkyWalking traceId to be printed.
MDC Principle
MDC is defined in the slf4j-api jar. All MDC operations delegate to the MDCAdapter interface. Logback provides LogbackMDCAdapter, which uses a ThreadLocal map to store context data.
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);
}
}The MDCConverter reads the configured key (e.g., traceId) and returns MDC.get(key) during log formatting.
public class MDCConverter extends ClassicConverter {
private String key;
@Override
public void start() {
String[] keyInfo = extractDefaultReplacement(getFirstOption());
key = keyInfo[0];
super.start();
}
@Override
public String convert(ILoggingEvent event) {
Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
if (mdcPropertyMap == null) return "";
if (key == null) return outputMDCForAllKeys(mdcPropertyMap);
String value = mdcPropertyMap.get(key);
return value != null ? value : "";
}
}Logback Placeholders
Logback’s PatternLayout registers converters for common placeholders. The %X{key} or %mdc{key} placeholders are handled by MDCConverter, which fetches the value from the MDC map.
DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());Other converters (e.g., %thread, %level, %msg) are similarly registered.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
