import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from "@nestjs/common"; import { Observable, throwError } from "rxjs"; import { tap, catchError } from "rxjs/operators"; import type { Request, Response } from "express"; import { TelemetryService } from "./telemetry.service"; import type { Span } from "@opentelemetry/api"; import { SpanKind } from "@opentelemetry/api"; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, ATTR_URL_PATH, } from "@opentelemetry/semantic-conventions"; /** * Interceptor that automatically creates OpenTelemetry spans for all HTTP requests. * Records HTTP method, URL, status code, and trace context in response headers. * * @example * ```typescript * // Apply globally in AppModule * @Module({ * providers: [ * { * provide: APP_INTERCEPTOR, * useClass: TelemetryInterceptor, * }, * ], * }) * export class AppModule {} * ``` */ @Injectable() export class TelemetryInterceptor implements NestInterceptor { private readonly logger = new Logger(TelemetryInterceptor.name); constructor(private readonly telemetryService: TelemetryService) {} /** * Intercept HTTP requests and wrap them in OpenTelemetry spans. * * @param context - The execution context * @param next - The next call handler * @returns Observable of the response with tracing applied */ intercept(context: ExecutionContext, next: CallHandler): Observable { const httpContext = context.switchToHttp(); const request = httpContext.getRequest(); const response = httpContext.getResponse(); const method = request.method; const path = request.path || request.url; const spanName = `${method} ${path}`; const span = this.telemetryService.startSpan(spanName, { kind: SpanKind.SERVER, attributes: { [ATTR_HTTP_REQUEST_METHOD]: method, [ATTR_URL_PATH]: path, [ATTR_URL_FULL]: request.url, "code.function": context.getHandler().name, "code.namespace": context.getClass().name, }, }); return next.handle().pipe( tap(() => { this.finalizeSpan(span, response); }), catchError((error: Error) => { this.telemetryService.recordException(span, error); this.finalizeSpan(span, response); return throwError(() => error); }) ); } /** * Finalize the span by setting status code and adding trace context to headers. * * @param span - The span to finalize * @param response - The HTTP response */ private finalizeSpan(span: Span, response: Response): void { try { const statusCode = response.statusCode; span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, statusCode); // Add trace context to response headers for distributed tracing const spanContext = span.spanContext(); if (spanContext.traceId && !response.headersSent && !response.writableEnded) { response.setHeader("x-trace-id", spanContext.traceId); } } catch (error) { this.logger.warn("Failed to finalize span", error); } finally { span.end(); } } }