Skip to main content

NestJS

Introduction​

Implement OpenTelemetry instrumentation for NestJS applications to enable comprehensive application performance monitoring (APM), distributed tracing, and observability across your enterprise Node.js applications. This guide shows you how to auto-instrument NestJS controllers, services, guards, interceptors, TypeORM queries, BullMQ background jobs, and WebSocket gateways using the OpenTelemetry Node.js SDK.

NestJS applications benefit from automatic instrumentation of the dependency injection container, decorators, HTTP endpoints, TypeORM database queries, Redis operations, BullMQ job processing, WebSocket connections, GraphQL resolvers, and microservice communication. With OpenTelemetry, you can trace requests through the entire dependency injection hierarchy, monitor async context propagation, identify N+1 query problems, debug background job failures, and track distributed transactions across microservices without significant code changes.

Whether you're implementing observability for the first time, migrating from New Relic or Datadog, troubleshooting performance issues in production, or building enterprise-grade monitoring for microservices, this guide provides production-ready configurations and best practices for NestJS OpenTelemetry instrumentation with Base14 Scout.

Who This Guide Is For​

This documentation is designed for:

  • NestJS developers: implementing observability and distributed tracing for enterprise applications with dependency injection
  • Backend engineers: deploying NestJS microservices with comprehensive production monitoring requirements
  • DevOps teams: standardizing observability across multiple NestJS services in Kubernetes environments
  • Enterprise architects: building observable systems with GraphQL, WebSockets, message queues, and microservices
  • Full-stack developers: debugging TypeORM queries, BullMQ jobs, and async operations in production NestJS apps

Prerequisites​

Before starting, ensure you have:

  • Node.js 18.x or later (20.x LTS recommended for production)
  • NestJS 10.x or later installed (@nestjs/core, @nestjs/common)
  • TypeScript 4.9+ (5.x recommended)
  • Scout Collector configured and accessible
  • Basic understanding of OpenTelemetry concepts (traces, spans, attributes)
  • Familiarity with NestJS dependency injection and decorators

Compatibility Matrix​

ComponentMinimum VersionRecommended Version
Node.js18.0.020.x LTS
NestJS9.0.010.3.0+
@opentelemetry/sdk-node0.40.00.54.0+
@opentelemetry/auto-inst...0.40.00.54.0+
TypeORM (if used)0.3.00.3.20+
BullMQ (if used)4.0.05.x
@nestjs/websockets (optional)10.0.010.3.0+
TypeScript4.9.05.3.0+

Installation​

Install the OpenTelemetry SDK and auto-instrumentation packages:

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

Install NestJS-specific packages if not already installed:

npm install --save \
@nestjs/core \
@nestjs/common \
@nestjs/platform-express

Configuration​

Create a NestJS module for OpenTelemetry initialization:

src/tracing/tracing.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
} from '@opentelemetry/semantic-conventions';

@Module({})
export class TracingModule implements OnModuleInit {
private sdk: NodeSDK;

onModuleInit() {
this.sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]:
process.env.OTEL_SERVICE_NAME || 'nestjs-api',
[SEMRESATTRS_SERVICE_VERSION]:
process.env.npm_package_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({
'@opentelemetry/instrumentation-fs': {
enabled: false,
},
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
const ignorePaths = ['/health', '/metrics'];
return ignorePaths.some((path) => req.url?.includes(path));
},
},
}),
],
});

this.sdk.start();
}

async onModuleDestroy() {
await this.sdk.shutdown();
}
}

Import the module in your root AppModule:

src/app.module.ts
import { Module } from '@nestjs/common';
import { TracingModule } from './tracing/tracing.module';
import { UsersModule } from './users/users.module';

@Module({
imports: [
TracingModule, // Import FIRST for proper initialization
UsersModule,
// ... other modules
],
})
export class AppModule {}

Production Configuration​

For production deployments with BatchSpanProcessor and resource attributes:

src/tracing/tracing.production.ts
import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_INSTANCE_ID,
} from '@opentelemetry/semantic-conventions';

@Module({})
export class TracingModule implements OnModuleInit, OnModuleDestroy {
private sdk: NodeSDK;

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

this.sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME,
[SEMRESATTRS_SERVICE_VERSION]: process.env.npm_package_version,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
[SEMRESATTRS_SERVICE_INSTANCE_ID]: process.env.HOSTNAME || process.pid.toString(),
'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) => {
return ['/health', '/metrics', '/ready'].some((path) =>
req.url?.includes(path),
);
},
},
}),
],
});

this.sdk.start();
console.log('OpenTelemetry SDK initialized');
}

async onModuleDestroy() {
console.log('Shutting down OpenTelemetry SDK...');
await this.sdk.shutdown();
}
}

