Logging Context with MDC in Java: Complete Guide for Contextual Logging

Introduction to MDC (Mapped Diagnostic Context)

MDC is a powerful feature in logging frameworks that allows you to store context information in a thread-local manner, making it automatically available in log messages. This is essential for distributed systems where you need to track requests across multiple services and components.

Key Benefits of MDC

  • Request Tracing: Correlate logs across distributed systems
  • Context Enrichment: Add business context to log messages
  • Thread Safety: Thread-local storage ensures isolation
  • Framework Agnostic: Works with SLF4J, Logback, Log4j2
  • Minimal Code Impact: Set once, log everywhere with context

Core MDC Implementation

Dependencies

Add to your pom.xml:

<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Logback Classic (implementation) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<!-- Log4j2 (alternative) -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.21.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.21.1</version>
</dependency>
<!-- For web applications -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- For Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.5</version>
</dependency>
</dependencies>

Basic MDC Usage

Simple MDC Manager

package com.example.mdc;
import org.slf4j.MDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
public class BasicMDCManager {
private static final Logger logger = LoggerFactory.getLogger(BasicMDCManager.class);
// Common MDC keys
public static final String TRACE_ID = "traceId";
public static final String SPAN_ID = "spanId";
public static final String USER_ID = "userId";
public static final String SESSION_ID = "sessionId";
public static final String REQUEST_ID = "requestId";
public static final String TENANT_ID = "tenantId";
public static final String CORRELATION_ID = "correlationId";
public static void setTraceId(String traceId) {
MDC.put(TRACE_ID, traceId);
}
public static void setSpanId(String spanId) {
MDC.put(SPAN_ID, spanId);
}
public static void setUserId(String userId) {
MDC.put(USER_ID, userId);
}
public static void setSessionId(String sessionId) {
MDC.put(SESSION_ID, sessionId);
}
public static void setRequestId(String requestId) {
MDC.put(REQUEST_ID, requestId);
}
public static void setTenantId(String tenantId) {
MDC.put(TENANT_ID, tenantId);
}
public static void setCorrelationId(String correlationId) {
MDC.put(CORRELATION_ID, correlationId);
}
public static String getTraceId() {
return MDC.get(TRACE_ID);
}
public static String getUserId() {
return MDC.get(USER_ID);
}
public static String getCorrelationId() {
return MDC.get(CORRELATION_ID);
}
public static void clear() {
MDC.clear();
}
public static Map<String, String> getCopyOfContextMap() {
return MDC.getCopyOfContextMap();
}
public static void setContextMap(Map<String, String> context) {
MDC.setContextMap(context);
}
// Generate a new trace ID
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
// Generate a new span ID
public static String generateSpanId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
}
// Initialize a new request context
public static void initializeRequestContext() {
setTraceId(generateTraceId());
setSpanId(generateSpanId());
setRequestId(generateTraceId());
setCorrelationId(generateTraceId());
}
// Initialize context with existing trace ID
public static void initializeRequestContext(String traceId, String spanId) {
setTraceId(traceId);
setSpanId(spanId);
setRequestId(generateTraceId());
setCorrelationId(traceId); // Use traceId as correlationId
}
// Example usage
public static void demonstrateBasicUsage() {
// Set context
initializeRequestContext();
setUserId("user-12345");
setTenantId("acme-corp");
// Log messages - context is automatically included
logger.info("User login successful");
logger.debug("Processing user request");
// Clear context when done
clear();
}
// Execute with context
public static <T> T executeWithContext(Map<String, String> context, Callable<T> task) {
try {
setContextMap(context);
return task.call();
} catch (Exception e) {
logger.error("Error executing task with context", e);
throw new RuntimeException("Task execution failed", e);
} finally {
clear();
}
}
// Run with context (void tasks)
public static void runWithContext(Map<String, String> context, Runnable task) {
try {
setContextMap(context);
task.run();
} finally {
clear();
}
}
}

Advanced MDC Context Manager

