Nestjs框架: 可集成在Nestjs上的日志模块pino和winston的使用

文章讲述了在NestJS中使用Pino和Winston两种日志模块的实践,比较了它们的易用性、配置灵活性和性能。Pino适合懒人使用,自动打印日志,但输出不友好;Winston提供了更多的自定义选项,但需手动添加日志。NestJS内置的Logger不够详细,适合基本需求,但对性能有要求的场景可考虑Pino或Winston。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

日志模块 pino

  • npmjs.com/package/pino

  • getpino.io

  • 安装:$ npm i nestjs-pino

  • 注册:

    import { LoggerModule, Logger } from 'nestjs-pino'
    
    @Module({
      controllers: [AppController],
      imports: [LoggerModule.forRoot()] // 注意这里
    })
    
  • 在控制器中测试使用

    import { Logger } from 'nestjs-pino'
    
    @Controller('user')
    export class UserController {
      private logger = Logger
      constructor() {
        this.logger.log('init') // 类似这样,在需要的地方调用
      }
    }
    
    • 这样写,在访问这个控制器的时候,会打印两次,因为不需要手动调用,所以
    • 需要删除这个 this.logger.log('init') 调用,因为这个是没有必要的
    • 所以 pino 是一个比较适合懒人的库
    • 它会默认打印日志,但是目前看来,日志输出不友好(不好阅读)

友好化 pino 方案

  • $ npm i pino-pretty pino-roll

  • 在 user.module.ts 中

    import { LoggerModule } from 'nestjs-pino'
    const isDev = process.env.NODE_ENV === 'development'
    @Module({
      imports: [LoggerModule.forRoot({
        pinoHttp: {
          transport: isDev ? {
            target: 'pino-pretty',
            level: 'info',
            options: {
              colorize: true
            }
          } : {
            target: 'pino-roll',
            level: 'info',
            options: {
              file: path.join('log', 'log.txt'), // 文件名称可以用时间
              frequency: 'daily', // hourly
              size: '10m' // 只要超过这个,就会多生成一个文件
              mkdir: true
            }
          }
        }
      })]
    })
    
  • 这里开发环境和生产环境用的不同的pino插件

  • pino是一个开箱即用,稍稍配置就可以使用的日志插件

高度集成的日志模块 winston

  • 如果把pino比作成vite, winston就像是webpack

  • npmjs.com/package/winston

  • winston官方没有提供与nestjs集成的demo

  • nest-winston 可以和nestjs高度集成,安装:

    • $ npm i -S nest-winston winston
  • 示例都在 npm 上

  • 找到 文档 Replacing the Nest logger (also for bootstrapping)

    • 这里的配置可以用到nestjs工程里
    • 这里就是在启动的时候,把 nest 的 logger 替换为 winston 的 logger
  • 集成

    // main.ts
    import { transports, format, createLogger } from 'winston';
    import { WinstonModule, utilities } from 'nest-winston';
    
    async function bootstrap() {
      // 第一步 创建 ins
      const instance = createLogger({
        transports: [
          new transports.Console({
            format: format.combine(
              format.timestamp(),
              utilities.format.nestLike(),
            )
          })
        ]
      })
      const app = await NestFactory.create(AppModule, {
        // 第二步 配置 logger
        logger: WinstonModule.createLogger({
          instance, // 使用 ins
        })
      })
    }
    
  • 通过以上就创建好了,但是 还要在 app.module.ts 中全局提供logger

    import { Logger, Module } from '@nestjs/common'
    
    @Module({
      providers: [Logger] // 后面进行 DI 依赖注入
    })
    
    export class AppModule {}
    
  • 这里为什么要导入 @nestjs/common 中的 logger

    • 我们可以使用 implements LoggerService 来重构官方的logger方法
    • 如果使用官方的 demo 在user.controller.ts中来使用 @Inject来注册则会报错
  • 解决问题

    • 我们替换了nestjs默认的logger, 并且在app.module.ts中providers提供了logger
    • 但是其他模块并不知道,这样,有3种解决方式
      • 第一种是每个controller中引用的时候,在对应的module,比如user.module.ts的 providers中提供,不推荐
        • 参考:https://github.com/gremo/nest-winston/tree/main/sample/replace-nest-logger-bootstrap/src
        • 这里写法和上面我们的一致,它在自己的控制器中引入,并使用
        • 我们知道Nest整个系统的运行是先告诉自己的DI, 哪些实例去进行注册,模块与模块之间如果要相互引用
        • 就要把它 export 出来 或者把这个模块注册成为一个全局模块,其他模块才能去使用
        • 所以这个问题就是,DI系统不知道 Logger 从哪里来,虽然替换了 Nestjs中的 Logger, 并且在 app.module中提供了Logger
        • 但是其他模块并不知道,常见的处理方法是,比如 user.module中需要,就在 user.module.ts 中的providers中再次配置一次 Logger
        • 但是这就比较麻烦了
      • 第二种是在app.module.ts中providers下面加一个exports的配置,在其他模块进行import即可
        • 在 app.module 中添加 exports
          // app.module
          providers: [Logger]
          exports: [Logger]
          
          • 在其他模块,比如在user.module中进行import
      • 第三种是全局使用:
        • 如果想让logger变成全局模块,logger中的方法在全局可直接使用,则需要这样
          import {} from '@nest/common'
          // app.module
          @Global() // 加上global
          @Module({}) // 内部不变,仍要加 exports
          
        • 也就是说使用Global注解,将app.module模块全局模块
        • 使用的时候,在 user.module 中,其实这个logger 不需要这样写,在 constructor 参数中
          import { Logger } from '@nestjs/common'
          constructor(
            private userService: UserService,
            private configService: ConfigService,
            @Inject(Logger) private readonly logger: LoggerService
          )
          
        • 换成下面的写法
          import { Logger } from '@nestjs/common'
          constructor(
            private userService: UserService,
            private configService: ConfigService,
            private readonly logger: Logger, // 注意这里
          )
          
        • 之后,就可以在某个方法(API)中进行测试了
          this.logger.log('test log', UserController.name)
          this.logger.warn('test warn', UserController.name)
          this.logger.error('test error', UserController.name)
          this.logger.debug('test debug', UserController.name)
          this.logger.verbose('test verbose', UserController.name)
          
        • 像是上面几种,都是有颜色区分的

