OpenTelemetry Node SDK
Instrument Node.js 24 services with the OpenTelemetry Node SDK - auto-instrument HTTP and database drivers, export OTLP traces, and add manual spans for business logic.
Recipe
Quick-reference recipe card - copy-paste ready.
// src/instrumentation.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: new Resource({ [ATTR_SERVICE_NAME]: "orders-api" }),
traceExporter: new OTLPTraceExporter(),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();// src/main.ts
import "./instrumentation.js";
import { startServer } from "./server.js";
startServer();When to reach for this:
- Need distributed traces across microservices.
- Debugging p99 latency with waterfall views in Honeycomb/Datadog/Jaeger.
- Standardizing instrumentation before picking an APM vendor.
- Replacing ad-hoc
console.timetiming in production.
Working Example
// instrumentation.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
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]: process.env.OTEL_SERVICE_NAME ?? "orders-api",
[ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? "0.0.0",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
}),
exportIntervalMillis: 60_000,
}),
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": { enabled: false },
}),
],
});
sdk.start();
process.on("SIGTERM", () => sdk.shutdown());// server.ts
import express from "express";
import { trace } from "@opentelemetry/api";
const app = express();
const tracer = trace.getTracer("orders");
app.get("/orders/:id", async (req, res) => {
await tracer.startActiveSpan("loadOrder", async (span) => {
span.setAttribute("order.id", req.params.id);
const order = await db.findOrder(req.params.id);
res.json(order);
});
});
app.listen(3000);What this demonstrates:
- Resource attributes identify service in trace backend.
- Separate OTLP endpoints for traces and metrics (common in collectors).
- FS instrumentation disabled to reduce noise.
- Graceful
sdk.shutdown()on SIGTERM flushes pending spans.
Deep Dive
How It Works
- SDK registers tracers and meters with the global
@opentelemetry/api. - Auto-instrumentation patches
http,https,fetch,pg,ioredis, etc. at require time. - Spans export via OTLP (OpenTelemetry Protocol) to collector or agent.
- W3C
traceparentheader propagates context to downstream HTTP calls.
Environment Variables
| Variable | Purpose |
|---|---|
OTEL_SERVICE_NAME | service.name resource attribute |
OTEL_EXPORTER_OTLP_ENDPOINT | Base OTLP URL |
OTEL_TRACES_SAMPLER | parentbased_traceidratio etc. |
OTEL_NODE_RESOURCE_DETECTORS | cloud, container, host |
Load Order
// CORRECT
import "./instrumentation";
import express from "express";
// WRONG - missed HTTP spans
import express from "express";
import "./instrumentation";- NestJS:
import "./instrumentation"first line inmain.ts. - tsx dev:
node --import ./src/instrumentation.ts src/main.ts.
Sampling
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1- 10% head sampling for high-RPS services - adjust cost vs coverage.
- Always sample errors if vendor supports tail-based sampling.
Gotchas
- Instrumentation after imports - missing HTTP spans. Fix: instrumentation first line.
- Double instrumentation - vendor agent + OTel SDK conflict. Fix: pick one path per signal.
- High-cardinality span attributes -
userIdon every span explodes cost. Fix: low-cardinality attrs only. - FS spans noise - thousands of
readFilespans. Fix: disable@opentelemetry/instrumentation-fs. - No shutdown on SIGTERM - lost spans on deploy. Fix:
sdk.shutdown()in termination handler. - Dev without collector - export errors flood logs. Fix:
OTEL_TRACES_EXPORTER=nonelocally or run Jaeger docker.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| OTel Node SDK | Vendor-neutral, multi-signal | Zero config single-vendor agent suffices |
| Datadog dd-trace | All-in on Datadog APM | Want OTLP portability |
| New Relic agent | NR-only stack | Multi-backend OTLP |
| Manual logs only | Tiny internal tool | Production SLA APIs |
FAQs
Which packages to install?
@opentelemetry/sdk-node, @opentelemetry/auto-instrumentations-node, OTLP exporters, @opentelemetry/api for manual spans.
Does it work with Fastify?
Yes. HTTP auto-instrumentation covers Fastify's underlying server. Load instrumentation before Fastify import.
Logs signal in OTel?
OTel logs SDK is evolving. Most teams use Pino stdout + trace_id injection for log-trace correlation.
Prisma instrumentation?
Community instrumentation or manual spans around queries. Enable @opentelemetry/instrumentation-pg for raw SQL drivers.
Local Jaeger?
Run collector with OTLP receiver on 4318. Point OTEL_EXPORTER_OTLP_ENDPOINT there.
NestJS?
opentelemetry-instrumentation-nestjs-core or manual spans in interceptors. Still load SDK first in main.ts.
Performance overhead?
Typically low single-digit % with sampling. Profile with and without on staging at peak RPS.
BullMQ workers?
Separate service.name (e.g. orders-worker). Start SDK in worker entry same as HTTP.
Related
- Observability Basics - three pillars intro
- Metrics that Matter - RED metrics
- APM Tools - Datadog, Honeycomb patterns
- Request Correlation IDs - traceparent header
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.