# Nest.js

> Instrument NestJS applications with OpenTelemetry for comprehensive monitoring and observability

Source: https://last9.io/docs/integrations/frameworks/javascript/nestjs/

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**

```bash
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
```

**yarn**

```bash
yarn 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.0
```

## Configuration

1. **Set Environment Variables**

   Configure the required environment variables for Last9 OTLP integration:

   ```bash
   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:

   ```typescript
   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`:

   ```typescript
   // 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:

```typescript
// 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:

```typescript
// 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

```typescript
// 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);
  }
}
```

```typescript
// 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:

```typescript
// 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
# 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:

```typescript
// 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.
