Alerts That Mean Something
Most monitoring setups are reactive: CPU alert fires, you look at dashboards, you guess what broke. Good production monitoring flips this — it tells you what users are experiencing before they file tickets, alerts on symptoms not causes, and gives you the context to diagnose fast. The Four Golden Signals and SLOs are the framework.
- You get paged at 2am for CPU at 90% while the service is actually fine
- Your error rate is 5% but you don't know if that's normal or a crisis
- Incident investigation starts with “check the logs” with no obvious starting point
- You have 200 Datadog monitors but none of them correlate to user impact
SLO-based alerting cuts alert fatigue by 70%+ and ensures the pages you get are ones that actually require human response.
The Four Golden Signals
Google SRE defined four signals to watch for any service. They're sufficient for detecting most production problems:
flowchart TD
signals["Four Golden Signals"] --> latency
signals --> traffic
signals --> errors
signals --> saturation
latency["Latency
How long requests take
(p50, p95, p99 — not average)"]
traffic["Traffic
How much demand
(requests/sec, events/sec)"]
errors["Errors
Failed requests rate
(5xx, timeouts, business errors)"]
saturation["Saturation
How full the service is
(CPU, memory, connection pool, queue depth)"]
style signals fill:#0078D4,color:#fff,stroke:#005a9e
SLI / SLO / SLA
// SLI — Service Level Indicator
// A quantitative measure of service behaviour
// Example: proportion of requests served in < 200ms
const sli = {
name: 'API latency',
query: 'sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m])) / sum(rate(http_request_duration_seconds_count[5m]))',
// Value: 0.0 to 1.0
};
// SLO — Service Level Objective
// The target value for an SLI
// Example: 99.5% of requests in < 200ms over a 30-day rolling window
const slo = {
sli: 'API latency',
target: 0.995, // 99.5%
window: '30d',
};
// SLA — Service Level Agreement
// The contractual commitment to customers (usually weaker than SLO)
// Example: 99.9% uptime (weaker than your 99.5% latency SLO)
// Error budget = time/requests you can "spend" on incidents
// Error budget = (1 - SLO target) * window
// = (1 - 0.995) * 30 days
// = 0.005 * 43,200 minutes
// = 216 minutes per month of allowed degraded latencyError Budget Alerts
Alert on error budget burn rate, not on individual metric thresholds. A 5xx spike at 3am that uses 2% of your monthly error budget is not worth waking someone up. A sustained 5x burn rate that will exhaust the budget in 6 hours is.
# Prometheus alerting rules — SLO burn rate approach
groups:
- name: api_slo_burn_rate
rules:
# Fast burn: high severity — page immediately
# 14.4x burn rate = budget exhausted in 2 hours
- alert: APILatencySLOHighBurn
expr: |
sum(rate(http_request_duration_seconds_bucket{le="0.2"}[1h]))
/ sum(rate(http_request_duration_seconds_count[1h])) < 0.931
and
sum(rate(http_request_duration_seconds_bucket{le="0.2"}[5m]))
/ sum(rate(http_request_duration_seconds_count[5m])) < 0.931
labels:
severity: critical
annotations:
summary: "API latency SLO burning fast — page on-call"
description: "14.4x burn rate. Budget exhausted in ~2h if sustained."
# Slow burn: ticket only — budget exhausted in 3 days
- alert: APILatencySLOSlowBurn
expr: |
sum(rate(http_request_duration_seconds_bucket{le="0.2"}[6h]))
/ sum(rate(http_request_duration_seconds_count[6h])) < 0.985
for: 60m
labels:
severity: warning
annotations:
summary: "API latency SLO degraded — create ticket"
description: "Budget burning at elevated rate. No immediate action needed."
# Don't do this:
# - alert: HighCPU
# expr: cpu_usage > 0.9
# This is a cause, not a symptom. CPU at 90% may be fine.Structured Logging
import pino from 'pino';
// ✅ Structured — queryable, filterable, correlatable
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: {
service: 'api-server',
version: process.env.APP_VERSION,
env: process.env.NODE_ENV,
},
});
// Log with context that enables downstream queries
export function logRequest(req: Request, res: Response, durationMs: number) {
logger.info({
msg: 'HTTP request',
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs,
requestId: req.headers['x-request-id'],
userId: req.user?.id, // correlate errors to users
traceId: req.traceId, // correlate to distributed traces
});
}
// Error logging with full context
export function logError(error: Error, context: Record<string, unknown>) {
logger.error({
msg: 'Unhandled error',
err: {
message: error.message,
stack: error.stack,
name: error.name,
},
...context,
});
}
// ❌ Unstructured — impossible to query at scale
// console.log('User ' + userId + ' failed to checkout: ' + error.message);Distributed Tracing
// OpenTelemetry — vendor-neutral tracing
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
// src/instrumentation.ts — loaded before everything else
const sdk = new NodeSDK({
serviceName: 'api-server',
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [
new HttpInstrumentation(), // auto-instrument all HTTP calls
new ExpressInstrumentation(), // auto-instrument Express routes
new PgInstrumentation(), // auto-instrument PostgreSQL queries
],
});
sdk.start();
// Manual span for custom operations
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('api-server');
async function processPayment(orderId: string) {
return tracer.startActiveSpan('process-payment', async (span) => {
try {
span.setAttribute('order.id', orderId);
const result = await paymentProvider.charge(orderId);
span.setAttribute('payment.provider', 'stripe');
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}Pitfalls
Alerting on averages
Average latency hides tail latency problems. If 99% of your requests complete in 50ms but 1% take 10 seconds, your average might look fine at 150ms. Always use percentiles (p95, p99) for latency SLOs. Your slowest users are the ones who churn.
Too many dashboards, not enough context
A 50-panel Grafana dashboard that shows everything doesn't help you during an incident. Create a single “service health” dashboard that shows the four golden signals and nothing else. Link from alert to dashboard to runbook — make the path automatic.
No runbooks
Every alert should have a runbook: what to check first, what actions to take, when to escalate. A runbook doesn't need to be long — three bullet points written by whoever created the alert is 10x better than nothing. The goal is any on-call engineer can handle it, not just the one who wrote the code.

