Mastering MDC in Java: Ensure TraceId Propagation Across Threads and HTTP Calls
This article explains what MDC is, its API, common pitfalls like traceId loss in child threads and HTTP calls, and provides practical solutions using custom thread pool wrappers and interceptors for HttpClient, OkHttp, and RestTemplate to reliably propagate trace identifiers.
Introduction
Through this article you will learn what MDC is, the problems that arise when using MDC, and how to solve those problems.
MDC Overview
What is MDC? Mapped Diagnostic Context (MDC) is a feature provided by log4j, logback, and log4j2 that allows convenient logging in multi‑threaded environments. MDC can be seen as a hash table bound to the current thread, where key‑value pairs can be stored and accessed by code executing in the same thread. Child threads inherit the MDC of their parent. When logging, the required information can be retrieved from MDC.
API
clear() – remove all entries from MDC
get(String key) – get the value of a specific key
getContext() – get the entire MDC map
put(String key, Object value) – store a key‑value pair
remove(String key) – delete a specific entry
Advantages
Code is concise and log format is unified; no need to manually concatenate traceId in log statements.
MDC Usage
Add an interceptor
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Use upstream traceId 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 Log Pattern
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>The key part is %X{traceId}, which must match the MDC key name.
Problems with MDC
TraceId is lost in child thread logs
TraceId is lost in HTTP calls
Solution for Child Thread Log Loss
Wrap thread pools so that tasks inherit the MDC context. Example wrapper class:
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> 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 <T> Future<T> submit(Runnable task, T result) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
}
@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()));
}
}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 <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();
}
};
}
}Before executing a task, the wrapper sets the parent thread’s MDC map into the child thread, ensuring traceId is available.
Solution for HTTP Call TraceId Loss
Implement interceptors for different HTTP clients to propagate traceId via request headers.
HttpClient
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
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
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()));Third‑Party Service Interceptor
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);
}
}Finally, add %X{traceId} to the log pattern to print the trace identifier.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
