MDC (Mapped Diagnostic Context) is a powerful logging feature that allows you to store contextual information in thread-local storage, making it available to all logging statements within the same thread. This comprehensive guide covers MDC implementation, best practices, and advanced patterns.
Architecture Overview
Application → MDC Context → ThreadLocal Storage → Logging Statements → Context Enrichment → Async Processing → Context Propagation → Web Requests → Filter/Interceptor → Spring Integration → AOP/Annotations
Prerequisites and Setup
Maven Dependencies
<properties> <slf4j.version>2.0.7</slf4j.version> <logback.version>1.4.11</logback.version> <spring.boot.version>3.1.0</spring.boot.version> </properties> <dependencies> <!-- SLF4J API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <!-- Logback Classic (includes MDC implementation) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- Spring Boot (optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>${spring.boot.version}</version> </dependency> </dependencies> Logback Configuration
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- MDC property definitions --> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId},%X{spanId},%X{userId},%X{sessionId},%X{requestId}] - %msg%n"/> <!-- Console Appender with MDC --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <!-- File Appender with MDC --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <!-- JSON Layout for structured logging --> <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.json</file> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp/> <logLevel/> <loggerName/> <message/> <mdc/> <threadName/> <stackTrace/> </providers> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.json</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <!-- Async Appender for better performance --> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE"/> <queueSize>1000</queueSize> <discardingThreshold>0</discardingThreshold> <includeCallerData>true</includeCallerData> </appender> <!-- Root logger --> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC"/> <appender-ref ref="JSON_FILE"/> </root> <!-- Custom logger for business context --> <logger name="com.yourapp" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC"/> </logger> </configuration> Core MDC Implementation
1. MDC Context Manager
package com.yourapp.logging.mdc; import org.slf4j.MDC; import org.springframework.stereotype.Component; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; @Component public class MdcContextManager { // MDC Key Constants 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 CLIENT_IP = "clientIp"; public static final String USER_AGENT = "userAgent"; public static final String REQUEST_PATH = "requestPath"; public static final String REQUEST_METHOD = "requestMethod"; public static final String CORRELATION_ID = "correlationId"; public static final String TENANT_ID = "tenantId"; public static final String APPLICATION_NAME = "applicationName"; /** * Initialize MDC context for new request */ public void initializeContext() { clearContext(); put(TRACE_ID, generateTraceId()); put(SPAN_ID, generateSpanId()); put(REQUEST_ID, generateRequestId()); } /** * Initialize MDC context with existing trace ID */ public void initializeContext(String traceId, String spanId) { clearContext(); put(TRACE_ID, traceId != null ? traceId : generateTraceId()); put(SPAN_ID, spanId != null ? spanId : generateSpanId()); put(REQUEST_ID, generateRequestId()); } /** * Put value in MDC */ public void put(String key, String value) { if (value != null) { MDC.put(key, value); } } /** * Get value from MDC */ public String get(String key) { return MDC.get(key); } /** * Remove key from MDC */ public void remove(String key) { MDC.remove(key); } /** * Clear entire MDC context */ public void clearContext() { MDC.clear(); } /** * Get current MDC context as map */ public Map<String, String> getContext() { return MDC.getCopyOfContextMap(); } /** * Set entire MDC context from map */ public void setContext(Map<String, String> context) { if (context != null) { MDC.setContextMap(context); } } /** * Execute runnable with MDC context */ public void executeWithContext(Runnable runnable, Map<String, String> context) { Map<String, String> previousContext = getContext(); try { setContext(context); runnable.run(); } finally { setContext(previousContext); } } /** * Execute callable with MDC context */ public <T> T executeWithContext(Callable<T> callable, Map<String, String> context) throws Exception { Map<String, String> previousContext = getContext(); try { setContext(context); return callable.call(); } finally { setContext(previousContext); } } /** * Wrap runnable with current MDC context */ public Runnable wrapWithContext(Runnable runnable) { Map<String, String> context = getContext(); return () -> { Map<String, String> previousContext = getContext(); try { setContext(context); runnable.run(); } finally { setContext(previousContext); } }; } /** * Wrap callable with current MDC context */ public <T> Callable<T> wrapWithContext(Callable<T> callable) { Map<String, String> context = getContext(); return () -> { Map<String, String> previousContext = getContext(); try { setContext(context); return callable.call(); } finally { setContext(previousContext); } }; } /** * Check if MDC context is initialized */ public boolean isContextInitialized() { return get(TRACE_ID) != null; } /** * Generate trace ID */ private String generateTraceId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 16); } /** * Generate span ID */ private String generateSpanId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 8); } /** * Generate request ID */ private String generateRequestId() { return UUID.randomUUID().toString().replace("-", "").substring(0, 12); } /** * Create child span context */ public Map<String, String> createChildContext() { Map<String, String> parentContext = getContext(); Map<String, String> childContext = new java.util.HashMap<>(parentContext); childContext.put(SPAN_ID, generateSpanId()); childContext.put(REQUEST_ID, generateRequestId()); return childContext; } } 2. Logging Context Model
package com.yourapp.logging.mdc; import java.util.HashMap; import java.util.Map; public class LoggingContext { private final Map<String, String> context; private LoggingContext(Map<String, String> context) { this.context = new HashMap<>(context); } public static Builder builder() { return new Builder(); } public Map<String, String> getContext() { return new HashMap<>(context); } public String getTraceId() { return context.get(MdcContextManager.TRACE_ID); } public String getSpanId() { return context.get(MdcContextManager.SPAN_ID); } public String getUserId() { return context.get(MdcContextManager.USER_ID); } public static class Builder { private final Map<String, String> context = new HashMap<>(); public Builder traceId(String traceId) { context.put(MdcContextManager.TRACE_ID, traceId); return this; } public Builder spanId(String spanId) { context.put(MdcContextManager.SPAN_ID, spanId); return this; } public Builder userId(String userId) { context.put(MdcContextManager.USER_ID, userId); return this; } public Builder sessionId(String sessionId) { context.put(MdcContextManager.SESSION_ID, sessionId); return this; } public Builder requestId(String requestId) { context.put(MdcContextManager.REQUEST_ID, requestId); return this; } public Builder clientIp(String clientIp) { context.put(MdcContextManager.CLIENT_IP, clientIp); return this; } public Builder userAgent(String userAgent) { context.put(MdcContextManager.USER_AGENT, userAgent); return this; } public Builder requestPath(String requestPath) { context.put(MdcContextManager.REQUEST_PATH, requestPath); return this; } public Builder requestMethod(String requestMethod) { context.put(MdcContextManager.REQUEST_METHOD, requestMethod); return this; } public Builder correlationId(String correlationId) { context.put(MdcContextManager.CORRELATION_ID, correlationId); return this; } public Builder tenantId(String tenantId) { context.put(MdcContextManager.TENANT_ID, tenantId); return this; } public Builder applicationName(String applicationName) { context.put(MdcContextManager.APPLICATION_NAME, applicationName); return this; } public Builder custom(String key, String value) { context.put(key, value); return this; } public LoggingContext build() { return new LoggingContext(context); } } } Web Integration
3. MDC Web Filter
package com.yourapp.logging.web; import com.yourapp.logging.mdc.MdcContextManager; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @Component @Order(1) public class MdcLoggingFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(MdcLoggingFilter.class); private final MdcContextManager mdcContextManager; public MdcLoggingFilter(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; // Wrap request/response for multiple reads ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(httpRequest); ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(httpResponse); long startTime = System.currentTimeMillis(); try { // Initialize MDC context initializeMdcContext(wrappedRequest); // Log request logRequest(wrappedRequest); // Continue filter chain chain.doFilter(wrappedRequest, wrappedResponse); // Log response logResponse(wrappedRequest, wrappedResponse, startTime); } finally { // Copy response body and clear MDC wrappedResponse.copyBodyToResponse(); mdcContextManager.clearContext(); } } private void initializeMdcContext(HttpServletRequest request) { mdcContextManager.initializeContext(); // Extract from headers or generate new String traceId = getHeader(request, "X-Trace-Id"); String spanId = getHeader(request, "X-Span-Id"); String correlationId = getHeader(request, "X-Correlation-Id"); if (traceId != null || spanId != null) { mdcContextManager.initializeContext(traceId, spanId); } // Set request-specific context mdcContextManager.put(MdcContextManager.CORRELATION_ID, correlationId != null ? correlationId : mdcContextManager.get(MdcContextManager.TRACE_ID)); mdcContextManager.put(MdcContextManager.CLIENT_IP, getClientIp(request)); mdcContextManager.put(MdcContextManager.USER_AGENT, request.getHeader("User-Agent")); mdcContextManager.put(MdcContextManager.REQUEST_PATH, request.getRequestURI()); mdcContextManager.put(MdcContextManager.REQUEST_METHOD, request.getMethod()); // Extract user context if available String userId = extractUserId(request); if (userId != null) { mdcContextManager.put(MdcContextManager.USER_ID, userId); } String sessionId = request.getSession(false) != null ? request.getSession().getId() : null; if (sessionId != null) { mdcContextManager.put(MdcContextManager.SESSION_ID, sessionId); } } private void logRequest(HttpServletRequest request) { if (logger.isInfoEnabled()) { Map<String, Object> logData = new HashMap<>(); logData.put("method", request.getMethod()); logData.put("uri", request.getRequestURI()); logData.put("query", request.getQueryString()); logData.put("clientIp", getClientIp(request)); logData.put("userAgent", request.getHeader("User-Agent")); logData.put("headers", getHeaders(request)); logger.info("HTTP Request: {}", logData); } } private void logResponse(HttpServletRequest request, ContentCachingResponseWrapper response, long startTime) { if (logger.isInfoEnabled()) { long duration = System.currentTimeMillis() - startTime; int status = response.getStatus(); Map<String, Object> logData = new HashMap<>(); logData.put("method", request.getMethod()); logData.put("uri", request.getRequestURI()); logData.put("status", status); logData.put("duration", duration); logData.put("responseSize", response.getContentSize()); // Log as different levels based on status code if (status >= 400 && status < 500) { logger.warn("HTTP Response: {}", logData); } else if (status >= 500) { logger.error("HTTP Response: {}", logData); } else { logger.info("HTTP Response: {}", logData); } } } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } String xRealIp = request.getHeader("X-Real-IP"); if (xRealIp != null && !xRealIp.isEmpty()) { return xRealIp; } return request.getRemoteAddr(); } private String getHeader(HttpServletRequest request, String headerName) { String value = request.getHeader(headerName); return value != null && !value.isEmpty() ? value : null; } private Map<String, String> getHeaders(HttpServletRequest request) { Map<String, String> headers = new HashMap<>(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headers.put(headerName, request.getHeader(headerName)); } return headers; } private String extractUserId(HttpServletRequest request) { // Extract from security context, JWT token, or session // This is application-specific return null; } @Override public void init(FilterConfig filterConfig) { logger.info("MDC Logging Filter initialized"); } @Override public void destroy() { logger.info("MDC Logging Filter destroyed"); } } 4. REST Controller Advice for MDC
package com.yourapp.logging.web; import com.yourapp.logging.mdc.MdcContextManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class MdcExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(MdcExceptionHandler.class); private final MdcContextManager mdcContextManager; public MdcExceptionHandler(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleAllExceptions(Exception ex, WebRequest request) { // Log exception with MDC context Map<String, String> mdcContext = mdcContextManager.getContext(); Map<String, Object> errorDetails = new HashMap<>(); errorDetails.put("timestamp", System.currentTimeMillis()); errorDetails.put("error", "Internal Server Error"); errorDetails.put("traceId", mdcContext.get(MdcContextManager.TRACE_ID)); errorDetails.put("path", request.getDescription(false)); // Structured logging with MDC context logger.error("Unhandled exception occurred: {}", ex.getMessage(), ex); return ResponseEntity.status(500).body(errorDetails); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<Map<String, Object>> handleBadRequest(Exception ex, WebRequest request) { Map<String, String> mdcContext = mdcContextManager.getContext(); Map<String, Object> errorDetails = new HashMap<>(); errorDetails.put("timestamp", System.currentTimeMillis()); errorDetails.put("error", "Bad Request"); errorDetails.put("message", ex.getMessage()); errorDetails.put("traceId", mdcContext.get(MdcContextManager.TRACE_ID)); logger.warn("Bad request: {}", ex.getMessage()); return ResponseEntity.status(400).body(errorDetails); } } Async and Thread Pool Integration
5. MDC Aware Thread Pool
package com.yourapp.logging.concurrent; import com.yourapp.logging.mdc.MdcContextManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 { private static final Logger logger = LoggerFactory.getLogger(MdcAwareThreadPoolTaskExecutor.class); private final MdcContextManager mdcContextManager; public MdcAwareThreadPoolTaskExecutor(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } @Override public void execute(Runnable task) { super.execute(mdcContextManager.wrapWithContext(task)); } @Override public <T> Future<T> submit(Callable<T> task) { return super.submit(mdcContextManager.wrapWithContext(task)); } @Override public Future<?> submit(Runnable task) { return super.submit(mdcContextManager.wrapWithContext(task)); } /** * Execute with specific MDC context */ public void executeWithContext(Runnable task, Map<String, String> context) { super.execute(() -> mdcContextManager.executeWithContext(task, context)); } /** * Submit with specific MDC context */ public <T> Future<T> submitWithContext(Callable<T> task, Map<String, String> context) { return super.submit(() -> mdcContextManager.executeWithContext(task, context)); } } 6. Async Configuration
package com.yourapp.logging.config; import com.yourapp.logging.concurrent.MdcAwareThreadPoolTaskExecutor; import com.yourapp.logging.mdc.MdcContextManager; 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 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { private final MdcContextManager mdcContextManager; public AsyncConfig(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } @Override public Executor getAsyncExecutor() { MdcAwareThreadPoolTaskExecutor executor = new MdcAwareThreadPoolTaskExecutor(mdcContextManager); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("MDC-Async-"); executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } @Bean(name = "mdcTaskExecutor") public Executor mdcTaskExecutor() { MdcAwareThreadPoolTaskExecutor executor = new MdcAwareThreadPoolTaskExecutor(mdcContextManager); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(500); executor.setThreadNamePrefix("MDC-Task-"); executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } } AOP and Annotation-Based Logging
7. Logging Annotations
package com.yourapp.logging.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Loggable { /** * Log level for method entry */ LogLevel level() default LogLevel.DEBUG; /** * Whether to log method arguments */ boolean logArgs() default true; /** * Whether to log return value */ boolean logResult() default true; /** * Whether to log execution time */ boolean logExecutionTime() default true; /** * Custom message for method entry */ String entryMessage() default ""; /** * Custom message for method exit */ String exitMessage() default ""; /** * MDC context to set before method execution */ MdcContext[] mdc() default {}; enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR } } @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MdcContext { /** * MDC key */ String key(); /** * Value expression (SpEL) */ String value(); /** * Whether to remove this key after method execution */ boolean removeAfter() default true; } @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface BusinessOperation { /** * Business operation name */ String value(); /** * Business domain */ String domain() default ""; /** * Operation type (CREATE, READ, UPDATE, DELETE, etc.) */ OperationType type() default OperationType.READ; enum OperationType { CREATE, READ, UPDATE, DELETE, SEARCH, PROCESS, VALIDATE, TRANSFORM } } 8. Logging Aspect
package com.yourapp.logging.aop; import com.yourapp.logging.annotation.Loggable; import com.yourapp.logging.annotation.MdcContext; import com.yourapp.logging.annotation.BusinessOperation; import com.yourapp.logging.mdc.MdcContextManager; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @Aspect @Component public class LoggingAspect { private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class); private final MdcContextManager mdcContextManager; private final ExpressionParser expressionParser; public LoggingAspect(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; this.expressionParser = new SpelExpressionParser(); } @Around("@annotation(loggable)") public Object logMethodExecution(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName(); // Set MDC context from annotation Map<String, String> previousMdcContext = setMdcContextFromAnnotation(joinPoint, loggable); long startTime = System.currentTimeMillis(); Level logLevel = getLogLevel(loggable.level()); boolean isLoggable = isLogLevelEnabled(logLevel); try { // Log method entry if (isLoggable && loggable.logArgs()) { logAtLevel(logLevel, "Entering {} with arguments: {}", methodName, getArgumentsString(joinPoint.getArgs())); } else if (isLoggable) { logAtLevel(logLevel, "Entering {}", methodName); } // Execute method Object result = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - startTime; // Log method exit if (isLoggable) { if (loggable.logResult()) { logAtLevel(logLevel, "Exiting {} with result: {} (execution time: {} ms)", methodName, getResultString(result), executionTime); } else if (loggable.logExecutionTime()) { logAtLevel(logLevel, "Exiting {} (execution time: {} ms)", methodName, executionTime); } else { logAtLevel(logLevel, "Exiting {}", methodName); } } return result; } catch (Throwable throwable) { long executionTime = System.currentTimeMillis() - startTime; logger.error("Exception in {} (execution time: {} ms): {}", methodName, executionTime, throwable.getMessage(), throwable); throw throwable; } finally { // Restore previous MDC context restoreMdcContext(previousMdcContext); } } @Around("@annotation(businessOperation)") public Object logBusinessOperation(ProceedingJoinPoint joinPoint, BusinessOperation businessOperation) throws Throwable { String operationName = businessOperation.value(); String domain = businessOperation.domain(); BusinessOperation.OperationType operationType = businessOperation.type(); // Set business context in MDC Map<String, String> previousMdcContext = mdcContextManager.getContext(); mdcContextManager.put("businessOperation", operationName); mdcContextManager.put("businessDomain", domain); mdcContextManager.put("operationType", operationType.name()); long startTime = System.currentTimeMillis(); try { logger.info("Starting business operation: {} in domain: {}", operationName, domain); Object result = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - startTime; logger.info("Completed business operation: {} in {} ms", operationName, executionTime); return result; } catch (Exception e) { long executionTime = System.currentTimeMillis() - startTime; logger.error("Failed business operation: {} after {} ms", operationName, executionTime, e); throw e; } finally { mdcContextManager.setContext(previousMdcContext); } } private Map<String, String> setMdcContextFromAnnotation(ProceedingJoinPoint joinPoint, Loggable loggable) { Map<String, String> previousContext = mdcContextManager.getContext(); Map<String, String> newContext = new HashMap<>(previousContext); for (MdcContext mdcAnnotation : loggable.mdc()) { try { String value = evaluateExpression(joinPoint, mdcAnnotation.value()); newContext.put(mdcAnnotation.key(), value); mdcContextManager.put(mdcAnnotation.key(), value); } catch (Exception e) { logger.warn("Failed to evaluate MDC expression: {}", mdcAnnotation.value(), e); } } return previousContext; } private void restoreMdcContext(Map<String, String> previousContext) { mdcContextManager.setContext(previousContext); } private String evaluateExpression(ProceedingJoinPoint joinPoint, String expression) { StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("args", joinPoint.getArgs()); context.setVariable("method", joinPoint.getSignature().getName()); context.setVariable("target", joinPoint.getTarget()); Expression expr = expressionParser.parseExpression(expression); return expr.getValue(context, String.class); } private Level getLogLevel(Loggable.LogLevel level) { return switch (level) { case TRACE -> Level.TRACE; case DEBUG -> Level.DEBUG; case INFO -> Level.INFO; case WARN -> Level.WARN; case ERROR -> Level.ERROR; }; } private boolean isLogLevelEnabled(Level level) { return switch (level) { case TRACE -> logger.isTraceEnabled(); case DEBUG -> logger.isDebugEnabled(); case INFO -> logger.isInfoEnabled(); case WARN -> logger.isWarnEnabled(); case ERROR -> logger.isErrorEnabled(); }; } private void logAtLevel(Level level, String format, Object... arguments) { switch (level) { case TRACE -> logger.trace(format, arguments); case DEBUG -> logger.debug(format, arguments); case INFO -> logger.info(format, arguments); case WARN -> logger.warn(format, arguments); case ERROR -> logger.error(format, arguments); } } private String getArgumentsString(Object[] args) { if (args == null || args.length == 0) { return "[]"; } StringBuilder sb = new StringBuilder("["); for (int i = 0; i < args.length; i++) { if (i > 0) { sb.append(", "); } sb.append(getObjectString(args[i])); } sb.append("]"); return sb.toString(); } private String getResultString(Object result) { return getObjectString(result); } private String getObjectString(Object obj) { if (obj == null) { return "null"; } // For collections and arrays, show size instead of full content if (obj instanceof java.util.Collection) { return "Collection(size=" + ((java.util.Collection<?>) obj).size() + ")"; } else if (obj instanceof java.util.Map) { return "Map(size=" + ((java.util.Map<?, ?>) obj).size() + ")"; } else if (obj.getClass().isArray()) { return "Array(length=" + java.lang.reflect.Array.getLength(obj) + ")"; } else if (obj instanceof String) { String str = (String) obj; return str.length() > 100 ? str.substring(0, 100) + "..." : str; } else { return obj.toString(); } } } Advanced MDC Patterns
9. MDC Context Scopes
package com.yourapp.logging.mdc; import org.slf4j.MDC; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class MdcScopeManager { private final ThreadLocal<Map<String, Map<String, String>>> scopeStack = ThreadLocal.withInitial(ConcurrentHashMap::new); /** * Push new MDC scope */ public void pushScope(String scopeName) { Map<String, String> currentContext = MDC.getCopyOfContextMap(); scopeStack.get().put(scopeName, currentContext != null ? new ConcurrentHashMap<>(currentContext) : new ConcurrentHashMap<>()); } /** * Pop MDC scope and restore previous context */ public void popScope(String scopeName) { Map<String, Map<String, String>> scopes = scopeStack.get(); Map<String, String> previousContext = scopes.remove(scopeName); if (previousContext != null) { MDC.setContextMap(previousContext); } else { MDC.clear(); } if (scopes.isEmpty()) { scopeStack.remove(); } } /** * Execute in new MDC scope */ public void executeInScope(String scopeName, Runnable task) { pushScope(scopeName); try { task.run(); } finally { popScope(scopeName); } } /** * Get current scope context */ public Map<String, String> getScopeContext(String scopeName) { return scopeStack.get().get(scopeName); } /** * Set value in current scope */ public void setInScope(String scopeName, String key, String value) { Map<String, Map<String, String>> scopes = scopeStack.get(); Map<String, String> scopeContext = scopes.get(scopeName); if (scopeContext != null) { scopeContext.put(key, value); // Also update current MDC if this is the active scope if (isActiveScope(scopeName)) { MDC.put(key, value); } } } /** * Check if scope is active */ public boolean isActiveScope(String scopeName) { Map<String, Map<String, String>> scopes = scopeStack.get(); return scopes.containsKey(scopeName) && scopes.size() == 1; } /** * Get all active scopes */ public java.util.Set<String> getActiveScopes() { return scopeStack.get().keySet(); } } 10. Structured Logging Service
package com.yourapp.logging.service; import com.yourapp.logging.mdc.MdcContextManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class StructuredLoggingService { private static final Logger logger = LoggerFactory.getLogger(StructuredLoggingService.class); private final MdcContextManager mdcContextManager; public StructuredLoggingService(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } /** * Log business event with structured data */ public void logBusinessEvent(String eventType, String eventName, Map<String, Object> data) { Map<String, Object> logEvent = createLogEvent(eventType, eventName, data); logger.info("Business Event: {}", logEvent); } /** * Log security event */ public void logSecurityEvent(String action, String resource, String outcome, Map<String, Object> details) { Map<String, Object> securityEvent = createLogEvent("SECURITY", action, details); securityEvent.put("resource", resource); securityEvent.put("outcome", outcome); if ("SUCCESS".equals(outcome)) { logger.info("Security Event: {}", securityEvent); } else { logger.warn("Security Event: {}", securityEvent); } } /** * Log performance metric */ public void logPerformanceMetric(String operation, long duration, String unit, Map<String, Object> tags) { Map<String, Object> metric = createLogEvent("PERFORMANCE", operation, tags); metric.put("duration", duration); metric.put("unit", unit); metric.put("timestamp", System.currentTimeMillis()); // Log at different levels based on duration if (duration > 10000) { // 10 seconds logger.error("Performance Issue: {}", metric); } else if (duration > 5000) { // 5 seconds logger.warn("Performance Warning: {}", metric); } else if (duration > 1000) { // 1 second logger.info("Performance Metric: {}", metric); } else { logger.debug("Performance Metric: {}", metric); } } /** * Log audit event */ public void logAuditEvent(String action, String target, String userId, Map<String, Object> changes) { Map<String, Object> auditEvent = createLogEvent("AUDIT", action, changes); auditEvent.put("target", target); auditEvent.put("userId", userId); auditEvent.put("timestamp", System.currentTimeMillis()); logger.info("Audit Event: {}", auditEvent); } /** * Log error with context */ public void logError(String errorCode, String message, Throwable throwable, Map<String, Object> context) { Map<String, Object> errorEvent = createLogEvent("ERROR", errorCode, context); errorEvent.put("message", message); errorEvent.put("exception", throwable != null ? throwable.getClass().getName() : null); errorEvent.put("exceptionMessage", throwable != null ? throwable.getMessage() : null); logger.error("Error Event: {}", errorEvent, throwable); } private Map<String, Object> createLogEvent(String eventType, String eventName, Map<String, Object> data) { Map<String, Object> event = new HashMap<>(); event.put("eventType", eventType); event.put("eventName", eventName); event.put("timestamp", System.currentTimeMillis()); // Add MDC context Map<String, String> mdcContext = mdcContextManager.getContext(); if (mdcContext != null && !mdcContext.isEmpty()) { event.put("context", new HashMap<>(mdcContext)); } // Add custom data if (data != null && !data.isEmpty()) { event.put("data", new HashMap<>(data)); } return event; } } Testing Support
11. Test Utilities
package com.yourapp.logging.test; import com.yourapp.logging.mdc.MdcContextManager; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.MDC; public class MdcTestExtension implements BeforeEachCallback, AfterEachCallback { private final MdcContextManager mdcContextManager; public MdcTestExtension(MdcContextManager mdcContextManager) { this.mdcContextManager = mdcContextManager; } @Override public void beforeEach(ExtensionContext context) { mdcContextManager.initializeContext(); mdcContextManager.put("testClass", context.getRequiredTestClass().getSimpleName()); mdcContextManager.put("testMethod", context.getRequiredTestMethod().getName()); } @Override public void afterEach(ExtensionContext context) { mdcContextManager.clearContext(); } /** * Execute test with specific MDC context */ public static void withMdcContext(Runnable test, java.util.Map<String, String> context) { java.util.Map<String, String> previousContext = MDC.getCopyOfContextMap(); try { if (context != null) { MDC.setContextMap(context); } test.run(); } finally { if (previousContext != null) { MDC.setContextMap(previousContext); } else { MDC.clear(); } } } } 12. Test Configuration
package com.yourapp.logging.test; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.List; import java.util.stream.Collectors; @SpringBootTest @ExtendWith({SpringExtension.class, MockitoExtension.class}) public abstract class BaseLoggingTest { protected ListAppender<ILoggingEvent> getListAppenderForLogger(String loggerName) { Logger logger = (Logger) LoggerFactory.getLogger(loggerName); ListAppender<ILoggingEvent> listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); return listAppender; } protected List<String> getFormattedMessages(ListAppender<ILoggingEvent> listAppender) { return listAppender.list.stream() .map(ILoggingEvent::getFormattedMessage) .collect(Collectors.toList()); } protected List<String> getMdcValues(ListAppender<ILoggingEvent> listAppender, String key) { return listAppender.list.stream() .map(event -> event.getMDCPropertyMap().get(key)) .collect(Collectors.toList()); } protected boolean containsMdcValue(ListAppender<ILoggingEvent> listAppender, String key, String value) { return listAppender.list.stream() .anyMatch(event -> value.equals(event.getMDCPropertyMap().get(key))); } } Best Practices and Configuration
13. Application Properties
# Logging Configuration logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId},%X{spanId},%X{userId}] - %msg%n logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId},%X{spanId},%X{userId},%X{sessionId},%X{requestId}] - %msg%n logging.level.com.yourapp=DEBUG # MDC Configuration mdc.enabled=true mdc.cleanup.enabled=true mdc.async.propagation=true # Web Logging web.logging.enabled=true web.logging.include-headers=true web.logging.max-payload-size=1024 14. Best Practices Summary
- Always clear MDC after request processing to prevent memory leaks
- Propagate MDC context to async threads using wrapper classes
- Use meaningful keys that clearly identify the context information
- Avoid storing sensitive data in MDC (passwords, tokens, PII)
- Keep MDC values small to minimize memory usage
- Use structured logging for better log analysis
- Test MDC propagation in multi-threaded environments
- Monitor MDC usage in production for performance impact
This comprehensive MDC implementation provides a robust foundation for contextual logging in Java applications, supporting web requests, async processing, AOP-based logging, and structured logging patterns.