Trace Requests Across Threads with TraceId, MDC, and SkyWalking in Java

This article explains how to solve interleaved logs in multi‑threaded pods by adding a unique X‑App‑Trace‑Id header, propagating it through a TraceIdFilter, storing it in SLF4J MDC, adapting Feign and thread pools, and integrating SkyWalking to enrich log patterns with trace identifiers.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Trace Requests Across Threads with TraceId, MDC, and SkyWalking in Java

Pain Points

When viewing online logs, logs from multiple threads within the same Pod interleave, making it hard to trace each request's log information.

Log collection tools aggregate logs from many Pods into a single database, worsening the chaos.

Solution

TraceId + MDC

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

Backend extracts the header in a TraceIdFilter. If the header is missing, a UUID or Snowflake algorithm generates a traceId. The traceId is then stored in SLF4J MDC and printed via the logback pattern %X{traceId}:

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>

Integrating Feign

When services call each other via Feign, a Feign RequestInterceptor copies the traceId from MDC into the outgoing request header:

@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 a child thread executes, copy the parent MDC map; after execution, clear the child MDC. Example for ThreadPoolExecutor:

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

Spring TaskDecorator adaptation:

@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 method decorates a Runnable to set the parent MDC before execution and remove it afterwards:

@Slf4j
public abstract class MdcTaskUtils {
    public static Runnable adaptMdcRunnable(Runnable runnable, Map<String, String> 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);
            }
        };
    }
}

Integrating SkyWalking

SkyWalking provides apm-toolkit-logback-1.x to print SkyWalking traceId. By using the TraceIdPatternLogbackLayout class, the log pattern can include both the custom X-App-Trace-Id and SkyWalking traceId via %tid:

<?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 custom layout registers converters tid (maps to SkyWalking traceId) and sw_ctx (SkyWalking context).

MDC Principle

MDC is defined by the SLF4J MDCAdapter interface; Logback implements it with LogbackMDCAdapter using a ThreadLocal map.

public class MDC {
    public static void put(String key, String val) { mdcAdapter.put(key, val); }
}

The adapter stores a copy‑on‑write map to avoid contention between threads.

Logback Placeholders

Logback’s PatternLayout registers many converters, including X for MDC ( %X{key}), thread, level, etc.

DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class.getName());
// ... other converters

The MDCConverter reads the configured key and returns the corresponding value from the event’s MDC map:

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> map = event.getMDCPropertyMap();
        if (key == null) return outputMDCForAllKeys(map);
        String value = map.get(key);
        return value != null ? value : "";
    }
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootlogbacktraceidSkyWalkingmdc
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

0 followers
Reader feedback

How this landed with the community

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.