Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
.git
.gitignore
.gitattributes

# ── Secrets & credentials ──────────────────────────────────────────────────────
.env
.env.*
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IncidentManagementModule } from './incident-management/incident-managem
import { MonitoringModule } from './monitoring/monitoring.module';
import { ModerationModule } from './moderation/moderation.module';
import { IdempotencyModule } from './common/modules/idempotency.module';
import { CorrelationModule } from './common/modules/correlation.module';
import { DeepLinkModule } from './deep-link/deep-link.module';
import { InvoicesModule } from './payments/invoices/invoices.module';
import { PaymentMethodsModule } from './payments/payment-methods/payment-methods.module';
Expand Down Expand Up @@ -66,6 +67,7 @@ const featureFlags = loadFeatureFlags();
MonitoringModule,
ShardingModule,

CorrelationModule,
IdempotencyModule,
DeepLinkModule,

Expand Down
27 changes: 27 additions & 0 deletions src/common/modules/correlation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CorrelationIdMiddleware } from '../../middleware/correlation-id.middleware';

/**
* CorrelationModule
*
* Registers `CorrelationIdMiddleware` for every route (`*`).
* Import this module in `AppModule` (or any feature module that needs
* per-request correlation tracking) to activate automatic propagation.
*
* The middleware:
* - Accepts an incoming `x-correlation-id` (or the legacy `x-request-id`).
* - Generates a fresh ID when none is provided.
* - Stores the ID in `AsyncLocalStorage` so `getCorrelationId()` works
* anywhere in the call stack without explicit parameter passing.
* - Echoes the ID on every response via `x-correlation-id`.
* - Logs request start and completion events with the ID attached.
*/
@Module({
providers: [CorrelationIdMiddleware],
exports: [CorrelationIdMiddleware],
})
export class CorrelationModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(CorrelationIdMiddleware).forRoutes('*');
}
}
93 changes: 67 additions & 26 deletions src/common/utils/correlation.utils.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
import { AsyncLocalStorage } from 'async_hooks';
import { Request, Response, NextFunction } from 'express';
export const CORRELATION_ID_HEADER = 'x-request-id';

/**
* Canonical outbound header for the correlation ID.
* Downstream services should forward this header to propagate the ID.
*/
export const CORRELATION_ID_HEADER = 'x-correlation-id';

/**
* Alternate inbound header accepted for backwards compatibility with clients
* that send `x-request-id` instead of `x-correlation-id`.
*/
export const REQUEST_ID_HEADER_ALIAS = 'x-request-id';

export interface ICorrelationContext {
correlationId: string;
/** Epoch milliseconds when the request entered the middleware. */
requestStartMs: number;
}

const correlationStorage = new AsyncLocalStorage<ICorrelationContext>();

// ---------------------------------------------------------------------------
// Core helpers
// ---------------------------------------------------------------------------

