5 Logging Patterns That Turn Debugging from Hours to Minutes
When production failures happen at odd hours, useless logs can turn a five‑minute fix into a five‑hour nightmare, but applying five practical logging patterns—contextual data, proper log levels, performance‑aware messages, structured error codes, and correlation IDs—lets engineers locate and resolve issues in minutes.
Most Common Logging Problems
Junior developers often write logs like simple System.out.println() statements, producing messages such as “Error: null” that lack context, user identification, searchable identifiers, proper log levels, and meaningful content.
No context (which order failed?)
No user identifier (who is affected?)
No searchable identifier (how to find it in logs?)
Wrong log level (using System.out instead of a logger)
Message provides no useful information (e.g., “Success!”)
Pattern 1: Every Log Entry Must Carry Context – Who & What
Problem: Generic log messages cannot be searched and provide little value for troubleshooting.
Solution: Include identifiers that trace the full workflow.
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void processOrder(Order order) {
// Contextual info log
logger.info("Processing order",
"orderId", order.getId(),
"userId", order.getUserId(),
"itemCount", order.getItems().size(),
"total", order.getTotal());
try {
long startTime = System.currentTimeMillis();
paymentService.charge(order);
// Success log with metric
logger.info("Order completed",
"orderId", order.getId(),
"userId", order.getUserId(),
"processingTimeMs", System.currentTimeMillis() - startTime);
} catch (PaymentException e) {
// Full‑context error log
logger.error("Order payment failed",
"orderId", order.getId(),
"userId", order.getUserId(),
"amount", order.getTotal(),
"reason", e.getReason(),
"errorCode", "PAY-101");
throw e;
}
}
}Performance comparison (1 M requests per day):
// Bad logging
- String concatenation: 30 ms × 1 M = 8.3 h CPU time
- Unnecessary method calls: 20 ms × 1 M = 5.6 h waste
// Good logging
- Conditional checks: 2 ms × 1 M = 33 min CPU time
- Saves ~13.6 h CPU time per daySearch by orderId reveals the complete order flow.
Search by userId shows all activity for a user.
Error codes can be filtered by type.
Processing time highlights performance issues.
Every log answers “What happened?”
Pattern 2: Treat Log Levels Seriously
Problem: Most developers only use ERROR and INFO, making logs impossible to filter.
Solution: Assign a clear purpose to each level in production.
@Service
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public Payment charge(Order order) {
// DEBUG – only in dev, disabled in prod
logger.debug("Entering payment processing",
"orderId", order.getId(),
"gatewayConfig", getGatewayConfig());
// INFO – normal business event
logger.info("Initiating payment charge",
"orderId", order.getId(),
"amount", order.getTotal(),
"gateway", "Stripe");
Payment payment = stripeGateway.charge(order);
// WARN – unexpected but handled (slow processing)
if (payment.getProcessingTime() > 2000) {
logger.warn("Payment processing slow",
"orderId", order.getId(),
"expectedMs", 500,
"actualMs", payment.getProcessingTime(),
"gateway", "Stripe");
}
// ERROR – real failure needing investigation
if (payment.isFailed()) {
logger.error("Payment charge failed",
"orderId", order.getId(),
"errorCode", "PAY-101",
"reason", payment.getFailureReason());
throw new PaymentException(payment.getFailureReason());
}
return payment;
}
}Production logs stay clean (only INFO/WARN/ERROR).
Filtering ERROR surfaces real problems instantly. WARN warns before performance degrades.
On‑call engineers know ERROR means immediate alert.
Pattern 3: Avoid Expensive Logging That Hurts Performance
Problem: Even when a log level is disabled, string concatenation and heavy operations still execute.
Solution: Use conditional logging and parameterized messages.
@Service
public class InventoryService {
private static final Logger logger = LoggerFactory.getLogger(InventoryService.class);
public void checkInventory(List<OrderItem> items) {
// ❌ Bad – string built even if DEBUG is off
logger.debug("Checking inventory items: " +
items.stream()
.map(OrderItem::toString)
.collect(Collectors.joining(", ")));
// ✅ Good – compute only when DEBUG enabled
if (logger.isDebugEnabled()) {
logger.debug("Checking inventory items: {}",
items.stream()
.map(OrderItem::toString)
.collect(Collectors.joining(", ")));
}
// ✅ Better – lazy lambda evaluation
logger.debug("Checking inventory items",
"itemDetails", () -> items.stream()
.map(OrderItem::toString)
.collect(Collectors.joining(", ")));
}
}Shows deep understanding of Java performance.
Prevents debug‑level logs from slowing production.
Demonstrates awareness of hidden costs.
Leverages modern Java (lambda lazy loading).
Pattern 4: Use Structured Error Codes for Searchable Failures
Problem: Vague error messages cannot be filtered or used for alerts.
Solution: Adopt structured error codes and consistent fields.
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void processOrder(Order order) {
try {
validateOrder(order);
reserveInventory(order);
chargePayment(order);
confirmOrder(order);
} catch (InsufficientInventoryException e) {
logger.error("Order failed – insufficient inventory",
"errorCode", "INV-101",
"orderId", order.getId(),
"userId", order.getUserId(),
"productId", e.getProductId(),
"requested", e.getRequestedQuantity(),
"available", e.getAvailableQuantity());
throw e;
} catch (PaymentDeclinedException e) {
logger.error("Order failed – payment declined",
"errorCode", "PAY-101",
"orderId", order.getId(),
"userId", order.getUserId(),
"amount", order.getTotal(),
"reason", e.getDeclineReason(),
"retryable", e.isRetryable());
throw e;
} catch (Exception e) {
logger.error("Order failed – unexpected error",
"errorCode", "ORD-999",
"orderId", order.getId(),
"userId", order.getUserId(),
"errorType", e.getClass().getSimpleName(),
"message", e.getMessage());
throw e;
}
}
}Search errorCode:"PAY-101" to find all payment failures.
Set alerts such as “if errorCode:PAY-101 > 100 per hour, notify me”.
Error codes feed dashboards and analysis.
Consistent structure makes log parsing reliable.
Pattern 5: Correlate Logs Across Services with MDC (Mapped Diagnostic Context)
Problem: When a user reports a failed order, you cannot trace related logs across services.
Solution: Use MDC to inject a request‑wide correlation ID.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
String requestId = UUID.randomUUID().toString();
try {
MDC.put("requestId", requestId);
MDC.put("userId", request.getUserId());
logger.info("Creating order", "itemCount", request.getItems().size());
Order order = orderService.createOrder(request);
logger.info("Order created successfully", "orderId", order.getId());
return ResponseEntity.ok(OrderResponse.from(order));
} finally {
MDC.clear();
}
}
}
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(OrderRequest request) {
logger.info("Processing order request", "itemCount", request.getItems().size());
Order order = buildOrder(request);
orderRepository.save(order);
logger.info("Order persisted", "orderId", order.getId());
return order;
}
}Resulting log lines (JSON format):
{"time":"10:00:01","level":"INFO","requestId":"abc-123","userId":"789","msg":"Creating order","itemCount":3}
{"time":"10:00:02","level":"INFO","requestId":"abc-123","userId":"789","msg":"Processing order request","itemCount":3}
{"time":"10:00:03","level":"INFO","requestId":"abc-123","userId":"789","msg":"Order persisted","orderId":"456"}
{"time":"10:00:04","level":"INFO","requestId":"abc-123","userId":"789","msg":"Order created successfully","orderId":"456"}To trace the whole flow, simply grep for the requestId:
grep 'requestId":"abc-123"' app.logTracks a complete user journey across services.
Enables effective debugging of distributed systems.
No need to manually pass IDs through method signatures.
Industry‑standard, scalable practice.
The difference between junior and senior logging is not the quantity of logs but the quality and searchability of the information they contain.
Start with one pattern—add an order ID to error logs, adjust a log level, or introduce a correlation ID—and watch debugging time shrink from hours to minutes.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
