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_PROVIDER | LoggerService | NestJS Logger 接口实现 | Controllers, Services |
| WINSTON_MODULE_PROVIDER | winston.Logger | 原生 Winston Logger | Filters, 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 系统中注册 Winstonapp.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分层策略
| 目录 | 日志级别 | 保留时间 | 用途 |
|---|---|---|---|
| access | info | 7天 | 访问日志,量大但价值低 |
| application | info/warn/error | 30天 | 业务日志,常规排查 |
| errors | error | 60天 | 错误日志,长期追踪 |
日志轮转配置
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/
*.log2. 敏感信息保护
生产环境不返回堆栈:
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 辅助方法来简化使用。
核心要点
- ✅ 使用
WinstonModule.forRootAsync()- 支持依赖注入和动态配置 - ✅ 在 main.ts 中使用
app.useLogger()- 替换框架 Logger - ✅ 区分两种 Provider - 根据场景选择合适的注入方式
- ✅ 使用
createLogger辅助方法 - 既保留 DI 优势,又实现自动 context - ✅ 完全遵循 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 的最佳实践,既强大又优雅!🎉