NestJS
Instrument NestJS applications with OpenTelemetry for comprehensive monitoring and observability
Instrument your NestJS application with OpenTelemetry to send comprehensive telemetry data to Last9. This integration provides automatic instrumentation for HTTP requests, controllers, services, and decorators, giving you deep visibility into your NestJS application’s performance.
Prerequisites
- Node.js 16.0 or higher
- NestJS 8.0 or higher
- TypeScript configuration
- Last9 account with OTLP endpoint configured
Installation
Install the required OpenTelemetry packages for NestJS instrumentation:
npm install \ @opentelemetry/api@1.9.0 \ @opentelemetry/auto-instrumentations-node@0.59.0 \ @opentelemetry/exporter-trace-otlp-grpc@0.201.1 \ @opentelemetry/exporter-trace-otlp-http@0.201.1 \ @opentelemetry/instrumentation@0.201.1 \ @opentelemetry/resources@2.0.1 \ @opentelemetry/sdk-node@0.201.1 \ @opentelemetry/sdk-trace-base@2.0.1 \ @opentelemetry/sdk-trace-node@2.0.1 \ @opentelemetry/semantic-conventions@1.34.0yarn add \ @opentelemetry/api@1.9.0 \ @opentelemetry/auto-instrumentations-node@0.59.0 \ @opentelemetry/exporter-trace-otlp-grpc@0.201.1 \ @opentelemetry/exporter-trace-otlp-http@0.201.1 \ @opentelemetry/instrumentation@0.201.1 \ @opentelemetry/resources@2.0.1 \ @opentelemetry/sdk-node@0.201.1 \ @opentelemetry/sdk-trace-base@2.0.1 \ @opentelemetry/sdk-trace-node@2.0.1 \ @opentelemetry/semantic-conventions@1.34.0Configuration
-
Set Environment Variables
Configure the required environment variables for Last9 OTLP integration:
export OTEL_SERVICE_NAME="your-nestjs-service"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_TRACES_SAMPLER="always_on"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"export OTEL_LOG_LEVEL="error" -
Create Instrumentation Setup
Create
instrumentation.tsin your project root:import {NodeTracerProvider,TracerConfig,} from "@opentelemetry/sdk-trace-node";import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";import { registerInstrumentations } from "@opentelemetry/instrumentation";import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";import { resourceFromAttributes } from "@opentelemetry/resources";// Uncomment for debugging:// import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);const providerConfig: TracerConfig = {resource: resourceFromAttributes({["service.name"]: process.env.OTEL_SERVICE_NAME || "nestjs-app",["deployment.environment"]: process.env.NODE_ENV || "development",["service.version"]: process.env.npm_package_version || "1.0.0",["service.framework"]: "nestjs",}),spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,headers: {Authorization:process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=","",) || "",},}),),],};// Initialize and register the tracer providerconst provider = new NodeTracerProvider(providerConfig);provider.register();// Auto-instrument NestJS and Node.js librariesregisterInstrumentations({instrumentations: [getNodeAutoInstrumentations({// Disable filesystem instrumentation to reduce noise"@opentelemetry/instrumentation-fs": {enabled: false,},// Configure Express instrumentation (NestJS uses Express under the hood)"@opentelemetry/instrumentation-express": {enabled: true,requestHook: (span, info) => {span.setAttributes({"nestjs.controller": info.request.route?.path || "unknown","http.request.body.size":info.request.get("content-length") || 0,"http.user_agent": info.request.get("user-agent") || "",});},},// Configure HTTP instrumentation"@opentelemetry/instrumentation-http": {enabled: true,requestHook: (span, request) => {span.setAttributes({"http.request.method": request.method,"http.request.url": request.url,});},},}),],});console.log(`[OpenTelemetry] Initialized NestJS instrumentation for service: ${process.env.OTEL_SERVICE_NAME}`,); -
Import Instrumentation in Main.ts
Critical: Import the instrumentation file before any other imports in your
main.ts:// main.tsimport "./instrumentation"; // Must be first importimport { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module";import { ValidationPipe } from "@nestjs/common";import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";async function bootstrap() {const app = await NestFactory.create(AppModule);// Global validation pipeapp.useGlobalPipes(new ValidationPipe());// Swagger configurationconst config = new DocumentBuilder().setTitle("NestJS API").setDescription("API with OpenTelemetry instrumentation").setVersion("1.0").build();const document = SwaggerModule.createDocument(app, config);SwaggerModule.setup("api", app, document);await app.listen(3000);console.log("NestJS application listening on port 3000");}bootstrap();
NestJS-Specific Instrumentation
Custom Tracing Service
Create a tracing service for manual instrumentation in NestJS:
// tracing.service.tsimport { Injectable } from "@nestjs/common";import { trace, SpanStatusCode } from "@opentelemetry/api";
@Injectable()export class TracingService { private readonly tracer = trace.getTracer("nestjs-app");
async traceAsyncOperation<T>( operationName: string, operation: () => Promise<T>, attributes?: Record<string, string | number | boolean>, ): Promise<T> { const span = this.tracer.startSpan(operationName);
if (attributes) { span.setAttributes(attributes); }
try { const result = await operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); throw error; } finally { span.end(); } }
traceSyncOperation<T>( operationName: string, operation: () => T, attributes?: Record<string, string | number | boolean>, ): T { const span = this.tracer.startSpan(operationName);
if (attributes) { span.setAttributes(attributes); }
try { const result = operation(); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); throw error; } finally { span.end(); } }}Tracing Decorator
Create a custom decorator for automatic method tracing:
// trace.decorator.tsimport { trace } from "@opentelemetry/api";
export function Trace(operationName?: string) { return function ( target: any, propertyName: string, descriptor: PropertyDescriptor, ) { const method = descriptor.value; const tracer = trace.getTracer("nestjs-app");
descriptor.value = function (...args: any[]) { const spanName = operationName || `${target.constructor.name}.${propertyName}`; const span = tracer.startSpan(spanName);
span.setAttributes({ "method.class": target.constructor.name, "method.name": propertyName, "method.args.count": args.length, });
try { const result = method.apply(this, args);
if (result && typeof result.then === "function") { // Handle async methods return result .then((value: any) => { span.setStatus({ code: trace.SpanStatusCode.OK }); return value; }) .catch((error: any) => { span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message, }); throw error; }) .finally(() => { span.end(); }); } else { // Handle sync methods span.setStatus({ code: trace.SpanStatusCode.OK }); return result; } } catch (error) { span.recordException(error); span.setStatus({ code: trace.SpanStatusCode.ERROR, message: error.message, }); throw error; } finally { if (!result || typeof result.then !== "function") { span.end(); } } }; };}Usage in Controllers and Services
// users.controller.tsimport { Controller, Get, Post, Body, Param } from "@nestjs/common";import { UsersService } from "./users.service";import { CreateUserDto } from "./dto/create-user.dto";import { Trace } from "../decorators/trace.decorator";
@Controller("users")export class UsersController { constructor(private readonly usersService: UsersService) {}
@Get() @Trace("get-all-users") async findAll() { return this.usersService.findAll(); }
@Get(":id") @Trace() async findOne(@Param("id") id: string) { return this.usersService.findOne(+id); }
@Post() @Trace("create-user") async create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); }}// users.service.tsimport { Injectable } from "@nestjs/common";import { TracingService } from "../services/tracing.service";import { CreateUserDto } from "./dto/create-user.dto";
@Injectable()export class UsersService { constructor(private readonly tracingService: TracingService) {}
async findAll() { return this.tracingService.traceAsyncOperation( "database-find-all-users", async () => { // Simulate database operation await new Promise((resolve) => setTimeout(resolve, 100)); return [{ id: 1, name: "John Doe" }]; }, { "db.operation": "select", "db.table": "users", }, ); }
async findOne(id: number) { return this.tracingService.traceAsyncOperation( "database-find-user-by-id", async () => { // Simulate database operation await new Promise((resolve) => setTimeout(resolve, 50)); return { id, name: "John Doe" }; }, { "db.operation": "select", "db.table": "users", "user.id": id, }, ); }
async create(createUserDto: CreateUserDto) { return this.tracingService.traceAsyncOperation( "database-create-user", async () => { // Simulate database operation await new Promise((resolve) => setTimeout(resolve, 200)); return { id: Date.now(), ...createUserDto }; }, { "db.operation": "insert", "db.table": "users", }, ); }}Module Configuration
Register the TracingService in your app module:
// app.module.tsimport { Module } from "@nestjs/common";import { UsersModule } from "./users/users.module";import { TracingService } from "./services/tracing.service";
@Module({ imports: [UsersModule], providers: [TracingService], exports: [TracingService],})export class AppModule {}Docker Configuration
For containerized NestJS applications:
# DockerfileFROM node:18-alpine
WORKDIR /app
COPY package*.json ./RUN npm ci --only=production
COPY . .
# Build the applicationRUN npm run build
# Set OpenTelemetry environment variablesENV OTEL_SERVICE_NAME=nestjs-docker-appENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"ENV OTEL_RESOURCE_ATTRIBUTES="deployment.environment=docker"
EXPOSE 3000CMD ["node", "dist/main"]Troubleshooting
Common Issues
-
No traces appearing:
- Verify instrumentation is imported before NestJS modules
- Check environment variables are correctly set
- Enable debug logging for OpenTelemetry
-
Missing controller/service spans:
- Ensure TracingService is properly injected
- Verify @Trace decorator is applied correctly
-
Performance impact:
- Use sampling:
export OTEL_TRACES_SAMPLER_ARG=0.1 - Disable unnecessary instrumentations
- Monitor memory usage with instrumentation
- Use sampling:
Debug Mode
Enable detailed logging:
// Add to instrumentation.tsimport { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);Monitoring Capabilities
This integration automatically captures:
- HTTP Requests: All incoming requests to controllers
- Controller Methods: Execution time and parameters
- Service Methods: Business logic performance
- Database Operations: When using instrumented database clients
- External API Calls: Outbound HTTP requests
- Exception Tracking: Detailed error information
- Custom Business Metrics: Through manual instrumentation
Best Practices
- Service Naming: Use descriptive, environment-specific names
- Custom Spans: Add meaningful business context to spans
- Error Handling: Always record exceptions in custom spans
- Resource Attributes: Include deployment environment and version
- Performance: Monitor the overhead of instrumentation in production
Your NestJS application will now provide comprehensive observability data to Last9, enabling detailed performance monitoring and error tracking across your entire application stack.