Course topics

By WebNest Studio

Spring Boot Tutorial

Structured Logging in Spring Boot

Plain text logs are easy for humans to read on one screen, but in production logs from dozens of instances flow into systems like Elasticsearch, Loki, Graylog, Splunk or CloudWatch. There you want to search "all ERROR logs for tenant acme with traceId X", which requires each log line to be structured data — JSON with named fields — rather than free text.

Spring Boot has built-in structured logging: one property switches console or file output to JSON in the Elastic Common Schema (ECS), Graylog (GELF) or Logstash format. This lesson shows how to enable it, add custom fields and MDC context, log key-value pairs, and follow logging practices that make production debugging fast and safe.

Enabling Structured Output

Set logging.structured.format.console (and/or logging.structured.format.file) to ecs, gelf or logstash. Each log event becomes one JSON line with timestamp, level, logger, thread, message, service name, and — when tracing is active — trace and span ids. A common setup is human-readable logs locally and JSON in production, controlled by profile.

Adding Context

Put request-wide context such as correlation id, tenant or user id in the MDC (for example in a filter); it is added to every JSON log line automatically. SLF4J's fluent API (log.atInfo().addKeyValue("orderId", id).log(...)) adds per-event fields. Static fields for every line (environment, region) come from logging.structured.json.add.*.

Logging Practices

Good production logging is deliberate:

  • Log events, not noise: one INFO line per important business action, DEBUG for details.
  • Use placeholders (log.info("Order {} placed", id)) rather than string concatenation.
  • Log exceptions once, where they are handled, with the stack trace as the last argument.
  • Never log passwords, tokens, full card numbers or unnecessary personal data.
  • Write logs to stdout in containers and let the platform ship them; avoid local files.
  • Adjust levels at runtime through the /actuator/loggers endpoint when debugging.

Examples

Switching to JSON logs in production

Java
# application-prod.yml
logging:
  structured:
    format:
      console: ecs
    json:
      add:
        environment: production
        region: ap-south-1
  level:
    root: INFO
    com.webnest: INFO

@Service
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void place(Order order) {
        log.info("Order {} placed", order.getNumber());
    }
}
Output
{"@timestamp":"2026-09-27T10:12:03.412Z","log":{"level":"INFO","logger":"com.webnest.shop.OrderService"},"process":{"pid":1,"thread":{"name":"tomcat-handler-12"}},"service":{"name":"webnest-shop"},"message":"Order WN-10231 placed","traceId":"6f1c2b9e4d3a8f7e1c2b9e4d3a8f7e1c","spanId":"a1b2c3d4e5f60718","environment":"production","region":"ap-south-1","ecs":{"version":"8.11"}}

Per-event key-value pairs and MDC context

Java
// In a filter: context for every log line of this request
MDC.put("tenant", tenantId);
MDC.put("userId", userId);
try {
    chain.doFilter(request, response);
} finally {
    MDC.remove("tenant");
    MDC.remove("userId");
}

// In business code: structured fields for one event (SLF4J 2 fluent API)
log.atInfo()
   .setMessage("Payment captured")
   .addKeyValue("orderId", order.getNumber())
   .addKeyValue("amount", order.getTotal())
   .addKeyValue("method", "upi")
   .log();
Output
{"@timestamp":"...","log":{"level":"INFO",...},"message":"Payment captured","orderId":"WN-10231","amount":2999.00,"method":"upi","tenant":"acme","userId":"42",...}

Kibana / Loki query: message:"Payment captured" AND tenant:"acme" AND method:"upi"

Changing a log level at runtime

Java
# expose the loggers endpoint (internal network only)
management:
  endpoints:
    web:
      exposure:
        include: health, loggers

curl -X POST localhost:9090/actuator/loggers/com.webnest.shop.payment \
     -H "Content-Type: application/json" -d '{"configuredLevel":"DEBUG"}'

curl localhost:9090/actuator/loggers/com.webnest.shop.payment
Output
{"configuredLevel":"DEBUG","effectiveLevel":"DEBUG"}
(DEBUG logs for the payment package appear immediately, without a restart; set back to null afterwards)

Common Mistakes

  • Building JSON log lines by hand with string concatenation instead of using structured logging.
  • Logging the same exception at every layer, producing several stack traces per failure.
  • Logging secrets, tokens or full request bodies.
  • Putting high-volume DEBUG logs in production permanently, increasing cost and hiding important events.
  • Forgetting to clear MDC values, leaking context into unrelated requests on reused threads.

Key Points to Remember

  • logging.structured.format.console/file = ecs, gelf or logstash turns logs into JSON.
  • MDC values and SLF4J key-value pairs become searchable fields.
  • Trace and span ids are included automatically when tracing is enabled.
  • Log meaningful events once, with placeholders, and never log secrets.
  • Use /actuator/loggers to change levels at runtime while debugging.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.