Skip to main content

Node.js OpenTelemetry Instrumentation - HTTP, DB & Queue Tracing

Introduction

Implement OpenTelemetry instrumentation for Node.js applications to enable comprehensive application performance monitoring (APM), distributed tracing, and observability. This guide covers auto-instrumentation setup for popular Node.js frameworks including Express, NestJS, Fastify, and Koa, with production-ready configurations for collecting traces and metrics.

Node.js applications benefit from automatic instrumentation of the event loop, async operations, popular frameworks (Express, NestJS, Fastify), database clients (MongoDB, PostgreSQL, MySQL), Redis, message queues (BullMQ, RabbitMQ), and HTTP clients. With OpenTelemetry, you can monitor async context propagation, identify performance bottlenecks, trace distributed transactions across microservices, and debug issues in production without significant code changes.

Whether you're implementing observability for the first time, migrating from commercial APM solutions like New Relic or Datadog, or troubleshooting async performance issues in production, this guide provides framework-agnostic patterns and best practices for Node.js OpenTelemetry instrumentation with Base14 Scout.

TL;DR

Install @opentelemetry/sdk-node and @opentelemetry/auto-instrumentations-node, create an instrumentation.js file that initializes NodeSDK, then start your app with node --require ./instrumentation.js server.js. Express, NestJS, databases, Redis, and message queues are all traced automatically with no per-route code changes.

Who This Guide Is For

This documentation is designed for:

  • Node.js developers: implementing observability and distributed tracing across Express, NestJS, or other frameworks
  • Backend engineers: deploying Node.js microservices with production monitoring requirements
  • DevOps teams: standardizing observability across multiple Node.js services and containers
  • Full-stack developers: debugging performance issues in async operations, database queries, and API calls
  • Platform engineers: migrating from DataDog, New Relic, or Dynatrace to OpenTelemetry-based solutions

Overview

This guide covers Node.js OpenTelemetry instrumentation across all major frameworks. For framework-specific details, see:

  • Express.js - Express 4.x and 5.x instrumentation with MongoDB, Redis, WebSockets
  • NestJS - Enterprise framework with DI, TypeORM, BullMQ, WebSocket gateway
  • Next.js - React framework with App Router, MongoDB, BullMQ workers
  • Fastify - High-performance framework instrumentation (coming soon)
  • Koa - Middleware-based framework patterns (coming soon)

What You'll Learn

  • Auto-instrument Node.js applications with zero code changes
  • Configure OpenTelemetry SDK for production deployments
  • Trace async operations and maintain context across event loop
  • Monitor database queries, HTTP requests, and external API calls
  • Implement custom instrumentation for business logic
  • Optimize performance and reduce telemetry overhead
  • Debug common issues and verify trace collection

Prerequisites

Before starting, ensure you have:

  • Node.js 18.x or later (20.x LTS recommended for production)
  • npm 9.x or later or yarn 1.22+ package manager
  • Scout Collector configured and accessible
  • Basic understanding of OpenTelemetry concepts (traces, spans, attributes)
  • Familiarity with async/await patterns in Node.js

Compatibility Matrix

ComponentMinimum VersionRecommended Version
Node.js18.0.020.x LTS or 22.x
@opentelemetry/sdk-node0.40.00.54.0+
@opentelemetry/auto-inst...0.40.00.54.0+
TypeScript (optional)4.5.05.3.0+

Supported Libraries

OpenTelemetry auto-instrumentation automatically traces these popular Node.js libraries:

Web Frameworks: Express, NestJS, Fastify, Koa, Hapi, Restify

Databases: MongoDB (Mongoose), PostgreSQL (pg, Sequelize), MySQL, Redis, Prisma, TypeORM

HTTP Clients: axios, node-fetch, got, request, http/https (built-in)

Message Queues: BullMQ, RabbitMQ (amqplib), Kafka

Other: Socket.IO, GraphQL, gRPC, Winston, Pino (logging)

Installation

Install the OpenTelemetry SDK and auto-instrumentation packages:

Install OpenTelemetry for Node.js
npm install --save \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions

For TypeScript projects, add type definitions:

npm install --save-dev @types/node

Configuration

Create a dedicated file to initialize OpenTelemetry before your application starts:

instrumentation.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
} = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: process.env.SERVICE_NAME || 'nodejs-service',
[SEMRESATTRS_SERVICE_VERSION]: process.env.SERVICE_VERSION || '1.0.0',
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]:
process.env.NODE_ENV || 'development',
}),
traceExporter: new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
'http://localhost:4318/v1/traces',
}),
instrumentations: [
getNodeAutoInstrumentations({
// Customize per-instrumentation config
'@opentelemetry/instrumentation-fs': {
enabled: false, // Disable filesystem tracing
},
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
// Skip health check endpoints
return req.url?.includes('/health');
},
},
}),
],
});

sdk.start();

// Graceful shutdown
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});

module.exports = sdk;

Update your application startup:

server.js
// IMPORTANT: Require instrumentation FIRST, before any other imports
require('./instrumentation');

