Skip to content

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.

Markdown
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#

Terminal
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node

CommonJS (require)#

Load the register file before your app:

Terminal
export OTEL_SEMCONV_STABILITY_OPT_IN=httpexport OTEL_NODE_RESOURCE_DETECTORS=env,host,osnode --require @opentelemetry/auto-instrumentations-node/register app.js

If 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:

Terminal
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.mjs

Your 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=http makes 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,os describes the machine without the process detector, 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:

Terminal
export OTEL_NODE_DISABLED_INSTRUMENTATIONS=fs,dns,net

Short-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