package com.example.mdc;
import org.slf4j.MDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class MDCContextManager {
private static final Logger logger = LoggerFactory.getLogger(MDCContextManager.class);
private static final ThreadLocal<Map<String, String>> CONTEXT_STACK = 
ThreadLocal.withInitial(HashMap::new);
public static class MDCContext implements AutoCloseable {
private final Map<String, String> previousContext;
private MDCContext(Map<String, String> context) {
this.previousContext = MDC.getCopyOfContextMap();
MDC.setContextMap(context);
}
@Override
public void close() {
if (previousContext != null) {
MDC.setContextMap(previousContext);
} else {
MDC.clear();
}
}
}
public static MDCContext withContext(Map<String, String> context) {
return new MDCContext(context);
}
public static MDCContext withContext(String key, String value) {
Map<String, String> context = new HashMap<>();
context.put(key, value);
return new MDCContext(context);
}
public static MDCContext withTraceContext(String traceId, String spanId) {
Map<String, String> context = new HashMap<>();
context.put(BasicMDCManager.TRACE_ID, traceId);
context.put(BasicMDCManager.SPAN_ID, spanId);
context.put(BasicMDCManager.CORRELATION_ID, traceId);
return new MDCContext(context);
}
public static MDCContext withUserContext(String userId, String tenantId) {
Map<String, String> context = new HashMap<>();
context.put(BasicMDCManager.USER_ID, userId);
context.put(BasicMDCManager.TENANT_ID, tenantId);
return new MDCContext(context);
}
public static <T> T executeWithContext(Map<String, String> context, Supplier<T> supplier) {
try (MDCContext ignored = withContext(context)) {
return supplier.get();
}
}
public static void executeWithContext(Map<String, String> context, Runnable runnable) {
try (MDCContext ignored = withContext(context)) {
runnable.run();
}
}
// Push context to stack (for nested contexts)
public static void pushContext(Map<String, String> context) {
Map<String, String> currentStack = CONTEXT_STACK.get();
currentStack.putAll(context);
MDC.setContextMap(currentStack);
}
// Pop context from stack
public static void popContext() {
Map<String, String> currentStack = CONTEXT_STACK.get();
if (!currentStack.isEmpty()) {
// Remove the most recently added entries
// This is a simplified implementation
MDC.setContextMap(currentStack);
}
}
// Clear entire stack
public static void clearStack() {
CONTEXT_STACK.get().clear();
MDC.clear();
}
}

Web Application Integration

Servlet Filter for MDC

package com.example.mdc.web;
import com.example.mdc.BasicMDCManager;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
public class MDCLoggingFilter implements Filter {
private static final String[] HEADER_NAMES = {
"X-Trace-ID", "X-Correlation-ID", "X-Request-ID", 
"X-User-ID", "X-Tenant-ID", "X-Session-ID"
};
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// Extract or generate trace context
setupMDCContext(httpRequest);
// Add MDC context to response headers
addMDCToResponse(httpResponse);
// Continue with the filter chain
chain.doFilter(request, response);
} finally {
// Always clear MDC context
MDC.clear();
}
}
private void setupMDCContext(HttpServletRequest request) {
// Extract trace ID from headers or generate new one
String traceId = getHeaderOrDefault(request, "X-Trace-ID", 
BasicMDCManager.generateTraceId());
String correlationId = getHeaderOrDefault(request, "X-Correlation-ID", traceId);
String requestId = getHeaderOrDefault(request, "X-Request-ID", 
BasicMDCManager.generateTraceId());
// Set basic tracing context
BasicMDCManager.setTraceId(traceId);
BasicMDCManager.setCorrelationId(correlationId);
BasicMDCManager.setRequestId(requestId);
BasicMDCManager.setSpanId(BasicMDCManager.generateSpanId());
// Set user context if available
String userId = getHeaderOrDefault(request, "X-User-ID", null);
if (StringUtils.hasText(userId)) {
BasicMDCManager.setUserId(userId);
}
// Set tenant context if available
String tenantId = getHeaderOrDefault(request, "X-Tenant-ID", null);
if (StringUtils.hasText(tenantId)) {
BasicMDCManager.setTenantId(tenantId);
}
// Set session context if available
String sessionId = getHeaderOrDefault(request, "X-Session-ID", null);
if (StringUtils.hasText(sessionId)) {
BasicMDCManager.setSessionId(sessionId);
}
// Add request-specific information
MDC.put("http.method", request.getMethod());
MDC.put("http.uri", request.getRequestURI());
MDC.put("http.query", request.getQueryString());
MDC.put("http.remoteAddr", request.getRemoteAddr());
MDC.put("http.userAgent", request.getHeader("User-Agent"));
}
private void addMDCToResponse(HttpServletResponse response) {
// Add tracing headers to response
response.setHeader("X-Trace-ID", BasicMDCManager.getTraceId());
response.setHeader("X-Correlation-ID", BasicMDCManager.getCorrelationId());
response.setHeader("X-Request-ID", BasicMDCManager.getCorrelationId());
// Add other context headers if available
String userId = BasicMDCManager.getUserId();
if (StringUtils.hasText(userId)) {
response.setHeader("X-User-ID", userId);
}
}
private String getHeaderOrDefault(HttpServletRequest request, String headerName, String defaultValue) {
String value = request.getHeader(headerName);
return StringUtils.hasText(value) ? value : defaultValue;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization logic if needed
}
@Override
public void destroy() {
// Cleanup logic if needed
}
}

