From f47bf8edd9bb09a44a615de553fc2d90dcf30b19 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:00:06 +0100 Subject: [PATCH] refactor(todo): use express middleware for HTTP metrics Moved HTTP request metrics tracking from NestJS interceptor to Express middleware in main.ts. This ensures ALL requests are tracked, including those rejected by auth guards before reaching the handler. - Remove MetricsInterceptor (wasn't capturing guard exceptions) - Add Express middleware in main.ts for metrics collection - Track all HTTP requests including 401/403/404 responses Co-Authored-By: Claude Opus 4.5 --- apps/todo/apps/backend/src/app.module.ts | 9 +-- apps/todo/apps/backend/src/main.ts | 40 ++++++++++++ apps/todo/apps/backend/src/metrics/index.ts | 1 - .../src/metrics/metrics.interceptor.ts | 65 ------------------- 4 files changed, 41 insertions(+), 74 deletions(-) delete mode 100644 apps/todo/apps/backend/src/metrics/metrics.interceptor.ts diff --git a/apps/todo/apps/backend/src/app.module.ts b/apps/todo/apps/backend/src/app.module.ts index 7e31c9f35..702d910cd 100644 --- a/apps/todo/apps/backend/src/app.module.ts +++ b/apps/todo/apps/backend/src/app.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; -import { APP_INTERCEPTOR } from '@nestjs/core'; import { DatabaseModule } from './db/database.module'; import { HealthModule } from './health/health.module'; import { ProjectModule } from './project/project.module'; @@ -10,7 +9,7 @@ import { LabelModule } from './label/label.module'; import { ReminderModule } from './reminder/reminder.module'; import { KanbanModule } from './kanban/kanban.module'; import { NetworkModule } from './network/network.module'; -import { MetricsModule, MetricsInterceptor } from './metrics'; +import { MetricsModule } from './metrics'; @Module({ imports: [ @@ -29,11 +28,5 @@ import { MetricsModule, MetricsInterceptor } from './metrics'; KanbanModule, NetworkModule, ], - providers: [ - { - provide: APP_INTERCEPTOR, - useClass: MetricsInterceptor, - }, - ], }) export class AppModule {} diff --git a/apps/todo/apps/backend/src/main.ts b/apps/todo/apps/backend/src/main.ts index 4c6b4f922..787548de7 100644 --- a/apps/todo/apps/backend/src/main.ts +++ b/apps/todo/apps/backend/src/main.ts @@ -1,11 +1,51 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; import { AppModule } from './app.module'; +import { MetricsService } from './metrics/metrics.service'; + +// Normalize route paths to prevent high cardinality +function normalizeRoute(path: string): string { + return path + .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') + .replace(/\/\d+/g, '/:id'); +} async function bootstrap() { const logger = new Logger('Bootstrap'); const app = await NestFactory.create(AppModule); + // Get MetricsService for request tracking + const metricsService = app.get(MetricsService); + + // Global Express middleware to track ALL HTTP requests + // This runs before guards/interceptors, so it catches auth failures etc. + app.use((req: Request, res: Response, next: NextFunction) => { + // Skip metrics endpoint + if (req.path === '/metrics') { + return next(); + } + + const startTime = Date.now(); + const method = req.method; + const route = normalizeRoute(req.path); + + res.once('finish', () => { + const duration = (Date.now() - startTime) / 1000; + metricsService.httpRequestsTotal.inc({ + method, + route, + status: res.statusCode.toString(), + }); + metricsService.httpRequestDuration.observe( + { method, route, status: res.statusCode.toString() }, + duration + ); + }); + + next(); + }); + // Enable CORS for all platforms app.enableCors({ origin: (origin, callback) => { diff --git a/apps/todo/apps/backend/src/metrics/index.ts b/apps/todo/apps/backend/src/metrics/index.ts index 8bc40907c..860cd0cdf 100644 --- a/apps/todo/apps/backend/src/metrics/index.ts +++ b/apps/todo/apps/backend/src/metrics/index.ts @@ -1,4 +1,3 @@ export * from './metrics.module'; export * from './metrics.service'; -export * from './metrics.interceptor'; export * from './metrics.controller'; diff --git a/apps/todo/apps/backend/src/metrics/metrics.interceptor.ts b/apps/todo/apps/backend/src/metrics/metrics.interceptor.ts deleted file mode 100644 index bfa01ef89..000000000 --- a/apps/todo/apps/backend/src/metrics/metrics.interceptor.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, - HttpException, - HttpStatus, -} from '@nestjs/common'; -import { Observable, tap, catchError, throwError } from 'rxjs'; -import { Request, Response } from 'express'; -import { MetricsService } from './metrics.service'; - -@Injectable() -export class MetricsInterceptor implements NestInterceptor { - constructor(private readonly metricsService: MetricsService) {} - - intercept(context: ExecutionContext, next: CallHandler): Observable { - const httpContext = context.switchToHttp(); - const request = httpContext.getRequest(); - const response = httpContext.getResponse(); - - // Skip metrics endpoint itself - if (request.path === '/metrics') { - return next.handle(); - } - - const startTime = Date.now(); - const method = request.method; - // Normalize route (remove IDs to prevent high cardinality) - const route = this.normalizeRoute(request.path); - - return next.handle().pipe( - tap(() => { - this.recordMetrics(method, route, response.statusCode, startTime); - }), - catchError((error) => { - // Extract status code from HttpException or use 500 - const status = - error instanceof HttpException ? error.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; - this.recordMetrics(method, route, status, startTime); - return throwError(() => error); - }) - ); - } - - private recordMetrics(method: string, route: string, status: number, startTime: number): void { - const duration = (Date.now() - startTime) / 1000; - const statusStr = status.toString(); - - this.metricsService.httpRequestsTotal.inc({ - method, - route, - status: statusStr, - }); - - this.metricsService.httpRequestDuration.observe({ method, route, status: statusStr }, duration); - } - - private normalizeRoute(path: string): string { - // Replace UUIDs and numeric IDs with placeholders - return path - .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ':id') - .replace(/\/\d+/g, '/:id'); - } -}