Skip to content

Node.js Instrumentation

Subscription required

This section describes functionality which requires an active IAPM subscription. Start your subscription by choosing the plan right for you.

Instrument your Node.js applications with OpenTelemetry to send traces, metrics, and logs to IAPM. This guide covers Express.js, NestJS, auto-instrumentation, and manual SDK usage.

Back to Instrument Overview

Install Packages

Install the core OpenTelemetry packages:

npm install @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/exporter-metrics-otlp-grpc \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions
yarn add @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/exporter-metrics-otlp-grpc \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

Auto-Instrumentation Setup

Create a tracing.js (or tracing.ts) file that initializes OpenTelemetry before your application code loads:

// tracing.js - must be required/imported BEFORE your app
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'my-node-app',
    [ATTR_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'https://otlp.iapm.app',
    headers: { 'API-Key': 'YOUR-API-KEY' },
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: 'https://otlp.iapm.app',
      headers: { 'API-Key': 'YOUR-API-KEY' },
    }),
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

// Graceful shutdown
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry shut down'))
    .catch((err) => console.error('Error shutting down OpenTelemetry', err))
    .finally(() => process.exit(0));
});

Running with the Tracing File

Load the tracing file before your application:

node -r ./tracing.js app.js
ts-node -r ./tracing.ts app.ts
NODE_OPTIONS="--require ./tracing.js" node app.js

Express.js Setup

With auto-instrumentation configured via tracing.js, Express routes are traced automatically:

// app.js
const express = require('express');
const app = express();

app.get('/api/orders', async (req, res) => {
  const orders = await db.query('SELECT * FROM orders');
  res.json(orders);
});

app.get('/api/orders/:id', async (req, res) => {
  const order = await db.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
  res.json(order);
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Run with:

node -r ./tracing.js app.js

Each HTTP request automatically generates a trace span with route, status code, and duration.

NestJS Setup

For NestJS, load the tracing module before the application bootstraps:

// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'my-nestjs-app',
  }),
  traceExporter: new OTLPTraceExporter({
    url: 'https://otlp.iapm.app',
    headers: { 'API-Key': 'YOUR-API-KEY' },
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

Update your main.ts or start command:

node -r ./tracing.js dist/main.js

Or in package.json:

{
  "scripts": {
    "start": "node -r ./tracing.js dist/main.js",
    "start:dev": "NODE_OPTIONS='--require ./tracing.js' nest start --watch"
  }
}

Manual Instrumentation

Create custom spans for your business logic:

const { trace, SpanStatusCode } = require('@opentelemetry/api');

const tracer = trace.getTracer('myapp.orders');

async function processOrder(orderId) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    span.setAttribute('order.id', orderId);

    try {
      const order = await getOrder(orderId);
      span.setAttribute('order.total', order.total);

      await chargePayment(order);
      span.addEvent('payment.completed');

      await fulfillOrder(order);

      span.setStatus({ code: SpanStatusCode.OK });
      return order;
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

Custom Metrics

const { metrics } = require('@opentelemetry/api');

const meter = metrics.getMeter('myapp.orders');

const ordersCounter = meter.createCounter('orders.processed', {
  unit: 'orders',
  description: 'Number of orders processed',
});

const orderDuration = meter.createHistogram('orders.processing.duration', {
  unit: 'ms',
  description: 'Order processing time',
});

async function processOrder(order) {
  const start = Date.now();

  await doProcessing(order);

  const duration = Date.now() - start;
  ordersCounter.add(1, { 'order.type': order.type });
  orderDuration.record(duration);
}

Environment Variable Configuration

The OpenTelemetry Node.js SDK reads these variables automatically:

Variable Value Description
OTEL_EXPORTER_OTLP_ENDPOINT https://otlp.iapm.app OTLP collector endpoint
OTEL_EXPORTER_OTLP_HEADERS API-Key=YOUR-API-KEY Authentication header
OTEL_SERVICE_NAME your-service-name Service name shown in IAPM
OTEL_EXPORTER_OTLP_PROTOCOL grpc Protocol (grpc or http/protobuf)
OTEL_TRACES_EXPORTER otlp Trace exporter type
OTEL_METRICS_EXPORTER otlp Metrics exporter type
OTEL_LOG_LEVEL info SDK diagnostic log level
NODE_OPTIONS --require ./tracing.js Load tracing before app

When using environment variables, the SDK auto-configures the exporters:

const sdk = new NodeSDK({
  instrumentations: [getNodeAutoInstrumentations()],
  // endpoint and headers come from environment variables
});

Verify It's Working

  1. Run your instrumented application with node -r ./tracing.js app.js
  2. Send a few HTTP requests to generate traces
  3. Open portal.iapm.app and select your Grid
  4. Click Enter - you should see your service and traces within a few minutes

Quick verification with console exporter

Add the console exporter to see spans in your terminal during development:

npm install @opentelemetry/sdk-trace-base
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base');

const sdk = new NodeSDK({
  traceExporter: new ConsoleSpanExporter(),
  // ...
});

Troubleshooting

No data appearing in IAPM

  • Verify that tracing.js is loaded before your application code. Use node -r ./tracing.js or NODE_OPTIONS="--require ./tracing.js".
  • Verify the API key by copying a fresh one from portal.iapm.app under Administration > Grids > Instrument.
  • Check network connectivity: curl -v https://otlp.iapm.app.

gRPC connection errors

  • Ensure @grpc/grpc-js is installed: npm install @grpc/grpc-js.
  • If behind a corporate proxy, you may need to use the HTTP/protobuf exporter instead:

    npm install @opentelemetry/exporter-trace-otlp-proto
    
    const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto');
    

ES Modules (import/export) compatibility

  • For ESM applications, use --import instead of --require:

    node --import ./tracing.mjs app.mjs
    
  • Ensure your tracing.mjs uses import syntax.

Memory leaks with auto-instrumentation

  • Disable instrumentations you do not need:

    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': { enabled: false },
      '@opentelemetry/instrumentation-dns': { enabled: false },
    })
    

Further Reading