Understanding and Implementing MDC (Mapped Diagnostic Context) in Java Logging
This article introduces MDC, explains its API and advantages, and provides practical solutions for traceId propagation in multithreaded environments, HTTP calls, and custom thread pools using Java logging frameworks such as Log4j, Logback, and Log4j2.
MDC Introduction
MDC (Mapped Diagnostic Context) is a feature provided by log4j, logback, and log4j2 that allows you to store key‑value pairs bound to the current thread, making them accessible throughout the thread's execution. Child threads inherit the MDC of their parent, enabling consistent logging information such as traceId.
1. API Overview
clear() : Remove all entries from MDC.
get(String key) : Retrieve the value associated with the given key.
getContext() : Get the entire MDC map of the current thread.
put(String key, Object o) : Store a key‑value pair in the current thread's MDC.
remove(String key) : Delete the specified entry from MDC.
2. Advantages
Using MDC makes code concise and logs uniform; you no longer need to manually concatenate traceId in each log statement, e.g., LOGGER.info("traceId:{} ", traceId) .
MDC Usage
1. Adding 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);
}
}2. Modifying Log Pattern
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>The key %X{traceId} must match the MDC key name.
Problems with MDC
TraceId lost in child thread logs.
TraceId lost during HTTP calls.
Solutions
Child Thread TraceId Loss
Wrap tasks submitted to thread pools so that the MDC context is transferred.
ThreadPoolExecutorMdcWrapper.java
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()));
}
}ThreadMdcUtil.java
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();
}
};
}
}HTTP Call TraceId Loss
For HTTP client libraries, add interceptors that copy the traceId from MDC into 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);
}
}
}
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);
}
}
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);
}
}
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));Third‑party services must also add a similar interceptor to read the traceId from incoming headers and put it into MDC.
Final Note
The author encourages readers to like, share, and follow the "Code Monkey Technical Column" public account for more advanced Spring Cloud, Spring Boot, and MyBatis tutorials.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.