Observability Basics
8 examples to get you started with Observability for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-nodePin Node 24.18.0 and load the OTel SDK before other imports in production entrypoints.
Basic Examples
1. Three Pillars in One Request
Logs, metrics, and traces describe the same incident from different angles.
// Log (Pino)
logger.info({ requestId, statusCode: 200, durationMs: 45 }, "request_complete");
// Metric (counter)
requestCounter.add(1, { method: "GET", route: "/users", status: "200" });
// Trace (span attribute)
span.setAttribute("http.status_code", 200);- Logs: discrete events with context - great for debugging one failure.
- Metrics: aggregates over time - great for SLOs and alerts.
- Traces: latency breakdown across services - great for slow requests.
Related: Metrics that Matter - RED method
2. Health vs Readiness Endpoints
Liveness and readiness serve different orchestrator probes.
app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/ready", async (_req, res) => {
const dbOk = await db.ping();
res.status(dbOk ? 200 : 503).json({ ready: dbOk });
});/health- process up (liveness). No dependency checks./ready- can serve traffic (readiness). Fails when DB down.- Do not log readiness failures at
erroron every kube probe - sample or use metrics.
Related: Resilience Basics - fail fast on boot
3. RED Metrics Sketch
Rate, Errors, Duration - minimum HTTP service metrics.
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("orders-api");
const requestDuration = meter.createHistogram("http.server.duration", { unit: "ms" });
const requestCount = meter.createCounter("http.server.requests");
function recordRequest(method: string, route: string, status: number, ms: number) {
requestCount.add(1, { method, route, status: String(status) });
requestDuration.record(ms, { method, route, status: String(status) });
}- Rate -
http.server.requestscounter. - Errors - same counter filtered by
status=5xx. - Duration - histogram for p50/p95/p99 in Prometheus or Datadog.
Related: SLOs & Error Budgets - SLO from histograms
4. Structured Log with trace_id
Correlate logs to traces in APM UIs.
import { trace } from "@opentelemetry/api";
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;
logger.info({ trace_id: traceId, event: "payment_captured" }, "payment ok");- Many APM tools link log lines to trace waterfall by
trace_id. - Use consistent field name (
trace_id) across services. - Pino
mixincan inject trace_id automatically.
Related: Request Correlation IDs - requestId vs trace_id
5. Counter for Business Events
Metrics are not only HTTP - track domain events.
const ordersCreated = meter.createCounter("orders.created");
ordersCreated.add(1, { plan: "pro", region: "eu" });- Business counters power product dashboards, not just ops.
- Low cardinality labels only - not
userIdon every metric. - Pair with logs for individual order debugging.
Related: Logging Basics - event field naming
6. Span for Manual Instrumentation
Auto-instrumentation misses your business logic boundaries.
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("billing");
async function capturePayment(orderId: string) {
return tracer.startActiveSpan("capturePayment", async (span) => {
span.setAttribute("order.id", orderId);
try {
return await stripeCapture(orderId);
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: 2 });
throw err;
} finally {
span.end();
}
});
}- Name spans after operations (
capturePayment), not function internals. recordExceptionattaches error to trace for APM error tracking.- End spans in
finally- avoid leaked spans on early return.
Related: OpenTelemetry Node SDK - SDK setup
Intermediate Examples
7. Minimal OTel SDK Bootstrap
Load instrumentation before Express/Fastify imports.
// src/instrumentation.ts - import first in main.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();instrumentation.tsmust load beforeimport expressin entry file.- OTLP HTTP exporter sends to collector sidecar (4318) or SaaS agent.
- Auto-instrumentation covers HTTP,
fetch, and common DB drivers.
Related: OpenTelemetry Node SDK - full configuration
8. Alert on Symptoms, Not Logs
Page on SLO burn, not every stack trace.
# Pseudo-alert rule
alert: HighErrorRate
expr: rate(http_server_requests{status=~"5.."}[5m]) / rate(http_server_requests[5m]) > 0.05
for: 10m- Symptom: elevated 5xx rate over 5 minutes.
- Not: single
ERRORlog line from one pod restart. - Tie alerts to SLOs & Error Budgets.
Related: Observability Best Practices - alerting rules
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.