feat(#131): add OpenTelemetry tracing infrastructure
Implement comprehensive distributed tracing for HTTP requests and LLM operations using OpenTelemetry with GenAI semantic conventions. Features: - TelemetryService: SDK initialization with OTLP HTTP exporter - TelemetryInterceptor: Automatic HTTP request spans - @TraceLlmCall decorator: LLM operation tracing - GenAI semantic conventions for model/token tracking - Graceful degradation when tracing disabled Instrumented: - All HTTP requests (automatic spans) - OllamaProvider chat/chatStream/embed operations - Token counts, model names, durations Environment: - OTEL_ENABLED (default: true) - OTEL_SERVICE_NAME (default: mosaic-api) - OTEL_EXPORTER_OTLP_ENDPOINT (default: localhost:4318) Tests: 23 passing with full coverage Fixes #131 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
100
apps/api/src/telemetry/telemetry.interceptor.ts
Normal file
100
apps/api/src/telemetry/telemetry.interceptor.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
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<unknown> {
|
||||
const httpContext = context.switchToHttp();
|
||||
const request = httpContext.getRequest<Request>();
|
||||
const response = httpContext.getResponse<Response>();
|
||||
|
||||
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.setHeader("x-trace-id", spanContext.traceId);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn("Failed to finalize span", error);
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user