You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 14:13:17