Claude-code-java logging-patterns
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.
install
source · Clone the upstream repo
git clone https://github.com/decebals/claude-code-java
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/decebals/claude-code-java "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/logging-patterns" ~/.claude/skills/decebals-claude-code-java-logging-patterns && rm -rf "$T"
manifest:
.claude/skills/logging-patterns/SKILL.mdsource content
Logging Patterns Skill
Effective logging for Java applications with focus on structured, AI-parsable formats.
When to Use
- User says "add logging" / "improve logs" / "debug this"
- Analyzing application flow from logs
- Setting up structured logging (JSON)
- Request tracing with correlation IDs
- AI/Claude Code needs to analyze application behavior
AI-Friendly Logging
Key insight: JSON logs are better for AI analysis - faster parsing, fewer tokens, direct field access.
Why JSON for AI/Claude Code?
# 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 | field |
| Filtering | grep patterns | queries |
Recommended Setup for AI-Assisted Development
# 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
Log Format Optimized for AI Analysis
{ "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:
- group all logs from same requestrequestId
- track progress through flowstep
- identify slow operationsduration_ms
- quick filter for errorslevel
Reading Logs with AI/Claude Code
When asking AI to analyze logs:
# 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:
- Parse JSON directly (no guessing)
- Follow request flow via requestId
- Identify exactly where errors occurred
- Measure timing between steps
Quick Setup (Spring Boot 3.4+)
Native Structured Logging
Spring Boot 3.4+ has built-in support - no extra dependencies!
# application.yml logging: structured: format: console: logstash # or "ecs" for Elastic Common Schema # Supported formats: logstash, ecs, gelf
Profile-Based Switching
# 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:
# Default: JSON (for AI, CI/CD, production) ./mvnw spring-boot:run # Human-readable when needed ./mvnw spring-boot:run -Dspring.profiles.active=human-logs
Setup for Spring Boot < 3.4
Logstash Logback Encoder
pom.xml:
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version> </dependency>
logback-spring.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>
Adding Custom Fields (Logstash Encoder)
import 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"}
SLF4J Basics
Logger Declaration
import 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 }
Parameterized Logging
// ✅ 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()); }
Log Levels
| 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) |
log.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 (Mapped Diagnostic Context)
MDC adds context to every log entry in a request - essential for tracing.
Request ID Filter
@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(); } } }
Add User Context
// After authentication MDC.put("userId", authentication.getName()); // All subsequent logs include userId automatically log.info("User action performed"); // {"userId":"john123","message":"User action performed"}
MDC in Async Operations
// 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(); } });
What to Log
Business Events (INFO)
// 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"));
External Calls (with timing)
long 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; }
Flow Steps (for AI tracing)
public 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; }
What NOT to Log
// ❌ 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));
Exception Logging
Log Once at Boundary
// ❌ 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); } }
Include Context
// ❌ 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);
Quick Reference
// === 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
Analyzing Logs (AI/Human)
# 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}'
Related Skills
- Spring Boot configurationspring-boot-patterns
- Database logging (SQL queries)jpa-patterns- Future:
- Metrics, tracing, full observabilityobservability-patterns