Monitoring and Observability
Production systems require visibility into their behavior. Observability goes beyond simple monitoring by enabling you to ask arbitrary questions about system state using logs, metrics, and traces. This guide covers instrumentation, collection, visualization, alerting, and the operational patterns that prevent alert fatigue while keeping systems reliable.
The Three Pillars
| Pillar | What It Captures | Best For | Key Tools | |--------|-----------------|----------|-----------| | Logs | Discrete events with context | Debugging specific requests, audit trails | ELK, Loki, CloudWatch Logs | | Metrics | Numeric measurements over time | Trends, thresholds, capacity planning | Prometheus, Datadog, CloudWatch Metrics | | Traces | Request flow across services | Latency breakdown, dependency mapping | Jaeger, Tempo, X-Ray |
| Question | Signal | |----------|--------| | "Why did this request fail?" | Logs (event detail) + Traces (call chain) | | "Is error rate increasing?" | Metrics (counters over time) | | "Which service is slow?" | Traces (span timing) | | "What happened at 3:42 AM?" | Logs (timestamped events) | | "Are we within SLO budget?" | Metrics (error ratio, latency percentiles) | | "How do services depend on each other?" | Traces (service graph) |
OpenTelemetry SDK Setup
OpenTelemetry provides a vendor-neutral API for emitting all three signals. Instrument once, export anywhere.
Node.js Auto-Instrumentation
// tracing.ts -- Load BEFORE application code
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: process.env.APP_VERSION || '0.0.0',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
exportIntervalMillis: 15000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-http': {
ignoreIncomingPaths: ['/healthz', '/readyz', '/metrics'],
},
}),
],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown().finally(() => process.exit(0)));
Manual Span Creation
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service', '1.0.0');
async function processOrder(orderId: string): Promise<Order> {
return tracer.startActiveSpan('processOrder', async (span) => {
try {
span.setAttribute('order.id', orderId);
const order = await tracer.startActiveSpan('db.fetchOrder', async (dbSpan) => {
dbSpan.setAttribute('db.system', 'postgresql');
const result = await db.orders.findById(orderId);
dbSpan.end();
return result;
});
span.addEvent('order.validated', { 'order.total': order.total });
await chargePayment(order);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
Prometheus Metrics
Prometheus uses a pull model -- it scrapes HTTP endpoints at intervals.
| Type | Use Case | Example | |------|----------|---------| | Counter | Cumulative count (only goes up) | Total requests, errors, bytes sent | | Gauge | Current value (goes up and down) | Active connections, queue depth | | Histogram | Distribution of values in buckets | Request latency, response size | | Summary | Quantiles calculated client-side | Legacy use -- prefer histograms |
Express Application Metrics
import express from 'express';
import promClient from 'prom-client';
const app = express();
promClient.collectDefaultMetrics({ prefix: 'orderservice_' });
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
});
const httpRequestsTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
const labels = { method: req.method, route: req.route?.path || req.path, status_code: res.statusCode.toString() };
end(labels);
httpRequestsTotal.inc(labels);
});
next();
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
Grafana Dashboard
Define dashboards as JSON for version control. Key panels for any service overview:
{
"dashboard": {
"title": "Order Service Overview",
"refresh": "30s",
"panels": [
{
"title": "Request Rate",
"type": "timeseries",
"targets": [{
"expr": "sum(rate(http_requests_total{service=\"order-service\"}[5m])) by (status_code)",
"legendFormat": "{{status_code}}"
}],
"fieldConfig": { "defaults": { "unit": "reqps" } }
},
{
"title": "P99 Latency",
"type": "timeseries",
"targets": [{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service=\"order-service\"}[5m])) by (le, route))",
"legendFormat": "{{route}}"
}],
"fieldConfig": { "defaults": { "unit": "s" } }
},
{
"title": "Error Rate (SLO: 99.9%)",
"type": "stat",
"targets": [{
"expr": "1 - (sum(rate(http_requests_total{service=\"order-service\", status_code=~\"5..\"}[1h])) / sum(rate(http_requests_total{service=\"order-service\"}[1h])))",
"instant": true
}],
"fieldConfig": { "defaults": { "unit": "percentunit" } }
}
]
}
}
SLO-Based Alerting
Alert on Service Level Objectives, not raw thresholds. This ties alerting to user-visible impact.
| Concept | Definition | Example | |---------|-----------|---------| | SLI | Service Level Indicator -- a measured metric | Proportion of requests < 300ms | | SLO | Service Level Objective -- target for the SLI | 99.9% of requests < 300ms | | Error Budget | Allowed failures = 1 - SLO | 0.1% can be slow (43 min/month) | | Burn Rate | How fast you consume error budget | 2x = burning twice as fast as sustainable |
Multi-Window Burn Rate Alerts
# alerts/slo-alerts.yml
groups:
- name: slo-burn-rate
rules:
# Fast burn -- 2% of 30-day budget in 1 hour
- alert: HighErrorBudgetBurnRate_Fast
expr: |
(
sum(rate(http_requests_total{status_code=~"5..", service="order-service"}[1h]))
/ sum(rate(http_requests_total{service="order-service"}[1h]))
) > (14.4 * 0.001)
and
(
sum(rate(http_requests_total{status_code=~"5..", service="order-service"}[5m]))
/ sum(rate(http_requests_total{service="order-service"}[5m]))
) > (14.4 * 0.001)
for: 2m
labels:
severity: critical
annotations:
summary: "Burning error budget 14.4x f