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.