winston的滚动日志

  • 安装 winston-daily-rotate-file 这个库 $ npm i -S winston-daily-rotate-file
  • 安装后,在main.ts中
    import 'winston-daily-rotate-file'
    
    // 之后在 bootstrap 的 createLogger中的 transports中 新 new 一个
    // 而且提供一些 events: archive, 
    new winston.transports.DailyRotateFile({
      // ...填入官方配置
      level: 'warn',
      dirname: 'logs', // 在生产环境中指定一个绝对路径比较好,方便做一个文件映射
      filename: 'application-%DATE%log'
      datePattern: 'YYYY-MM-DD-HH',
      zippedArchive: true, // 对文件压缩
      maxSize: '20m',
      maxFiles: '14d', // 超过14天会自动删除
      format: winston.format.combine(
        winston.format.timestamp(),
        utilities.format.nestLike(),
        // winston.format.simple(), // 打印简洁版本
      )
    })
    
  • 它提供的 events 比如每次打包的时候,archive; 执行的时候 rotate
  • 注意,上述 new winston.transports.DailyRotateFile 方法可以复制多份
  • 比如基于 level 配置项的不同,而进行不同的打印或输出
  • 当然可以封装一个方法来复用,参数化进行区分
  • 更多信息,浏览文档

winston 业务场景的应用

  • winston 这个对性能损耗会高一些,但是很多场景下,需要手动添加日志,方便调试

  • 开发了很多接口,这些接口,一般在逻辑处理的地方加try catch,

  • 在catch里加入logger, 但是每一个都要加,非常难受

  • 我们希望让错误的场景全都记录到日志中,方便调试

  • 在nestjs中内部提供了try-catch这种运行机制, 对所有Exception进行捕获

  • 之后交给 Exception Filter HttpException

  • 这里过滤器又分为:全局过滤器,控制器过滤器,路由过滤器

  • 我们想要全局的日志放入到日志文件中去

    Exception(异常) ---->  try...catch...(Nestjs自动) ----normal---> 运行逻辑准备结果返回
                              |                                          |
                              | error                                    |
                              |                                          |
                              ↓                                          ↓
                      Exception Filter  -----------4xx--5xx------>   响应给前端
                      HttpException
                          (全局Filter)
                          (控制器Filter)
                          (路由Filter)
    
  • 异常过滤器是内置的异常层,负责处理整个应用程序中所有抛出的异常

  • 处于整个nestjs生命周期的末端,是由三个部分组成

    • 路由过滤器
    • 控制器过滤器
    • 全局过滤器
  • 作用是给与前端友好响应

1 )这种单个接口定制的处理

import { UnauthorizedException } from '@nestjs/common'

// 针对某种逻辑,抛出指定异常
if (isSomethingLogin) throw new UnauthorizedException('无权限') // 响应用户时,会自动修改状态码

2 )全局

  • 新建 filter: src/filters/http-exception.filter.ts

    import {
      ArgumentsHost,
      Catch,
      ExceptionFilter,
      HttpException,
      LoggerService
    } from '@nestjs/common'
    
    // 固定注解
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
      constructor(private logger: LoggerService) {}
      // ExceptionFilter 类中有一个 catch 方法, 这里来进行实现
      catch(exception: HttpException, host: ArgumentsHost) {
        // host代表nestjs整个进程
        const ctx = host.switchToHttp() // 找到上下文,所有参数都会在ctx上面 
        const res = ctx.getResponse() // 响应对象
        const req = ctx.getRequest() // 请求对象
        const status = exception.getStatus() // http状态码
        this.logger.error(exception.message, exception.stack) // 日志记录到文件
        // 响应给用户
        res.status(status).json({
          code: status,
          timestamp: new Date().toISOString(),
          // path: req.url,
          // method: req.method,
          message: exception.message || exception.name
        })
        throw new Error('Method not implemented.')
      }
    }
    
  • 在main.ts 中添加Filter

    app.useGlobalFilters(new HttpExceptionFilter(logger)) // 注意全局filter只能有一个
    

总结

  • 两个第三方的包:winston, pino
    • winston好处:DailyRotateFile 方便
    • winston缺点:是需要在自己手动加入日志,, 麻烦
    • pino的好处:开箱即用
    • pino的缺点:配置没有winston丰富
  • Nest官方内置的日志,够用,但是不详细
  • 关于选择
    • 懒人用pino, 而且性能较高
    • 用 winston,一般日志手动加,方便调试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值