在NestJs/Node.js中实现互斥式日志上下文
针对NestJS请求级上下文日志的解决方案
你需要的核心是请求隔离的日志上下文——每个请求拥有独立的元数据容器,贯穿整个处理流程且互不干扰。下面提供两种经过实践验证的方案,包括解决你之前Winston落地的问题,以及更省心的开箱即用方案。
方案1:AsyncLocalStorage + Winston(解决你之前的Winston痛点)
Node.js的AsyncLocalStorage(ALS)是实现请求上下文隔离的核心,它能在异步流程中保留专属上下文,不会被并发请求污染。结合Winston的自定义格式,就能实现元数据的自动追加。
步骤1:实现上下文管理服务
创建一个全局服务封装ALS,负责初始化、读写请求元数据:
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { AsyncLocalStorage } from 'async_hooks'; interface LogContext { [key: string]: any; } @Injectable() export class LogContextService implements OnModuleInit, OnModuleDestroy { private als = new AsyncLocalStorage<LogContext>(); onModuleInit() { this.als.enable(); } onModuleDestroy() { this.als.disable(); } // 启动新请求的上下文,传入初始元数据 startContext(initialData: LogContext = {}) { return this.als.run({ ...initialData }, () => null); } // 追加元数据到当前请求上下文 setMetadata(key: string, value: any) { const context = this.als.getStore(); if (context) context[key] = value; } // 获取当前请求的全部元数据 getMetadata(): LogContext { return this.als.getStore() || {}; } }
步骤2:全局拦截器初始化请求上下文
在请求进入时生成requestId等基础元数据,启动上下文:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; import { LogContextService } from './log-context.service'; @Injectable() export class LogContextInterceptor implements NestInterceptor { constructor(private readonly logContextService: LogContextService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const requestId = uuidv4(); this.logContextService.startContext({ requestId, path: request.path, method: request.method, timestamp: new Date().toISOString(), }); return next.handle(); } }
步骤3:定制Winston格式自动注入上下文
修改Winston的日志格式,自动合并当前请求的元数据:
import { createLogger, format, transports } from 'winston'; import { LogContextService } from './log-context.service'; export const createContextLogger = (logContextService: LogContextService) => { return createLogger({ format: format.combine( format(info => { // 合并上下文元数据到日志内容 const context = logContextService.getMetadata(); return { ...info, ...context }; })(), format.json() // 可替换为simple/printf等自定义格式 ), transports: [new transports.Console()], }); };
步骤4:全局注册与使用
在模块中全局注册拦截器和日志服务,之后在控制器/服务中直接注入使用:
import { Module, Global } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LogContextService } from './log-context.service'; import { LogContextInterceptor } from './log-context.interceptor'; import { createContextLogger } from './logger'; @Global() @Module({ providers: [ LogContextService, { provide: APP_INTERCEPTOR, useClass: LogContextInterceptor, }, { provide: 'Logger', useFactory: (logContext: LogContextService) => createContextLogger(logContext), inject: [LogContextService], }, ], exports: ['Logger', LogContextService], }) export class LogModule {}
在业务代码中追加元数据并打日志:
import { Injectable, Inject } from '@nestjs/common'; import { LogContextService } from './log-context.service'; @Injectable() export class UserService { constructor( @Inject('Logger') private readonly logger: any, private readonly logContext: LogContextService, ) {} async getUser(id: string) { // 追加业务元数据 this.logContext.setMetadata('userId', id); // 日志自动包含requestId、userId等所有上下文数据 this.logger.info('开始查询用户信息'); return { id, name: 'test' }; } }
方案2:nestjs-pino(开箱即用,性能更优)
Pino是高性能日志库,nestjs-pino官方封装包原生支持请求上下文隔离,底层同样基于ALS,无需手动实现上下文管理,更适合快速落地。
步骤1:安装依赖
npm install nestjs-pino pino-http
步骤2:全局配置日志模块
import { Module } from '@nestjs/common'; import { LoggerModule } from 'nestjs-pino'; @Module({ imports: [ LoggerModule.forRoot({ pinoHttp: { transport: { target: 'pino-pretty' }, // 开发环境美化输出,生产可移除 autoLogging: false, // 关闭默认请求日志,自定义控制 formatters: { level: (label) => ({ level: label }), }, }, }), ], }) export class AppModule {}
步骤3:使用上下文日志
在拦截器中初始化基础元数据:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { v4 as uuidv4 } from 'uuid'; @Injectable() export class LogContextInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); // 初始化请求上下文元数据 request.log.setBindings({ requestId: uuidv4(), path: request.path, method: request.method, }); return next.handle(); } }
在业务代码中追加元数据:
import { Injectable } from '@nestjs/common'; import { PinoLogger } from 'nestjs-pino'; @Injectable() export class OrderService { constructor(private readonly logger: PinoLogger) { this.logger.setContext(OrderService.name); } async createOrder(userId: string) { // 创建带业务元数据的子日志器,自动继承请求上下文 const contextLogger = this.logger.child({ userId }); contextLogger.info('开始创建订单'); return { orderId: 'ORD-123' }; } }
关键注意事项
- 绝对不要用全局变量存储请求级元数据,必须依赖
AsyncLocalStorage实现隔离 - 所有异步操作(数据库查询、Promise、微服务调用)都会自动继承ALS上下文,无需额外处理
- Winston方案适合已有Winston技术栈的场景,Pino方案更轻量、性能更佳
内容的提问来源于stack exchange,提问作者Nayan Srivastava
相关产品推荐
相关产品推荐

