Structured logging
log.info("user " + id + " failed to pay $" + amt) reads fine to a human and
tells a machine nothing. You cannot filter it, group it, or count it without a
regex that shatters the next time the sentence changes. Structured logging
emits events as fields so a query engine can answer questions the author never
thought to ask.
Method
- Emit an event with fields, not an interpolated string. Write
log.info("payment_failed", user_id=id, amount=amt, currency="usd") and let
the logger render JSON. Now amount>100 and currency="usd" is a query, not
a grep: the message becomes a stable name and the variables become
searchable fields.
- Keep the event name constant and move nouns to fields. The first argument
is a fixed token like
order_placed or db_timeout, identical on every
emit. Everything that varies, ids, counts, durations, lands in fields.
Constant names let you count occurrences without matching free text.
- Pin a schema and reuse names across services. Agree that the user is
always
user_id, latency is always duration_ms, the request key is always
request_id. When every service spells them alike, one query joins them all;
uid, userId, and user fragment the same data into three.
- Control cardinality: bounded values are fields, unbounded ones get
sampled. A
status with a dozen values is a fine group-by. A raw SQL
string or full stack trace as a field explodes index size and cost. Keep
high-cardinality blobs in an unindexed message field, or sample them.
- Bind context once at the entry point. Attach
request_id and user_id
with logger.bind(request_id=rid) so every line in that request carries them
without repeating arguments. Correlation turns automatic instead of a field
you forget on the one line that mattered.
- Log durations and counts as numbers. Emit
duration_ms=214 as an
integer, never "took 214ms". Numeric fields let the backend compute
averages, percentiles, and thresholds; a number wrapped in a sentence has to
be parsed back out before it is usable.
Checks
- Can you answer a new question with a field filter, touching no logging code?
- Do the same concepts carry the same field name in every service?
- Is any field's value unbounded, and if so is it unindexed or sampled?
Boundaries
Structure fixes the shape of a line, not its urgency: which level it fires at is
log-levels. Chasing the id across services once it is logged is
distributed-tracing. Match the field names the project already uses over a
tidier scheme of your own, because a shared schema is the entire point.
1---2name: structured-logging3description: Emit machine-readable key-value events instead of prose sentences, with a stable schema and controlled field cardinality. Use when logs need to be queried and aggregated, not just read one line at a time by a person.4---56# Structured logging78`log.info("user " + id + " failed to pay $" + amt)` reads fine to a human and9tells a machine nothing. You cannot filter it, group it, or count it without a10regex that shatters the next time the sentence changes. Structured logging11emits events as fields so a query engine can answer questions the author never12thought to ask.1314## Method15161. **Emit an event with fields, not an interpolated string.** Write17 `log.info("payment_failed", user_id=id, amount=amt, currency="usd")` and let18 the logger render JSON. Now `amount>100 and currency="usd"` is a query, not19 a grep: the message becomes a stable name and the variables become20 searchable fields.212. **Keep the event name constant and move nouns to fields.** The first argument22 is a fixed token like `order_placed` or `db_timeout`, identical on every23 emit. Everything that varies, ids, counts, durations, lands in fields.24 Constant names let you count occurrences without matching free text.253. **Pin a schema and reuse names across services.** Agree that the user is26 always `user_id`, latency is always `duration_ms`, the request key is always27 `request_id`. When every service spells them alike, one query joins them all;28 `uid`, `userId`, and `user` fragment the same data into three.294. **Control cardinality: bounded values are fields, unbounded ones get30 sampled.** A `status` with a dozen values is a fine group-by. A raw SQL31 string or full stack trace as a field explodes index size and cost. Keep32 high-cardinality blobs in an unindexed message field, or sample them.335. **Bind context once at the entry point.** Attach `request_id` and `user_id`34 with `logger.bind(request_id=rid)` so every line in that request carries them35 without repeating arguments. Correlation turns automatic instead of a field36 you forget on the one line that mattered.376. **Log durations and counts as numbers.** Emit `duration_ms=214` as an38 integer, never `"took 214ms"`. Numeric fields let the backend compute39 averages, percentiles, and thresholds; a number wrapped in a sentence has to40 be parsed back out before it is usable.4142## Checks4344- Can you answer a new question with a field filter, touching no logging code?45- Do the same concepts carry the same field name in every service?46- Is any field's value unbounded, and if so is it unindexed or sampled?4748## Boundaries4950Structure fixes the shape of a line, not its urgency: which level it fires at is51log-levels. Chasing the id across services once it is logged is52distributed-tracing. Match the field names the project already uses over a53tidier scheme of your own, because a shared schema is the entire point.