Add structured JSON logs with redaction and a labelled request counter to a request handler.
Why it matters
Once software is running on servers you do not sit in front of, the only way to know what it is doing is the signals it sends out. Observability is how well you can answer questions about a running system from those signals: is it healthy, is it slow, for whom, and why? Teams with good observability find problems before users report them and fix them in minutes. Teams without it guess. The three standard signals are logs, metrics and traces.
The three signals
| Signal | What it is | Good for | Example |
|---|---|---|---|
| Logs | Timestamped records of individual events | Details of one request or error | "checkout failed for order 991: card declined" |
| Metrics | Numbers aggregated over time | Trends, dashboards, alerts | requests per second, error rate, p95 latency |
| Traces | The path of one request through several services, with timings | Finding which step is slow | API 40 ms -> auth 5 ms -> database 310 ms |
They work together: a metric alert tells you the error rate jumped, a trace shows which service is failing, and the logs for those requests tell you why.
Structured logging
Compare two log lines:
User 118 checkout failed after 412ms: card declined
{"level":"error","time":"2026-10-23T09:14:02.120Z","msg":"checkout failed","userId":118,"durationMs":412,"reason":"card_declined","requestId":"r-7f3a"}
The first is easy to read but hard to search: to find all card declines over 400 ms you would need a fragile regular expression. The second is structured: one JSON object per line with consistent field names. Log tools can filter (reason = card_declined AND durationMs > 400), group and count it directly.
A minimal structured logger is only a few lines. Inject the output and the clock so it can be tested:
function createLogger(write, clock, base = {}) {
function log(level, msg, fields = {}) {
const entry = { level, time: clock.now().toISOString(), msg, ...base, ...fields };
write(JSON.stringify(entry));
}
return {
info: (msg, fields) => log("info", msg, fields),
error: (msg, fields) => log("error", msg, fields),
};
}
const logger = createLogger(console.log, { now: () => new Date() }, { service: "orders" });
logger.info("order created", { orderId: 991, totalCents: 4599 });
Libraries such as pino or winston do the same with more features (log levels, fast serialisation, redaction). Good habits whichever tool you use:
- Levels:
debugfor development detail,infofor normal events,warnfor something odd but handled,errorfor failures that need attention. - Context fields: request id, user id, route, duration. A request id generated at the edge and passed through every service lets you follow one request everywhere.
- Never log secrets or sensitive data: passwords, tokens, full card numbers, and usually raw email addresses. Redact them in the logger so a careless call cannot leak them.
Metrics
Metrics answer "how much, how often, how fast" cheaply, because they store numbers rather than every event. The common types:
- Counter: only goes up.
http_requests_total,orders_created_total. - Gauge: goes up and down. Queue length, memory in use.
- Histogram: counts values in buckets, so you can compute percentiles. Request duration.
Metrics carry labels (also called tags or dimensions), such as route and status:
http_requests_total{route="/checkout",status="500"} 37
Keep label values to a small, fixed set. A label like userId creates one time series per user (a "high cardinality" problem) and can overload a metrics system.
Two well-known starting sets: the RED method for services (Rate, Errors, Duration) and the four golden signals from Google's SRE book (latency, traffic, errors, saturation). Alert on symptoms users feel, such as error rate and latency, rather than on every internal number.
Traces
A trace follows one request across services. Each step is a span with a start time, duration and attributes; spans nest to show which call waited on which. OpenTelemetry is the open, vendor-neutral standard for producing logs, metrics and traces, and most observability tools can receive its data. You rarely write tracing code by hand: OpenTelemetry's automatic instrumentation creates spans for HTTP calls and database queries.
Common mistakes
- Logging unstructured sentences that nobody can search.
- Logging everything at
infountil the useful lines drown, or logging nothing until there is an outage. - Putting passwords, tokens or personal data in logs.
- Using unbounded values (user ids, URLs with ids) as metric labels.
- Alerts that fire constantly, so people learn to ignore them.
Recap
- Logs for details, metrics for trends and alerts, traces for where time goes.
- Structured JSON logs with levels, a request id and redaction of secrets.
- Counters, gauges and histograms with a small set of labels; start with RED or the golden signals.
Try it
In the Do tab, build a small structured logger with redaction and a request counter, then use both in a request handler.