eutral instrumentation, Prometheus for metric storage and evaluation, and Alertmanager for intelligent routing.
Step 1: Instrument with OpenTelemetry
Use OpenTelemetry SDK for Node.js to capture traces, metrics, and structured logs. Auto-instrumentation covers HTTP frameworks, databases, and message queues. Custom metrics must align with RED (Rate, Errors, Duration) and USE (Utilization, Saturation, Errors) methodologies.
// otel-setup.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { SEMRESOURCESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
export const otelSDK = new NodeSDK({
resource: new Resource({
[SEMRESOURCESATTRS_SERVICE_NAME]: process.env.SERVICE_NAME || 'backend-api',
environment: process.env.NODE_ENV || 'production',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }),
exportIntervalMillis: 15000,
}),
instrumentations: [getNodeAutoInstrumentations()],
});
otelSDK.start();
process.on('SIGTERM', () => otelSDK.shutdown().catch(console.error));
Step 2: Define Business-Critical Metrics
Create custom metrics that map to user journeys. Avoid instrumenting internal implementation details unless they directly correlate with user impact.
// metrics.ts
import { MeterProvider, Meter } from '@opentelemetry/api';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
const meterProvider = new MeterProvider();
const meter: Meter = meterProvider.getMeter('backend-metrics');
export const checkoutCounter = meter.createCounter('app.checkout.attempts', {
description: 'Total checkout attempts',
});
export const checkoutErrorCounter = meter.createCounter('app.checkout.errors', {
description: 'Failed checkout attempts by reason',
});
export const requestLatency = meter.createHistogram('app.http.request.duration', {
description: 'HTTP request latency in milliseconds',
unit: 'ms',
});
export function recordCheckoutSuccess() {
checkoutCounter.add(1);
}
export function recordCheckoutFailure(reason: string) {
checkoutErrorCounter.add(1, { reason });
}
Expose metrics via /metrics endpoint and define burn-rate alerting rules. Burn-rate alerting evaluates how quickly an SLO error budget is consumed, preventing premature paging for transient spikes.
# prometheus/alerts.yml
groups:
- name: backend_slo_alerts
rules:
- alert: CheckoutErrorRateHigh
expr: |
rate(app_checkouts_errors_total{job="backend-api"}[5m])
/ rate(app_checkouts_total{job="backend-api"}[5m]) > 0.05
for: 2m
labels:
severity: critical
team: payments
annotations:
summary: "Checkout error rate exceeds 5% for 2 minutes"
runbook: "https://runbooks.internal/payments/checkout-errors"
- alert: ErrorBudgetBurnRateFast
expr: |
(
sum(rate(app_checkouts_errors_total{job="backend-api"}[1h]))
/ sum(rate(app_checkouts_total{job="backend-api"}[1h]))
) > 14.4 * (1 - 0.99)
for: 5m
labels:
severity: page
team: payments
annotations:
summary: "Error budget burning 14.4x faster than acceptable"
Step 4: Route with Alertmanager
Group alerts by service, suppress duplicates, and enforce escalation policies. Use inhibition rules to prevent cascading pages when a root cause is already identified.
# alertmanager/config.yml
route:
group_by: ['alertname', 'team']
group_wait: 10s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-pagerduty'
routes:
- match:
severity: page
receiver: 'pagerduty-critical'
continue: false
- match:
severity: critical
receiver: 'slack-engineering'
receivers:
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: ${PD_SERVICE_KEY}
severity: critical
description: '{{ .CommonAnnotations.summary }}'
Architecture Decisions & Rationale
- Pull-based metric collection: Prometheus scrapes
/metrics endpoints, avoiding push API rate limits and ensuring metric freshness aligns with scrape intervals.
- Burn-rate alerting over static thresholds: Static thresholds fire on every breach regardless of trend. Burn-rate math (
error_budget / time_window) fires only when degradation is sustained and economically meaningful.
- Vendor-neutral instrumentation: OpenTelemetry decouples instrumentation from backend, enabling metric exporter swaps without code changes.
- Correlation via trace context: Inject
trace_id into structured logs to bridge metric alerts with distributed traces during incident response.
Pitfall Guide
1. Alerting on Infrastructure Symptoms Instead of User Impact
Monitoring CPU saturation or GC pauses without mapping them to request latency or error rates creates noise. High CPU may be normal during batch processing. Best practice: always trace infrastructure metrics to RED/USE application metrics before alerting.
2. Ignoring Error Budgets and Burn-Rate Mathematics
Firing alerts on every SLO violation exhausts on-call capacity and normalizes paging. Best practice: implement multi-window burn-rate alerting (e.g., 1h/30m windows) to distinguish transient spikes from sustained degradation.
3. Hardcoding Thresholds Without Traffic Context
Static thresholds fail during traffic seasonality, deployments, or regional outages. Best practice: use dynamic baselines or percentile-based thresholds (histogram_quantile(0.99, ...)) and validate thresholds against historical traffic patterns.
4. Alert Routing Sprawl and Duplicate Pages
Routing the same alert to Slack, email, PagerDuty, and SMS creates coordination paralysis. Best practice: enforce a single source of truth for alert routing, use grouping keys (alertname, service, environment), and suppress non-page alerts during maintenance windows.
5. Siloed Logs, Metrics, and Traces
Engineers waste 40% of incident time switching between dashboards. Best practice: embed trace_id and span_id in structured logs, configure log aggregators to index correlation IDs, and use OpenTelemetry context propagation across service boundaries.
6. Not Testing Alert Rules in Staging
Alert rules that work in production but fail in staging create false confidence. Best practice: run synthetic load tests against staging, validate alert evaluation using promtool check rules, and simulate PagerDuty/Slack routing with mock webhooks.
7. Measuring Everything, Acting on Nothing
Instrumenting 500+ metrics without alerting policies or runbooks inflates storage costs and obscures signal. Best practice: enforce a metric lifecycle policy. Archive or drop metrics without associated alerting rules, runbooks, or dashboard usage.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / MVP (<10 services) | Application-centric metrics + static thresholds | Low operational overhead, fast deployment, sufficient for early traffic patterns | Low ($50-150/mo tooling) |
| Growth / Mid-market (10-50 services) | SLO-driven burn-rate alerting + Alertmanager routing | Reduces alert fatigue, aligns engineering with business continuity, scales with team growth | Medium ($200-500/mo + on-call training) |
| Enterprise / Compliance (>50 services) | OpenTelemetry + centralized metric store + policy-driven alert governance | Vendor lock-in avoidance, audit-ready SLO tracking, cross-team standardization | High ($1k-3k/mo + platform engineering headcount) |
Configuration Template
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'backend-api'
metrics_path: '/metrics'
static_configs:
- targets: ['backend-api:3000']
rule_files:
- 'alerts.yml'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
// otel-collector-config.yaml (OpenTelemetry Collector)
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
timeout: 5s
send_batch_max_size: 1000
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
namespace: "otel"
logging:
loglevel: warn
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
Quick Start Guide
- Install OpenTelemetry SDK: Run
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-metrics-otlp-proto in your backend project.
- Initialize SDK at Entry Point: Import and call
otelSDK.start() before framework initialization (Express/Fastify/Hapi). Ensure process.on('SIGTERM') shutdown hook is registered.
- Expose Metrics Endpoint: Add
app.get('/metrics', async (req, res) => { res.set('Content-Type', 'text/plain'); res.send(await register.metrics()); }) using prom-client or OTLP HTTP exporter.
- Deploy Prometheus + Alertmanager: Use Docker Compose with official images, mount
prometheus.yml and alerts.yml, and verify http://localhost:9090/targets shows your backend as UP.
- Validate with Synthetic Request: Send 50 requests to a monitored endpoint, check Prometheus UI for
app_http_request_duration histogram, and confirm alert rules evaluate without syntax errors using promtool check rules alerts.yml.