Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

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.0

Configuration

  1. 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"
  2. Create Instrumentation Setup

    Create instrumentation.ts in 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 provider
    const provider = new NodeTracerProvider(providerConfig);
    provider.register();
    // Auto-instrument NestJS and Node.js libraries
    registerInstrumentations({
    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}`,
    );
  3. Import Instrumentation in Main.ts

    Critical: Import the instrumentation file before any other imports in your main.ts:

    // main.ts
    import "./instrumentation"; // Must be first import
    import { 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 pipe
    app.useGlobalPipes(new ValidationPipe());
    // Swagger configuration
    const 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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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:

# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Build the application
RUN npm run build
# Set OpenTelemetry environment variables
ENV OTEL_SERVICE_NAME=nestjs-docker-app
ENV OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
ENV OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
ENV OTEL_RESOURCE_ATTRIBUTES="deployment.environment=docker"
EXPOSE 3000
CMD ["node", "dist/main"]

Troubleshooting

Common Issues

  1. No traces appearing:

    • Verify instrumentation is imported before NestJS modules
    • Check environment variables are correctly set
    • Enable debug logging for OpenTelemetry
  2. Missing controller/service spans:

    • Ensure TracingService is properly injected
    • Verify @Trace decorator is applied correctly
  3. Performance impact:

    • Use sampling: export OTEL_TRACES_SAMPLER_ARG=0.1
    • Disable unnecessary instrumentations
    • Monitor memory usage with instrumentation

Debug Mode

Enable detailed logging:

// Add to instrumentation.ts
import { 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

  1. Service Naming: Use descriptive, environment-specific names
  2. Custom Spans: Add meaningful business context to spans
  3. Error Handling: Always record exceptions in custom spans
  4. Resource Attributes: Include deployment environment and version
  5. 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.