On this page
What Is Structured Logging?
Structured logging emits machine-readable key-value events instead of free text. Learn why it matters, how to adopt it, and what AI logs get wrong.
Quick answer
- Structured logging emits each line as machine-readable data — usually one JSON object with named fields — instead of a human-written sentence.
- Free text can only be read; structured data can be filtered, grouped, counted, and alerted on.
- Emit a
messagefield for humans and named fields for machines.- The performance risk is logging too much, not logging structured. Log less, asynchronously.
What is structured logging?
Structured logging is the practice of writing each log entry as a machine-readable record with named fields, most commonly a single JSON object per line, rather than as a free-form sentence. Where a text log says user bob failed to login, a structured log records an event named login_failed with separate fields for the user identifier, the failure reason, and a timestamp.
The difference is what you can do with it afterwards. Free text can only be read by someone scanning it. Structured data can be queried: count login failures grouped by user over the last five minutes, find every request slower than two seconds, trace one correlation ID across services. Those questions are trivial against structured data and effectively unanswerable against prose.
This is the same idea that separates monitoring from observability. Monitoring tells you a number crossed a threshold. Observability means you can ask an open question about a running system and get an answer. Structured logging is a large part of how that becomes possible.
Where this bites vibecoders
The specific way this bites vibecoders is the shape of the default output. An assistant writing debug logging produces
console.log("user", userId, "action", action, "error", err.message)— which printsuser 42 action login error Invalid password. Every field is a positional argument to a human-readable string, with no names attached at the point of the call and no structure at the point of the output. Reading it in the terminal is fine. The moment you need to count failures or filter by user, the information is gone, because it was never recorded as a field — it was only ever concatenated into a string.
What does a structured log line look like?
{"timestamp":"2026-09-29T10:15:04.221Z","level":"error","event":"login_failed","user_id":42,"reason":"invalid_password","ip":"203.0.113.10","request_id":"req_8f2a1c","duration_ms":12}Every element is doing work:
timestamp— when it happened, in UTC, so entries from different machines line up. See Why Should Servers Always Use UTC?.level— severity, so you can filter errors without reading messages.event— a stable, machine-readable name. This is the field you group and alert on. Keep the set small and consistent:login_failed, notLogin Failure!!.- Domain fields —
user_id,reason,ip. These are what make the line queryable. request_id— a correlation identifier tying this line to everything else in the same request, which is what makes distributed tracing possible.duration_ms— timings, so you can measure rather than guess.
Compare the text equivalent — 2026-09-29 login failed for user 42: invalid password (req_8f2a1c) — and the difference is that in the second version, “user” and “42” are separated by a space, and nothing guarantees that convention holds in the next log line written by someone else.
How do you adopt structured logging?
You need a logger that emits JSON, and a discipline about which fields to include.
Pick a library and stop calling console.log. Every major language has a maintained option that emits JSON, and using it is cheaper than designing a convention:
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: { service: "api", request_id: currentRequestId() },
});
// Named fields, not positional arguments
logger.info({ event: "order_created", order_id: "o_123", total: 49.99 }, "order created");
logger.error({ event: "payment_failed", order_id: "o_123", reason: "timeout" }, err);The critical discipline is the shape of the call: a single object of named fields, plus optionally one human message. Passing positional arguments to a logger reproduces the console.log problem in a new library.
Always include a request or correlation ID. Without one, you cannot follow a single user’s journey through your system, which is most of what logs are for once you have more than one service.
Use a small, fixed set of event names. An event vocabulary is what makes alerting possible. “Too many different event names” is the same failure as “too many different log messages”, and it is a design decision you have to make early.
Never log secrets. Passwords, tokens, full card numbers, and personal data must be stripped at the logger, not by remembering. Log a user ID, not the raw request body.
Start in production mode and keep local human-readable. The instinct to have pretty local output and JSON in production is reasonable; many libraries support both from one configuration.
Where AI coding assistants get this wrong
- Positional
console.logarguments, which produce exactly the unstructured text logging was meant to replace. - Interpolating values into the message string —
logger.info(`user ${id} logged in`)— so the field is embedded in prose and cannot be filtered. - Logging entire objects, which can include passwords, tokens, and personal data you did not intend to write to disk.
- Logging every line inside a loop, producing volume that costs money and buries the signal.
- No levels, so everything is the same severity and alerting is impossible.
- No correlation ID, so a single request cannot be reconstructed.
- Inconsistent event names — the same event spelled three ways across three files, which breaks grouping.
- Logging the error but not the context, leaving you an exception with nothing to attach it to.
Checklist
- Replace direct
console.logcalls with a structured logger. - Always pass named fields in an object, never positional arguments.
- Keep a small, consistent set of event names.
- Attach a request or correlation ID to every entry.
- Use UTC timestamps.
- Log levels consistently:
errorfor action needed,warn,infofor lifecycle,debugfor detail. - Strip secrets and personal data at the logger configuration, not by discipline.
- Set an appropriate level in production so debug volume never reaches production by default.
- Log less: boundaries and failures, not every iteration.
- Include durations on anything you might later need to measure.
- Make local output human-readable and production output JSON from one config.
- Confirm you can answer real questions — errors by type in the last hour, slow requests, one user’s requests — against your logs.
FAQ
What is structured logging in simple terms?
Structured logging means writing each log line as structured data — usually one JSON object with named fields — rather than a sentence a human wrote. Instead of ‘user bob failed to login’, you record an event name of ‘login_failed’ plus separate fields for the user, the reason, and the timestamp, which means software can search, group, and alert on them.
Why is structured logging better than print statements?
Because free text can only be read by a human scanning it. Structured logs can be filtered by a field, grouped by a field, counted, and turned into an alert — ‘count login_failed by user in the last five minutes’ is a query, not a grep pattern you have to invent each time. It is also what makes automated log search and alerting possible at all.
Is JSON logging a performance problem?
Not meaningfully, for most applications. Serialising a small object is cheap compared with the I/O of writing the line. The real performance risks are logging far too much — logging every request body or every loop iteration — and synchronously blocking on output. Use buffered or asynchronous logging, and log less rather than optimising the format.
Should I keep human-readable messages too?
Yes, as a field rather than as the whole line. A message field keeps logs readable when a human opens them, while the structured fields make them queryable. The failure mode to avoid is a message that is the only content, because then you are back to unstructured logs with extra punctuation.
Do I need an ELK stack or a paid tool to benefit?
No. The benefit comes from the format, not the destination. If you emit JSON with consistent field names, you can grep and jq it effectively on a single server, and move to a hosted tool later without changing the log calls. Adopting a heavy stack before the logs are structured inverts the order of work and is the most common way this effort stalls.
How much should I log?
Enough to reconstruct a request’s path and its failures, and no more. Log entry and exit of significant operations, every error with its context, and anything you might need to measure later. Avoid logging every request body, every loop iteration, or full user objects — the volume costs storage and money, and it buries the lines that matter.
Related topics
- What Is Log Rotation (and Why Do Your Logs Keep Disappearing)?
- What Is Observability (and How Is It Different From Monitoring)?
- How to Debug AI-Generated Code: A Complete System for When You Don’t Understand What the AI Wrote
- Why Does My App Work Locally but Not in Production?
- How to Get Alerted When Your Site Goes Down