日志模块 pino
-
安装:$
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
-
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
- 在 app.module 中添加 exports
- 第三种是全局使用:
- 如果想让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)
- 像是上面几种,都是有颜色区分的
- 如果想让logger变成全局模块,logger中的方法在全局可直接使用,则需要这样
- 第一种是每个controller中引用的时候,在对应的module,比如user.module.ts的 providers中提供,不推荐
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,一般日志手动加,方便调试