Búsqueda de sitios web

node.js - ¿Cómo instrumentar un controlador en un interceptor de nestjs?


Quiero instrumentar cada método de un controlador de NestJS con fines de APM. Escribí el siguiente interceptor para instrumentar la invocación del controlador.

Sin embargo, no sé cómo envolver correctamente la llamada a next.handle().
No tengo ninguna experiencia usando Observables de RxJS.

Pregunta: ¿Es posible envolver la invocación correctamente y, de ser así, cómo?

El enfoque actual parece medir el tiempo de ejecución del controlador, pero no establece un alcance de trazador correcto para el método del controlador. Supongo que el problema es que next.handle() también debe envolverse.

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import { PATH_METADATA } from '@nestjs/common/constants';
import tracer from "dd-trace";

@Injectable()
export class ApmInterceptor implements NestInterceptor {
    constructor(private readonly reflector: Reflector) {}
    
    public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
        const request: Request = context.switchToHttp().getRequest();

        const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler()); 
        const method = request.method;

        const observable = next.handle();

        tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
            observable.subscribe({
                complete: resolve,
            });
        }));

        return observable;
    }
}

Respuesta #1

Me encontré con un problema similar usando OpenTelemetry-js, para establecer el alcance correcto tengo que envolver el handle() Observable en una promesa Async para establecer el contexto, y luego envolver la promesa nuevamente como Observable para la tubería de rxjs (Observable -> Promise -> Observable)

import {from, Observable} from 'rxjs';
...

 async intercept(executionContext: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request: Request = context.switchToHttp().getRequest();

    const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler()); 
    const method = request.method;

    const observable = tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
      return next.handle().toPromise();
    }));
    return observable.pipe(
        map(value => {
          // Here you can stop your trace manually
          return value;
        }),
        catchError(error => {
          // Here you can stop your trace manually
          throw error;
        }))
}

Para OpenTelemetry tienes que crear/detener el span y establecer el contexto correcto:

const span = trace.getTracer('default').startSpan(spanName);
const observable = from(context.with(trace.setSpan(context.active(), span), async () => {
  return next.handle().toPromise();
}));
return observable.pipe(
    map(value => {
      span.stop();
      return value;
    }),
    catchError(error => {
      span.addEvent('error', {error: error});
      span.stop();
      throw error;
    }))