diff --git a/package.json b/package.json index b81cece..eea75be 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.2.0", "lodash": "^4.17.21", + "nanoid": "^5.1.5", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "trolleyhq": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fd9f1a..bb6236d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: dotenv: specifier: ^16.5.0 version: 16.5.0 + json-stringify-safe: + specifier: ^5.0.1 + version: 5.0.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -50,6 +53,9 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -90,6 +96,9 @@ importers: '@types/jest': specifier: ^29.5.14 version: 29.5.14 + '@types/json-stringify-safe': + specifier: ^5.0.3 + version: 5.0.3 '@types/lodash': specifier: ^4.17.16 version: 4.17.16 @@ -1239,6 +1248,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json-stringify-safe@5.0.3': + resolution: {integrity: sha512-oNOjRxLfPeYbBSQ60maucaFNqbslVOPU4WWs5t/sHvAh6tyo/CThXSG+E24tEzkgh/fzvxyDrYdOJufgeNy1sQ==} + '@types/jsonwebtoken@9.0.9': resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} @@ -2692,6 +2704,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2941,6 +2956,11 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4980,6 +5000,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/json-stringify-safe@5.0.3': {} + '@types/jsonwebtoken@9.0.9': dependencies: '@types/ms': 2.1.0 @@ -6761,6 +6783,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -6989,6 +7013,8 @@ snapshots: mute-stream@2.0.0: {} + nanoid@5.1.5: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} diff --git a/src/api/api.module.ts b/src/api/api.module.ts index 3714dfe..2da3f93 100644 --- a/src/api/api.module.ts +++ b/src/api/api.module.ts @@ -3,6 +3,7 @@ import { HealthCheckController } from './health-check/healthCheck.controller'; import { GlobalProvidersModule } from 'src/shared/global/globalProviders.module'; import { APP_GUARD } from '@nestjs/core'; import { TokenValidatorMiddleware } from 'src/core/auth/middleware/tokenValidator.middleware'; +import { CreateRequestStoreMiddleware } from 'src/core/request/createRequestStore.middleware'; import { AuthGuard, RolesGuard } from 'src/core/auth/guards'; import { TopcoderModule } from 'src/shared/topcoder/topcoder.module'; import { OriginRepository } from './repository/origin.repo'; @@ -48,5 +49,6 @@ import { WithdrawalModule } from './withdrawal/withdrawal.module'; export class ApiModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(TokenValidatorMiddleware).forRoutes('*'); + consumer.apply(CreateRequestStoreMiddleware).forRoutes('*'); } } diff --git a/src/core/request/createRequestStore.middleware.ts b/src/core/request/createRequestStore.middleware.ts new file mode 100644 index 0000000..1b45180 --- /dev/null +++ b/src/core/request/createRequestStore.middleware.ts @@ -0,0 +1,14 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Response, NextFunction } from 'express'; +import { RequestMetadata, saveStore } from './requestStore'; + +@Injectable() +export class CreateRequestStoreMiddleware implements NestMiddleware { + constructor() {} + + use(req: any, res: Response, next: NextFunction) { + const requestMetaData = new RequestMetadata({}); + + saveStore(requestMetaData, next); + } +} diff --git a/src/core/request/requestStore.ts b/src/core/request/requestStore.ts new file mode 100644 index 0000000..3f77d07 --- /dev/null +++ b/src/core/request/requestStore.ts @@ -0,0 +1,39 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { NextFunction } from 'express'; +import { nanoid } from 'nanoid'; + +// Class for storing request specific metadata +export class RequestMetadata { + requestId: string; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type + constructor(params: { requestId?: string }) { + this.requestId = params.requestId ?? nanoid(11); + } +} + +// Create a AsyncLocalStorage of type RequestMetaData for storing request specific data +const asyncStorage = new AsyncLocalStorage(); + +// Gets the RequestMetadada object associated with the current request +export function getStore(): RequestMetadata { + let store = asyncStorage.getStore(); + if (store === undefined) { + store = new RequestMetadata({ + requestId: '', + }); + } + + return store; +} + +// For use in middleware +// Saves RequestMetadata for the current request +export function saveStore( + requestMetaData: RequestMetadata, + next: NextFunction, +) { + asyncStorage.run(requestMetaData, () => { + next(); + }); +} diff --git a/src/shared/global/logger.ts b/src/shared/global/logger.ts index 5821ff0..01577a9 100644 --- a/src/shared/global/logger.ts +++ b/src/shared/global/logger.ts @@ -1,7 +1,12 @@ import { Logger as NestLogger } from '@nestjs/common'; import stringify from 'json-stringify-safe'; +import { getStore } from 'src/core/request/requestStore'; export class Logger extends NestLogger { + private get store() { + return getStore(); + } + log(...messages: any[]): void { super.log(this.formatMessages(messages)); } @@ -19,7 +24,10 @@ export class Logger extends NestLogger { } private formatMessages(messages: any[]): string { - return messages + const requestIdPrefix = this.store.requestId + ? [`{${this.store.requestId}}`] + : []; + return [...requestIdPrefix, ...messages] .map((msg) => typeof msg === 'object' ? stringify(msg, null, 2) : String(msg), )