const express = require('express');
const app = express();

// Your application code here
app.get('/', (req, res) => {
res.send('Hello World!');
});

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

Production Configuration

For production deployments, use BatchSpanProcessor with optimized settings:

instrumentation.production.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const {
BatchSpanProcessor,
} = require('@opentelemetry/sdk-trace-base');
const {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_INSTANCE_ID,
} = require('@opentelemetry/semantic-conventions');

const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: {
// Optional: Add authentication headers for Scout
// 'Authorization': `Bearer ${process.env.SCOUT_API_KEY}`
},
timeoutMillis: 15000,
});

const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: process.env.SERVICE_NAME,
[SEMRESATTRS_SERVICE_VERSION]: process.env.SERVICE_VERSION,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
[SEMRESATTRS_SERVICE_INSTANCE_ID]:
process.env.HOSTNAME || `${process.pid}`,
'service.namespace': process.env.SERVICE_NAMESPACE || 'default',
'container.id': process.env.CONTAINER_ID,
'k8s.pod.name': process.env.K8S_POD_NAME,
'k8s.namespace.name': process.env.K8S_NAMESPACE,
}),
spanProcessor: new BatchSpanProcessor(traceExporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false,
},
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
const ignorePaths = ['/health', '/metrics', '/ready'];
return ignorePaths.some((path) => req.url?.includes(path));
},
},
}),
],
});

sdk.start();

// Handle graceful shutdown
const shutdown = () => {
sdk
.shutdown()
.then(() => console.log('SDK shut down successfully'))
.catch((error) => console.error('Error shutting down SDK', error))
.finally(() => process.exit(0));
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

Docker Deployment

Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Set OpenTelemetry environment variables
ENV OTEL_SERVICE_NAME=nodejs-api
ENV OTEL_TRACES_EXPORTER=otlp
ENV NODE_OPTIONS="--require ./instrumentation.js"

EXPOSE 3000

CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'

services:
nodejs-api:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- OTEL_SERVICE_NAME=nodejs-api
- OTEL_SERVICE_VERSION=1.0.0
- OTEL_EXPORTER_OTLP_ENDPOINT=http://scout-collector:4318
- OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production

scout-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ['--config=/etc/otel-collector-config.yaml']
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- '4318:4318'

Custom Instrumentation

For business logic and application-specific operations, add manual spans:

services/order-service.js
const { trace } = require('@opentelemetry/api');

class OrderService {
async createOrder(userId, items) {
const tracer = trace.getTracer('order-service');

return tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttributes({
'user.id': userId,
'order.items.count': items.length,
'order.total': items.reduce((sum, item) => sum + item.price, 0),
});

// Validate items
await tracer.startActiveSpan('validateItems', async (validateSpan) => {
await this.validateItems(items);
validateSpan.end();
});

// Create order in database
const order = await tracer.startActiveSpan(
'saveOrderToDatabase',
async (dbSpan) => {
const result = await this.db.orders.create({
userId,
items,
createdAt: new Date(),
});
dbSpan.setAttribute('order.id', result.id);
dbSpan.end();
return result;
}
);

// Send confirmation email
await tracer.startActiveSpan('sendConfirmation', async (emailSpan) => {
await this.emailService.sendOrderConfirmation(userId, order.id);
emailSpan.end();
});

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

async validateItems(items) {
// Validation logic
if (items.length === 0) {
throw new Error('Order must contain at least one item');
}
}
}

module.exports = OrderService;

Running Your Application

Development Mode

# With console output for debugging
export OTEL_TRACES_EXPORTER=console
node --require ./instrumentation.js server.js

Production Mode

export NODE_ENV=production
export OTEL_SERVICE_NAME=nodejs-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://scout.yourdomain.com/v1/traces
node --require ./instrumentation.js server.js

Using PM2

ecosystem.config.js
module.exports = {
apps: [
{
name: 'nodejs-api',
script: 'server.js',
node_args: '--require ./instrumentation.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
OTEL_SERVICE_NAME: 'nodejs-api',
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://scout-collector:4318',
},
},
],
};

Troubleshooting

Issue: No Traces Appearing in Scout Dashboard

Solutions:

  1. Verify collector connectivity:
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');

const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
});

// Send test span
exporter
.export([{ name: 'test-span' }], (result) => {
console.log('Export result:', result);
})
.catch(console.error);
  1. Enable debug logging:
export OTEL_LOG_LEVEL=debug
node --require ./instrumentation.js server.js
  1. Check if instrumentation loads before application:
// WRONG - instrumentation loaded too late
const express = require('express');
require('./instrumentation');

// CORRECT - instrumentation loaded first
require('./instrumentation');
const express = require('express');

Issue: Missing Async Context in Traces

Solutions:

Ensure async operations use async/await or properly propagate context:

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

// WRONG - loses context
async function processData() {
setTimeout(() => {
// This runs in different async context
const span = trace.getActiveSpan(); // undefined!
}, 1000);
}

