...
Back

Your Logs Are a Schema Whether or Not You Designed One

Nobody plans a log format. Everybody ends up with one, discovered at 3am by grepping for a string somebody typed a year ago.

Your Logs Are a Schema Whether or Not You Designed One

Your Logs Are a Schema 📋

Logging feels like the least architectural thing in a codebase. You add a line where you are confused, it prints, you move on. But every one of those lines becomes part of the interface you will use to understand production, and that interface accumulates without anyone designing it.

You discover its shape the first time you need it in a hurry, which is the worst possible time.


What incidents actually require

Watch how logs get used during a real incident and the requirements become obvious:

One request's complete story. Not all logs from that minute — the specific path one request took across every service it touched. Without a correlation identifier that propagates, you are reconstructing this by timestamp, which fails exactly when you need it most, because the volume that makes an incident interesting is the volume that makes timestamp correlation useless.

Filtering by dimensions, not text. "All failures for this endpoint in this region" is a query. If severity, endpoint, and region are embedded in a sentence, it is a regex, and a fragile one.

Counting. "Is this happening twice or two thousand times" changes the diagnosis completely, and you cannot count things that are not structured.

All three are impossible after the fact. They are properties of how the line was written, not of how you search it.


The rules that buy the most

Structure the fields, keep the message human. A JSON line with a readable message plus typed fields is greppable and queryable. Interpolating values into a sentence throws away every dimension you will want later:

// Every value you might filter by is now trapped in prose.
log.info(`user ${id} upgraded to ${plan} after ${ms}ms`)
 
// Same sentence, plus a queryable record.
log.info("subscription upgraded", { userId: id, plan, durationMs: ms })

Propagate a request ID from the edge. Generate it at the first entry point, put it in every log line, pass it to every downstream call, and return it in error responses. That last part is underrated: a user who can quote an ID has handed you the exact trace, which turns a vague report into a one-query investigation.

Log decisions, not just events. "Cache miss" is an event. "Cache miss: key absent, falling back to origin" is a decision with a reason. When behavior is wrong rather than crashed, you need to know why the code chose what it chose, and the reason is exactly what nobody logs.

Use levels the way an operator reads them. ERROR should mean someone may need to act. If routine, expected conditions log as errors, the level stops carrying information and everybody learns to ignore it — at which point you have all the cost of logging and none of the signal.


What not to log

This part is not optional and it is not only a compliance concern.

Never log credentials, tokens, session identifiers, or keys — including inside request bodies, error objects, and exception dumps, which is where they usually escape. Never log the raw contents of user data you would not show to a support agent.

Two specifics that bite repeatedly:

  • Error objects can carry the request that caused them, headers included. Logging a caught exception verbatim is one of the most common ways an authorization header ends up in a log aggregator with a long retention window.
  • Anything that can be used to authenticate is a credential, whether or not it is called one — a device registration key, a signed URL, a recovery token, a configuration blob containing any of those. If pasting it into a client would grant access, it does not go in a log.

Logs are frequently the least protected copy of your data: broadly readable, long-retained, shipped to third parties. Treat a log line as a publication.


Where to start if you have none of this

You do not need a migration. Three changes, in order of return:

  1. A request ID from the edge, in every line. This single change does more for incident response than everything else combined.
  2. Structured fields on the paths that matter — authentication, payment, anything with a retry. Not everywhere; the hot paths where you already know you have questions.
  3. An audit for secrets in existing log statements. Grep for logging calls that pass whole objects, headers, or caught errors. Fix those first; they are the ones with a real downside.

None of this is glamorous, and all of it is cheaper than the alternative — which is being unable to answer a simple question about production while the question is urgent.