Understanding and Implementing MDC (Mapped Diagnostic Context) for TraceId Propagation in Java Backend Applications
This article explains the concept of MDC (Mapped Diagnostic Context) in Java logging, details its API, demonstrates how to propagate traceId across threads and HTTP calls using interceptors and custom thread‑pool wrappers, and shows how to configure log patterns to include the traceId.
MDC (Mapped Diagnostic Context) is a feature provided by logging frameworks such as log4j, logback and log4j2 that allows a thread‑bound hash map to store key‑value pairs, which can be accessed by any code executing in the same thread. It is commonly used in web applications to store request‑level data at the beginning of request processing.
The main API includes clear() , get(String key) , getContext() , put(String key, Object value) and remove(String key) . Using MDC makes log statements concise and uniform, e.g., LOGGER.info("traceId:{} ", traceId) .
To ensure the traceId is available throughout the request lifecycle, the article shows how to add a Spring MVC interceptor that extracts the traceId from the incoming request header (or generates a new one) and puts it into MDC.
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// If there is an upstream call, use its ID
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 logging pattern must be modified to include the traceId, for example:
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>Two common problems are traceId loss in child threads and in HTTP calls. The article provides solutions:
For child threads, wrap tasks with a custom ThreadPoolExecutorMdcWrapper that copies the MDC context to the worker thread.
For HTTP calls, add interceptors to HttpClient, OkHttp and RestTemplate that forward the traceId via request headers.
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
// 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);
}
// other submit/execute overrides omitted
} public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
if (MDC.get(Constants.TRACE_ID) == null) {
MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
}
}
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();
}
};
}
// Callable wrapper omitted for brevity
}HTTP client interceptors:
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);
}
}
} 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);
}
} 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);
}
}Finally, the article reminds to add %X{traceId} to the logging pattern so that the traceId appears in every log line.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.