Structured Logging
Logs machines can read: JSON logging, correlation with trace IDs, and log levels you can change at runtime.
On this page
When one app runs on one machine, System.out.println and a scroll of console text get you a long
way. In production - many instances, many services, logs shipped to a central store - humans stop
reading logs line by line. Machines do. Structured logging makes your logs something a machine can
parse, index, and search.
From text lines to structured events
A traditional log line is a string: readable to you, opaque to tooling. A log aggregator (Elasticsearch/Kibana, Loki, Datadog) can only grep it. A structured log is a record with named fields - typically JSON:
{"timestamp":"2026-07-21T09:14:22Z","level":"ERROR","logger":"c.b.lending.LendingService",
"message":"Borrow failed: no copies","traceId":"a1b2c3","memberId":42,"isbn":"978-0134685991"}Now the aggregator can do what strings can't: 'show me every ERROR with isbn=978-0134685991 in
the last hour', or 'group failures by memberId'. Spring Boot 3.4+ makes this a one-line
config change - no custom appender:
logging:
structured:
format:
console: ecs # Elastic Common Schema JSON (or 'logstash', 'gelf')Human-readable locally, structured in production
You don't want JSON scrolling past while developing. Enable structured output only in the profiles
that ship (e.g. application-prod.yml), and keep the friendly, colored console format for local
runs. Same code, different config - the twelve-factor way.
Correlation: tying lines to a request
A single request produces many log lines, scattered among lines from other concurrent requests. What ties its lines together is a value in the MDC (Mapped Diagnostic Context) - a per-thread (or per-request) bag of key/values automatically added to every line. The most important key is the trace ID:
[traceId=a1b2c3 spanId=d4e5] Borrowing book 978-0134685991 for member 42
[traceId=a1b2c3 spanId=d4e5] Loan created, 3 copies remainingWhen Micrometer Tracing is present (the tracing lesson), Spring populates traceId/spanId in the
MDC for you, so filtering logs by one trace ID reconstructs a request's whole story - even across
services. You can add your own MDC keys (MDC.put("memberId", ...)) for domain context.
A pile of books is 'searchable' only by picking each up and reading the spine. A catalog records each book as fields - title, author, subject, shelf - so you can query 'every book by this author on this subject' instantly. Plain text logs are the pile; structured logs are the catalog. Same books, but now findable.
Levels you can turn without redeploying
Logging frameworks have levels - ERROR, WARN, INFO, DEBUG, TRACE - and you set a
threshold so noise stays out of production. The catch: when you need DEBUG to diagnose a live
incident, redeploying to change it is slow. Actuator's loggers endpoint changes a level at
runtime:
# turn on DEBUG for one package on a running instance - no restart
curl -X POST localhost:8080/actuator/loggers/com.bookvault.lending \
-H 'Content-Type: application/json' -d '{"configuredLevel":"DEBUG"}'Production is serving 500 requests a second across three instances, all logging to one central
store. One user reports their borrow failed. Explain how structured logging plus a trace ID lets
you isolate exactly that request's log lines, when a plain-text grep for 'failed' would drown you.
Why is structured (JSON) logging with a trace ID valuable in production?
Key takeaways
- In production, logs are read by machines (aggregators), so they must be machine-parseable - structured JSON, not free text.
- Spring Boot enables structured logging (ECS/Logstash/GELF) via one property; keep human-readable format for local dev.
- The MDC adds per-request context (especially traceId/spanId) to every line, so filtering by trace ID reconstructs a request's story - even across services.
- Log levels (ERROR..TRACE) control noise; Actuator's loggers endpoint changes them at runtime without a redeploy.
- Logs are the 'what happened' pillar of observability, complementing metrics and traces.