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>
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|