Spring Boot Configuration

package com.example.mdc.config;
import com.example.mdc.web.MDCLoggingFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class MDCConfig {
@Bean
public FilterRegistrationBean<MDCLoggingFilter> mdcLoggingFilter() {
FilterRegistrationBean<MDCLoggingFilter> registrationBean = 
new FilterRegistrationBean<>();
registrationBean.setFilter(new MDCLoggingFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
registrationBean.setName("MDCLoggingFilter");
return registrationBean;
}
}

REST Controller with MDC

package com.example.mdc.controller;
import com.example.mdc.BasicMDCManager;
import com.example.mdc.MDCContextManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/users/{userId}")
public ResponseEntity<Map<String, Object>> getUser(@PathVariable String userId) {
// Set user context in MDC
BasicMDCManager.setUserId(userId);
logger.info("Fetching user details");
try {
// Simulate business logic
Map<String, Object> user = findUserById(userId);
logger.debug("User found: {}", user.get("username"));
return ResponseEntity.ok(user);
} catch (Exception e) {
logger.error("Error fetching user: {}", userId, e);
throw e;
}
}
@PostMapping("/users")
public ResponseEntity<Map<String, Object>> createUser(@RequestBody Map<String, Object> userData) {
String userId = UUID.randomUUID().toString();
// Use try-with-resources for MDC context
try (var context = MDCContextManager.withUserContext(userId, "default-tenant")) {
logger.info("Creating new user with data: {}", userData);
// Simulate user creation
Map<String, Object> createdUser = createUser(userId, userData);
logger.info("User created successfully: {}", userId);
return ResponseEntity.ok(createdUser);
}
}
@GetMapping("/users/{userId}/orders")
public ResponseEntity<Map<String, Object>> getUserOrders(@PathVariable String userId) {
// Set multiple context values
Map<String, String> context = new HashMap<>();
context.put(BasicMDCManager.USER_ID, userId);
context.put("operation", "list-orders");
context.put("resource", "orders");
return MDCContextManager.executeWithContext(context, () -> {
logger.info("Fetching orders for user");
// Simulate fetching orders
Map<String, Object> orders = findUserOrders(userId);
logger.debug("Found {} orders for user", orders.get("count"));
return ResponseEntity.ok(orders);
});
}
private Map<String, Object> findUserById(String userId) {
// Simulate database lookup
Map<String, Object> user = new HashMap<>();
user.put("id", userId);
user.put("username", "user-" + userId);
user.put("email", "user" + userId + "@example.com");
user.put("status", "active");
// Log with MDC context automatically included
logger.debug("User lookup completed for: {}", userId);
return user;
}
private Map<String, Object> createUser(String userId, Map<String, Object> userData) {
// Simulate user creation
Map<String, Object> user = new HashMap<>();
user.put("id", userId);
user.put("username", userData.get("username"));
user.put("email", userData.get("email"));
user.put("status", "created");
user.put("createdAt", System.currentTimeMillis());
logger.debug("User creation logic completed");
return user;
}
private Map<String, Object> findUserOrders(String userId) {
// Simulate orders lookup
Map<String, Object> orders = new HashMap<>();
orders.put("userId", userId);
orders.put("count", 5);
orders.put("orders", new String[]{"order1", "order2", "order3", "order4", "order5"});
// Add business-specific MDC context
MDC.put("order.count", "5");
logger.info("Orders retrieval completed");
MDC.remove("order.count"); // Clean up temporary context
return orders;
}
}

