Skip to content

NestJS Winston 日志系统完全指南(基于 nest-winston 最佳实践)

本文档介绍如何在 NestJS 中使用 nest-winston 官方推荐的方式集成 Winston 日志系统,包括依赖注入配置、动态配置、多种使用场景等核心知识点。

功能概述

本指南实现一个生产级的 NestJS 日志系统,包含以下功能:

  • ✅ 基于 nest-winston 官方最佳实践的依赖注入方式
  • ✅ 支持动态配置(可通过 ConfigService 注入配置)
  • ✅ HTTP 请求日志记录(中间件实现)
  • ✅ 全局异常捕获与记录(过滤器实现)
  • ✅ 按环境区分日志格式(开发环境文本、生产环境 JSON)
  • ✅ 规范的日志文件组织结构

技术栈

json
{
  "dependencies": {
    "nest-winston": "^1.10.2",          // NestJS Winston 官方集成模块
    "winston": "^3.19.0",                // Winston 日志核心库
    "winston-daily-rotate-file": "^5.0.0", // 日志文件按日期分割
    "@nestjs/config": "^4.0.3",          // 配置管理(支持动态注入)
    "uuid": "9.0.0"                      // 生成唯一请求ID
  }
}

核心架构

整体架构图

┌─────────────────────────────────────────────────────────┐
│  main.ts                                                │
│  └─ app.useLogger(WINSTON_MODULE_NEST_PROVIDER)         │
│     (将 NestJS 框架 Logger 替换为 Winston)              │
├─────────────────────────────────────────────────────────┤
│  LoggerModule.forRootAsync()                            │
│  └─ 在 DI 系统中注册 Winston,支持依赖注入              │
├─────────────────────────────────────────────────────────┤
│  Controllers & Services                                 │
│  └─ @Inject(WINSTON_MODULE_NEST_PROVIDER)               │
│     (注入 LoggerService,手动传入 context)               │
├─────────────────────────────────────────────────────────┤
│  Filters (需要结构化 metadata)                          │
│  └─ @Inject(WINSTON_MODULE_PROVIDER)                   │
│     (注入原生 winston.Logger,支持丰富 metadata)         │
├─────────────────────────────────────────────────────────┤
│  Middleware (独立日志)                                  │
│  └─ createAccessLogger()                               │
│     (独立的访问日志 Logger)                             │
└─────────────────────────────────────────────────────────┘

两种 Provider 的区别

Provider类型用途使用场景
WINSTON_MODULE_NEST_PROVIDERLoggerServiceNestJS Logger 接口实现Controllers, Services
WINSTON_MODULE_PROVIDERwinston.Logger原生 Winston LoggerFilters, Interceptors

实现步骤

1. Winston 配置文件

首先创建 Winston 配置:

winston.config.ts

typescript
import winston, { createLogger } from 'winston';
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import 'winston-daily-rotate-file';

/**
 * 创建 Winston Logger 配置(用于 forRootAsync)
 */
export const createWinstonLoggerConfig = (): winston.LoggerOptions => {
  return {
    transports: [
      // 终端打印
      new winston.transports.Console({
        level: 'debug',
        format: winston.format.combine(
          winston.format.timestamp({
            format: 'YYYY-MM-DD HH:mm:ss.SSS ZZ',
          }),
          winston.format.ms(),
          nestWinstonModuleUtilities.format.nestLike(),
        ),
      }),

      // 主日志文件:记录应用日志(info、warn、error)
      new winston.transports.DailyRotateFile({
        level: 'info',
        dirname: 'logs/application',
        filename: 'application-%DATE%.log',
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '30d',
        format: logFormat,
      }),

      // 错误日志文件:只记录错误日志
      new winston.transports.DailyRotateFile({
        level: 'error',
        dirname: 'logs/errors',
        filename: 'error-%DATE%.log',
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '60d',
        format: logFormat,
      }),
    ],
  };
};

