A log line gets written in a very particular state of mind: the author has the function in front of them, knows what the variables mean, knows what just happened above and what’s about to happen below, and wants a marker that this branch was taken. So they write processing batch or entering retry path or, memorably, here. In that moment it’s perfectly informative, because the surrounding context is free. The reader will not have any of it. The reader is looking at one line, hours or months later, extracted from a stream of thousands, with no idea which function it came from or what “batch” refers to.

That mismatch produces logs that are technically present and practically useless. processing batch doesn’t say which batch, how large, on behalf of what, or where it came from. When something goes wrong the line proves the code got that far and nothing else — and getting that far was rarely the question. Meanwhile the actual question, which is almost always some form of “what was this specific request doing,” can’t be answered because the fields that would identify the specific request were the ones that felt redundant to include, since the author already knew.

The corrective is to write for someone with no context, because that’s the only reader logs ever actually have — including you, six months on. Concretely that means every line carries the identifiers needed to tie it to a request, a user, a job, or a tenant, and states what happened in terms that make sense outside the function. processing batch becomes something closer to a fact: this job, this many records, this source, this attempt number. The test isn’t whether the line is meaningful next to the code that emits it. It’s whether the line is meaningful alone, in a search result, next to lines from twenty other subsystems.

The other half of writing for a reader is admitting when logs aren’t what’s needed. A line that fires on every request to say the request happened is a metric wearing a costume — you want the rate, not thousands of individual statements of the obvious. A line emitted purely so someone can trace the path through a function is usually a trace, or a sign the function is doing too much to follow. Logs earn their cost when they capture something specific and unusual enough that you’d want to read that one instance: this request failed for this reason with these inputs. Used for everything else, they mostly bury the lines you needed under lines nobody will ever read.

None of that requires a new tool. It requires imagining the reader — tired, mid-incident, with a search box and a timestamp — and asking whether what you’re about to write would help them. That’s a small shift in the moment and a large one in aggregate, because it’s the difference between a log that reconstructs what happened and one that merely proves the code ran.