如何在NestJS中便捷分配correlationId并让日志自动携带该ID?
接手了一个NestJS项目,前任开发者写的中间件能生成correlationId并附加到请求里,功能正常。但开发和写测试时,每次都要用到扩展了correlationId的express.Request类型(也就是AppRequest),冗余代码多,想优化使用体验,把重复逻辑封装起来。
现在控制器代码是这样的:
async createUser(@Req() request: AppRequest, @Body() payload: any, @Res() response: Response) { return this.users.create(request, payload); }
必须把request传给service,才能让CloudWatch日志带上correlationId;要是直接用console.log,日志会因为时间差没法关联到对应请求。我希望在this.users.create()里调用logger.info()时,日志能自动带上correlationId,就像Spring Boot里的做法一样,想知道Node.js和NestJS能不能实现。
现有中间件代码如下:
const onHeaders = require('on-headers'); @Injectable() export class LoggerMiddleware implements NestMiddleware { use(request: Request, response: Response, next: NextFunction) { httpContext.middleware(request, response, () => { const startTime = process.hrtime(); const correlationId = ( request.headers['request-id'] || request.headers['x-request-id'] || request.headers['x-correlation-id'] || request.headers.correlationId || uuidv4() ).toString(); const { method, baseUrl, body } = request; const url = (baseUrl || '') + (request.url || '-'); const route = `${method} ${request.route ? request.route.path : url}`; const logger = rootLogger.child({ correlationId, route, url, method }); onHeaders(response, function onHeaders() { const diff = process.hrtime(startTime); const responseTime = diff[0] * 1e3 + diff[1] * 1e-6; response.setHeader('X-Response-Time', responseTime); logger.info( { responseTime, statusCode: response.statusCode, }, 'Responsed', ); }); request.logger = logger; request.correlationId = correlationId; response.setHeader('x-correlation-id', correlationId); next(); }); } }
当然可以实现,以下是几个能大幅优化开发体验的方案:
方案1:基于请求上下文封装全局请求作用域Logger
你已经在用httpContext处理请求上下文了,完全可以把带correlationId的Logger实例存在上下文里,不用每次手动传递request。
实现步骤:
- 封装请求作用域的Logger服务
创建一个自定义Logger服务,从httpContext中获取当前请求的Logger实例:
import { Injectable } from '@nestjs/common'; import httpContext from 'express-http-context'; @Injectable() export class RequestLogger { private readonly logger: any; // 替换成你实际的Logger类型 constructor() { this.logger = httpContext.get('logger'); } info(message: string, meta?: Record<string, any>) { this.logger.info(meta || {}, message); } error(message: string, meta?: Record<string, any>) { this.logger.error(meta || {}, message); } // 根据需要扩展warn、debug等方法 }
- 修改中间件,将Logger存入httpContext
在现有中间件中,设置request.logger的同时,把Logger实例存入httpContext:
// 中间代码补充 httpContext.set('logger', logger); // 新增这一行 request.logger = logger; request.correlationId = correlationId;
- Service中直接注入使用
现在Service里无需再接收request参数,直接注入RequestLogger即可自动获取带correlationId的Logger:
import { Injectable } from '@nestjs/common'; import { RequestLogger } from './request.logger'; @Injectable() export class UsersService { constructor(private readonly logger: RequestLogger) {} async create(payload: any) { // 打日志自动携带correlationId this.logger.info('开始创建用户', { payload }); // 业务逻辑... return result; } }
- 简化控制器代码
控制器不再需要传递request给Service,代码更简洁:
async createUser(@Body() payload: any) { return this.users.create(payload); }
方案2:自定义装饰器直接获取Logger/CorrelationId
如果不想依赖httpContext,可以通过自定义装饰器,直接从request中提取Logger或correlationId,注入到控制器或Service的参数中。
自定义Logger装饰器:
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const RequestLogger = createParamDecorator( (_, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.logger; }, );
控制器中使用装饰器:
async createUser(@Body() payload: any, @RequestLogger() logger: any) { logger.info('收到创建用户请求'); return this.users.create(payload, logger); }
Service中结合自定义提供者注入:
要在Service中直接使用,可以通过自定义提供者将request.logger注入:
// 在对应模块的providers数组中配置 providers: [ UsersService, { provide: 'REQUEST_LOGGER', useFactory: (request: any) => request.logger, inject: [REQUEST], // REQUEST是NestJS内置的请求对象令牌 }, ]
然后在Service中注入使用:
import { Inject, Injectable } from '@nestjs/common'; @Injectable() export class UsersService { constructor(@Inject('REQUEST_LOGGER') private readonly logger: any) {} async create(payload: any) { this.logger.info('创建用户', { payload }); // 业务逻辑... } }
方案3:扩展NestJS全局Logger,利用AsyncLocalStorage跟踪上下文
这个方案类似Spring Boot的ThreadLocal,用Node.js原生的AsyncLocalStorage来跟踪请求上下文,扩展NestJS自带的Logger,实现全局自动携带correlationId。
实现步骤:
- 创建AsyncLocalStorage实例
import { AsyncLocalStorage } from 'async_hooks'; // 定义上下文类型 type RequestContext = { correlationId: string; logger: any; }; export const requestContext = new AsyncLocalStorage<RequestContext>();
- 修改中间件,用AsyncLocalStorage包裹请求逻辑
替换原来的httpContext,用requestContext.run来存储请求上下文:
use(request: Request, response: Response, next: NextFunction) { // ... 生成correlationId和logger的逻辑保持不变 requestContext.run({ correlationId, logger }, () => { onHeaders(response, () => { // 响应日志逻辑不变 }); request.correlationId = correlationId; response.setHeader('x-correlation-id', correlationId); next(); }); }
- 扩展NestJS自带Logger
import { Logger } from '@nestjs/common'; import { requestContext } from './request-context'; export class CorrelationLogger extends Logger { log(message: string, context?: string) { const ctx = requestContext.getStore(); if (ctx) { super.log(`[${ctx.correlationId}] ${message}`, context); } else { super.log(message, context); } } error(message: string, trace?: string, context?: string) { const ctx = requestContext.getStore(); if (ctx) { super.error(`[${ctx.correlationId}] ${message}`, trace, context); } else { super.error(message, trace, context); } } // 同理扩展warn、debug等方法 }
- 全局注册自定义Logger
在main.ts中替换默认Logger:
async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: new CorrelationLogger(), }); // ... 其他配置 await app.listen(3000); }
之后在控制器或Service中直接注入NestJS的Logger,打日志时会自动带上correlationId。
内容的提问来源于stack exchange,提问作者tftd

