Logging Context with MDC (Mapped Diagnostic Context) in Java

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

  1. Always clear MDC after request processing to prevent memory leaks
  2. Propagate MDC context to async threads using wrapper classes
  3. Use meaningful keys that clearly identify the context information
  4. Avoid storing sensitive data in MDC (passwords, tokens, PII)
  5. Keep MDC values small to minimize memory usage
  6. Use structured logging for better log analysis
  7. Test MDC propagation in multi-threaded environments
  8. 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.

Leave a Reply

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


Macro Nepal Helper