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.