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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-25 14:00:06 +01:00
parent 11411ff0a0
commit f47bf8edd9
4 changed files with 41 additions and 74 deletions

View file

@ -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 {}

View file

@ -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) => {

View file

@ -1,4 +1,3 @@
export * from './metrics.module';
export * from './metrics.service';
export * from './metrics.interceptor';
export * from './metrics.controller';

View file

@ -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<unknown> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest<Request>();
const response = httpContext.getResponse<Response>();
// 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');
}
}