Backend Development 11 min read

Using MDC for TraceId Propagation in Java Applications

This article explains what MDC (Mapped Diagnostic Context) is, shows its API and advantages, demonstrates how to add interceptors and log patterns to include a traceId, and provides solutions for traceId loss in child threads and HTTP calls by wrapping thread pools and implementing HTTP client interceptors.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Using MDC for TraceId Propagation in Java Applications

Through this article you will learn what MDC (Mapped Diagnostic Context) is, the problems that can arise when using MDC, and how to solve those problems.

MDC Introduction

MDC is a feature provided by log4j, logback, and log4j2 that allows you to store key‑value pairs in a hash table bound to the current thread. The stored data can be accessed by any code executing in the same thread, and child threads inherit the parent thread's MDC content.

API

clear() – removes all entries from MDC

get(String key) – retrieves the value for the given key

getContext() – returns the entire MDC map

put(String key, Object o) – stores a key‑value pair

remove(String key) – deletes the entry for the given key

Advantages

The code stays concise and log format is unified; you no longer need to manually concatenate the traceId in each log statement, e.g. LOGGER.info("traceId:{} ", traceId) .

MDC Usage – Adding an Interceptor

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // Get traceId from request header if present
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }
        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.remove(Constants.TRACE_ID);
    }
}

Modify the log pattern to include the traceId:

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

Problems with MDC

TraceId is lost in child threads

TraceId is lost in HTTP calls

Solution for Child‑Thread TraceId Loss – ThreadPool Wrapper

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    // other constructors omitted for brevity
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public
Future
submit(Runnable task, T result) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
    }
    @Override
    public
Future
submit(Callable
task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public Future
submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

Utility class for wrapping tasks:

public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.TRACE_ID) == null) {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }
    public static
Callable
wrap(final Callable
callable, final Map
context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }
    public static Runnable wrap(final Runnable runnable, final Map
context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

The wrapper copies the parent thread’s MDC map to the child thread, ensures a traceId exists, executes the original task, and finally clears MDC.

Solution for HTTP Call TraceId Loss

Implement interceptors for the most common HTTP clients and add the traceId to the request header.

HttpClient interceptor :

public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
    @Override
    public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        if (traceId != null) {
            httpRequest.addHeader(Constants.TRACE_ID, traceId);
        }
    }
}

Register the interceptor:

private static CloseableHttpClient httpClient = HttpClientBuilder.create()
        .addInterceptorFirst(new HttpClientTraceIdInterceptor())
        .build();

OkHttp interceptor :

public class OkHttpTraceIdInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        Request request = chain.request();
        if (traceId != null) {
            request = request.newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
        }
        return chain.proceed(request);
    }
}

Register the interceptor:

private static OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new OkHttpTraceIdInterceptor())
        .build();

RestTemplate interceptor :

public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        if (traceId != null) {
            httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
        }
        return execution.execute(httpRequest, body);
    }
}

Register the interceptor:

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

For third‑party services, add a similar interceptor on the server side to read the traceId from the request header and put it into MDC.

Finally, ensure the log pattern contains %X{traceId} so that every log entry prints the propagated trace identifier.

JavathreadpoolloggingTraceIdHTTP InterceptorMDC
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

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.