From 9eddcbd2a75857b50bb3697001cfc3e3f34aa8b9 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 31 May 2026 09:18:34 +0330 Subject: [PATCH] Server-Timing --- .env.example | 4 +- .../interceptors/server-timing.interceptor.ts | 42 +++++++++++++++++++ src/main.ts | 16 ++++++- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/common/interceptors/server-timing.interceptor.ts diff --git a/.env.example b/.env.example index d300b7c..da02e7c 100644 --- a/.env.example +++ b/.env.example @@ -42,5 +42,5 @@ SMS_SENDER= # SMS_TRANSPORT=soap # SMS_DOMAIN=0098 -# Minimum minutes before a booking can be scheduled (default 60) -BOOKING_MIN_LEAD_MINUTES=60 +# Optional: log Server-Timing to PM2 stdout (total;dur=… per request) +# LOG_SERVER_TIMING=true diff --git a/src/common/interceptors/server-timing.interceptor.ts b/src/common/interceptors/server-timing.interceptor.ts new file mode 100644 index 0000000..d9bab78 --- /dev/null +++ b/src/common/interceptors/server-timing.interceptor.ts @@ -0,0 +1,42 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; + +@Injectable() +export class ServerTimingInterceptor implements NestInterceptor { + private readonly logger = new Logger(ServerTimingInterceptor.name); + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const http = context.switchToHttp(); + const response = http.getResponse<{ + setHeader(name: string, value: string): void; + getHeader(name: string): string | number | string[] | undefined; + }>(); + const request = http.getRequest<{ method?: string; url?: string }>(); + const start = performance.now(); + + return next.handle().pipe( + finalize(() => { + const dur = Math.max(0, Math.round(performance.now() - start)); + const existing = response.getHeader('Server-Timing'); + const total = `total;dur=${dur}`; + response.setHeader( + 'Server-Timing', + existing ? `${String(existing)}, ${total}` : total, + ); + if (process.env.LOG_SERVER_TIMING === 'true') { + const route = request.url ?? ''; + this.logger.log( + `${request.method ?? '?'} ${route} total;dur=${dur}`, + ); + } + }), + ); + } +} diff --git a/src/main.ts b/src/main.ts index 6911528..903fe2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,17 +5,28 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { join } from 'path'; import { TypeOrmExceptionFilter } from './common/filters/typeorm-exception.filter'; +import { ServerTimingInterceptor } from './common/interceptors/server-timing.interceptor'; import { AppModule } from './app.module'; +const CORS_EXPOSE_HEADERS = ['Server-Timing']; + function configureCors(app: Awaited>) { const raw = process.env.CORS_ORIGINS?.trim(); if (raw) { const origins = raw.split(',').map((o) => o.trim()).filter(Boolean); - app.enableCors({ origin: origins, credentials: true }); + app.enableCors({ + origin: origins, + credentials: true, + exposedHeaders: CORS_EXPOSE_HEADERS, + }); return; } if (process.env.NODE_ENV !== 'production') { - app.enableCors({ origin: true, credentials: true }); + app.enableCors({ + origin: true, + credentials: true, + exposedHeaders: CORS_EXPOSE_HEADERS, + }); } } @@ -32,6 +43,7 @@ async function bootstrap() { }), ); app.useGlobalFilters(new TypeOrmExceptionFilter()); + app.useGlobalInterceptors(new ServerTimingInterceptor()); const swaggerConfig = new DocumentBuilder() .setTitle('Montime API')