Docker Deployment​

Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:20-alpine

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

ENV NODE_ENV=production
ENV OTEL_SERVICE_NAME=nestjs-api

EXPOSE 3000

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

services:
nestjs-api:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- OTEL_SERVICE_NAME=nestjs-api
- OTEL_SERVICE_VERSION=1.0.0
- OTEL_EXPORTER_OTLP_ENDPOINT=http://scout-collector:4318
- DATABASE_URL=postgres://user:pass@postgres:5432/nestjs
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
- scout-collector

postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: nestjs
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
ports:
- '5432:5432'

redis:
image: redis:7-alpine
ports:
- '6379:6379'

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'

NestJS-Specific Instrumentation​

Controllers and Routes​

NestJS controllers are automatically instrumented via HTTP instrumentation:

src/users/users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

// Automatically traced as "GET /users"
@Get()
async findAll() {
return this.usersService.findAll();
}

// Automatically traced as "GET /users/:id"
@Get(':id')
async findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}

// Automatically traced as "POST /users"
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}

Traces show:

  • HTTP method and route pattern
  • Response status codes
  • Request/response headers (configurable)
  • Timing for entire request lifecycle

Services with Dependency Injection​

Services are traced when called from instrumented controllers:

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

// Database queries automatically traced by TypeORM instrumentation
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}

async findOne(id: number): Promise<User> {
return this.usersRepository.findOne({ where: { id } });
}

async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}

TypeORM Database Instrumentation​

TypeORM queries are automatically instrumented:

src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Order } from '../../orders/entities/order.entity';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
email: string;

@Column()
name: string;

@OneToMany(() => Order, (order) => order.user)
orders: Order[];
}

TypeORM Module configuration:

src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TracingModule } from './tracing/tracing.module';

@Module({
imports: [
TracingModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV === 'development',
}),
// ... other modules
],
})
export class AppModule {}

Traces show:

  • SQL queries with parameters
  • Query execution time
  • Connection pool metrics
  • Transaction boundaries

Guards and Authentication​

Guards are traced as part of the request lifecycle:

src/auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { trace } from '@opentelemetry/api';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
async canActivate(context: ExecutionContext): Promise<boolean> {
const tracer = trace.getTracer('auth-guard');

return tracer.startActiveSpan('JwtAuthGuard.canActivate', async (span) => {
try {
const result = (await super.canActivate(context)) as boolean;
span.setAttribute('auth.success', result);
span.setStatus({ code: 1 }); // OK
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
});
}
}

Interceptors for Custom Tracing​

Add custom attributes using interceptors:

src/common/interceptors/tracing.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { trace, context } from '@opentelemetry/api';

@Injectable()
export class TracingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const activeSpan = trace.getActiveSpan();

if (activeSpan) {
// Add custom attributes
activeSpan.setAttribute('user.id', request.user?.id);
activeSpan.setAttribute('tenant.id', request.headers['x-tenant-id']);
activeSpan.setAttribute('request.path', request.path);
}

return next.handle().pipe(
tap(() => {
if (activeSpan) {
activeSpan.setAttribute('response.status', 'success');
}
}),
);
}
}

Apply globally:

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TracingInterceptor } from './common/interceptors/tracing.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TracingInterceptor());
await app.listen(3000);
}
bootstrap();

BullMQ Background Jobs​

Instrument BullMQ job processing:

src/jobs/email.processor.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { trace } from '@opentelemetry/api';

