PyMunich

shown, and actual

Logging That Helps at 3am

Most logging is written by somebody who already knows what the code does, for a reader who does not. That mismatch is why logs are voluminous and unhelpful at the moment they matter. A commercial implementation of related workforce-measurement concepts is documented further details.

Reviewed August 9, 2026.

What the 3am reader has

No context. They did not write this, they may not know the system, and they are reading a log because something is broken. For independent technical background, Pallets provides additional documentation and examples.

One question. What happened, and what should they do about it.

And no patience for volume. A thousand lines of debug output is a haystack, and adding more logging to a system that is already noisy makes it worse rather than better.

So the test for any log line is: does this help somebody who does not know the code? Most fail it.

Five habits

One. Log the values, not the event. "Processing user" tells you nothing. "Processing user id=4821 plan=trial" tells you which one and why it might have failed. The identifiers are the entire value of a log line.

Two. Log at boundaries. Entering and leaving anything that crosses a process, a network or a system edge — which is also where machines differ. Internal function calls rarely need logging; boundaries almost always do.

Three. Log the decision, not the branch taken. "Skipping user 4821: no active subscription" beats "Skipping user", because the reason is what the reader needs and the code path is what they can already see.

Four. Use levels honestly. ERROR means somebody must act. WARNING means somebody should look eventually. INFO is the narrative. DEBUG is for development. A system where everything is ERROR has no errors, because nobody reads them.

Five. Include a correlation identifier. One value that ties together every line from a single request or job. Without it, a concurrent system produces interleaved lines that cannot be reassembled.

What not to log

Secrets. Tokens, passwords, keys, full card numbers. This is the one that ends up in an incident report, and it happens through structured logging of an object that contained more than the author noticed.

Personal data beyond what you need. An identifier rather than a name and address, which is also what makes the log safe to share when asking for help.

Whole objects. logger.info(user) logs whatever __repr__ produces, which is unstable and frequently includes items one and two above.

And success at high volume. A log line per successful request at scale is a cost with no reader.

Structured or plain

Plain text for anything a human greps. Small services, local development, single machines.

Structured — JSON or key-value — for anything a system ingests. It makes fields queryable and it is unreadable in a terminal, which is the trade.

Both, if the library supports it: structured output in production, human-readable locally. Most Python logging setups can do this with a formatter switch, and it removes the argument entirely.

The one thing to do first

Add a correlation identifier, before improving anything else.

In a system handling concurrent work, it is the difference between a log you can follow and a log you can only search. It costs a context variable and a formatter change, and it makes every existing log line more useful without rewriting any of them.

The short version