Logging That Helps at Two in the Morning

Logging That Helps at Two in the Morning

Every log line is written for one reader: someone at two in the morning, with customers who cannot pay, trying to find out why. Too often what that reader finds is Error occurred. Several hundred times, and nothing else.

Logging is the one part of a system written entirely for a future emergency, usually by someone who is not having one. Most of it goes in to chase a bug one afternoon and is never looked at again. Below are a few typical lines rewritten for the person who will really read them, then the principles behind the rewrites.

Before and after

An error with nothing attached

# before
ERROR Error occurred

# after
2025-12-04T02:14:36Z ERROR req=7f3a9c payment.charge failed: gateway timeout after 30s, attempt=3/3, order=10482 left unpaid

The second line answers what you would otherwise have to ask: when, which request, which operation, why, and what it left behind. "Left unpaid" is the part people forget. It tells the reader whether anything needs cleaning up.

A step with no decision in it

# before
INFO Processing order 10482
INFO Done

# after
INFO req=7f3a9c order=10482 invoice email not sent: customer has opted out of email

"Done" tells you the code reached the end of a function. The rewrite tells you what the system chose to do and why, which is exactly what you need when a customer says they never got their invoice.

A line that should not exist

# before
DEBUG login user=someone@example.com password=letmein1 token=eyJhbGciOi...

# after
INFO req=c41e07 login failed: wrong password, user_id=3317

The first version writes a password and a session token into a file that will be copied, backed up and read by people who should never see either. The second says what happened, and to whom, through an id that means nothing outside the system.

One request, one thread

02:13:05 INFO  req=7f3a9c POST /checkout user_id=3317 items=2
02:13:05 INFO  req=7f3a9c stock reserved for 2 items
02:13:06 INFO  req=7f3a9c payment.charge started, gateway=primary
02:13:36 WARN  req=7f3a9c payment.charge timed out after 30s, retrying (2/3)
02:14:06 WARN  req=7f3a9c payment.charge timed out after 30s, retrying (3/3)
02:14:36 ERROR req=7f3a9c payment.charge failed, order=10482 left unpaid, stock released

On a busy system these six lines are scattered among thousands of others. Searching for req=7f3a9c pulls them back into one story, in order.

The principles behind the rewrites

Log decisions, not just errors

The most useful line in a log is often not an error at all. It is the moment the system chose something: skipped an email, applied a discount, retried, fell back to a second provider, treated two records as duplicates. Errors tell you something broke. Decisions tell you why the outcome is what it is, and most support questions are about outcomes. "Why was this customer charged twice?" is rarely answered by a stack trace.

Give every request an id

Generate it once, where the work enters the system: the web request, the queued job, the scheduled run. Put it on every line that work writes, and pass it along when the work hands off to something else. Show it on the error page as well. A customer who reads you six characters over the phone has just told you exactly where to look.

Let the logging setup add it, so nobody has to remember. In Python, a small filter does the job:

import logging
import uuid
from contextvars import ContextVar

request_id = ContextVar("request_id", default="-")

class RequestIdFilter(logging.Filter):
    def filter(self, record):
        record.req = request_id.get()   # stamp every line
        return True

handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(logging.Formatter(
    "%(asctime)s %(levelname)s req=%(req)s %(message)s"))
log = logging.getLogger("shop")
log.addHandler(handler)
log.setLevel(logging.INFO)

def handle_checkout(order_id):
    request_id.set(uuid.uuid4().hex[:6])   # once, where the work enters
    log.info("checkout started order=%s", order_id)

Make the levels mean something

Agree what each level is for, and hold to it. ERROR: someone should act, probably today. WARN: something unusual happened and the system coped, for now. INFO: the normal story, in terms the business would recognise, such as order placed, invoice sent, import finished. DEBUG: detail for whoever is developing it, off in production unless you are chasing something.

An ERROR that fires forty times a day and gets ignored teaches everyone to ignore ERROR. Fix the cause or downgrade the line. Either is better than noise.

Never log secrets or personal data

Passwords, API keys, card numbers, session tokens: never, not even at DEBUG, because DEBUG gets switched on in a hurry and forgotten. Treat personal data with the same suspicion. Log the customer's id, not their name, email address or phone number. Logs get pasted into support tickets and chats and kept far longer than anyone intended; the id is enough to look someone up, with the right access, in the system built to hold their details.

Keep logs where you can search them

A log file on one server, readable by one person and rotated away after three days, will not help at two in the morning. Write lines a machine can filter (consistent key=value pairs, or JSON), send them somewhere you can search by request id, time and level, and decide on purpose how long to keep them. For a small system that can be as plain as files kept for a sensible period and someone who knows grep. What matters is that it was decided, not left to the defaults.

Searching only helps once someone knows to look, so an ERROR should reach a person rather than wait to be found. When I build an automation, logging and failure alerts go into the handover alongside the runbook, so nobody finds out in March that something stopped in January.

Try it on yesterday

There is a ten-minute test for all of this. Pick one real request from yesterday (an order, a login, an import) and, using nothing but the logs, tell its story: what came in, what the system decided, what it did and how it ended. If you can, your logs will hold up at two in the morning. If you cannot, the time to fix them is now, while nobody is waiting on the answer.