Implementing Lightweight Log Traceability with MDC in Java Microservices
This article explains how to use Log4j's Mapped Diagnostic Context (MDC) to generate, propagate, and record a traceId across micro‑service calls, providing a low‑cost, non‑intrusive solution for full‑stack log tracing and debugging.
In operation and debugging of micro‑service systems, locating all logs of a single API call is difficult; using a unified traceId can link logs across services without invasive code changes.
This article demonstrates a lightweight approach using Log4j’s Mapped Diagnostic Context (MDC) to generate, propagate, and record a traceId.
It explains MDC basics, its API ( clear() , get(String key) , getContext() , put(String key, Object o) , remove(String key) ), and shows how to configure Log4j pattern with %X{traceId} :
<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] [%p] %l[%t]%n%m%n" />
</layout>
</appender>Implementation steps include:
Generating a UUID as traceId.
Adding an interceptor that reads traceId from request headers or creates one and puts it into MDC.
Configuring the interceptor in Spring MVC.
Wrapping HTTP calls to forward the traceId header.
Ensuring MDC context is transferred to async threads by customizing ThreadPoolTaskExecutor and providing a TaskDecorator that copies MDC maps.
Providing custom thread‑pool executors (MdcTaskExecutor) that set and clear MDC around task execution.
Defining bean configurations for common and scheduled thread pools using the custom executor.
Log interceptor example:
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader(TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
MDC.put(TRACE_ID, UUID.randomUUID().toString());
} else {
MDC.put(TRACE_ID, traceId);
}
return true;
}
// afterCompletion and postHandle omitted for brevity
}Spring MVC configuration to register the interceptor:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/**");
}
}Utility to forward traceId in outgoing HTTP requests:
@Slf4j
public class HttpUtils {
public static String get(String url) throws URISyntaxException {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap
headers = new HttpHeaders();
headers.add("traceId", MDC.get("traceId"));
URI uri = new URI(url);
RequestEntity
requestEntity = new RequestEntity<>(headers, HttpMethod.GET, uri);
ResponseEntity
exchange = restTemplate.exchange(requestEntity, String.class);
if (exchange.getStatusCode().equals(HttpStatus.OK)) {
log.info("send http request success");
}
return exchange.getBody();
}
}Task decorator that copies MDC to async threads:
@Slf4j
@Component
public class TraceAsyncConfigurer implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-pool-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) ->
log.error("asyc execute error, method={}, params={}", method.getName(), Arrays.toString(params));
}
public static class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map
contextMap = MDC.getCopyOfContextMap();
return () -> {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
}Custom ThreadPoolTaskExecutor that propagates MDC:
public class MdcTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public
Future
submit(Callable
task) {
log.info("mdc thread pool task executor submit");
Map
context = MDC.getCopyOfContextMap();
return super.submit(() -> {
if (context != null) {
MDC.setContextMap(context);
} else {
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
}
try {
return task.call();
} finally {
try { MDC.clear(); } catch (Exception e) { log.warn("MDC clear exception", e); }
}
});
}
@Override
public void execute(Runnable task) {
log.info("mdc thread pool task executor execute");
Map
context = MDC.getCopyOfContextMap();
super.execute(() -> {
if (context != null) {
MDC.setContextMap(context);
} else {
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
}
try {
task.run();
} finally {
try { MDC.clear(); } catch (Exception e) { log.warn("MDC clear exception", e); }
}
});
}
}Bean configuration that creates common and scheduled thread pools using the custom executor:
@Slf4j
@Configuration
public class ThreadPoolConfig {
@Bean
public Executor commonThreadPool() {
log.info("start init common thread pool");
MdcTaskExecutor executor = new MdcTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(3000);
executor.setKeepAliveSeconds(120);
executor.setThreadNamePrefix("common-thread-pool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
@Bean
public Executor scheduleThreadPool() {
log.info("start init schedule thread pool");
MdcTaskExecutor executor = new MdcTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(3000);
executor.setKeepAliveSeconds(120);
executor.setThreadNamePrefix("schedule-thread-pool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
}The article also mentions extending the approach to JSF interface logging, implicit parameter passing via RpcContext, and applying the same MDC principle to message queues and other middleware.
Understanding these fundamentals helps developers grasp how larger tracing systems such as SkyWalking or JD’s PFinder operate.
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.