@Processor('email')
export class EmailProcessor {
@Process('send-welcome')
async handleWelcomeEmail(job: Job) {
const tracer = trace.getTracer('email-processor');

return tracer.startActiveSpan('EmailProcessor.sendWelcome', async (span) => {
try {
span.setAttributes({
'job.id': job.id.toString(),
'job.name': job.name,
'job.attempts': job.attemptsMade,
'user.email': job.data.email,
});

// Simulate email sending
await this.sendEmail(job.data.email, job.data.name);

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

private async sendEmail(email: string, name: string) {
// Email sending logic
console.log(`Sending welcome email to ${email}`);
}
}

Queue module setup:

src/jobs/jobs.module.ts
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { EmailProcessor } from './email.processor';

@Module({
imports: [
BullModule.registerQueue({
name: 'email',
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
},
}),
],
providers: [EmailProcessor],
})
export class JobsModule {}

WebSocket Gateway Instrumentation​

Trace WebSocket connections and messages:

src/chat/chat.gateway.ts
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayConnection,
} from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { trace } from '@opentelemetry/api';

@WebSocketGateway({ cors: true })
export class ChatGateway implements OnGatewayConnection {
private tracer = trace.getTracer('chat-gateway');

handleConnection(client: Socket) {
const span = this.tracer.startSpan('ChatGateway.handleConnection');
span.setAttributes({
'websocket.client.id': client.id,
'websocket.event': 'connection',
});
span.end();
}

@SubscribeMessage('message')
async handleMessage(
@MessageBody() data: { room: string; message: string },
@ConnectedSocket() client: Socket,
) {
return this.tracer.startActiveSpan('ChatGateway.handleMessage', async (span) => {
try {
span.setAttributes({
'websocket.client.id': client.id,
'websocket.room': data.room,
'message.length': data.message.length,
});

// Broadcast message to room
client.to(data.room).emit('message', {
sender: client.id,
message: data.message,
});

span.setStatus({ code: 1 });
return { status: 'sent' };
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
});
}
}

Custom Instrumentation​

For business logic and application-specific operations:

src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { Order } from './entities/order.entity';
import { CreateOrderDto } from './dto/create-order.dto';

@Injectable()
export class OrdersService {
private tracer = trace.getTracer('orders-service');

constructor(
@InjectRepository(Order)
private ordersRepository: Repository<Order>,
) {}

async createOrder(userId: number, createOrderDto: CreateOrderDto) {
return this.tracer.startActiveSpan('OrdersService.createOrder', async (span) => {
try {
span.setAttributes({
'user.id': userId,
'order.items.count': createOrderDto.items.length,
'order.total': this.calculateTotal(createOrderDto.items),
});

// Validate inventory
await this.tracer.startActiveSpan('validateInventory', async (validateSpan) => {
const available = await this.checkInventory(createOrderDto.items);
validateSpan.setAttribute('inventory.available', available);
if (!available) {
throw new Error('Insufficient inventory');
}
validateSpan.end();
});

// Create order
const order = await this.tracer.startActiveSpan('saveOrder', async (dbSpan) => {
const newOrder = this.ordersRepository.create({
userId,
items: createOrderDto.items,
total: this.calculateTotal(createOrderDto.items),
});
const saved = await this.ordersRepository.save(newOrder);
dbSpan.setAttribute('order.id', saved.id);
dbSpan.end();
return saved;
});

// Process payment
await this.tracer.startActiveSpan('processPayment', async (paymentSpan) => {
await this.processPayment(order.id, order.total);
paymentSpan.setAttribute('payment.status', 'completed');
paymentSpan.end();
});

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

private calculateTotal(items: any[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

private async checkInventory(items: any[]): Promise<boolean> {
// Inventory check logic
return true;
}

private async processPayment(orderId: number, amount: number): Promise<void> {
// Payment processing logic
}
}

Running Your Application​

Development Mode​

# With console output for debugging
export OTEL_TRACES_EXPORTER=console
npm run start:dev

Production Mode​

export NODE_ENV=production
export OTEL_SERVICE_NAME=nestjs-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://scout.yourdomain.com/v1/traces
npm run start:prod

Using PM2​

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

Start with PM2:

pm2 start ecosystem.config.js
pm2 logs nestjs-api

Troubleshooting​

Issue: No Traces from NestJS Controllers​

Solutions:

  1. Ensure TracingModule is imported first in AppModule:
@Module({
imports: [
TracingModule, // MUST be first
TypeOrmModule.forRoot(/*...*/),
// other modules
],
})
export class AppModule {}
  1. Verify HTTP instrumentation is enabled:
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
enabled: true, // Explicitly enable
},
}),
];

Issue: TypeORM Queries Not Appearing in Traces​

Solutions:

  1. Install TypeORM instrumentation explicitly if needed:
npm install @opentelemetry/instrumentation-typeorm
  1. Verify database connection is established after SDK initialization

  2. Check TypeORM logging is enabled in development:

TypeOrmModule.forRoot({
// ...
logging: true, // See queries in console
});

Issue: Missing Context in Async Operations​

Solutions:

Use async/await instead of callbacks:

// WRONG - loses context
setTimeout(() => {
const span = trace.getActiveSpan(); // undefined
}, 1000);

// CORRECT - preserves context
await new Promise((resolve) => setTimeout(resolve, 1000));
const span = trace.getActiveSpan(); // Works!

Issue: Guard/Interceptor Spans Not Showing​

Solutions:

Guards and interceptors need manual span creation. Add custom tracing as shown in the Guards and Interceptors sections above.

Security Considerations​

Sensitive Data Protection​

Avoid capturing passwords, tokens, and PII in spans:

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

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

HTTP Header Filtering​

Configure header filtering to exclude authentication tokens:

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

Database Query Sanitization​

TypeORM automatically sanitizes parameters, but verify in traces:

// Parameters are automatically sanitized
const user = await this.usersRepository.findOne({
where: { email: userEmail }, // Safe - uses parameterized query
});

Environment Variable Security​

Never log sensitive environment variables:

// BAD
console.log('DB_PASSWORD:', process.env.DB_PASSWORD);

// GOOD - Use configuration service
@Injectable()
export class ConfigService {
get(key: string): string {
const value = process.env[key];
if (!value && this.isProduction()) {
throw new Error(`Missing required config: ${key}`);
}
return value;
}
}

Performance Considerations​

OpenTelemetry adds minimal overhead to NestJS applications:

Expected Impact:

  • Latency: +0.5-2ms per request with auto-instrumentation
  • CPU: +2-5% in production with BatchSpanProcessor
  • Memory: +15-35MB for trace buffers and SDK
  • Throughput: <1% reduction in requests/second

Optimization Best Practices​

1. Use BatchSpanProcessor in Production​

import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

spanProcessor: new BatchSpanProcessor(traceExporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
});

2. Skip Health Check Endpoints​

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

3. Disable Filesystem Tracing​

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

4. Optimize TypeORM Queries​

Use query builder for complex queries to reduce overhead:

// Efficient - single query with joins
const users = await this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.active = :active', { active: true })
.getMany();

// Inefficient - N+1 queries
const users = await this.usersRepository.find({ where: { active: true } });
for (const user of users) {
user.orders = await this.ordersRepository.find({ where: { userId: user.id } });
}

FAQ​

Does OpenTelemetry work with NestJS dependency injection?​

Yes, OpenTelemetry fully supports NestJS DI. TracingModule can be imported and services are automatically traced when called from instrumented controllers.

What's the performance impact on NestJS applications?​

With BatchSpanProcessor, expect +0.5-2ms latency per request, +2-5% CPU, and +15-35MB memory. Minimal impact for most production workloads.

Can I trace TypeORM, Prisma, and Sequelize?​

Yes, auto-instrumentation includes TypeORM, Prisma, Sequelize, and other ORMs. Database queries are automatically traced with parameters.

How do I trace BullMQ background jobs?​

BullMQ jobs are automatically traced. Add custom spans in processors using trace.getTracer() for detailed business logic tracing.

Does it work with WebSocket gateways?​

WebSocket connections and messages require manual instrumentation. Use trace.getTracer() in gateway methods as shown in the WebSocket section.

How do I trace GraphQL resolvers?​

GraphQL queries are traced via HTTP instrumentation. Add custom spans in resolvers for field-level tracing using decorators or interceptors.

Can I use it with NestJS microservices?​

Yes, OpenTelemetry traces distributed microservices automatically. Context propagates across HTTP, gRPC, and message queue boundaries.

How do I handle multi-tenant applications?​

Add tenant ID as span attribute in guards or interceptors: span.setAttribute('tenant.id', tenantId) and filter in Scout Dashboard.

What's the difference between traces and metrics?​

Traces show request flow and timing through your NestJS app (spans). Metrics aggregate performance data (counters, histograms). Both are supported.

Can I trace custom decorators and metadata?​

Yes, use interceptors or method decorators to add custom spans. Access metadata using Reflector and add attributes to active spans.

What's Next?​

Framework-Specific Guides​

Advanced Topics​

Scout Platform Features​

Deployment and Operations​

Complete Example​

Here's a complete working NestJS application with OpenTelemetry instrumentation:

package.json​

package.json
{
"name": "nestjs-otel-example",
"version": "1.0.0",
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main"
},
"dependencies": {
"@nestjs/common": "^10.3.0",
"@nestjs/core": "^10.3.0",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/typeorm": "^10.0.1",
"@nestjs/bull": "^10.0.1",
"@opentelemetry/sdk-node": "^0.54.0",
"@opentelemetry/auto-instrumentations-node": "^0.54.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.54.0",
"@opentelemetry/resources": "^1.28.0",
"@opentelemetry/semantic-conventions": "^1.28.0",
"@opentelemetry/api": "^1.9.0",
"typeorm": "^0.3.20",
"pg": "^8.11.0",
"bull": "^4.12.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
}
}

Environment Variables​

.env.production
NODE_ENV=production
OTEL_SERVICE_NAME=nestjs-api
OTEL_SERVICE_VERSION=1.0.0
OTEL_EXPORTER_OTLP_ENDPOINT=http://scout-collector:4318

DATABASE_URL=postgres://user:pass@postgres:5432/nestjs
REDIS_URL=redis://redis:6379

GitHub Repository​

Complete working example: GitHub: base-14/examples/nodejs/nestjs-postgres

References​

Was this page helpful?