nestjs-otel

repository·main·Indexed 21 days ago

https://github.com/pragmaticivan/nestjs-otel

An OpenTelemetry (OTEL) integration module for NestJS version 8.1.0 designed to simplify observability. It provides decorators and services for tracing, metrics, and baggage management, including @Span and @Traceable for custom spans, MetricService for custom metrics, and WideEventInterceptor for emitting context-rich canonical log lines.

Tokens
10.5K
Snippets
39
Records
52
Agent score
73%

What's inside nestjs-otel

  1. Migrate metric names from v7 to v8

    main

    In version 8, the default naming convention for metrics generated by decorators has changed from using underscores (_) to using dots (.) to align with OpenTelemetry standards.

    Changes to metric formats:

    • Instance Counter: Changed from app_ClassName_instances_total to app.ClassName.instances.total.
    • Method Counter: Changed from app_ClassName_methodName_calls_total to app.ClassName.methodName.calls.total.

    Action Required: Update your dashboards (e.g., Grafana) or alerting rules to use the new dot-notation format.

    Note: If you manually specified names in decorators (e.g., @OtelMethodCounter({ name: 'my_custom_name' })), your custom names are preserved and do not require changes.

  2. Integrate OpenTelemetry with Pino logging

    main

    You can integrate OpenTelemetry with Pino in two ways:

    1. Automatic Injection: Use @opentelemetry/instrumentation-pino in your NodeSDK configuration to automatically inject spanId and traceId into logs.
    2. Custom Formatter: Manually use the global trace context to inject spanId and traceId into your structured logs via a Pino formatter.
    import Pino, { Logger, LoggerOptions } from 'pino';
    import { trace, context } from '@opentelemetry/api';
    
    export const loggerOptions: LoggerOptions = {
      formatters: {
        log(object) {
          const span = trace.getSpan(context.active());
          if (!span) return { ...object };
          const { spanId, traceId } = span.spanContext();
          return { ...object, spanId, traceId };
        },
      },
    };
    
    export const logger: Logger = Pino(loggerOptions);
  3. Implement Wide Events with WideEventInterceptor

    main

    Wide events (canonical log lines) emit one context-rich event per request by accumulating attributes across the request lifecycle and flushing them onto a single span marked with nestjs_otel.wide_event = true.

    To enable wide events for the entire application, register the WideEventInterceptor globally using APP_INTERCEPTOR. To limit wide events to specific controllers, apply the interceptor directly using @UseInterceptors(WideEventInterceptor) on the controller class.

    import { APP_INTERCEPTOR } from '@nestjs/core';
    import { OpenTelemetryModule, WideEventInterceptor } from 'nestjs-otel';
    
    @Module({
      imports: [OpenTelemetryModule.forRoot()],
      providers: [
        {
          provide: APP_INTERCEPTOR,
          useClass: WideEventInterceptor,
        },
      ],
    })
    export class AppModule {}
  4. Initialize OpenTelemetry SDK in NestJS

    main

    To properly instrument your application, you must initialize the OpenTelemetry SDK before the NestJS application starts.

    1. Create a tracing.ts file to configure your NodeSDK (exporters, propagators, and instrumentations).
    2. Import this file in your entry point (e.g., main.ts) and call otelSDK.start() before NestFactory.create().
    // main.ts
    import otelSDK from './tracing';
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      // Start SDK before nestjs factory create
      await otelSDK.start();
    
      const app = await NestFactory.create(AppModule);
      await app.listen(3000);
    }
    bootstrap();
  5. Target the trace root span with WideEventSpanProcessor

    main

    By default, wide event attributes land on the best-available recording span (often a nested child span created by instrumentations like @opentelemetry/instrumentation-nestjs-core). To ensure attributes land on the trace root span (e.g., the HTTP server span), register the WideEventSpanProcessor in your NodeSDK configuration.

    import { NodeSDK } from '@opentelemetry/sdk-node';
    import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
    import { WideEventSpanProcessor } from 'nestjs-otel';
    
    export const otelSDK = new NodeSDK({
      spanProcessors: [
        new WideEventSpanProcessor(),
        new BatchSpanProcessor(traceExporter),
      ],
      // ...instrumentations, contextManager, etc.
    });
  6. Configure baseline attributes with WideEvent seed

    main

    You can populate baseline attributes for every request using the seed option in OpenTelemetryModule.forRoot(). This runs after guards, so data like req.user is available. If the seed function throws, the error is recorded under wide_event.seed.error without breaking the request.

    OpenTelemetryModule.forRoot({
      wideEvents: {
        seed: (ctx) => {
          const req = ctx.switchToHttp().getRequest();
          return {
            'app.version': process.env.BUILD_SHA,
            'user.id': req.user?.id,
          };
        },
      },
    });
  7. How WideEventMiddleware captures the root span

    main

    The WideEventMiddleware is a NestJS middleware designed to capture the initial HTTP server span (the local-root span) at the very beginning of a request lifecycle. It stashes this span on the request object using the WIDE_EVENT_ROOT_SPAN symbol.

    This allows the WideEventInterceptor to later flush accumulated request attributes (wide events) onto the actual root span instead of onto nested child spans (like guards, interceptors, or handlers).

    For HTTP applications, this middleware is applied automatically by the OpenTelemetry module.

  8. Initialize the OpenTelemetry module with forRoot()

    main

    Use OpenTelemetryCoreModule.forRoot(options) to synchronously bootstrap the OpenTelemetry module in your NestJS application. This method configures the core providers for tracing, metrics, and wide events. It is a global module, so it should typically be called once in your AppModule.

    @Module({
      imports: [OpenTelemetryCoreModule.forRoot({ /* your OpenTelemetryModuleOptions */ })],
    })
    export class AppModule {}
  9. Use WideEventInterceptor to capture wide events

    main

    The WideEventInterceptor allows you to accumulate a collection of attributes (a "wide event") during a NestJS request lifecycle and flush them all at once onto the active OpenTelemetry span when the request completes. This is useful for attaching large amounts of metadata (like request bodies, user context, or error details) to a single span without polluting the trace with many small spans.

    Key Features

    • Automatic Metadata: Automatically captures code.function.name (formatted as ClassName.methodName) and error details (type, message, stack) if the request fails.
    • Seeding: You can provide a custom seed function via OpenTelemetryModuleOptions to automatically populate the attribute bag at the start of every request.
    • Smart Span Selection: It intelligently selects the best span to attach attributes to (preferring the local root span, then the middleware-captured root span, then the active interceptor span) while ensuring the span is still recording.
    • Error Handling: If the request results in an error, the interceptor automatically sets the span status to SpanStatusCode.ERROR.

    Registration

    You can register this interceptor globally in your AppModule using APP_INTERCEPTOR or locally on specific controllers using UseInterceptors.

    // Global registration example
    @Module({
      providers: [
        {
          provide: APP_INTERCEPTOR,
          useClass: WideEventInterceptor,
        },
      ],
    })
    export class AppModule {}
  10. Initialize the OpenTelemetry module with forRootAsync()

    main

    Use OpenTelemetryCoreModule.forRootAsync(options) to asynchronously bootstrap the OpenTelemetry module. This is useful when your OpenTelemetry configuration depends on other services or external configuration (e.g., from a ConfigService).

    You can provide options using:

    • useFactory: A function that returns the options.
    • useClass: A class that implements OpenTelemetryOptionsFactory to provide the options.
    • useExisting: An existing provider that implements OpenTelemetryOptionsFactory.
    @Module({
      imports: [
        OpenTelemetryCoreModule.forRootAsync({
          useFactory: (configService: ConfigService) => ({
            // your OpenTelemetryModuleOptions
          }),
          inject: [ConfigService],
        }),
      ],
    })
    export class AppModule {}
  11. Use WideEventSpanProcessor to enable wide event handling

    main

    The WideEventSpanProcessor tracks the local-root span (the span with no in-process parent, typically the HTTP server span) for every trace. This allows the WideEventInterceptor to flush accumulated attributes onto the root span instead of onto nested instrumentation spans (like those created by instrumentation-nestjs-core or @fastify/otel).

    To use it, register it in your NodeSDK configuration alongside your standard exporting processor (e.g., BatchSpanProcessor).

    new NodeSDK({
      spanProcessors: [
        new WideEventSpanProcessor(),
        new BatchSpanProcessor(traceExporter),
      ],
    });