Skip to content
0

NestJS 集成 Winston 日志

本指引详细介绍如何在 NestJS 项目中集成 Winston 日志系统,并通过中间件记录每一次接口调用日志,便于后续项目快速复用。

目录结构建议

bash
src/
├── common/
   └── middleware/
       └── logger.middleware.ts   # 接口调用日志中间件
├── config/
   └── winston.config.ts       # winston 日志配置
├── app.module.ts
└── main.ts

1. 安装依赖

bash
pnpm add chalk winston winston-daily-rotate-file nest-winston

提示

chalk 是一个用于在终端中输出彩色文本的库。
winston 是一个功能强大的日志库,支持多种传输方式和格式化选项。
winston-daily-rotate-file 是 Winston 的一个传输器,用于将日志输出到文件,并按天轮转。
nest-winston 是 NestJS 的 Winston 集成包,提供了更好的集成和配置方式。

2. 配置 Winston 日志

src/config/winston.config.ts 中配置 winston:

typescript
import chalk from "chalk";
import { createLogger, format, transports } from "winston";
import * as DailyRotateFile from "winston-daily-rotate-file";

const winstonLogger = createLogger({
  format: format.combine(format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" })),
  transports: [
    new DailyRotateFile({
      filename: "logs/errors/error-%DATE%.log", // 日志名称,占位符 %DATE% 取值为 datePattern 值。
      datePattern: "YYYY-MM-DD-HH", // 日志轮换的频率,此处表示 1 小时轮换一次。
      zippedArchive: true, // 是否通过压缩的方式归档被轮换的日志文件。
      maxSize: "20m", // 设置日志文件的最大大小,m 表示 mb 。
      level: "error", // 日志类型,此处表示只记录错误日志。
      format: format.combine(
        format.printf((info) => {
          return `${String(info.timestamp)}\n${"*".repeat(
            100
          )}\n错误信息: ${String(info.message)}\n堆栈信息: ${String(
            info.stack
          )}\n${"*".repeat(100)}\n`;
        })
      ),
    }),
    new DailyRotateFile({
      filename: "logs/warnings/warning-%DATE%.log",
      datePattern: "YYYY-MM-DD-HH",
      zippedArchive: true,
      maxSize: "20m",
      level: "warn",
      format: format.combine(
        format.printf((info) => {
          return `${String(info.timestamp)}\n${"*".repeat(
            100
          )}\n警告信息: ${String(info.message)}\n${"*".repeat(100)}\n`;
        })
      ),
    }),
    new DailyRotateFile({
      filename: "logs/info/info-%DATE%.log",
      datePattern: "YYYY-MM-DD-HH",
      zippedArchive: true,
      maxSize: "20m",
      level: "info",
      format: format.combine(
        format.printf((info) => {
          if (info.level === "error") {
            return `${String(info.timestamp)}\n${"*".repeat(100)}\n${"=".repeat(
              20
            )}>错误信息: ${String(info.message)}\n${"=".repeat(
              20
            )}>堆栈信息: ${String(info.stack)}\n${"*".repeat(100)}\n`;
          } else if (info.level === "warn") {
            return `${String(info.timestamp)}\n${"*".repeat(100)}\n${"=".repeat(
              20
            )}>警告信息: ${String(info.message)}\n${"*".repeat(100)}\n`;
          } else {
            return `${String(info.timestamp)} ${String(info.message)}`;
          }
        })
      ),
    }),
    new transports.Console({
      format: format.combine(
        format.printf((info) => {
          const chalkColor = getChalkColor(info.level);
          return `${chalkColor(info.timestamp)} ${chalkColor(info.message)}`;
        })
      ),
      level: "debug",
    }),
  ],
});

const getChalkColor = (level: string) => {
  switch (level) {
    case "error":
      return chalk.red;
    case "warn":
      return chalk.yellow;
    case "info":
      return chalk.green;
    case "debug":
      return chalk.blue;
    case "verbose":
      return chalk.cyan;
    default:
      return chalk.white;
  }
};

export default winstonLogger;

3. 在 AppModule 中集成 Winston

src/app.module.ts 中引入并注册 Winston 日志模块:

typescript
import { WinstonModule } from 'nest-winston'; 
import winstonLogger from './config/winston.config'; 

@Module({
  imports: [
    WinstonModule.forRoot({ 
      transports: winstonLogger.transports, 
      format: winstonLogger.format, 
      defaultMeta: winstonLogger.defaultMeta, 
      exitOnError: false, 
    }), 
  ],
  // ...
})

4. 在 main.ts 中设置全局 Logger

typescript
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston"; 

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 
  await app.listen(process.env.PORT ?? 3000);
}

5. 编写接口调用日志中间件

src/common/middleware/logger.middleware.ts

typescript
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger();
  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip, httpVersion, headers } = req;
    const { statusCode } = res;
    const logFormat = `接口调用 - 调用地址: [${originalUrl}] 调用方法: [${method}] HTTP 协议版本: [HTTP/${httpVersion}] 客户端IP: [${ip}] 状态码: [${statusCode}] User-Agent: [${headers["user-agent"]}]`;
    if (statusCode >= 500) {
      this.logger.error(logFormat, originalUrl);
    } else if (statusCode >= 400) {
      this.logger.warn(logFormat, originalUrl);
    } else {
      this.logger.log(logFormat, originalUrl);
    }
    next();
  }
}

如需将日志直接写入 winston,可将 Logger 替换为自定义的 winstonLogger。

6. 注册中间件

app.module.ts 中注册中间件:

typescript
import { LoggerMiddleware } from "./common/middleware/logger.middleware";
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes("*");
  }
}

7. 日志文件输出说明

  • 错误日志:logs/errors/error-YYYY-MM-DD-HH.log
  • 警告日志:logs/warnings/warning-YYYY-MM-DD-HH.log
  • 普通日志:logs/info/info-YYYY-MM-DD-HH.log

8. 常见问题排查

  • 日志文件为空:请确认有实际调用日志方法,且日志目录有写入权限。
  • 日志级别未触发:如只调用 info,error/warn 文件不会有内容。
  • 进程未正常退出可能导致日志未及时写入磁盘。

如需扩展日志内容或格式,可在 winston.config.ts 中自定义 format。