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

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](https://belvador.dev/en/docs/otel/quickstart#2-set-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

```bash
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
```

## CommonJS (`require`)

Load the register file before your app:

```bash
export OTEL_SEMCONV_STABILITY_OPT_IN=http
export OTEL_NODE_RESOURCE_DETECTORS=env,host,os
node --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 `import`s is instrumented — the app runs, but sends no spans and its logs carry no trace id:

```bash
export OTEL_SEMCONV_STABILITY_OPT_IN=http
export OTEL_NODE_RESOURCE_DETECTORS=env,host,os
node --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:

```bash
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](https://belvador.dev/en/docs/sdk/tracing#with-opentelemetry). Do not let two tools create spans in one process ([one tracing owner](https://belvador.dev/en/docs/otel/overview#one-tracing-owner)); for `@sentry/node`, see [With Sentry SDKs](https://belvador.dev/en/docs/otel/sentry-coexistence#node-js-sentry-node).
