If your production logs look like System.out.println("Order processed: " + orderId), you are making debugging unnecessarily difficult. Structured logging transforms your logs from human-readable lines into machine-parseable events.
Why Structured Logging?
Modern applications run in containers, orchestrated by Kubernetes, with logs aggregated by tools like the ELK stack, Datadog, or Grafana Loki. These tools work best with structured (JSON) log entries:
{
"timestamp": "2025-05-22T14:32:01.123Z",
"level": "INFO",
"logger": "com.example.OrderService",
"message": "Order processed successfully",
"orderId": "ORD-12345",
"customerId": "CUST-678",
"processingTimeMs": 142,
"traceId": "abc123def456"
}
Compare that to: 2025-05-22 14:32:01 INFO Order processed successfully for order ORD-12345. The first one is searchable, filterable, and aggregatable. The second requires complex regex parsing.
Implementation with Logback
Add the Logstash Logback Encoder to your pom.xml:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
Configure logback-spring.xml:
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</configuration>
Using MDC for Context
The Mapped Diagnostic Context (MDC) lets you attach metadata to every log entry within a request:
@Component
public class RequestContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws Exception {
try {
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("clientIp", request.getRemoteAddr());
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
Now every log entry within that request automatically includes requestId and clientIp.
Key Practices
- Use SLF4J parameterized messages:
log.info("Order {} processed in {}ms", orderId, duration) - Never concatenate strings in log statements — it wastes CPU even when the log level is disabled
- Add correlation IDs across service boundaries for distributed tracing
- Keep log levels meaningful:
DEBUGfor development,INFOfor business events,WARNfor recoverable issues,ERRORfor failures requiring attention
Structured logging is a small investment that pays enormous dividends when your 3 AM production alert fires.