diff --git a/src/search/search.service.spec.ts b/src/search/search.service.spec.ts index a10eac1..4fc7454 100644 --- a/src/search/search.service.spec.ts +++ b/src/search/search.service.spec.ts @@ -108,9 +108,9 @@ describe('SearchService', () => { it('should record analytics error on failure', async () => { analyticsService.recordSearch.mockRejectedValueOnce(new Error('analytics fail')); - await expect( - service.searchProperties('user-1', { query: 'test' }), - ).rejects.toThrow('analytics fail'); + await expect(service.searchProperties('user-1', { query: 'test' })).rejects.toThrow( + 'analytics fail', + ); }); }); diff --git a/src/tracing/trace.interceptor.ts b/src/tracing/trace.interceptor.ts index 901d6a8..49a41e6 100644 --- a/src/tracing/trace.interceptor.ts +++ b/src/tracing/trace.interceptor.ts @@ -1,16 +1,39 @@ -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; -import { Observable, tap } from 'rxjs'; +import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { randomUUID } from 'crypto'; @Injectable() export class TraceInterceptor implements NestInterceptor { + private readonly logger = new Logger(TraceInterceptor.name); + intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); - const traceId = request.headers['x-trace-id'] || `trace-${Date.now()}`; + const traceId = randomUUID(); + const startTime = Date.now(); + + const className = context.getClass().name; + const handlerName = context.getHandler().name; + + request.headers['x-trace-id'] = traceId; request.traceId = traceId; + + this.logger.log(`[${traceId}] ${className}.${handlerName} - started`); + return next.handle().pipe( - tap(() => { - const response = context.switchToHttp().getResponse(); - response.setHeader('X-Trace-Id', traceId); + tap({ + next: () => { + const duration = Date.now() - startTime; + const response = context.switchToHttp().getResponse(); + response.setHeader('X-Trace-Id', traceId); + this.logger.log(`[${traceId}] ${className}.${handlerName} - completed (${duration}ms)`); + }, + error: (error) => { + const duration = Date.now() - startTime; + this.logger.error( + `[${traceId}] ${className}.${handlerName} - failed (${duration}ms): ${error.message}`, + ); + }, }), ); } diff --git a/src/tracing/tracing.module.ts b/src/tracing/tracing.module.ts new file mode 100644 index 0000000..32aaf3c --- /dev/null +++ b/src/tracing/tracing.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { TraceInterceptor } from './trace.interceptor'; + +@Global() +@Module({ + providers: [TraceInterceptor], + exports: [TraceInterceptor], +}) +export class TracingModule {}