/**
 * 创建访问日志 Logger(仅用于 LoggingMiddleware)
 */
export const createAccessLogger = (): winston.Logger => {
  return createLogger({
    transports: [
      new winston.transports.DailyRotateFile({
        level: 'info',
        dirname: 'logs/access',
        filename: 'access-%DATE%.log',
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '7d',
        format: getAccessLogFormat(),
      }),
    ],
  });
};

2. Logger 模块配置

logger.module.ts

typescript
import { Module, Global } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import { createWinstonLoggerConfig } from './winston.config';

/**
 * LoggerModule - 全局日志模块
 *
 * 使用 WinstonModule.forRootAsync 注册 Winston 到 DI 系统
 */
@Global()
@Module({
  imports: [
    WinstonModule.forRootAsync({
      useFactory: () => createWinstonLoggerConfig(),
    }),
  ],
})
export class LoggerModule {}

关键点

  • ✅ 使用 @Global() 让全局可用
  • ✅ 使用 forRootAsync() 支持异步配置和依赖注入
  • ✅ 不需要 providers/exports,直接使用 nest-winston 提供的 token

3. main.ts 中替换 Logger

main.ts

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 关键:将 NestJS 框架的 Logger 替换为 Winston
  // 这会让框架层面的日志(启动、路由等)也使用 Winston
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

  await app.listen(3000);
}
bootstrap();

为什么需要这一步

  • WinstonModule.forRootAsync() 只在 DI 系统中注册 Winston
  • app.useLogger() 告诉 NestJS 框架使用 Winston
  • 两者配合才能实现完整的日志替换

使用方式

场景 1:Controller & Service(推荐 - 使用 createLogger)

使用 createLogger 辅助方法,既保留 DI 优势,又实现自动 context:

typescript
import { Controller, Get, Inject } from '@nestjs/common';
import type { LoggerService } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { createLogger } from '@/common/helpers/logger.helper';

@Controller('users')
export class UsersController {
  readonly logger: ReturnType<typeof createLogger>;

  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    logger: LoggerService,
    private readonly usersService: UsersService,
  ) {
    // 创建带自动 context 的 logger
    this.logger = createLogger(logger, UsersController.name);
  }

  getUsers() {
    // ✅ 不需要手动传入类名,自动使用 UsersController.name
    this.logger.log('getUsers called');
    this.logger.debug('getUsers called');
    this.logger.error('Error occurred');
  }
}

优势

  • 完全符合 DI 原则:通过 DI 注入的 logger
  • 自动 context:无需手动传入类名
  • 代码简洁:一次设置,处处使用
  • 智能 error():自动处理 error() 的特殊参数顺序
  • 易于测试:可以 mock DI 的 logger

辅助方法源码

typescript
// logger.helper.ts
export class ContextualLogger implements LoggerService {
  constructor(
    private methods: LoggerService,
    private context: string,
  ) {}

  log(message: unknown, context?: string): void {
    this.methods.log(message, context || this.context);
  }

  error(message: unknown, trace?: string, context?: string): void {
    // 智能判断:短字符串当作 context,长字符串/多行当作 trace
    if (trace && typeof trace === 'string' && trace.length < 100 && !trace.includes('\n')) {
      this.methods.error(message, undefined, trace);
    } else {
      this.methods.error(message, trace, context || this.context);
    }
  }

  // ... 其他方法
}

export const createLogger = (logger: LoggerService, context: string) => {
  return new ContextualLogger(logger, context);
};

场景 2:Filter & Interceptor(需要结构化 metadata)


