diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index d92e75457..5161d076d 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -76,6 +76,7 @@ "nanoid": "^5.0.9", "nest-commander": "^3.15.0", "nest-winston": "^1.9.7", + "nestjs-cls": "^4.5.0", "nestjs-throttler-storage-redis": "^0.5.1", "nodemailer": "^6.9.16", "on-headers": "^1.0.2", diff --git a/packages/backend/server/src/app.module.ts b/packages/backend/server/src/app.module.ts index 5237684ed..a7d832c9f 100644 --- a/packages/backend/server/src/app.module.ts +++ b/packages/backend/server/src/app.module.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'node:crypto'; + import { DynamicModule, ForwardReference, @@ -6,6 +8,7 @@ import { } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { get } from 'lodash-es'; +import { ClsModule } from 'nestjs-cls'; import { AppController } from './app.controller'; import { getOptionalModuleMetadata } from './base'; @@ -42,6 +45,17 @@ import { REGISTERED_PLUGINS } from './plugins'; import { ENABLED_PLUGINS } from './plugins/registry'; export const FunctionalityModules = [ + ClsModule.forRoot({ + global: true, + middleware: { + mount: true, + generateId: true, + idGenerator() { + // make every request has a unique id to tracing + return randomUUID(); + }, + }, + }), ConfigModule.forRoot(), RuntimeModule, EventModule, diff --git a/packages/backend/server/src/app.ts b/packages/backend/server/src/app.ts index 36833e9c6..d492af038 100644 --- a/packages/backend/server/src/app.ts +++ b/packages/backend/server/src/app.ts @@ -12,6 +12,7 @@ import { import { SocketIoAdapter } from './base/websocket'; import { AuthGuard } from './core/auth'; import { ENABLED_FEATURES } from './core/config/server-feature'; +import { responseRequestIdHeader } from './middleware/request-id'; import { serverTimingAndCache } from './middleware/timing'; export async function createApp() { @@ -31,6 +32,7 @@ export async function createApp() { } app.use(serverTimingAndCache); + app.use(responseRequestIdHeader); app.use( graphqlUploadExpress({ diff --git a/packages/backend/server/src/base/logger/service.ts b/packages/backend/server/src/base/logger/service.ts index 261bbf0ca..6fd9e4c79 100644 --- a/packages/backend/server/src/base/logger/service.ts +++ b/packages/backend/server/src/base/logger/service.ts @@ -1,6 +1,20 @@ -import { ConsoleLogger, Injectable } from '@nestjs/common'; +import { ConsoleLogger, Injectable, type LogLevel } from '@nestjs/common'; +import { ClsServiceManager } from 'nestjs-cls'; // DO NOT use this Logger directly // Use it via this way: `private readonly logger = new Logger(MyService.name)` @Injectable() -export class AFFiNELogger extends ConsoleLogger {} +export class AFFiNELogger extends ConsoleLogger { + override stringifyMessage(message: unknown, logLevel: LogLevel) { + const messageString = super.stringifyMessage(message, logLevel); + const requestId = AFFiNELogger.getRequestId(); + if (!requestId) { + return messageString; + } + return `<${requestId}> ${messageString}`; + } + + static getRequestId(): string | undefined { + return ClsServiceManager.getClsService()?.getId(); + } +} diff --git a/packages/backend/server/src/middleware/request-id.ts b/packages/backend/server/src/middleware/request-id.ts new file mode 100644 index 000000000..218567034 --- /dev/null +++ b/packages/backend/server/src/middleware/request-id.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from 'express'; +import { ClsServiceManager } from 'nestjs-cls'; +import onHeaders from 'on-headers'; + +export const responseRequestIdHeader = ( + _req: Request, + res: Response, + next: NextFunction +) => { + onHeaders(res, () => { + const requestId = ClsServiceManager.getClsService().getId(); + res.setHeader('X-Request-Id', requestId); + }); + + next(); +}; diff --git a/packages/backend/server/src/plugins/gcloud/logging/service.ts b/packages/backend/server/src/plugins/gcloud/logging/service.ts index d7da394f9..5b3c59e57 100644 --- a/packages/backend/server/src/plugins/gcloud/logging/service.ts +++ b/packages/backend/server/src/plugins/gcloud/logging/service.ts @@ -1,22 +1,28 @@ import { LoggingWinston } from '@google-cloud/logging-winston'; import { LoggerService, Provider } from '@nestjs/common'; -import { createLogger, transports } from 'winston'; +import { createLogger, format, transports } from 'winston'; import { AFFiNELogger as LoggerProvide } from '../../../base/logger'; import { AFFiNELogger } from './logger'; +const moreMetadata = format(info => { + info.requestId = LoggerProvide.getRequestId(); + return info; +}); + export const loggerProvider: Provider = { provide: LoggerProvide, useFactory: () => { const loggingWinston = new LoggingWinston(); // Create a Winston logger that streams to Cloud Logging const instance = createLogger({ - level: 'log', + level: 'info', transports: [ new transports.Console(), // Add Cloud Logging loggingWinston, ], + format: format.combine(moreMetadata(), format.json()), }); return new AFFiNELogger(instance); }, diff --git a/yarn.lock b/yarn.lock index a1a85feb0..dabbee56e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -838,6 +838,7 @@ __metadata: nanoid: "npm:^5.0.9" nest-commander: "npm:^3.15.0" nest-winston: "npm:^1.9.7" + nestjs-cls: "npm:^4.5.0" nestjs-throttler-storage-redis: "npm:^0.5.1" nodemailer: "npm:^6.9.16" nodemon: "npm:^3.1.7" @@ -27239,6 +27240,18 @@ __metadata: languageName: node linkType: hard +"nestjs-cls@npm:^4.5.0": + version: 4.5.0 + resolution: "nestjs-cls@npm:4.5.0" + peerDependencies: + "@nestjs/common": "> 7.0.0 < 11" + "@nestjs/core": "> 7.0.0 < 11" + reflect-metadata: "*" + rxjs: ">= 7" + checksum: 10/cbd807244aaf0e89480e5fa7d08415e27953d4f6264246318cd3aa5ec4817564988d5cbed4f700c79090f0ded6f72752887bc842543f7f0af49c515df41e937e + languageName: node + linkType: hard + "nestjs-throttler-storage-redis@npm:^0.5.1": version: 0.5.1 resolution: "nestjs-throttler-storage-redis@npm:0.5.1"