OpenTelemetry for Node.js (CommonJS and ES modules)
Zero-code instrumentation with auto-instrumentations-node, the ES modules loader hook, Pino/Winston logs linked to traces, and running it in Docker or on a PaaS.
On this page
Node.js needs no code changes: the OpenTelemetry auto-instrumentations patch http, fetch (undici), Express, Fastify, NestJS, database clients, Pino, Winston and more as they load, and the SDK reads the standard variables.
Tested on 2026-10-24 with node 22.23.4, @opentelemetry/sdk-node 0.300.0, @opentelemetry/auto-instrumentations-node 0.82.0, pino 10.4.0: traces passed, logs passed, logs carry the request's trace and span.
Install#
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-nodeCommonJS (require)#
Load the register file before your app:
export OTEL_SEMCONV_STABILITY_OPT_IN=httpexport OTEL_NODE_RESOURCE_DETECTORS=env,host,osnode --require @opentelemetry/auto-instrumentations-node/register app.jsIf you cannot change the start command (a PaaS, npm start, a process manager), put the flag in NODE_OPTIONS instead: NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register".
ES modules (import)#
ES modules need the loader hook as well, or nothing that your code imports is instrumented — the app runs, but sends no spans and its logs carry no trace id:
export OTEL_SEMCONV_STABILITY_OPT_IN=httpexport OTEL_NODE_RESOURCE_DETECTORS=env,host,osnode --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import @opentelemetry/auto-instrumentations-node/register app.mjsYour app is ESM when its package.json has "type": "module" or the entry file ends in .mjs. TypeScript compiled to CommonJS is CommonJS, whatever the source looks like.
Logs#
Pino and Bunyan are bridged automatically: every log call made while a request is being handled becomes an OpenTelemetry log record with that request's trace id and span id, and your normal output (stdout, files) is unchanged. Winston needs one more package, @opentelemetry/winston-transport, installed next to it. console.log is not captured — use your logger. (Belvador's run uses Pino.)
What the variables above set#
OTEL_SEMCONV_STABILITY_OPT_IN=httpmakes HTTP spans use the stable attribute names (url.path,http.request.method,http.response.status_code), which Belvador's filters use.OTEL_NODE_RESOURCE_DETECTORS=env,host,osdescribes the machine without theprocessdetector, which would copy the whole command line — and any secret passed in it — into every span.
Turning instrumentations off#
Each instrumentation can be disabled by name, for example noisy file-system spans:
export OTEL_NODE_DISABLED_INSTRUMENTATIONS=fs,dns,netShort-lived processes#
Scripts, CLIs and serverless handlers can exit before the exporter's batch is sent (about five seconds by default). The register file flushes on SIGTERM and on a normal exit; a process that calls process.exit() directly or is killed loses the last batch.
With the Belvador SDK#
For errors, keep your Belvador or Sentry SDK and pass the OpenTelemetry API to the Belvador SDK so errors carry the active span's ids — see Link requests. Do not let two tools create spans in one process (one tracing owner); for @sentry/node, see With Sentry SDKs.
Spotted something wrong or missing? Tell us: hello@belvador.dev