nestjs-otel
nestjs-otel copied to clipboard
Default API metrics for NestJS with fastify records full url path instead of full route
NestJS version: 8.0.0 nestjs-otel version: 3.0.1
NestJS-otel initialization code
OpenTelemetryModule.forRoot({
metrics: {
hostMetrics: true, // Includes Host Metrics
defaultMetrics: true, // Includes Default Metrics
apiMetrics: {
enable: true, // Includes api metrics
timeBuckets: [], // You can change the default time buckets
ignoreRoutes: ['/favicon.ico'], // You can ignore specific routes (See https://docs.nestjs.com/middleware#excluding-routes for options)
ignoreUndefinedRoutes: true, //Records metrics for all URLs, even undefined ones
},
},
})
NestJS App Initialization code
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: false }),
);
The default metric API collects the full URL instead of the route URL
example
/api/user/1
will be reported as /api/user/1
instead of /api/user/:id
The metric endpoint generate thousands of metrics for each id parameters instead of 1 metric only for this specific route
When i debugged the code, i found out that the default metric middle ware can't retrieve the route object from the request object.
any plans for this to be fixed?
I did some research on this, and apparently, the issue is that fastify uses middie to support middlewares, and instead of using the express request object, it uses the req.raw value. That's why the req.route value in the code below is undefined when using fastify. https://github.com/pragmaticivan/nestjs-otel/blob/main/src/middleware/api-metrics.middleware.ts#L99
here is an explanation of the middie/fastify/middleware problem https://github.com/nestjs/nest/issues/9927#issuecomment-1183520706
It seems like if we use interceptors
instead we would be able to get the fastify request and then the route we want...
const fastifyRequest = context.switchToHttp().getRequest()
fastifyRequest.routerPath...
I worked around this by adding route.path
to the fastify raw request with an interceptor.
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
import type { Observable } from 'rxjs';
interface RequestRoute {
route: {
path: string;
};
}
/**
* Add the router path so it is visibile in the middleware as req.route.path which
* nestjs-otel uses for route metrics.
* See https://github.com/pragmaticivan/nestjs-otel/issues/245
*/
@Injectable()
export class FastifyRoutePathInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
// eslint-disable-next-line no-type-assertion/no-type-assertion
const raw = request.raw as RawRequestDefaultExpression & RequestRoute;
raw.route = {
path: request.routeOptions.url,
};
return next.handle();
}
}