Logback Configuration

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- MDC context properties -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}, %X{spanId}, %X{userId}, %X{tenantId}] - %msg%n"/>
<property name="JSON_LOG_PATTERN" value='{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","thread":"%thread","level":"%level","logger":"%logger{36}","traceId":"%X{traceId}","spanId":"%X{spanId}","userId":"%X{userId}","tenantId":"%X{tenantId}","correlationId":"%X{correlationId}","message":"%msg","exception":"%ex"}%n'/>
<!-- Console Appender with MDC -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- JSON Console Appender -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</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>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Async Appender for better performance -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
<queueSize>1000</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>false</includeCallerData>
</appender>
<!-- MDC Filtering Appender -->
<appender name="USER_ACTIVITY" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/user-activity.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/user-activity.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>mdc.get("userId") != null</expression>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | %X{userId} | %X{operation} | %msg%n</pattern>
</encoder>
</appender>
<!-- Loggers -->
<logger name="com.example.mdc" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
<appender-ref ref="USER_ACTIVITY" />
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
</root>
</configuration>

Async and Thread Pool Integration

MDC-Aware Thread Pool

package com.example.mdc.concurrent;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
public class MDCAwareThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable task) {
super.execute(wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task, MDC.getCopyOfContextMap()));
}
private Runnable wrap(Runnable task, Map<String, String> context) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
MDC.setContextMap(context);
task.run();
} finally {
MDC.setContextMap(previous);
}
};
}
private <T> Callable<T> wrap(Callable<T> task, Map<String, String> context) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
MDC.setContextMap(context);
return task.call();
} finally {
MDC.setContextMap(previous);
}
};
}
}

MDC-Aware CompletableFuture

package com.example.mdc.concurrent;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
public class MDCAwareCompletableFuture {
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
return supplyAsync(supplier, null);
}
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier, Executor executor) {
Map<String, String> context = MDC.getCopyOfContextMap();
Supplier<T> wrappedSupplier = () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
MDC.setContextMap(context);
return supplier.get();
} finally {
MDC.setContextMap(previous);
}
};
return executor != null ? 
CompletableFuture.supplyAsync(wrappedSupplier, executor) :
CompletableFuture.supplyAsync(wrappedSupplier);
}
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return runAsync(runnable, null);
}
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) {
Map<String, String> context = MDC.getCopyOfContextMap();
Runnable wrappedRunnable = () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
MDC.setContextMap(context);
runnable.run();
} finally {
MDC.setContextMap(previous);
}
};
return executor != null ? 
CompletableFuture.runAsync(wrappedRunnable, executor) :
CompletableFuture.runAsync(wrappedRunnable);
}
// Wrap existing CompletableFuture to preserve MDC
public static <T> CompletableFuture<T> wrap(CompletableFuture<T> future) {
Map<String, String> context = MDC.getCopyOfContextMap();
return future.thenApply(result -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
MDC.setContextMap(context);
return result;
} finally {
MDC.setContextMap(previous);
}
});
}
}

Spring Async Configuration

package com.example.mdc.config;
import com.example.mdc.concurrent.MDCAwareThreadPoolTaskExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Bean(name = "taskExecutor")
@Override
public Executor getAsyncExecutor() {
MDCAwareThreadPoolTaskExecutor executor = new MDCAwareThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("MDCAwareAsync-");
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}

Business Service with MDC