// CORRECT - preserve context
async function processData() {
const activeContext = context.active();
setTimeout(() => {
context.with(activeContext, () => {
const span = trace.getActiveSpan(); // Works!
});
}, 1000);
}

Issue: High Memory Usage

Solutions:

  1. Reduce batch size and queue limits:
spanProcessor: new BatchSpanProcessor(traceExporter, {
maxQueueSize: 1024, // Reduced from 2048
maxExportBatchSize: 256, // Reduced from 512
}),
  1. Disable unnecessary instrumentations:
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
}),
];

Issue: TypeScript Compilation Errors

Solutions:

Install type definitions:

npm install --save-dev \
@types/node \
@types/express

Performance Considerations

OpenTelemetry instrumentation adds minimal overhead to Node.js applications:

Expected Impact:

  • Latency: +0.5-2ms per request (automatic instrumentation)
  • CPU: +2-5% in production with BatchSpanProcessor
  • Memory: +10-30MB for trace buffers and SDK
  • Event Loop: Minimal impact with proper batching

Optimization Best Practices

1. Use BatchSpanProcessor in Production

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

spanProcessor: new BatchSpanProcessor(traceExporter, {
maxQueueSize: 2048,
scheduledDelayMillis: 5000, // Export every 5 seconds
});

2. Skip Health Check and Metrics Endpoints

instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
return ['/health', '/metrics', '/ready'].some((path) =>
req.url?.includes(path)
);
},
},
}),
];

3. Disable Filesystem and DNS Tracing

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

Security Considerations

Sensitive Data in Spans

Avoid capturing sensitive information in span attributes:

// BAD - Exposes sensitive data
span.setAttributes({
'user.password': userPassword,
'credit_card.number': ccNumber,
});

// GOOD - Use safe identifiers
span.setAttributes({
'user.id': userId,
'payment.method': 'credit_card',
'payment.last4': last4Digits,
});

HTTP Header Filtering

Configure header filtering for sensitive authentication tokens:

'@opentelemetry/instrumentation-http': {
headersToSpanAttributes: {
requestHeaders: ['content-type', 'user-agent'],
responseHeaders: ['content-type'],
},
},

What's Next?

Framework-Specific Guides

  • Express.js Instrumentation - Detailed Express 4.x/5.x setup with MongoDB, Redis, and WebSockets
  • NestJS Instrumentation - Enterprise DI framework with TypeORM and BullMQ (coming soon)
  • Fastify Instrumentation - High-performance framework patterns (coming soon)

Advanced Topics

Scout Platform Features

Deployment and Operations

FAQ

Does OpenTelemetry work with TypeScript?

Yes, OpenTelemetry fully supports TypeScript with official type definitions. Install @types/node and use .ts instrumentation files.

What's the performance impact on Node.js applications?

With BatchSpanProcessor, expect +0.5-2ms latency per request, +2-5% CPU, and +10-30MB memory. Impact is minimal for most production workloads.

Can I use OpenTelemetry with Express, NestJS, and Fastify?

Yes, auto-instrumentation supports all major Node.js frameworks including Express, NestJS, Fastify, Koa, and Hapi automatically.

How do I trace async operations and callbacks?

OpenTelemetry automatically propagates context through async/await. For callbacks, manually propagate context using context.with().

Does it work with Mongoose, TypeORM, and Prisma?

Yes, auto-instrumentation includes MongoDB (Mongoose), PostgreSQL (pg, Sequelize, TypeORM), MySQL, and Prisma ORM.

Can I trace BullMQ and RabbitMQ jobs?

Yes, auto-instrumentation includes BullMQ, RabbitMQ (amqplib), and other message queue libraries.

How do I handle multi-tenant applications?

Add tenant identifiers as span attributes: span.setAttribute('tenant.id', tenantId) and filter in Scout Dashboard.

What's the difference between traces and metrics?

Traces show request flow and timing (spans), while metrics aggregate performance data (counters, histograms). Both are supported by OpenTelemetry.

Complete Example

This guide provides framework-agnostic Node.js instrumentation patterns. For complete working examples with full application code, see the framework-specific guides:

Express.js

Quick Start
git clone https://github.com/base-14/examples.git
cd examples/nodejs/express-mongodb

npm install
docker-compose up -d

# Run with tracing
node --require ./instrumentation.js server.js

See Express.js Instrumentation for the complete guide with MongoDB, Redis, WebSockets, and BullMQ integration.

NestJS

Quick Start
git clone https://github.com/base-14/examples.git
cd examples/nodejs/nestjs-typeorm

npm install
docker-compose up -d

npm run start:prod

See NestJS Instrumentation for enterprise patterns with TypeORM, BullMQ, and WebSocket gateway tracing.

Complete Examples Repository

All Node.js examples with Docker Compose, Kubernetes manifests, and production configurations are available at:

https://github.com/base-14/examples/tree/main/nodejs

Once instrumented, you can monitor Node.js services with Scout APM — track event loop latency, async operation traces, and HTTP handler performance across your applications.

References

Was this page helpful?