@pagopa/azure-tracing
v0.4.17
Published
A package that contains some utilities to enable Azure tracing on Node.js applications.
Readme
@pagopa/azure-tracing
This package provides utilities to seamlessly integrate Azure Monitor's Application Insights with OpenTelemetry for distributed tracing and telemetry in Node.js applications, especially in Azure Functions environments.
Installation
Install the package using:
pnpm add @pagopa/azure-tracingGetting Started
Instrumenting Azure Functions
To enable OpenTelemetry tracing and route telemetry to Azure Monitor in your Azure Functions (both v3 and v4 programming models), you primarily need to perform two steps: preload the instrumentation logic via NODE_OPTIONS and register lifecycle hooks.
Currently, ECMAScript Modules (ESM) support in OpenTelemetry is still experimental,
which makes direct instrumentation of Azure Functions a bit tricky.
So, if you have a Node.js project ESM based ("type": "module" in the package.json), to work around this, you have to preload the instrumentation logic at runtime using the NODE_OPTIONS environment variable.
[!NOTE] In case you have a CJS project (
"type": "commonjs"in thepackage.json), you could use the@pagopa/azure-tracingas well.
This package provides a wrapper that simplifies this setup.
Step 1: Enable Tracing via NODE_OPTIONS
Set the following environment variable to preload the instrumentation:
NODE_OPTIONS=--import @pagopa/azure-tracingThis will automatically enable OpenTelemetry tracing and route telemetry to Azure Monitor.
For more background on this workaround, see:
In order to enable tracing, you also need to set the following environment variables:
| Name | Required | Default | | ----------------------------------------- | ------------ | ----------- | | APPINSIGHTS_SAMPLING_PERCENTAGE | false | 5 | | APPLICATIONINSIGHTS_CONNECTION_STRING | true | - |
Step 2: Register Azure Function Lifecycle Hooks
Due to known limitations with the azure-functions-nodejs-opentelemetry library,
it's necessary to manually register lifecycle hooks to ensure proper dependency correlation in telemetry.
For more background on this workaround, see:
Add the following snippet to your main entry point (e.g., main.ts):
import { app } from "@azure/functions"; // Replace with your actual app import
import { registerAzureFunctionHooks } from "@pagopa/azure-tracing/azure-functions";
...
registerAzureFunctionHooks(app);
...Instrumenting Azure Functions v3 Handlers
For Azure Functions using the v3 programming model, if you need more granular control over OpenTelemetry context propagation within your function handlers, you can use the withOtelContextFunctionV3 helper function to wrap your handlers. This function is designed to work with the v3 Context object structure.
To wrap the execution of your Azure Function in the OpenTelemetry context, use the withOtelContextFunctionV3 helper as follows:
import { AzureFunction, Context as FunctionContext } from "@azure/functions"; // "@azure/functions": "^3"
import createAzureFunctionHandler from "@pagopa/express-azure-functions/dist/src/createAzureFunctionsHandler.js";
import { withOtelContextFunctionV3 } from "@pagopa/azure-tracing/azure-functions/v3"; // "@pagopa/azure-tracing": "^0.4"
export const expressToAzureFunction =
(app: Express): AzureFunction =>
(context: FunctionContext): void => {
app.set("context", context);
withOtelContextFunctionV3(context)(createAzureFunctionHandler(app)); // wrap the function execution in the OpenTelemetry context
};Enabling Azure Monitor Telemetry
[!NOTE] For Azure Functions, it is necessary to use the
NODE_OPTIONSenvironment variable and register lifecycle hooks as described in the "Instrumenting Azure Functions" section. Manual initialization withinitAzureMonitoris typically not required for Azure Functions (due to the issue previously explained), but it is useful for other Node.js applications (e.g., Azure App Services) where direct SDK initialization is preferred or necessary.
If you want to enable Azure Monitor telemetry in your application, and you don't have those issues previously described, you can do so in the following ways:
import { initAzureMonitor } from "@pagopa/azure-tracing/azure-monitor";
import { AzureMonitorOpenTelemetryOptions } from "@azure/monitor-opentelemetry";
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
...
const config: AzureMonitorOpenTelemetryOptions = {} // A valid AzureMonitorOpenTelemetryOptions object
const instrumentations = [new ExpressInstrumentation()] // A list of custom OpenTelemetry instrumentations
initAzureMonitor(instrumentations, config);
...Logging Custom Events
You can log custom events for additional observability using the emitCustomEvent function.
This utility accepts an event name and an optional payload, returning a logger function that also accepts a component or handler name.
import { emitCustomEvent } from "@pagopa/azure-tracing/logger";
...
emitCustomEvent("taskCreated", { id: task.id })("CreateTaskHandler");
...This is especially useful for tracing domain-specific actions (e.g., resource creation, user actions, error tracking).
Dependency Constraints
import-in-the-middle version must match @azure/monitor-opentelemetry
[!WARNING] Do not upgrade
import-in-the-middleindependently of@azure/monitor-opentelemetry. Doing so will silently break HTTP tracing (including CosmosDB calls) in ESM environments.
Why this constraint exists
The ESM instrumentation in this package works by registering a module loader hook via import-in-the-middle (iitm):
// packages/azure-tracing/src/azure/functions/index.mts
const { registerOptions } = createAddHookMessageChannel(); // uses iitm@X
register("import-in-the-middle/hook.mjs", ...); // registers iitm@X as the active loader@azure/monitor-opentelemetry internally creates an HttpInstrumentation instance that patches node:https (the transport layer used by CosmosDB, among others). This instrumentation registers its hooks using its own resolved version of iitm (transitively via @opentelemetry/instrumentation).
Each major version of import-in-the-middle maintains completely isolated internal state — hook registries are not shared across versions. If the loader is registered with iitm@X but HttpInstrumentation pushes its hooks into iitm@Y's registry, the loader never sees them. The result: node:https is never patched, and all HTTP dependency spans (including CosmosDB) are silently dropped.
The table below shows what was validated empirically against Application Insights:
| import-in-the-middle direct dep | @azure/monitor-opentelemetry | Loader iitm | HttpInstrumentation iitm |
| --------------------------------- | ------------------------------ | ----------- | ------------------------ |
| ^1.15.0 | 1.13.1 | 1.15.0 | 1.15.0 ✅ |
| ^3.0.0 | 1.13.1 | 3.0.0 | 1.15.0 ❌ |
| ^1.15.0 | 1.16.0 | 1.15.0 | 2.0.6 ❌ |
| ^2.0.0 | 1.16.0 | 2.0.6 | 2.0.6 ✅ |
How to determine the correct version
When upgrading @azure/monitor-opentelemetry, check which version of @opentelemetry/instrumentation it depends on, then look up the import-in-the-middle range that instrumentation version requires:
# Check what instrumentation version monitor-otel uses
npm view @azure/monitor-opentelemetry@<version> dependencies | grep '@opentelemetry/instrumentation'
# Check what iitm range that instrumentation version requires
npm view @opentelemetry/instrumentation@<version> dependencies | grep 'import-in-the-middle'Set import-in-the-middle in package.json to the same major range as the result.
