Observability and Instrumentation
Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting.
Instruments code so production behavior is visible and diagnosable. Use when adding logging, metrics, tracing, or alerting.
Code you can't observe is code you can't operate. Instrumentation is written alongside the feature, not as a post-launch add-on.
Before adding any instrumentation, write down 2–4 questions an on-call engineer will ask:
FEATURE: checkout payment retry
QUESTIONS ON-CALL WILL ASK:
1. What fraction of payments succeed on first attempt vs after retry?
2. When a payment fails permanently, why?
3. Is the payment provider slower than usual?
→ Every signal below must help answer one of these.
| Signal | Answers | Cost profile | |---|---|---| | Structured log | "What happened in this specific case?" | Per-event | | Metric | "How often / how fast, in aggregate?" | Fixed per series | | Trace | "Where did time go across services?" | Per-request |
Metrics tell you that something is wrong, traces tell you where, logs tell you why.
Log events, not prose:
// BAD: string interpolation — unqueryable
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);
// GOOD: stable event name + structured fields
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'payment failed');
Log levels:
| Level | Meaning | On-call action |
|---|---|---|
| error | Invariant broken; someone may need to act | Investigate |
| warn | Degraded but handled | Watch for trends |
| info | Significant business event | None |
| debug | Diagnostic detail | Off in production |
Correlation IDs are mandatory:
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
For request-driven services, instrument RED on every endpoint: Rate, Errors, Duration.
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_class'],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
Cardinality is the failure mode. Never use user IDs, raw URLs, or error messages as labels.
Track averages never, percentiles always.
Use OpenTelemetry — it's the vendor-neutral standard:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Alert on symptoms users feel, not on causes:
SYMPTOM (page-worthy): CAUSE (dashboard, not a page):
error rate > 1% for 5 min CPU at 85%
p99 latency > 2s one pod restarted
queue age > 10 min disk at 70%
Rules for every alert:
After instrumenting a feature: