Mastering MDC in Java: Solve TraceId Loss in Multithreaded and HTTP Calls

This article explains what MDC (Mapped Diagnostic Context) is, outlines its API and advantages, demonstrates common traceId loss problems in child threads and HTTP calls, and provides practical solutions including thread‑pool wrappers, MDC utilities, and interceptors for HttpClient, OkHttp, RestTemplate, and third‑party services, plus the required log pattern.

Programmer DD
Programmer DD
Programmer DD
Mastering MDC in Java: Solve TraceId Loss in Multithreaded and HTTP Calls

MDC Introduction

MDC (Mapped Diagnostic Context) is a feature provided by log4j, logback, and log4j2 that allows storing key‑value pairs in a hash map bound to the current thread, making the data accessible to any code executed in that thread. Child threads inherit the parent’s MDC, and applications typically populate MDC at the start of a request.

API

clear() – remove all entries

get(String key) – retrieve value for a key

getContext() – obtain the MDC map

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

remove(String key) – delete a specific entry

Advantages

Cleaner code and unified log format; no need to manually concatenate traceId in each log statement.

MDC Usage

Add an interceptor to populate MDC with traceId.

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

The simple usage works, but traceId may be lost in certain scenarios.

MDC traceId loss illustration
MDC traceId loss illustration

Problems with MDC

TraceId disappears in child‑thread logs.

TraceId disappears in HTTP calls.

Solving Child‑Thread TraceId Loss

Thread pool wrapper

Wrap tasks submitted to a thread pool so that the MDC context is copied to the worker thread.

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    // constructors omitted for brevity
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

ThreadMdcUtil provides the actual wrapping logic.

public class ThreadMdcUtil {
    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constants.TRACE_ID) == null) {
            MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> 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<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

HTTP Call TraceId Loss

When calling third‑party services, add the traceId to the request header and extract it on the server side.

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 with

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

Add with

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

Configure with

restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()))

.

Third‑party service interceptor

On the receiving side, extract the traceId from the request header and put it into MDC.

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String traceId = request.getHeader(Constants.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtils.getTraceId();
        }
        MDC.put("traceId", traceId);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.remove(Constants.TRACE_ID);
    }
}

Log pattern

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

Include %X{traceId} in the pattern to output the traceId.

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.

JavaThreadPoolloggingtraceidHTTP Interceptormdc
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.