Structured Logging: Make Your Logs Answer Questions, Not Just Tell Stories
Why small engineering teams should switch from free-text logs to structured JSON logging, and how to do it without heavy tooling.
At 2 a.m., when a customer says their payment "just disappeared," the difference between a five-minute fix and a two-hour archaeology dig usually comes down to one thing: whether your logs are searchable data or a pile of prose. Most small teams start with console.log("payment failed for user " + id) and never revisit it — until the night they really need to.
Structured logging is the simplest upgrade with the highest payoff in a small team's observability stack. It costs almost nothing to adopt and pays you back on every incident.
What "structured" actually means
Instead of formatting values into a sentence, you emit a machine-readable record — usually one JSON object per line:
{"level":"error","event":"payment_failed","order_id":"ORD-2291","gateway":"midtrans","reason":"timeout","duration_ms":30012}
The human-readable message is optional. The fields are the point. Now "show me every gateway timeout in the last hour" is a filter, not a regex-guessing game against strings someone typed differently in five places.
The three habits that matter
Tooling is the easy part — pino in Node, structlog in Python, slog in Go. The discipline is what makes it work:
- Name events, don't write sentences. Use a stable
eventkey likewebhook_receivedorrefund_issued. Sentences drift; event names become a vocabulary your whole team shares. - Always attach identifiers. Order ID, customer ID, correlation ID. A log line without an identifier is a rumor — you know something happened, but not to whom.
- Log at boundaries. Incoming requests, outgoing calls to gateways and APIs, queue publishes and consumes. Boundaries are where things break; interior noise mostly hides the signal.
A good log line answers who, what, and with which request — before anyone has to ask.
Levels are a contract, not decoration
Pick meanings and enforce them. A workable scheme: error means a human should eventually look, warn means something degraded but self-healed, info marks business events worth auditing, debug is off in production by default. When everything is info, nothing is. The fastest way to ruin logging is to let error-level noise train the team to ignore errors.
Also decide what never gets logged: full card numbers, passwords, raw webhook payloads containing personal data. Redact at the logger level so nobody has to remember at the call site.
You don't need an observability platform yet
A common objection: "we don't run Elasticsearch." You don't need to. JSON-per-line logs on disk are already queryable with grep and jq:
cat app.log | jq 'select(.event=="payment_failed" and .gateway=="midtrans")'
That one-liner replaces an hour of scrolling. When you eventually outgrow files and adopt Loki, CloudWatch, or a hosted service, structured logs slot straight in — the migration is configuration, not a rewrite. Teams that log free text first end up paying twice: once in painful debugging now, and again in a reformatting project later.
The takeaway
Structured logging is not an enterprise practice you defer until you're big. It is a small-team superpower precisely because you have no dedicated ops crew — your logs are your on-call engineer. Pick a JSON logger, define ten event names for your core flows, attach IDs everywhere, and enforce log levels in code review. The next time something disappears at 2 a.m., you'll find it with a filter instead of a flashlight.
Build with Abati Technology
We build software that ships — WhatsApp API, developer tools, POS, and mobile apps. Let's talk about your project.
Get in Touch →