MDC and TraceId Propagation in Java Backend Applications
This article explains what MDC is, the problems it faces such as traceId loss in child threads and HTTP calls, and provides concrete solutions including interceptor implementation, thread‑pool wrappers, and logging pattern adjustments to ensure reliable traceId propagation in Java backend services.
Introduction
Through this article you will learn what MDC is, the issues that arise when using it, and how to solve those problems.
MDC Overview
Brief
MDC (Mapped Diagnostic Context) 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 any code executing in the same thread.
Child threads inherit the MDC content of their parent thread. When logging, you simply retrieve the needed information from MDC. Typically, a web application stores data in MDC at the very beginning of a request.
API
clear() ⇒ remove all entries from MDC
get(String key) ⇒ get the value of the specified key in the current thread's MDC
getContext() ⇒ obtain 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 key from the current thread's MDC
Advantages
The code stays clean, log style is unified, and you no longer need to manually concatenate the traceId in each log statement, e.g. LOGGER.info("traceId:{} ", traceId) .
Using MDC
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);
}
}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>In some cases the traceId may be lost.
Problems with MDC
Child thread logs lose the traceId.
HTTP calls lose the traceId.
Solutions
Child‑Thread TraceId Loss
Wrap thread‑pool tasks so that the MDC context is transferred to the child thread.
Thread‑pool wrapper: 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()));
}
}Thread‑MDC utility: 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
When invoking third‑party services, add the traceId to the request header and provide an interceptor on the receiving side to put it into MDC.
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()));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, ensure the logging pattern contains %X{traceId} so that the traceId is printed with each log entry.
Thank you for reading, hope it helps :) Source: juejin.cn/post/6844904101483020295
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.