/**
* Generates correlation Id.
* @returns The resulting string value.
* Generates a lexicographically sortable, human-readable correlation ID.
* Format: `cid-<base36-timestamp>-<random>` (e.g. `cid-lzxj5b-a3f9k2m1`)
*/
export function generateCorrelationId(): string {
return `cid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}

/**
* Retrieves correlation Id.
* @returns The operation result.
* Returns the correlation ID bound to the current async context, or
* `undefined` when called outside a correlated scope.
*/
export function getCorrelationId(): string | undefined {
const store = correlationStorage.getStore();
return store?.correlationId;
return correlationStorage.getStore()?.correlationId;
}

/**
* Sets correlation Id.
* @param req The req.
* @param res The res.
* @param correlationId The correlation identifier.
* Returns the epoch-ms timestamp recorded when the correlated request
* entered the middleware, or `undefined` when outside a correlated scope.
*/
export function getRequestStartMs(): number | undefined {
return correlationStorage.getStore()?.requestStartMs;
}

/**
* Attaches the correlation ID to the request object (for downstream
* NestJS handlers) and echoes it on the response via the canonical header.
*/
export function setCorrelationId(req: Request, res: Response, correlationId: string): void {
(
Expand All @@ -40,38 +62,57 @@ export function setCorrelationId(req: Request, res: Response, correlationId: str
res.setHeader(CORRELATION_ID_HEADER, correlationId);
}

// ---------------------------------------------------------------------------
// Express middleware
// ---------------------------------------------------------------------------

/**
* Executes correlation Middleware.
* @param req The req.
* @param res The res.
* @param next The next.
* Express middleware that:
* 1. Reads an inbound `x-correlation-id` (or `x-request-id` as alias).
* 2. Generates a fresh ID when none is provided.
* 3. Runs subsequent handlers inside an `AsyncLocalStorage` context so that
* `getCorrelationId()` works anywhere in the call stack without explicit
* parameter threading.
* 4. Sets the canonical `x-correlation-id` response header.
*
* This middleware is already wired in `main.ts`. The NestJS class-based
* version (`CorrelationIdMiddleware`) delegates to this function so there is
* exactly one implementation.
*/
export function correlationMiddleware(req: Request, res: Response, next: NextFunction): void {
const incoming =
(req.headers[CORRELATION_ID_HEADER] as string) || (req.headers['x-correlation-id'] as string);
(req.headers[CORRELATION_ID_HEADER] as string | undefined) ||
(req.headers[REQUEST_ID_HEADER_ALIAS] as string | undefined);
const correlationId = incoming || generateCorrelationId();
correlationStorage.run({ correlationId }, () => {
const requestStartMs = Date.now();

correlationStorage.run({ correlationId, requestStartMs }, () => {
setCorrelationId(req, res, correlationId);
next();
});
}

// ---------------------------------------------------------------------------
// Utilities for outbound calls
// ---------------------------------------------------------------------------

/**
* Executes run With Correlation Id.
* @param callback The callback.
* @param correlationId The correlation identifier.
* @returns The resulting t.
* Executes `callback` within a new (or supplied) correlated async context.
* Useful for background jobs and queue workers that run outside an HTTP
* request lifecycle.
*/
export function runWithCorrelationId<T>(callback: () => T, correlationId?: string): T {
const id = correlationId || generateCorrelationId();
return correlationStorage.run({ correlationId: id }, callback);
return correlationStorage.run({ correlationId: id, requestStartMs: Date.now() }, callback);
}

/**
* Executes inject Correlation Id To Headers.
* @param headers The headers.
* @param correlationId The correlation identifier.
* @returns The resulting record<string, any>.
* Returns a copy of `headers` with the correlation ID injected under the
* canonical header name. Pass this to `axios`, `fetch`, or any HTTP client
* when making calls to downstream microservices.
*
* @example
* await axios.get(url, { headers: injectCorrelationIdToHeaders() });
*/
export function injectCorrelationIdToHeaders(
headers: Record<string, any> = {},
Expand Down
8 changes: 8 additions & 0 deletions src/logging/structured-logging.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getCorrelationId } from '../common/utils/correlation.utils';

export type LogMeta = Record<string, unknown>;

function timestamp(): string {
Expand All @@ -16,11 +18,17 @@ function formatStructured(level: string, service: string, args: unknown[], meta:
const message = typeof msgParts[0] === 'string' ? msgParts.shift() : undefined;
const extra = msgParts.length === 1 ? safeSerialize(msgParts[0]) : msgParts.map(safeSerialize);

// Automatically inject the correlation ID from AsyncLocalStorage when
// available so that every log line emitted during a request is traceable
// without manual wiring.
const correlationId = getCorrelationId();

const out: Record<string, unknown> = {
timestamp: timestamp(),
level,
service,
pid: process.pid,
...(correlationId ? { correlationId } : {}),
};

if (message) out.message = message;
Expand Down
Loading
Loading