| name | logging-patterns |
| description | Use when implementing or reviewing logging to enforce SLF4J best practices, structured JSON logging, and MDC usage for traceability. |
Logging Patterns for Spring Boot
SLF4J Best Practices
Logger Declaration
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public OrderResponse createOrder(OrderRequest request) {
log.info("Creating order for customer: {}", request.customerId());
log.info("Order created successfully: orderId={}", order.getId());
return response;
}
}
Log Levels
| Level | Usage | Example |
|---|
| ERROR | System failures requiring immediate attention | Database down, external service failure |
| WARN | Potential problems, recoverable issues | Retry succeeded, deprecated API usage |
| INFO | Business events, application lifecycle | Order placed, user logged in, app started |
| DEBUG | Detailed flow for troubleshooting | Method entry/exit, variable values |
| TRACE | Very detailed diagnostic information | Loop iterations, raw request/response bodies |
Parameterized Logging (Never Concatenate)
log.debug("Processing order " + orderId + " for customer " + customerId);
log.debug("Processing order {} for customer {}", orderId, customerId);
if (log.isDebugEnabled()) {
log.debug("Order details: {}", buildDetailedOrderSummary(order));
}
Exception Logging
log.error("Payment failed: " + e.getMessage());
log.error("Payment failed: {}", e);
log.error("Payment failed for order {}: {}", orderId, e.getMessage(), e);
try {
paymentGateway.charge(amount);
} catch (PaymentException e) {
log.error("Payment gateway error for order {}", orderId, e);
throw new ServiceException("Payment processing failed", e);
}
Structured JSON Logging
Logback Configuration for JSON Output
<configuration>
<springProfile name="default,dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="prod,staging">
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
<includeMdcKeyName>requestId</includeMdcKeyName>
<fieldNames>
<timestamp>@timestamp</timestamp>
<version>[ignore]</version>
</fieldNames>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_CONSOLE" />
</root>
</springProfile>
<logger name="org.springframework" level="WARN" />
<logger name="org.hibernate.SQL" level="DEBUG" />
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" />
</configuration>
Maven Dependency for JSON Logging
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
Structured Log Output Example
{
"@timestamp": "2024-01-15T10:30:45.123Z",
"level": "INFO",
"logger_name": "pl.piomin.services.order.OrderService",
"message": "Order created successfully",
"traceId": "abc123def456",
"userId": "user-42",
"requestId": "req-789",
"orderId": 12345,
"amount": 99.99
}
Structured Logging with Key-Value Pairs
import net.logstash.logback.argument.StructuredArguments;
import static net.logstash.logback.argument.StructuredArguments.*;
@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
public PaymentResult processPayment(Long orderId, BigDecimal amount) {
log.info("Processing payment: {} {}",
keyValue("orderId", orderId),
keyValue("amount", amount));
PaymentResult result = gateway.charge(amount);
log.info("Payment completed: {} {} {}",
keyValue("orderId", orderId),
keyValue("transactionId", result.getTransactionId()),
keyValue("status", result.getStatus()));
return result;
}
}
MDC (Mapped Diagnostic Context)
Request Tracing Filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcFilter implements Filter {
private static final String REQUEST_ID_HEADER = "X-Request-ID";
private static final String REQUEST_ID_KEY = "requestId";
private static final String CLIENT_IP_KEY = "clientIp";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestId = httpRequest.getHeader(REQUEST_ID_HEADER);
if (requestId == null || requestId.isBlank()) {
requestId = UUID.randomUUID().toString();
}
MDC.put(REQUEST_ID_KEY, requestId);
MDC.put(CLIENT_IP_KEY, httpRequest.getRemoteAddr());
httpResponse.setHeader(REQUEST_ID_HEADER, requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
User Context in MDC
@Component
public class UserMdcFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
MDC.put("userId", auth.getName());
}
filterChain.doFilter(request, response);
} finally {
MDC.remove("userId");
}
}
}
MDC with Async Operations
@Configuration
@EnableAsync
public class AsyncMdcConfig {
@Bean(name = "mdcTaskExecutor")
public Executor mdcAwareExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
Request/Response Logging
Logging Interceptor
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
log.info("Incoming request: {} {} from {}",
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
long startTime = (long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
log.info("Request completed: {} {} status={} duration={}ms",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration);
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final RequestLoggingInterceptor requestLoggingInterceptor;
public WebMvcConfig(RequestLoggingInterceptor requestLoggingInterceptor) {
this.requestLoggingInterceptor = requestLoggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/api/**");
}
}
What NOT to Log
log.info("User login: {}", password);
log.info("Payment card: {}", cardNumber);
log.info("Auth token: {}", bearerToken);
log.info("SSN: {}", socialSecurityNumber);
log.info("User login successful: userId={}", userId);
log.info("Payment processed: last4={}, status={}", last4Digits, status);
Logging Checklist