package com.example.mdc.service;
import com.example.mdc.BasicMDCManager;
import com.example.mdc.MDCContextManager;
import com.example.mdc.concurrent.MDCAwareCompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
private final InventoryService inventoryService;
private final PaymentService paymentService;
public OrderService(InventoryService inventoryService, PaymentService paymentService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
public Map<String, Object> processOrder(Map<String, Object> orderRequest) {
String orderId = (String) orderRequest.get("orderId");
String userId = (String) orderRequest.get("userId");
// Set order context
Map<String, String> context = new HashMap<>();
context.put("orderId", orderId);
context.put("userId", userId);
context.put("operation", "process-order");
return MDCContextManager.executeWithContext(context, () -> {
logger.info("Starting order processing for order: {}", orderId);
try {
// Step 1: Validate order
validateOrder(orderRequest);
// Step 2: Check inventory
boolean inventoryAvailable = checkInventory(orderRequest);
if (!inventoryAvailable) {
throw new RuntimeException("Inventory not available");
}
// Step 3: Process payment
processPayment(orderRequest);
// Step 4: Update inventory
updateInventory(orderRequest);
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("status", "completed");
result.put("message", "Order processed successfully");
logger.info("Order processing completed for order: {}", orderId);
return result;
} catch (Exception e) {
logger.error("Order processing failed for order: {}", orderId, e);
throw e;
}
});
}
@Async("taskExecutor")
public CompletableFuture<Map<String, Object>> processOrderAsync(Map<String, Object> orderRequest) {
return MDCAwareCompletableFuture.supplyAsync(() -> processOrder(orderRequest));
}
public CompletableFuture<Map<String, Object>> processOrderParallel(Map<String, Object> orderRequest) {
String orderId = (String) orderRequest.get("orderId");
// Set context for parallel processing
Map<String, String> context = BasicMDCManager.getCopyOfContextMap();
context.put("orderId", orderId);
context.put("operation", "parallel-order-processing");
CompletableFuture<Boolean> inventoryCheck = MDCAwareCompletableFuture.supplyAsync(
() -> inventoryService.checkAvailability(orderRequest), context);
CompletableFuture<Boolean> paymentCheck = MDCAwareCompletableFuture.supplyAsync(
() -> paymentService.validatePayment(orderRequest), context);
return inventoryCheck.thenCombine(paymentCheck, (inventory, payment) -> {
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("inventoryAvailable", inventory);
result.put("paymentValid", payment);
result.put("status", inventory && payment ? "approved" : "rejected");
logger.info("Parallel order processing completed for order: {}", orderId);
return result;
});
}
private void validateOrder(Map<String, Object> orderRequest) {
logger.debug("Validating order");
// Simulate validation logic
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Order validation interrupted", e);
}
logger.debug("Order validation completed");
}
private boolean checkInventory(Map<String, Object> orderRequest) {
logger.debug("Checking inventory");
// Simulate inventory check
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Inventory check interrupted", e);
}
logger.debug("Inventory check completed");
return true;
}
private void processPayment(Map<String, Object> orderRequest) {
logger.debug("Processing payment");
// Simulate payment processing
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Payment processing interrupted", e);
}
logger.debug("Payment processing completed");
}
private void updateInventory(Map<String, Object> orderRequest) {
logger.debug("Updating inventory");
// Simulate inventory update
try {
Thread.sleep(80);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Inventory update interrupted", e);
}
logger.debug("Inventory update completed");
}
}

Testing MDC Implementation

Unit Tests

package com.example.mdc;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
class MDCTest {
private static final Logger logger = LoggerFactory.getLogger(MDCTest.class);
@Test
void testBasicMDCOperations() {
// Set values in MDC
MDC.put("traceId", "test-trace-123");
MDC.put("userId", "test-user-456");
// Verify values are set
assertEquals("test-trace-123", MDC.get("traceId"));
assertEquals("test-user-456", MDC.get("userId"));
// Test copy of context map
Map<String, String> context = MDC.getCopyOfContextMap();
assertEquals(2, context.size());
assertTrue(context.containsKey("traceId"));
assertTrue(context.containsKey("userId"));
// Clear MDC
MDC.clear();
assertNull(MDC.get("traceId"));
assertNull(MDC.get("userId"));
}
@Test
void testMDCContextManager() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "context-trace-789");
context.put("operation", "test-operation");
try (var mdcContext = MDCContextManager.withContext(context)) {
assertEquals("context-trace-789", MDC.get("traceId"));
assertEquals("test-operation", MDC.get("operation"));
// Log with context
logger.info("Testing MDC context manager");
}
// Context should be cleared after try-with-resources
assertNull(MDC.get("traceId"));
assertNull(MDC.get("operation"));
}
@Test
void testBasicMDCManager() {
BasicMDCManager.initializeRequestContext();
assertNotNull(BasicMDCManager.getTraceId());
assertNotNull(BasicMDCManager.getCorrelationId());
BasicMDCManager.setUserId("test-user");
assertEquals("test-user", BasicMDCManager.getUserId());
BasicMDCManager.clear();
assertNull(BasicMDCManager.getTraceId());
assertNull(BasicMDCManager.getUserId());
}
@Test
void testMDCInMultiThreadedEnvironment() throws InterruptedException {
int threadCount = 5;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
executor.submit(() -> {
try {
// Each thread gets its own MDC context
MDC.put("threadId", String.valueOf(threadId));
MDC.put("traceId", "trace-" + threadId);
// Verify thread isolation
assertEquals(String.valueOf(threadId), MDC.get("threadId"));
assertEquals("trace-" + threadId, MDC.get("traceId"));
logger.info("Thread {} executing with its own MDC context", threadId);
} finally {
MDC.clear();
latch.countDown();
}
});
}
latch.await(5, TimeUnit.SECONDS);
executor.shutdown();
// Main thread MDC should not be affected
assertNull(MDC.get("threadId"));
assertNull(MDC.get("traceId"));
}
@Test
void testMDCAwareExecution() {
Map<String, String> context = new HashMap<>();
context.put("testKey", "testValue");
context.put("traceId", "execution-trace");
String result = MDCContextManager.executeWithContext(context, () -> {
// Verify context is set in the execution
assertEquals("testValue", MDC.get("testKey"));
assertEquals("execution-trace", MDC.get("traceId"));
logger.info("Executing with MDC context");
return "success";
});
assertEquals("success", result);
// Context should be cleared after execution
assertNull(MDC.get("testKey"));
assertNull(MDC.get("traceId"));
}
}

