Unstructured logs are a trap. They look simple until you need to find something.
Quick: find all login failures from a specific IP range in the last hour. Now try parsing the order ID from error messages. Hope you enjoy regex.
Structured Logs Change Everything
Same events, structured:
| |
Now queries become trivial:
| |
Fields are fields. No parsing, no regex, no guessing.
Choosing a Format
JSON is the default choice. Universal parser support, works with every log aggregator (ELK, Loki, Datadog), human-readable enough for debugging.
Logfmt is more compact: level=info event=user_login user=john@example.com. Great for high-volume systems where bytes matter.
JSON Lines (JSONL) — one JSON object per line — is the sweet spot for most applications. Streamable, greppable, parseable.
Implementation Patterns
Python (structlog)
| |
Node.js (pino)
| |
Pino is blazingly fast — it writes JSON directly without intermediate object creation.
Go (zerolog)
| |
Bash
Even shell scripts can emit structured logs:
| |
Essential Fields
Every log entry should include:
| Field | Purpose |
|---|---|
timestamp | ISO 8601 format, always UTC |
level | debug, info, warn, error, fatal |
event | What happened (snake_case verb) |
service | Which service emitted this |
trace_id | Request correlation (if applicable) |
Beyond that, add context relevant to the event. For HTTP requests: method, path, status, latency. For database queries: query type, table, duration. For errors: error type, message, stack trace.
Context Propagation
The real power comes from automatic context. Add fields once, include them everywhere:
| |
| |
Now every log in that request context carries correlation IDs. Tracing a single user’s journey becomes a simple filter.
Querying in Production
With structured logs in a log aggregator:
The query language varies, but the principle is the same: filter on fields, not regexes.
Performance Considerations
Structured logging adds overhead. A few ways to mitigate:
- Log asynchronously — Buffer and batch writes
- Sample high-frequency events — Log 1% of health checks
- Use efficient serializers — Pino, zerolog beat generic JSON libraries
- Avoid logging in hot paths — Aggregate metrics instead
For most applications, the debugging time saved vastly outweighs the microseconds spent serializing JSON.
Migration Strategy
If you’re stuck with unstructured logs:
- Start with new code — All new services emit structured logs
- Wrap existing loggers — Add a structured wrapper that calls the old logger
- Add correlation IDs first — Even unstructured logs benefit from trace IDs
- Convert high-value events — Errors, auth events, transactions
- Ship both formats temporarily — Old format for existing dashboards, new for modern tools
Don’t try to convert everything at once. Incremental improvement beats stalled perfection.
The Payoff
Last week I debugged a production issue by running:
| |
Five seconds to see exactly what one user experienced. No regex. No guessing at field positions. No hoping the log format didn’t change.
That’s the promise of structured logging: your logs become a queryable database of everything that happened. Worth the setup.
Computing Arts explores the craft of production systems. More at computingarts.com.