What good observability looks like — structured logs, trace spans, metrics, correlation ids, and the instrumentation rules that turn logs into answers. Use when adding a feature, reviewing a service, or debugging a prod issue.
Scanned 5/27/2026
Install via CLI
openskills install atuljha23/holocron---
name: observability-checklist
description: What good observability looks like — structured logs, trace spans, metrics, correlation ids, and the instrumentation rules that turn logs into answers. Use when adding a feature, reviewing a service, or debugging a prod issue.
---
# Observability checklist
The goal isn't logs — it's answering "what happened" in under five minutes at 2am. Design backward from the question.
## Logs
### Structured or nothing
- JSON (or logfmt), not free text.
- Every log has: `ts`, `level`, `service`, `request_id`, `msg`, and relevant fields.
- Fields are snake_case or camelCase — pick one per service. No mixing.
- Durations in milliseconds as numbers, not "took 2s" strings.
### Levels are a contract
- `error`: something failed the user can't recover from. Alert-worthy.
- `warn`: something unexpected but handled. Review-worthy.
- `info`: the shape of normal traffic. Sampling OK.
- `debug`: developer-only. Off in prod.
- Don't invent new levels.
### What to log
- Request entry (method, path, user, request_id).
- Outcome (status, duration).
- Branch decisions that matter (authz check, cache hit/miss, retry).
- Errors with the full context you need to reproduce.
### What NOT to log
- Secrets, tokens, passwords, PII — scrub before logging.
- The full request body by default. Specific fields, yes. Everything, no.
- Stack traces as separate lines — include as a `stack` field so they stay with the event.
## Traces
### Span per unit of work
- HTTP request handler = 1 span.
- Outbound call = child span.
- DB query = child span (with `db.statement` truncated / hashed, not raw if PII risk).
- Background job step = 1 span.
### Mandatory attributes
- `service.name`, `service.version`.
- For HTTP: `http.method`, `http.route`, `http.status_code`.
- For DB: `db.system`, `db.operation`.
- For errors: `exception.type`, `exception.message`.
### Propagation
- Every outbound request carries trace context headers.
- Every background job enqueues with trace context.
- A trace that ends at the HTTP edge and restarts at the worker is useless.
## Metrics
### RED for services
- **Rate** — requests per second, per endpoint.
- **Errors** — error rate, per endpoint.
- **Duration** — p50/p95/p99, per endpoint.
### USE for resources
- **Utilization** — % of capacity in use.
- **Saturation** — queue depth / wait time.
- **Errors** — failed operations.
### Cardinality discipline
- No unbounded tags (user_id, tenant_id, url with params). They explode your TSDB bill.
- Pre-aggregate cardinal dimensions; keep labels coarse.
- `http.route` (the template), not `http.url` (the instance).
## Correlation
- `request_id` in: logs, traces (as `request.id` attr), response headers, error payloads.
- `trace_id` in: logs (auto-injected by SDK), response headers for user bug reports.
- A user reporting "I got a 500" should give you enough to find the request — the `request_id` is visible and searchable.
## Per-language
### Node
- Pino or Winston with JSON transport.
- OpenTelemetry SDK (`@opentelemetry/sdk-node`).
- Prom-client for metrics.
### Python
- `structlog` with `ProcessorFormatter`.
- `opentelemetry-sdk` + `opentelemetry-instrumentation-*`.
- `prometheus_client`.
### Go
- `slog` (stdlib, Go 1.21+) with JSON handler.
- `go.opentelemetry.io/otel` + auto-instrumentation.
- `prometheus/client_golang`.
### Rust
- `tracing` + `tracing-subscriber` with JSON formatter.
- `opentelemetry-otlp`.
- `prometheus` crate.
## Anti-patterns
- "Log everything, we'll figure it out later" — the noise hides the signal you need.
- Traces without sampling — you'll drown; sample intelligently (head-based at edges, tail-based for errors).
- Metrics where logs would do — counting "user clicked button" 1M times/day is cheaper as a metric than a log.
- Logs where metrics would do — "latency: 247ms" as a log line is wasting money; it's a histogram.
- Alerts on everything — page only on symptoms that hurt users, not causes.
## Smells
- A single log line has more than ~10 fields — probably two events glued together.
- You use `grep` to find things — you should have a field + dashboard.
- Error messages are generic ("an error occurred") — they cost you the 2am minutes.
- `console.log` in prod code — belongs in a logger, tagged and leveled.
No comments yet. Be the first to comment!