Node.js uses a different module loader for ECMAScript Modules (ESM) vs. CommonJS (CJS). To verify whether your application is ESM or CJS, refer to Node.js docs for Determining Module System.
An .mjs
extension or type:module
in the built app's package.json
indicates the app is ESM.
Much of OpenTelemetry JS documentation is written assuming the compiled application is run as CJS. ESM support is ongoing; a few adjustments are needed for configuration and startup commands.
For more explanation about CJS and ESM, see the Node.js docs.
Many TypeScript projects today are written using ESM syntax, regardless of how they are compiled.
In the tsconfig.json
, there is an option to compile to ESM or CJS.
If the compiled code is ESM, those import statements will remain the same (e.g. import { foo } from 'bar';
).
If the compiled code is CJS, those import statements will become require()
statements (e.g. const { foo } = require('bar');
)
Instrumentation setup and configuration must be run before your application code.
If the SDK is initialized in a separate file (recommended), ensure it is imported first in application startup, or use the --require
or --import
flag during startup to preload the module.
For CJS, the NODE_OPTIONS
for the startup command should include --require ./telemetry.js
.
For ESM, minimum Node.js version of 18.19.0
is required.
The NODE_OPTIONS
for the startup command should include --import ./telemetry.js
.
If your application is written in JavaScript as ESM, or compiled to ESM from TypeScript, then a loader hook is required to properly patch instrumentation.
The custom hook for ESM instrumentation is --experimental-loader=@opentelemetry/instrumentation/hook.mjs
.
This flag must be passed to the node
binary, which is often done as a startup command and/or in the NODE_OPTIONS
environment variable.
Though the OpenTelemetry loader currently relies on import-in-the-middle
, direct usage of import-in-the-middle/hook.mjs
may cease to work in the future.
The only currently supported loader hook is @opentelemetry/instrumentation/hook.mjs
.
Note: Eventually the recommendation for how to setup OpenTelemetry for usage with ESM will change to no longer require --experimental-loader=@opentelemetry/instrumentation/hook.mjs
.
Instead the bootstrap code (in ./telemetry.js
) will use Node.js's newer module.register(...)
.
Refer to this issue for details.
Because of ongoing issues with loaders running TypeScript code as ESM in development environments, results may vary.
To use ts-node
to run the uncompiled TypeScript code, the module must be CJS.
To use tsx
to run the uncompiled TypeScript code as ESM, the --import
flag must be used.
The auto-instrumentations-node
package contains a register
entry-point that can be used with --require
or --import
to setup and start the SDK easily, without application code changes.
For ESM, the package also requires the usage of the loader hook.
Startup command for CJS:
node --require @opentelemetry/auto-instrumentations-node/register app.js
Startup command for ESM:
node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import @opentelemetry/auto-instrumentations-node/register app.js
/*telemetry.cjs*/
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Startup command:
node --require ./telemetry.cjs app.js
/*telemetry.ts | telemetry.mjs*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Startup command for compiled CJS:
node --require ./telemetry.js app.js
Startup command for compiled ESM:
node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import ./telemetry.js app.js
The entire startup command should include the following NODE_OPTIONS
:
Node.js Version | NODE_OPTIONS |
---|---|
16.x | --require ./telemetry.cjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs |
>=18.1.0 <18.19.0 | --require ./telemetry.cjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs |
^18.19.0 | --import ./telemetry.mjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs |
20.x | --import ./telemetry.mjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs |
22.x | --import ./telemetry.mjs --experimental-loader=@opentelemetry/instrumentation/hook.mjs |