**正确示例**:
```typescript
// ✅ 正确:不需要 trace 时传入 undefined
this.logger.error('Something went wrong', undefined, UsersController.name);
// 输出:[ERROR] [UsersController] Something went wrong

// ✅ 正确:需要记录堆栈时
try {
  // ...
} catch (error) {
  this.logger.error('Something went wrong', error.stack, UsersController.name);
  // 输出:[ERROR] [UsersController] Something went wrong
  //       + 完整的堆栈信息
}
  • ⚠️ error() 方法参数顺序特殊(message, trace, context)

场景 2:Filter & Interceptor(需要结构化 metadata)

使用 WINSTON_MODULE_PROVIDER 注入原生 winston.Logger

typescript
import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import winston from 'winston';

@Injectable()
export class AllExceptionsFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER)
    private readonly logger: winston.Logger,
  ) {}

  catch(exception: unknown, host: ArgumentsHost) {
    this.logger.error('Error occurred', {
      requestId: req.id,
      statusCode: 500,
      method: 'GET',
      url: '/api/users',
      stack: exception.stack,
      context: 'AllExceptionsFilter',
    });
  }
}

为什么使用原生 Logger

  • ✅ 支持丰富的结构化 metadata
  • ✅ 更灵活的日志记录方式
  • ✅ 适合需要记录详细信息的场景

场景 3:Middleware(独立日志)

使用独立的 Logger 实例:

typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { createAccessLogger } from '../modules/logger/winston.config';

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
  private readonly logger = createAccessLogger();

  use(req: Request, res: Response, next: NextFunction) {
    this.logger.info('Request received', {
      requestId: req.id,
      method: req.method,
      url: req.url,
    });
    next();
  }
}

为什么使用独立 Logger

  • ✅ 访问日志与业务日志分离
  • ✅ 独立的存储策略(访问日志只保留 7 天)
  • ✅ 避免污染业务日志

环境区分的日志格式

开发环境 - 文本格式

typescript
const createCustomLogFormat = () => {
  return winston.format.printf((info: any) => {
    const { timestamp, level, message, context, stack } = info;
    const contextStr = context ? `[${String(context)}] ` : '';
    const stackStr = stack ? `\n${String(stack)}` : '';
    return `[${timestamp}] [${String(level).toUpperCase()}] ${contextStr}${String(message)}${stackStr}`;
  });
};

输出示例

[2026-03-24 17:00:00.000 +0800] [INFO] [UsersController] getUsers called

生产环境 - JSON 格式

typescript
const createJsonLogFormat = () => {
  return winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json(),
  );
};

输出示例

json
{
  "timestamp": "2026-03-24T09:00:00.000Z",
  "level": "info",
  "message": "getUsers called",
  "context": "UsersController"
}

动态选择格式

typescript
const getLogFormat = () => {
  const isProduction = process.env.NODE_ENV === 'production';
  return isProduction ? createJsonLogFormat() : createCustomLogFormat();
};

日志文件组织结构

目录结构

logs/
├── access/          # 访问日志(HTTP 请求)
│   └── access-2026-03-24-16.log
├── application/     # 应用日志(info、warn、error)
│   └── application-2026-03-24-16.log
└── errors/          # 错误日志(仅 error)
    └── error-2026-03-24-16.log

分层策略

目录日志级别保留时间用途
accessinfo7天访问日志,量大但价值低
applicationinfo/warn/error30天业务日志,常规排查
errorserror60天错误日志,长期追踪

日志轮转配置

typescript
new winston.transports.DailyRotateFile({
  level: 'info',
  dirname: 'logs/application',
  filename: 'application-%DATE%.log',
  datePattern: 'YYYY-MM-DD-HH',  // 按小时轮转
  zippedArchive: true,            // 自动压缩
  maxSize: '20m',                 // 单文件最大 20MB
  maxFiles: '30d',                // 保留 30 天
});

访问日志记录(中间件)

核心功能

1. 生成请求 ID

typescript
import { v4 as uuidv4 } from 'uuid';

req.id = uuidv4().split('-')[0]; // 使用 UUID 前 8 位
req.startTime = Date.now();

// 添加到响应头(便于前端追踪)
res.setHeader('X-Request-ID', requestId);

2. 记录请求开始

typescript
this.logger.info(`${method} ${originalUrl} - ${ip} - ${userAgent}`, {
  requestId,
  context: 'LoggingMiddleware',
});

3. 监听响应结束

typescript
res.on('finish', () => {
  const responseTime = Date.now() - req.startTime;

  // 根据状态码选择日志级别
  const logMethod =
    statusCode >= 500 ? 'error'    // 5xx:服务器错误
    : statusCode >= 400 ? 'warn'   // 4xx:客户端错误
    : 'info';                       // 2xx/3xx:正常

  this.logger[logMethod](
    `${method} ${originalUrl} - ${statusCode} - ${responseTime}ms`,
    { requestId, statusCode, responseTime },
  );
});

注册中间件

typescript
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggingMiddleware).forRoutes('*');
  }
}

全局异常过滤器

过滤器实现

typescript
@Injectable()
@Catch() // 捕获所有异常
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER)
    private readonly logger: winston.Logger,
  ) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // 区分异常类型
    const isHttpException = exception instanceof HttpException;
    const status = isHttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    // 获取堆栈信息
    const stack = exception instanceof Error ? exception.stack : undefined;

    // 记录错误日志
    this.logger.error(`${request.method} ${request.originalUrl} - ${status}`, {
      requestId: request.id,
      statusCode: status,
      method: request.method,
      url: request.originalUrl,
      ip: request.ip,
      stack,
      context: 'AllExceptionsFilter',
    });

    // 统一响应格式
    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: errorMessage,
      // 只在开发环境返回堆栈
      ...(process.env.NODE_ENV === 'development' && stack ? { stack } : {}),
    };

    response.status(status).json(errorResponse);
  }
}

注册过滤器(使用 APP_FILTER)

typescript
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

为什么使用 APP_FILTER

  • ✅ 支持依赖注入
  • ✅ 自动注册为全局过滤器
  • ✅ 无需在 main.ts 中手动创建实例

动态配置(高级用法)

从 ConfigService 注入配置

typescript
// logger.module.ts
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    WinstonModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        level: configService.get<string>('LOG_LEVEL', 'info'),
        transports: [
          new winston.transports.Console({
            format: winston.format.combine(
              winston.format.timestamp(),
              nestWinstonModuleUtilities.format.nestLike(),
            ),
          }),
          new winston.transports.DailyRotateFile({
            filename: configService.get<string>('LOG_FILE_PATH'),
            maxSize: configService.get<string>('LOG_MAX_SIZE', '20m'),
          }),
        ],
      }),
      inject: [ConfigService],
    }),
  ],
})
export class LoggerModule {}

环境变量配置

.env

env
LOG_LEVEL=info
LOG_FILE_PATH=logs/application/app.log
LOG_MAX_SIZE=20m
LOG_MAX_DAYS=30

与旧方案的对比

方案 1:WinstonModule.createLogger + 类属性(旧方案)

typescript
// main.ts
const app = await NestFactory.create(AppModule, {
  logger: WinstonModule.createLogger({
    instance: createWinstonLogger(),
  }),
});

// controller
export class UsersController {
  private readonly logger = new Logger(UsersController.name);

  getUsers() {
    this.logger.log('getUsers called'); // ✅ 自动有 context
  }
}

方案 2:forRootAsync + 手动传 context(基础 DI 方案)

typescript
// main.ts
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

// logger.module.ts
WinstonModule.forRootAsync({
  useFactory: () => createWinstonLoggerConfig(),
})

// controller
export class UsersController {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,
  ) {}

  getUsers() {
    this.logger.log('getUsers called', UsersController.name); // ⚠️ 需手动传 context
    this.logger.error('Error', undefined, UsersController.name); // ⚠️ error() 参数特殊
  }
}

方案 3:forRootAsync + createLogger(最佳实践 ⭐ 推荐)

typescript
// main.ts
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

// logger.module.ts
WinstonModule.forRootAsync({
  useFactory: () => createWinstonLoggerConfig(),
})

// controller
export class UsersController {
  readonly logger: ReturnType<typeof createLogger>;

  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    logger: LoggerService,
  ) {
    this.logger = createLogger(logger, UsersController.name); // ✅ 一次设置
  }

  getUsers() {
    this.logger.log('getUsers called');  // ✅ 自动 context
    this.logger.error('Error occurred'); // ✅ 智能 error()
  }
}

三种方案对比表

维度方案 1 (旧方案)方案 2 (基础 DI)方案 3 (createLogger)
Context 处理✅ 自动⚠️ 手动传参✅ 自动
error() 调用✅ 简单❌ 需注意参数顺序✅ 智能处理
代码简洁度✅ 1 行⚠️ 繁琐✅ 简洁
DI 原则❌ 绕过 DI✅ 完全遵循✅ 完全遵循
动态配置❌ 不支持✅ 支持✅ 支持
内存占用⚠️ 每个类一个实例✅ 共享实例✅ 共享实例
可测试性⚠️ 难以 mock✅ 易于 mock✅ 易于 mock
官方推荐度⚠️ 基础用法✅ 标准用法✅ 最佳实践

选择建议

  • 小型项目:方案 1 最简单(自动 context,无需 DI)
  • 中大型项目方案 3 最推荐(兼顾 DI 优势和代码简洁)
  • 需要动态配置:必须使用方案 2 或方案 3
  • 追求最佳实践方案 3 是最优选择

注意事项

1. 日志目录初始化

如果使用 Git,需要在 .gitignore 中添加:

gitignore
logs/
*.log

2. 敏感信息保护

生产环境不返回堆栈

typescript
{
  // 只在开发环境返回堆栈信息
  ...(process.env.NODE_ENV === 'development' && stack ? { stack } : {}),
}

3. TypeScript 类型扩展

typescript
declare module 'express' {
  export interface Request {
    id: string;        // 请求 ID
    startTime: number; // 请求开始时间
  }
}

4. 日志级别的使用建议

级别用途示例
error系统错误、异常数据库连接失败、未捕获的异常
warn警告信息已处理的异常、降级服务
info关键业务流程用户登录、订单创建
debug调试信息函数入参、中间变量
verbose详细日志详细的执行流程

总结

本指南实现了基于 nest-winston 官方最佳实践的完整日志系统,并提供了 createLogger 辅助方法来简化使用。

核心要点

  1. 使用 WinstonModule.forRootAsync() - 支持依赖注入和动态配置
  2. 在 main.ts 中使用 app.useLogger() - 替换框架 Logger
  3. 区分两种 Provider - 根据场景选择合适的注入方式
  4. 使用 createLogger 辅助方法 - 既保留 DI 优势,又实现自动 context
  5. 完全遵循 NestJS 理念 - 依赖注入、模块化、可测试

关键代码片段

Logger 模块

typescript
WinstonModule.forRootAsync({
  useFactory: () => createWinstonLoggerConfig(),
})

main.ts

typescript
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

Controller 使用(推荐方式)

typescript
import { createLogger } from '@/common/helpers/logger.helper';

export class UsersController {
  readonly logger: ReturnType<typeof createLogger>;

  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    logger: LoggerService,
  ) {
    this.logger = createLogger(logger, UsersController.name);
  }

  getUsers() {
    this.logger.log('getUsers called');  // ✅ 自动 context
    this.logger.error('Error occurred'); // ✅ 智能 error()
  }
}

最终推荐方案

使用 createLogger 辅助方法,它完美地解决了以下问题:

问题解决方案
需要手动传 context✅ 自动使用类名
error() 参数顺序特殊✅ 智能识别 trace vs context
绕过 DI✅ 完全遵循 DI 原则
代码繁琐✅ 简洁易用
难以测试✅ 易于 mock

这就是 nest-winston 的最佳实践,既强大又优雅!🎉

基于 VitePress 构建