From e6a329978f44dbd37d3622b32b63ba9f6ffb7932 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Fri, 27 Mar 2026 04:20:30 -0700 Subject: [PATCH 1/4] Implement Request Correlation ID Middleware for Distributed Tracing --- .../correlation-exception-filter.ts | 31 ++++++++++++ .../correlation-http-interceptor.service.ts | 15 ++++++ .../monitoring/correlation-id.middleware.ts | 27 ++++++++++ .../src/monitoring/correlation-id.storage.ts | 17 +++++++ .../monitoring/correlation-logger.service.ts | 49 +++++++++++++++++++ .../src/monitoring/correlation.module.ts | 14 ++++++ middleware/src/monitoring/index.ts | 7 +-- 7 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 middleware/src/monitoring/correlation-exception-filter.ts create mode 100644 middleware/src/monitoring/correlation-http-interceptor.service.ts create mode 100644 middleware/src/monitoring/correlation-id.middleware.ts create mode 100644 middleware/src/monitoring/correlation-id.storage.ts create mode 100644 middleware/src/monitoring/correlation-logger.service.ts create mode 100644 middleware/src/monitoring/correlation.module.ts diff --git a/middleware/src/monitoring/correlation-exception-filter.ts b/middleware/src/monitoring/correlation-exception-filter.ts new file mode 100644 index 0000000..f2a328e --- /dev/null +++ b/middleware/src/monitoring/correlation-exception-filter.ts @@ -0,0 +1,31 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; +import { CorrelationIdStorage } from './correlation-id.storage'; + +@Catch() +export class CorrelationExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const correlationId = CorrelationIdStorage.getCorrelationId(); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + correlationId: correlationId || 'N/A', + message: exception?.message || 'Internal server error', + path: ctx.getRequest().url, + }); + } +} diff --git a/middleware/src/monitoring/correlation-http-interceptor.service.ts b/middleware/src/monitoring/correlation-http-interceptor.service.ts new file mode 100644 index 0000000..6a1f1ed --- /dev/null +++ b/middleware/src/monitoring/correlation-http-interceptor.service.ts @@ -0,0 +1,15 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { CorrelationIdStorage } from '../monitoring/correlation-id.storage'; + +@Injectable() +export class CorrelationHttpInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const correlationId = CorrelationIdStorage.getCorrelationId(); + if (correlationId) { + // If we are dealing with standard req/res, + // the middleware already handled it. + } + return next.handle(); + } +} diff --git a/middleware/src/monitoring/correlation-id.middleware.ts b/middleware/src/monitoring/correlation-id.middleware.ts new file mode 100644 index 0000000..514ad09 --- /dev/null +++ b/middleware/src/monitoring/correlation-id.middleware.ts @@ -0,0 +1,27 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { randomUUID } from 'node:crypto'; +import { CorrelationIdStorage } from './correlation-id.storage'; + +@Injectable() +export class CorrelationIdMiddleware implements NestMiddleware { + private readonly HEADER_NAME = 'X-Correlation-ID'; + + use(req: Request, res: Response, next: NextFunction) { + // 1. Extract from header or generate new UUID v4 + const correlationId = (req.header(this.HEADER_NAME) || randomUUID()) as string; + + // 2. Attach to request headers for propagation + req.headers[this.HEADER_NAME.toLowerCase()] = correlationId; + + // 3. Attach to response headers + res.setHeader(this.HEADER_NAME, correlationId); + + // 4. Run the rest of the request lifecycle within CorrelationIdStorage context + CorrelationIdStorage.run(correlationId, () => { + // 5. Store in request object as well for easy access without storage if needed + (req as any).correlationId = correlationId; + next(); + }); + } +} diff --git a/middleware/src/monitoring/correlation-id.storage.ts b/middleware/src/monitoring/correlation-id.storage.ts new file mode 100644 index 0000000..6a89a51 --- /dev/null +++ b/middleware/src/monitoring/correlation-id.storage.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +export interface CorrelationContext { + correlationId: string; +} + +export class CorrelationIdStorage { + private static readonly storage = new AsyncLocalStorage(); + + static run(correlationId: string, fn: () => R): R { + return this.storage.run({ correlationId }, fn); + } + + static getCorrelationId(): string | undefined { + return this.storage.getStore()?.correlationId; + } +} diff --git a/middleware/src/monitoring/correlation-logger.service.ts b/middleware/src/monitoring/correlation-logger.service.ts new file mode 100644 index 0000000..b6bff27 --- /dev/null +++ b/middleware/src/monitoring/correlation-logger.service.ts @@ -0,0 +1,49 @@ +import { Injectable, LoggerService, Scope } from '@nestjs/common'; +import { CorrelationIdStorage } from './correlation-id.storage'; + +@Injectable({ scope: Scope.TRANSIENT }) +export class CorrelationLoggerService implements LoggerService { + private context?: string; + + constructor(context?: string) { + this.context = context; + } + + setContext(context: string) { + this.context = context; + } + + log(message: any, context?: string) { + this.printLog('info', message, context); + } + + error(message: any, trace?: string, context?: string) { + this.printLog('error', message, context, trace); + } + + warn(message: any, context?: string) { + this.printLog('warn', message, context); + } + + debug(message: any, context?: string) { + this.printLog('debug', message, context); + } + + verbose(message: any, context?: string) { + this.printLog('verbose', message, context); + } + + private printLog(level: string, message: any, context?: string, trace?: string) { + const correlationId = CorrelationIdStorage.getCorrelationId(); + const logOutput = { + timestamp: new Date().toISOString(), + level, + correlationId: correlationId || 'N/A', + context: context || this.context, + message: typeof message === 'string' ? message : JSON.stringify(message), + ...(trace ? { trace } : {}), + }; + + console.log(JSON.stringify(logOutput)); + } +} diff --git a/middleware/src/monitoring/correlation.module.ts b/middleware/src/monitoring/correlation.module.ts new file mode 100644 index 0000000..3b0e170 --- /dev/null +++ b/middleware/src/monitoring/correlation.module.ts @@ -0,0 +1,14 @@ +import { Module, Global } from '@nestjs/common'; +import { CorrelationIdMiddleware } from './correlation-id.middleware'; +import { CorrelationLoggerService } from './correlation-logger.service'; + +@Global() +@Module({ + providers: [ + CorrelationLoggerService, + ], + exports: [ + CorrelationLoggerService, + ], +}) +export class CorrelationModule {} diff --git a/middleware/src/monitoring/index.ts b/middleware/src/monitoring/index.ts index f759ac3..958d63e 100644 --- a/middleware/src/monitoring/index.ts +++ b/middleware/src/monitoring/index.ts @@ -1,3 +1,4 @@ -// Placeholder: monitoring middleware exports will live here. - -export const __monitoringPlaceholder = true; +export * from './correlation-id.middleware'; +export * from './correlation-id.storage'; +export * from './correlation-logger.service'; +export * from './correlation.module'; From dca309a290571f27d46929f13895784e781bf0dc Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Fri, 27 Mar 2026 04:20:44 -0700 Subject: [PATCH 2/4] fix --- .../correlation-propagation.utils.ts | 29 +++++++++++++++++++ middleware/src/monitoring/index.ts | 1 + 2 files changed, 30 insertions(+) create mode 100644 middleware/src/monitoring/correlation-propagation.utils.ts diff --git a/middleware/src/monitoring/correlation-propagation.utils.ts b/middleware/src/monitoring/correlation-propagation.utils.ts new file mode 100644 index 0000000..2d77aaf --- /dev/null +++ b/middleware/src/monitoring/correlation-propagation.utils.ts @@ -0,0 +1,29 @@ +import { CorrelationIdStorage } from './correlation-id.storage'; + +/** + * Utility to get headers for downstream calls to propagate correlation ID. + */ +export const getCorrelationHeaders = (headers: Record = {}) => { + const correlationId = CorrelationIdStorage.getCorrelationId(); + if (correlationId) { + return { + ...headers, + 'X-Correlation-ID': correlationId, + }; + } + return headers; +}; + +/** + * Wraps a function to execute within the current correlation context. + * Useful for async workers, emitters, and timers. + */ +export const withCorrelation = any>(fn: T): T => { + const correlationId = CorrelationIdStorage.getCorrelationId(); + if (!correlationId) { + return fn; + } + return ((...args: any[]) => { + return CorrelationIdStorage.run(correlationId, () => fn(...args)); + }) as T; +}; diff --git a/middleware/src/monitoring/index.ts b/middleware/src/monitoring/index.ts index 958d63e..1eb9b01 100644 --- a/middleware/src/monitoring/index.ts +++ b/middleware/src/monitoring/index.ts @@ -2,3 +2,4 @@ export * from './correlation-id.middleware'; export * from './correlation-id.storage'; export * from './correlation-logger.service'; export * from './correlation.module'; +export * from './correlation-exception-filter'; From 2329d56b559e3e40306d5bfa564712d20f7eddd8 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Fri, 27 Mar 2026 04:21:39 -0700 Subject: [PATCH 3/4] fix --- middleware/src/monitoring/correlation-id.storage.ts | 1 + middleware/src/monitoring/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/middleware/src/monitoring/correlation-id.storage.ts b/middleware/src/monitoring/correlation-id.storage.ts index 6a89a51..ad20033 100644 --- a/middleware/src/monitoring/correlation-id.storage.ts +++ b/middleware/src/monitoring/correlation-id.storage.ts @@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; export interface CorrelationContext { correlationId: string; + userId?: string; } export class CorrelationIdStorage { diff --git a/middleware/src/monitoring/index.ts b/middleware/src/monitoring/index.ts index 1eb9b01..3c90977 100644 --- a/middleware/src/monitoring/index.ts +++ b/middleware/src/monitoring/index.ts @@ -3,3 +3,4 @@ export * from './correlation-id.storage'; export * from './correlation-logger.service'; export * from './correlation.module'; export * from './correlation-exception-filter'; +export * from './correlation-propagation.utils'; From 9ef215bbb471f90635379301e711fd6c57d5d7bd Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Fri, 27 Mar 2026 04:23:00 -0700 Subject: [PATCH 4/4] fix --- middleware/src/monitoring/correlation-exception-filter.ts | 2 ++ middleware/src/monitoring/correlation-id.middleware.ts | 3 ++- middleware/src/monitoring/correlation-id.storage.ts | 8 ++++++-- middleware/src/monitoring/correlation-logger.service.ts | 2 ++ .../src/monitoring/correlation-propagation.utils.ts | 3 ++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/middleware/src/monitoring/correlation-exception-filter.ts b/middleware/src/monitoring/correlation-exception-filter.ts index f2a328e..a73e46a 100644 --- a/middleware/src/monitoring/correlation-exception-filter.ts +++ b/middleware/src/monitoring/correlation-exception-filter.ts @@ -19,11 +19,13 @@ export class CorrelationExceptionFilter implements ExceptionFilter { : HttpStatus.INTERNAL_SERVER_ERROR; const correlationId = CorrelationIdStorage.getCorrelationId(); + const userId = CorrelationIdStorage.getUserId(); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), correlationId: correlationId || 'N/A', + userId: userId || 'N/A', message: exception?.message || 'Internal server error', path: ctx.getRequest().url, }); diff --git a/middleware/src/monitoring/correlation-id.middleware.ts b/middleware/src/monitoring/correlation-id.middleware.ts index 514ad09..86ae87b 100644 --- a/middleware/src/monitoring/correlation-id.middleware.ts +++ b/middleware/src/monitoring/correlation-id.middleware.ts @@ -18,7 +18,8 @@ export class CorrelationIdMiddleware implements NestMiddleware { res.setHeader(this.HEADER_NAME, correlationId); // 4. Run the rest of the request lifecycle within CorrelationIdStorage context - CorrelationIdStorage.run(correlationId, () => { + const userId = (req as any).user?.id || (req as any).userId; + CorrelationIdStorage.run(correlationId, userId, () => { // 5. Store in request object as well for easy access without storage if needed (req as any).correlationId = correlationId; next(); diff --git a/middleware/src/monitoring/correlation-id.storage.ts b/middleware/src/monitoring/correlation-id.storage.ts index ad20033..ed7174f 100644 --- a/middleware/src/monitoring/correlation-id.storage.ts +++ b/middleware/src/monitoring/correlation-id.storage.ts @@ -8,11 +8,15 @@ export interface CorrelationContext { export class CorrelationIdStorage { private static readonly storage = new AsyncLocalStorage(); - static run(correlationId: string, fn: () => R): R { - return this.storage.run({ correlationId }, fn); + static run(correlationId: string, userId: string | undefined, fn: () => R): R { + return this.storage.run({ correlationId, userId }, fn); } static getCorrelationId(): string | undefined { return this.storage.getStore()?.correlationId; } + + static getUserId(): string | undefined { + return this.storage.getStore()?.userId; + } } diff --git a/middleware/src/monitoring/correlation-logger.service.ts b/middleware/src/monitoring/correlation-logger.service.ts index b6bff27..a07e668 100644 --- a/middleware/src/monitoring/correlation-logger.service.ts +++ b/middleware/src/monitoring/correlation-logger.service.ts @@ -35,10 +35,12 @@ export class CorrelationLoggerService implements LoggerService { private printLog(level: string, message: any, context?: string, trace?: string) { const correlationId = CorrelationIdStorage.getCorrelationId(); + const userId = CorrelationIdStorage.getUserId(); const logOutput = { timestamp: new Date().toISOString(), level, correlationId: correlationId || 'N/A', + userId: userId || 'N/A', context: context || this.context, message: typeof message === 'string' ? message : JSON.stringify(message), ...(trace ? { trace } : {}), diff --git a/middleware/src/monitoring/correlation-propagation.utils.ts b/middleware/src/monitoring/correlation-propagation.utils.ts index 2d77aaf..7e9748a 100644 --- a/middleware/src/monitoring/correlation-propagation.utils.ts +++ b/middleware/src/monitoring/correlation-propagation.utils.ts @@ -20,10 +20,11 @@ export const getCorrelationHeaders = (headers: Record = {}) => { */ export const withCorrelation = any>(fn: T): T => { const correlationId = CorrelationIdStorage.getCorrelationId(); + const userId = CorrelationIdStorage.getUserId(); if (!correlationId) { return fn; } return ((...args: any[]) => { - return CorrelationIdStorage.run(correlationId, () => fn(...args)); + return CorrelationIdStorage.run(correlationId, userId, () => fn(...args)); }) as T; };