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'); - } -}