Why JSON logs win
A line like User 91 failed login from 1.2.3.4 is readable but useless at scale — you cannot reliably filter, aggregate, or alert on free text. Structured logging emits each event as a JSON object so log platforms (Datadog, Elastic, Loki, CloudWatch) can index every field and let you query like a database.
{"level":"warn","msg":"login_failed","user_id":"usr_91","ip":"1.2.3.4","ts":"2026-05-28T09:00:00Z"}Now "show all login_failed events for usr_91 in the last hour" is a one-line query.
The fields every log line should have
| Field | Purpose |
|---|---|
ts | ISO 8601 timestamp (UTC) |
level | debug, info, warn, error |
msg | A short, stable event name — not a sentence |
service | Which service emitted it |
trace_id | Correlate logs across services for one request |
Use a stable event name in msg (order_created, not Order #4 was created) and put the variable data in separate fields. Stable names are what you build dashboards and alerts on.
Use a logging library, not console
Mature libraries produce JSON, manage levels, and add context efficiently:
import pino from "pino";
const log = pino();
log.info({ user_id: "usr_91", plan: "pro" }, "subscription_started");
// {"level":30,"time":...,"user_id":"usr_91","plan":"pro","msg":"subscription_started"}import structlog
log = structlog.get_logger()
log.info("payment_captured", amount=4995, currency="INR")Context propagation
Attach request-scoped context once and have it appear on every log line for that request:
const reqLog = log.child({ trace_id: req.id, route: req.path });
reqLog.info("handler_start");
reqLog.error({ err: e.message }, "handler_failed");A shared trace_id lets you reconstruct a single request's journey across multiple services — the foundation of distributed tracing.
What not to log
- Secrets and credentials — passwords, tokens, API keys. Redact them.
- Full PII — mask emails and card numbers; log a hashed or partial value if you must correlate.
- Huge payloads — log an ID and a size, not the entire request body.
Best practices checklist
- One JSON object per line (newline-delimited) so log shippers parse each event independently.
- UTC ISO 8601 timestamps.
- Stable, lowercase, snake_case event names in
msg. - Consistent field names across services (
user_ideverywhere, notuserIdhere anduidthere). - Log levels used consistently; reserve
errorfor things that need attention. - Sample or rate-limit noisy debug logs in production.