Introduction
SLF4J's Mapped Diagnostic Context (MDC) provides a powerful mechanism for contextual logging in multi-threaded applications. It allows you to store request-specific information that can be automatically included in log messages, making it invaluable for distributed tracing, debugging, and monitoring in complex systems.
Core Concepts
Understanding MDC
public class MDCCoreConcepts { // MDC is a thread-local map that stores contextual information // Data put in MDC is automatically available in log messages // Perfect for request tracing, user sessions, and correlation IDs } Basic MDC Operations
Core MDC Methods
import org.slf4j.MDC; import java.util.Map; public class BasicMDCOperations { public void demonstrateBasicOperations() { // Put values in MDC MDC.put("requestId", "req-12345"); MDC.put("userId", "user-67890"); MDC.put("sessionId", "sess-abcde"); // Get value from MDC String requestId = MDC.get("requestId"); System.out.println("Request ID: " + requestId); // Get copy of current context Map<String, String> context = MDC.getCopyOfContextMap(); System.out.println("Full context: " + context); // Remove specific key MDC.remove("sessionId"); // Clear entire context MDC.clear(); } public void safeMDCOperations() { // Always check if MDC is available if (MDC.getMDCAdapter() != null) { MDC.put("operation", "safeOperation"); try { // Perform logging operations logger.info("Processing with safe MDC"); } finally { MDC.remove("operation"); } } } } Request Tracing Implementation
Request Context Management
public class RequestContext { private static final String REQUEST_ID_KEY = "requestId"; private static final String USER_ID_KEY = "userId"; private static final String SESSION_ID_KEY = "sessionId"; private static final String CORRELATION_ID_KEY = "correlationId"; private static final String CLIENT_IP_KEY = "clientIp"; private static final String USER_AGENT_KEY = "userAgent"; private RequestContext() { // Utility class } public static void initializeContext(HttpServletRequest request) { String requestId = generateRequestId(); String correlationId = getCorrelationId(request); String userId = getUserIdFromRequest(request); String sessionId = request.getSession().getId(); String clientIp = getClientIp(request); String userAgent = request.getHeader("User-Agent"); MDC.put(REQUEST_ID_KEY, requestId); MDC.put(CORRELATION_ID_KEY, correlationId); MDC.put(USER_ID_KEY, userId); MDC.put(SESSION_ID_KEY, sessionId); MDC.put(CLIENT_IP_KEY, clientIp); MDC.put(USER_AGENT_KEY, userAgent); } public static void initializeContext(String requestId, String userId) { MDC.put(REQUEST_ID_KEY, requestId); MDC.put(USER_ID_KEY, userId); MDC.put(CORRELATION_ID_KEY, generateCorrelationId()); } public static void clearContext() { MDC.remove(REQUEST_ID_KEY); MDC.remove(USER_ID_KEY); MDC.remove(SESSION_ID_KEY); MDC.remove(CORRELATION_ID_KEY); MDC.remove(CLIENT_IP_KEY); MDC.remove(USER_AGENT_KEY); } public static String getRequestId() { return MDC.get(REQUEST_ID_KEY); } public static String getUserId() { return MDC.get(USER_ID_KEY); } public static String getCorrelationId() { return MDC.get(CORRELATION_ID_KEY); } public static void setCorrelationId(String correlationId) { MDC.put(CORRELATION_ID_KEY, correlationId); } public static Map<String, String> getCurrentContext() { return MDC.getCopyOfContextMap(); } public static void restoreContext(Map<String, String> context) { if (context != null) { MDC.setContextMap(context); } } private static String generateRequestId() { return "req-" + UUID.randomUUID().toString().substring(0, 8); } private static String generateCorrelationId() { return "corr-" + UUID.randomUUID().toString().substring(0, 8); } private static String getCorrelationId(HttpServletRequest request) { String correlationId = request.getHeader("X-Correlation-ID"); return correlationId != null ? correlationId : generateCorrelationId(); } private static String getUserIdFromRequest(HttpServletRequest request) { // Extract from authentication token or session Principal principal = request.getUserPrincipal(); return principal != null ? principal.getName() : "anonymous"; } private static String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0]; } return request.getRemoteAddr(); } } Web Application Integration
Servlet Filter for MDC Management
@WebFilter("/*") public class MDCRequestFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(MDCRequestFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // Initialize MDC context RequestContext.initializeContext(httpRequest); try { // Log request start logger.info("Request started: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI()); // Add MDC context to response headers for microservices addMDCHeadersToResponse(httpResponse); // Continue request processing chain.doFilter(request, response); } catch (Exception e) { // Log errors with MDC context logger.error("Request processing failed", e); throw e; } finally { // Log request completion logger.info("Request completed: Status {}", httpResponse.getStatus()); // Clear MDC context RequestContext.clearContext(); } } private void addMDCHeadersToResponse(HttpServletResponse response) { String requestId = RequestContext.getRequestId(); String correlationId = RequestContext.getCorrelationId(); if (requestId != null) { response.setHeader("X-Request-ID", requestId); } if (correlationId != null) { response.setHeader("X-Correlation-ID", correlationId); } } @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("MDC Request Filter initialized"); } @Override public void destroy() { logger.info("MDC Request Filter destroyed"); } } Spring Boot Interceptor
@Component public class MDCInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(MDCInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // Initialize MDC context RequestContext.initializeContext(request); // Log request details logger.debug("Incoming request: {} {} from {}", request.getMethod(), request.getRequestURI(), RequestContext.getClientIp()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // Log request completion if (ex != null) { logger.error("Request failed with error", ex); } else { logger.debug("Request completed successfully: Status {}", response.getStatus()); } // Clear MDC context RequestContext.clearContext(); } } @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private MDCInterceptor mdcInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(mdcInterceptor) .addPathPatterns("/**") .excludePathPatterns("/health", "/metrics"); } } Advanced MDC Patterns
Thread Pool Management
public class MDCAwareThreadPool { public static ExecutorService newMDCAwareThreadPool(int corePoolSize, int maxPoolSize) { return new ThreadPoolExecutor( corePoolSize, maxPoolSize, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new MDCAwareThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() ); } public static class MDCAwareThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; public MDCAwareThreadFactory() { this("mdc-aware-pool"); } public MDCAwareThreadFactory(String poolName) { namePrefix = poolName + "-thread-"; } @Override public Thread newThread(Runnable r) { Map<String, String> parentMDC = MDC.getCopyOfContextMap(); String threadName = namePrefix + threadNumber.getAndIncrement(); Thread t = new Thread(() -> { // Restore MDC context in new thread if (parentMDC != null) { MDC.setContextMap(parentMDC); } try { r.run(); } finally { MDC.clear(); } }, threadName); t.setDaemon(true); return t; } } public static Runnable wrapWithMDC(Runnable runnable) { Map<String, String> parentMDC = MDC.getCopyOfContextMap(); return () -> { if (parentMDC != null) { MDC.setContextMap(parentMDC); } try { runnable.run(); } finally { MDC.clear(); } }; } public static <T> Callable<T> wrapWithMDC(Callable<T> callable) { Map<String, String> parentMDC = MDC.getCopyOfContextMap(); return () -> { if (parentMDC != null) { MDC.setContextMap(parentMDC); } try { return callable.call(); } finally { MDC.clear(); } }; } } CompletableFuture with MDC
public class MDCAwareCompletableFuture { public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) { Map<String, String> context = MDC.getCopyOfContextMap(); return CompletableFuture.supplyAsync(() -> { if (context != null) { MDC.setContextMap(context); } try { return supplier.get(); } finally { MDC.clear(); } }); } public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier, Executor executor) { Map<String, String> context = MDC.getCopyOfContextMap(); return CompletableFuture.supplyAsync(() -> { if (context != null) { MDC.setContextMap(context); } try { return supplier.get(); } finally { MDC.clear(); } }, executor); } public static CompletableFuture<Void> runAsync(Runnable runnable) { Map<String, String> context = MDC.getCopyOfContextMap(); return CompletableFuture.runAsync(() -> { if (context != null) { MDC.setContextMap(context); } try { runnable.run(); } finally { MDC.clear(); } }); } // Method to preserve MDC across thenApply, thenAccept, etc. public static <T, U> CompletableFuture<U> thenApplyWithMDC( CompletableFuture<T> future, Function<T, U> function) { Map<String, String> context = MDC.getCopyOfContextMap(); return future.thenApply(result -> { if (context != null) { MDC.setContextMap(context); } try { return function.apply(result); } finally { MDC.clear(); } }); } } Logback Configuration
Pattern Layout with MDC
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- Console Appender with MDC --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{requestId} %X{userId} %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- File Appender with MDC --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{requestId} %X{userId} %X{sessionId} %X{correlationId} %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- JSON Layout for structured logging --> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp/> <logLevel/> <loggerName/> <message/> <mdc/> <threadName/> <pattern> <pattern> { "service": "my-application", "version": "1.0.0" } </pattern> </pattern> </providers> </encoder> </appender> <!-- Separate appender for request tracing --> <appender name="REQUEST_TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/request-trace.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/request-trace.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | %X{requestId} | %X{userId} | %X{clientIp} | %X{userAgent} | %m%n</pattern> </encoder> </appender> <!-- Logger for request tracing --> <logger name="com.example.RequestTracer" level="DEBUG" additivity="false"> <appender-ref ref="REQUEST_TRACE" /> </logger> <!-- Root logger --> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </root> </configuration> Spring Boot Auto-Configuration
Auto-configuration for MDC
@Configuration @EnableConfigurationProperties(MDCProperties.class) public class MDCAutoConfiguration { @Bean @ConditionalOnWebApplication public MDCInterceptor mdcInterceptor() { return new MDCInterceptor(); } @Bean @ConditionalOnMissingBean public RequestContextFilter requestContextFilter() { return new RequestContextFilter(); } @Bean public MDCAwareThreadPoolTaskExecutor mdcAwareThreadPoolTaskExecutor( MDCProperties properties) { MDCAwareThreadPoolTaskExecutor executor = new MDCAwareThreadPoolTaskExecutor(); executor.setCorePoolSize(properties.getThreadPool().getCoreSize()); executor.setMaxPoolSize(properties.getThreadPool().getMaxSize()); executor.setQueueCapacity(properties.getThreadPool().getQueueCapacity()); executor.setThreadNamePrefix("mdc-aware-"); executor.setTaskDecorator(new MDCTaskDecorator()); executor.initialize(); return executor; } } @ConfigurationProperties(prefix = "mdc") @Data public class MDCProperties { private boolean enabled = true; private List<String> requiredFields = Arrays.asList("requestId", "correlationId"); private ThreadPool threadPool = new ThreadPool(); @Data public static class ThreadPool { private int coreSize = 10; private int maxSize = 50; private int queueCapacity = 100; } } @Component public class MDCTaskDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { Map<String, String> context = MDC.getCopyOfContextMap(); return () -> { if (context != null) { MDC.setContextMap(context); } try { runnable.run(); } finally { MDC.clear(); } }; } } Distributed Tracing Integration
Microservices Correlation
@Component public class CorrelationIdPropagator { private static final String CORRELATION_ID_HEADER = "X-Correlation-ID"; private static final String REQUEST_ID_HEADER = "X-Request-ID"; public void propagateToRestTemplate(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors(); interceptors.add(new MDCPropagationInterceptor()); restTemplate.setInterceptors(interceptors); } public void propagateToWebClient(WebClient.Builder webClientBuilder) { webClientBuilder.filter(new MDCWebClientFilter()); } private static class MDCPropagationInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String correlationId = RequestContext.getCorrelationId(); String requestId = RequestContext.getRequestId(); if (correlationId != null) { request.getHeaders().add(CORRELATION_ID_HEADER, correlationId); } if (requestId != null) { request.getHeaders().add(REQUEST_ID_HEADER, requestId); } // Log outgoing request LoggerFactory.getLogger(MDCPropagationInterceptor.class) .debug("Outgoing request to: {} {}", request.getMethod(), request.getURI()); return execution.execute(request, body); } } private static class MDCWebClientFilter implements ExchangeFilterFunction { @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { String correlationId = RequestContext.getCorrelationId(); String requestId = RequestContext.getRequestId(); ClientRequest.Builder requestBuilder = ClientRequest.from(request); if (correlationId != null) { requestBuilder.header(CORRELATION_ID_HEADER, correlationId); } if (requestId != null) { requestBuilder.header(REQUEST_ID_HEADER, requestId); } // Log outgoing request LoggerFactory.getLogger(MDCWebClientFilter.class) .debug("Outgoing WebClient request to: {} {}", request.method(), request.url()); return next.exchange(requestBuilder.build()); } } } Message Queue Integration
@Component public class MDCMessagePropagator { public <T> Message<T> injectMDC(Message<T> message) { Map<String, String> mdcContext = MDC.getCopyOfContextMap(); if (mdcContext == null || mdcContext.isEmpty()) { return message; } MessageBuilder<T> builder = MessageBuilder.fromMessage(message); mdcContext.forEach((key, value) -> builder.setHeader("MDC_" + key, value)); return builder.build(); } public void extractMDC(Message<?> message) { Map<String, String> mdcContext = new HashMap<>(); message.getHeaders().forEach((key, value) -> { if (key.startsWith("MDC_") && value instanceof String) { String mdcKey = key.substring(4); // Remove "MDC_" prefix mdcContext.put(mdcKey, (String) value); } }); if (!mdcContext.isEmpty()) { MDC.setContextMap(mdcContext); } } @EventListener public void handleApplicationEvent(ApplicationEvent event) { if (event instanceof MessageReceivedEvent) { MessageReceivedEvent msgEvent = (MessageReceivedEvent) event; extractMDC(msgEvent.getMessage()); } } } Testing MDC Implementation
Test Utilities
@ExtendWith(MockitoExtension.class) public class MDCTest { @Test public void testMDCContextPropagation() { // Given String requestId = "test-request-123"; String userId = "test-user-456"; // When RequestContext.initializeContext(requestId, userId); // Then assertThat(MDC.get("requestId")).isEqualTo(requestId); assertThat(MDC.get("userId")).isEqualTo(userId); assertThat(RequestContext.getRequestId()).isEqualTo(requestId); // Cleanup RequestContext.clearContext(); } @Test public void testMDCInAsyncOperations() throws Exception { // Given RequestContext.initializeContext("async-request", "async-user"); Map<String, String> originalContext = MDC.getCopyOfContextMap(); ExecutorService executor = MDCAwareThreadPool.newMDCAwareThreadPool(2, 4); // When CompletableFuture<String> future = MDCAwareCompletableFuture.supplyAsync(() -> { // Verify MDC is propagated assertThat(MDC.get("requestId")).isEqualTo("async-request"); assertThat(MDC.get("userId")).isEqualTo("async-user"); return "Completed with MDC"; }, executor); String result = future.get(5, TimeUnit.SECONDS); // Then assertThat(result).isEqualTo("Completed with MDC"); // Verify original context is preserved assertThat(MDC.getCopyOfContextMap()).isEqualTo(originalContext); // Cleanup executor.shutdown(); RequestContext.clearContext(); } @Test public void testMDCFilter() throws Exception { // Given MockHttpServletRequest request = new MockHttpServletRequest(); request.setRequestURI("/test"); request.setMethod("GET"); request.setRemoteAddr("192.168.1.1"); request.addHeader("User-Agent", "Test-Agent"); MockHttpServletResponse response = new MockHttpServletResponse(); MockFilterChain filterChain = new MockFilterChain(); MDCRequestFilter filter = new MDCRequestFilter(); // When filter.doFilter(request, response, filterChain); // Then assertThat(response.getHeader("X-Request-ID")).isNotNull(); assertThat(response.getHeader("X-Correlation-ID")).isNotNull(); assertThat(MDC.getCopyOfContextMap()).isEmpty(); // Should be cleared } } @SpringBootTest class MDCIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test void testRequestTracing() { // When ResponseEntity<String> response = restTemplate.getForEntity("/api/test", String.class); // Then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // Verify tracing headers are present HttpHeaders headers = response.getHeaders(); assertThat(headers.getFirst("X-Request-ID")).isNotNull(); assertThat(headers.getFirst("X-Correlation-ID")).isNotNull(); } } Performance Monitoring with MDC
Performance Context
public class PerformanceContext { private static final String START_TIME_KEY = "startTime"; private static final String OPERATION_NAME_KEY = "operationName"; private static final String DURATION_KEY = "durationMs"; private static final Logger perfLogger = LoggerFactory.getLogger("PERFORMANCE"); public static void startOperation(String operationName) { MDC.put(OPERATION_NAME_KEY, operationName); MDC.put(START_TIME_KEY, String.valueOf(System.currentTimeMillis())); } public static void endOperation() { String startTimeStr = MDC.get(START_TIME_KEY); String operationName = MDC.get(OPERATION_NAME_KEY); if (startTimeStr != null && operationName != null) { long startTime = Long.parseLong(startTimeStr); long duration = System.currentTimeMillis() - startTime; MDC.put(DURATION_KEY, String.valueOf(duration)); perfLogger.info("Operation completed: {}", operationName); // Alert if operation took too long if (duration > 1000) { // 1 second threshold perfLogger.warn("Slow operation detected: {} took {}ms", operationName, duration); } } MDC.remove(START_TIME_KEY); MDC.remove(OPERATION_NAME_KEY); MDC.remove(DURATION_KEY); } public static void measure(String operationName, Runnable operation) { startOperation(operationName); try { operation.run(); } finally { endOperation(); } } public static <T> T measure(String operationName, Supplier<T> operation) { startOperation(operationName); try { return operation.get(); } finally { endOperation(); } } } Best Practices and Patterns
MDC Best Practices
public class MDCBestPractices { // 1. Always use try-finally for MDC operations public void safeMDCUsage() { MDC.put("key", "value"); try { // Perform operations logger.info("Operation with MDC"); } finally { MDC.remove("key"); } } // 2. Use consistent key names across application public static class MDCKeys { public static final String REQUEST_ID = "requestId"; public static final String USER_ID = "userId"; public static final String CORRELATION_ID = "correlationId"; public static final String SESSION_ID = "sessionId"; public static final String CLIENT_IP = "clientIp"; public static final String OPERATION = "operation"; } // 3. Avoid storing large objects in MDC public void avoidLargeObjects() { // Good - store identifiers only MDC.put(MDCKeys.USER_ID, "user123"); // Bad - storing large objects // MDC.put("userObject", user.toString()); // Don't do this! } // 4. Use MDC for cross-cutting concerns only public void properMDCUsage() { // Good - tracing, auditing, monitoring MDC.put(MDCKeys.REQUEST_ID, generateRequestId()); // Bad - business logic data // MDC.put("orderTotal", "100.50"); // Don't do this! } // 5. Clear MDC in finally blocks public void processRequest(HttpServletRequest request) { RequestContext.initializeContext(request); try { // Process request businessService.process(); } catch (Exception e) { logger.error("Request failed", e); throw e; } finally { RequestContext.clearContext(); // Always clear! } } // 6. Use MDC-aware executors for async operations public void asyncWithMDC() { CompletableFuture<Void> future = MDCAwareCompletableFuture.runAsync(() -> { // MDC context is automatically available here logger.info("Processing async operation"); }); } } This comprehensive SLF4J MDC implementation provides robust request tracing capabilities for Java applications. It ensures that contextual information flows seamlessly through synchronous and asynchronous operations, making debugging and monitoring much more effective in distributed systems.