Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Java logging best practices with SLF4J, structured logging (JSON), and MDC for request tracing. Includes AI-friendly log formats for Claude Code debugging. Use when user asks about logging, debugging application flow, or analyzing logs.
.claude/skills/decebals-logging-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 61% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 211% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 284% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 183% | 0% |
Effective logging for Java applications with focus on structured, AI-parsable formats.
> Key insight: JSON logs are better for AI analysis - faster parsing, fewer tokens, direct field access.
# Text format - AI must "interpret" the string
2026-01-29 10:15:30 INFO OrderService - Order 12345 created for user-789, total: 99.99
# JSON format - AI extracts fields directly
{"timestamp":"2026-01-29T10:15:30Z","level":"INFO","orderId":12345,"userId":"user-789","total":99.99}| Aspect | Text | JSON | |--------|------|------| | Parsing | Regex/interpretation | Direct field access | | Token usage | Higher (repeated patterns) | Lower (structured) | | Error extraction | Parse stack trace text | exception field | | Filtering | grep patterns | jq queries |
yaml# application.yml - JSON by default logging: structured: format: console: logstash # Spring Boot 3.4+ # When YOU need to read logs manually: # Option 1: Use jq # tail -f app.log | jq . # Option 2: Switch profile temporarily # java -jar app.jar --spring.profiles.active=human-logs
json{ "timestamp": "2026-01-29T10:15:30.123Z", "level": "INFO", "logger": "com.example.OrderService", "message": "Order created", "requestId": "req-abc123", "traceId": "trace-xyz", "orderId": 12345, "userId": "user-789", "duration_ms": 45, "step": "payment_completed" }
Key fields for AI debugging:
requestId - group all logs from same requeststep - track progress through flowduration_ms - identify slow operationslevel - quick filter for errorsWhen asking AI to analyze logs:
bash# Get recent errors cat app.log | jq 'select(.level == "ERROR")' | tail -20 # Follow specific request cat app.log | jq 'select(.requestId == "req-abc123")' # Find slow operations cat app.log | jq 'select(.duration_ms > 1000)'
AI can then:
Spring Boot 3.4+ has built-in support - no extra dependencies!
yaml# application.yml logging: structured: format: console: logstash # or "ecs" for Elastic Common Schema # Supported formats: logstash, ecs, gelf
yaml# application.yml (default - JSON for AI/prod) spring: profiles: default: json-logs --- spring: config: activate: on-profile: json-logs logging: structured: format: console: logstash --- spring: config: activate: on-profile: human-logs # No structured format = human-readable default logging: pattern: console: "%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n"
Usage:
bash# Default: JSON (for AI, CI/CD, production) ./mvnw spring-boot:run # Human-readable when needed ./mvnw spring-boot:run -Dspring.profiles.active=human-logs
pom.xml:
xml<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency>
logback-spring.xml:
xml<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- JSON (default) --> <springProfile name="!human-logs"> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdcKeyName>requestId</includeMdcKeyName> <includeMdcKeyName>userId</includeMdcKeyName> </encoder> </appender> <root level="INFO"> <appender-ref ref="JSON"/> </root> </springProfile> <!-- Human-readable (optional) --> <springProfile name="human-logs"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </springProfile> </configuration>
javaimport static net.logstash.logback.argument.StructuredArguments.kv; // Fields appear as separate JSON keys log.info("Order created", kv("orderId", order.getId()), kv("userId", user.getId()), kv("total", order.getTotal()), kv("step", "order_created") ); // Output: // {"message":"Order created","orderId":123,"userId":"u-456","total":99.99,"step":"order_created"}
javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; @Service public class OrderService { private static final Logger log = LoggerFactory.getLogger(OrderService.class); } // Or with Lombok @Slf4j @Service public class OrderService { // use `log` directly }
java// ✅ GOOD: Evaluated only if level enabled log.debug("Processing order {} for user {}", orderId, userId); // ❌ BAD: Always concatenates log.debug("Processing order " + orderId + " for user " + userId); // ✅ For expensive operations if (log.isDebugEnabled()) { log.debug("Full order details: {}", order.toJson()); }
| Level | When | Example | |-------|------|---------| | ERROR | Failures needing attention | Unhandled exception, service down | | WARN | Unexpected but handled | Retry succeeded, deprecated API used | | INFO | Business events | Order created, payment processed | | DEBUG | Technical details | Method params, SQL queries | | TRACE | Very detailed | Loop iterations (rarely used) |
javalog.error("Payment failed", kv("orderId", id), kv("reason", reason), exception); log.warn("Retry succeeded", kv("attempt", 3), kv("orderId", id)); log.info("Order shipped", kv("orderId", id), kv("trackingNumber", tracking)); log.debug("Fetching from DB", kv("query", "findById"), kv("id", id));
MDC adds context to every log entry in a request - essential for tracing.
java@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class RequestContextFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { try { String requestId = Optional.ofNullable(request.getHeader("X-Request-ID")) .filter(s -> !s.isBlank()) .orElse(UUID.randomUUID().toString().substring(0, 8)); MDC.put("requestId", requestId); response.setHeader("X-Request-ID", requestId); chain.doFilter(request, response); } finally { MDC.clear(); } } }
java// After authentication MDC.put("userId", authentication.getName()); // All subsequent logs include userId automatically log.info("User action performed"); // {"userId":"john123","message":"User action performed"}
java// MDC doesn't auto-propagate to new threads! // ✅ Copy MDC context Map<String, String> context = MDC.getCopyOfContextMap(); CompletableFuture.runAsync(() -> { try { if (context != null) MDC.setContextMap(context); log.info("Async task running"); // Has requestId, userId } finally { MDC.clear(); } });
java// Include key identifiers and state log.info("Order created", kv("orderId", id), kv("userId", userId), kv("total", total), kv("itemCount", items.size()), kv("step", "order_created")); log.info("Payment processed", kv("orderId", id), kv("amount", amount), kv("method", "card"), kv("step", "payment_completed"));
javalong start = System.currentTimeMillis(); try { Result result = externalService.call(params); log.info("External call succeeded", kv("service", "PaymentGateway"), kv("operation", "charge"), kv("duration_ms", System.currentTimeMillis() - start)); return result; } catch (Exception e) { log.error("External call failed", kv("service", "PaymentGateway"), kv("operation", "charge"), kv("duration_ms", System.currentTimeMillis() - start), e); throw e; }
javapublic Order processOrder(CreateOrderRequest request) { log.info("Processing started", kv("step", "start"), kv("requestData", request.summary())); Order order = createOrder(request); log.info("Order created", kv("step", "order_created"), kv("orderId", order.getId())); validateInventory(order); log.info("Inventory validated", kv("step", "inventory_ok"), kv("orderId", order.getId())); processPayment(order); log.info("Payment processed", kv("step", "payment_done"), kv("orderId", order.getId())); log.info("Processing completed", kv("step", "complete"), kv("orderId", order.getId())); return order; }
java// ❌ NEVER log sensitive data log.info("Login", kv("password", password)); // Passwords log.info("Payment", kv("cardNumber", card)); // Full card numbers log.info("Request", kv("token", jwtToken)); // Tokens log.info("User", kv("ssn", socialSecurity)); // PII // ✅ Safe alternatives log.info("Login attempted", kv("userId", userId)); log.info("Payment", kv("cardLast4", last4)); log.info("Token validated", kv("subject", sub), kv("exp", expiry));
java// ❌ BAD: Logs same exception multiple times void methodA() { try { methodB(); } catch (Exception e) { log.error("Error", e); throw e; } // Log #1 } void methodB() { try { methodC(); } catch (Exception e) { log.error("Error", e); throw e; } // Log #2 } // ✅ GOOD: Log at service boundary only @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<?> handle(Exception e, HttpServletRequest request) { log.error("Request failed", kv("path", request.getRequestURI()), kv("method", request.getMethod()), kv("errorType", e.getClass().getSimpleName()), e); // Full stack trace return ResponseEntity.status(500).body(errorResponse); } }
java// ❌ Useless log.error("Error occurred", e); // ✅ Useful for debugging log.error("Order processing failed", kv("orderId", orderId), kv("step", "payment"), kv("userId", userId), kv("attemptNumber", attempt), e);
java// === Setup === private static final Logger log = LoggerFactory.getLogger(MyClass.class); // === Logging with structured fields === import static net.logstash.logback.argument.StructuredArguments.kv; log.info("Event", kv("key1", value1), kv("key2", value2)); log.error("Failed", kv("context", ctx), exception); // === MDC === MDC.put("requestId", requestId); MDC.put("userId", userId); // ... all logs now include these MDC.clear(); // cleanup // === Levels === log.error() // Failures log.warn() // Handled issues log.info() // Business events log.debug() // Technical details
bash# Pretty print JSON logs tail -f app.log | jq . # Filter errors cat app.log | jq 'select(.level == "ERROR")' # Follow request flow cat app.log | jq 'select(.requestId == "abc123")' # Find slow operations (>1s) cat app.log | jq 'select(.duration_ms > 1000)' # Get timeline of steps cat app.log | jq 'select(.requestId == "abc123") | {time: .timestamp, step: .step, message: .message}'
spring-boot-patterns - Spring Boot configurationjpa-patterns - Database logging (SQL queries)observability-patterns - Metrics, tracing, full observabilityOther measured skills in the registry, with their headline benchmark lift.