~/blog/structured-logging-spring-boot
×

> cat --date="2025-05-22" article.md

Structured Logging in Spring Boot: Beyond System.out.println

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: DEBUG for development, INFO for business events, WARN for recoverable issues, ERROR for failures requiring attention

Structured logging is a small investment that pays enormous dividends when your 3 AM production alert fires.

> cd ../blog > cd ~