| name | logging-patterns |
| description | Structured logging with SLF4J, MDC context propagation, and observability patterns for Spring Boot |
Logging Patterns for Spring Boot
Structured Logging Configuration
Logback Configuration
<configuration>
<springProfile name="!production">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{correlationId}] %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="production">
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>correlationId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
<includeMdcKeyName>requestPath</includeMdcKeyName>
<customFields>{"service":"my-service","environment":"production"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</springProfile>
<logger name="org.springframework" level="WARN" />
<logger name="org.hibernate.SQL" level="WARN" />
<logger name="org.hibernate.type.descriptor.sql" level="WARN" />
<logger name="com.zaxxer.hikari" level="WARN" />
</configuration>
MDC Context Propagation
Request Correlation Filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String CORRELATION_HEADER = "X-Correlation-ID";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
var correlationId = request.getHeader(CORRELATION_HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
MDC.put("requestMethod", request.getMethod());
MDC.put("requestPath", request.getRequestURI());
response.setHeader(CORRELATION_HEADER, correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
User Context in MDC
@Component
public class UserContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
MDC.put("userId", auth.getName());
}
try {
chain.doFilter(request, response);
} finally {
MDC.remove("userId");
}
}
}
Logging Best Practices
Log Levels
@Service
@Slf4j
public class OrderService {
public OrderDto createOrder(CreateOrderRequest request) {
log.debug("Creating order with {} items for customer={}",
request.items().size(), request.customerId());
var order = processOrder(request);
log.info("Order created: orderId={} customerId={} totalAmount={}",
order.getId(), order.getCustomerId(), order.getTotalAmount());
return orderMapper.toDto(order);
}
public void processPayment(String orderId) {
try {
paymentGateway.charge(orderId);
} catch (PaymentDeclinedException e) {
log.warn("Payment declined for orderId={}: {}", orderId, e.getMessage());
throw e;
} catch (PaymentGatewayException e) {
log.error("Payment gateway failure for orderId={}", orderId, e);
throw new ServiceUnavailableException("Payment processing failed", e);
}
}
}
Request/Response Logging
@Component
@Slf4j
public class RequestLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
request.setAttribute("startTime", System.nanoTime());
log.info("Incoming request: method={} path={} query={}",
request.getMethod(), request.getRequestURI(), request.getQueryString());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
var startTime = (Long) request.getAttribute("startTime");
var duration = Duration.ofNanos(System.nanoTime() - startTime);
log.info("Request completed: method={} path={} status={} duration={}ms",
request.getMethod(), request.getRequestURI(),
response.getStatus(), duration.toMillis());
}
}
Sensitive Data Protection
log.info("User authenticated: email={} password={}", email, password);
log.info("Payment: card={}", cardNumber);
log.info("User authenticated: email={}", maskEmail(email));
log.info("Payment: card=****{}", lastFour(cardNumber));
public static String maskEmail(String email) {
int atIndex = email.indexOf('@');
if (atIndex <= 2) return "***" + email.substring(atIndex);
return email.substring(0, 2) + "***" + email.substring(atIndex);
}
Async MDC Propagation
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
var executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
var contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) MDC.setContextMap(contextMap);
runnable.run();
} finally {
MDC.clear();
}
};
}
}
Logging Checklist
- Levels -- Use appropriate log levels (DEBUG, INFO, WARN, ERROR)
- Structure -- Key-value pairs in log messages for parsing
- Correlation -- Correlation ID propagated across all log entries
- Context -- MDC contains userId, requestPath, and correlation data
- No Sensitive Data -- Passwords, tokens, PII are never logged
- Async Propagation -- MDC context propagated to async threads
- JSON in Production -- Structured JSON output for log aggregation
- Noise Reduction -- Framework loggers set to WARN or higher
- Performance -- Use parameterized logging (SLF4J
{} syntax)
- Exception Logging -- Pass exception as last parameter to preserve stack trace