Structured Logging Best Practices

Plain text log messages are fast to write but genuinely painful to query at scale — structured logging (consistently formatted, typically JSON) trades a bit of readability for dramatically better searchability.

# Unstructured: readable to a human, but a nightmare to query reliably
logger.info(f"User {user_id} placed order {order_id} for ${total}")

# Structured: a log aggregator can filter, aggregate, and alert on these fields directly
logger.info("order_placed", extra={
    "user_id": user_id,
    "order_id": order_id,
    "total": total,
    "currency": "EUR",
})
# Produces: {"event": "order_placed", "user_id": 42, "order_id": 1001, "total": 59.99, ...}

Why This Matters at Scale

With structured logs, “show me all orders over $1000 in the last hour that failed” becomes a genuine, precise query against typed fields — with unstructured text logs, the same question requires fragile regex parsing that breaks the moment someone slightly rewords a log message. This is exactly why every major log aggregation platform (Elasticsearch, Datadog, Splunk) is built around structured field querying, not free-text search alone.

Consistency Is the Hard Part

The real discipline structured logging requires is consistent field naming across the entire codebase — user_id in one service and userId in another breaks cross-service correlation entirely, so agreeing on and enforcing a shared schema is worth doing deliberately rather than letting it emerge organically.