Integration Test

package com.example.mdc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MDCIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testMDCPropagationThroughHTTP() {
HttpHeaders headers = new HttpHeaders();
headers.set("X-Trace-ID", "integration-test-trace");
headers.set("X-User-ID", "integration-test-user");
headers.set("X-Tenant-ID", "integration-test-tenant");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
"http://localhost:" + port + "/api/users/test-user",
HttpMethod.GET,
entity,
Map.class
);
assertEquals(200, response.getStatusCodeValue());
Map<String, Object> body = response.getBody();
assertNotNull(body);
assertEquals("test-user", body.get("id"));
// Verify response headers contain trace context
assertNotNull(response.getHeaders().get("X-Trace-ID"));
assertNotNull(response.getHeaders().get("X-Correlation-ID"));
}
@Test
void testMDCInAsyncOperations() throws Exception {
Map<String, Object> orderRequest = new HashMap<>();
orderRequest.put("orderId", "async-test-order");
orderRequest.put("userId", "async-test-user");
// This would test the async order processing
// Implementation depends on your specific async setup
}
}

Best Practices and Patterns

1. MDC Cleanup Pattern

package com.example.mdc.patterns;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
public class MDCCleanupPattern {
// Always use try-finally for MDC operations
public static void safeMDCOperation() {
Map<String, String> previousContext = MDC.getCopyOfContextMap();
try {
MDC.put("operation", "safe-operation");
// Perform your business logic
performBusinessLogic();
} finally {
MDC.setContextMap(previousContext);
}
}
// Use @ExceptionHandler in Spring controllers to ensure MDC cleanup
@ExceptionHandler(Exception.class)
public void handleException(Exception ex, HttpServletRequest request) {
// Log the error with MDC context
// MDC context is still available here
// Perform cleanup or additional logging
MDC.put("error", ex.getClass().getSimpleName());
// Don't forget to clear or restore MDC if needed
}
private static void performBusinessLogic() {
// Business logic implementation
}
}

2. MDC Context Enricher

package com.example.mdc.patterns;
import org.slf4j.MDC;
import java.util.function.Consumer;
public class MDCEnricher {
public static void withContext(String key, String value, Runnable task) {
String previousValue = MDC.get(key);
try {
MDC.put(key, value);
task.run();
} finally {
if (previousValue != null) {
MDC.put(key, previousValue);
} else {
MDC.remove(key);
}
}
}
public static <T> T withContext(String key, String value, Supplier<T> task) {
String previousValue = MDC.get(key);
try {
MDC.put(key, value);
return task.get();
} finally {
if (previousValue != null) {
MDC.put(key, previousValue);
} else {
MDC.remove(key);
}
}
}
public static void enrichContext(Consumer<MDC> enricher, Runnable task) {
Map<String, String> previousContext = MDC.getCopyOfContextMap();
try {
enricher.accept(MDC::put);
task.run();
} finally {
MDC.setContextMap(previousContext);
}
}
}

Conclusion

This comprehensive MDC implementation provides:

  • Thread-safe context management for logging
  • Web application integration with servlet filters
  • Async operation support with MDC-aware executors
  • Spring Boot integration for easy adoption
  • Comprehensive testing strategies
  • Production-ready patterns and best practices

Key benefits include:

  1. Distributed Tracing: Correlate logs across microservices
  2. Debugging Efficiency: Quickly identify request flows
  3. Audit Compliance: Track user actions with context
  4. Performance Monitoring: Understand operation timing with context
  5. Error Diagnosis: Rich context for troubleshooting

The implementation ensures that MDC context is properly propagated across thread boundaries and cleaned up to prevent memory leaks, making it suitable for production use in high-throughput applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper