Nest.js使用nest-pino记录日志
后端服务肯定少不了日志,没打日志出了问题就只能抓瞎。当然,如果你能找到神奇海螺问问,或者自己就会用水晶球占卜,不打日志还是可以的。
Node.js 流行的日志记录库,大概有 Winston(20.2K 星星)、Bunyan(7K 星星)、Pino.js(11.1K 星星)这几种,现在我一般都会优先考虑 Pino.js。原因很简单,馋它的高性能。具体性能对比可以参考 Pino.js 的 Benchmarks 。
Nest.js 底层依赖的 Web 框架我默认都是换成 Fastify 的,然后发现 Fastify 默认的日志记录器就是 Pino.js,就想着偷个懒,在初始化 FastfiyAdapter 的时候,传入参数配置,启用 Pino.js 然后将实例导出,供其他地方调用。
大致逻辑如下:
// fastify.adapter.ts import { FastifyAdapter } from '@nestjs/platform-fastify'; import { FastifyBaseLogger } from 'fastify'; const envLogger = { development: { transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', }, }, }, production: true, }; const fastifyAdapter: FastifyAdapter = new FastifyAdapter({ logger: envLogger[process.env.NODE_ENV] ?? true, }); const fastify: FastifyAdapter = fastifyAdapter.getInstance(); const logger = (fastify as any).log as FastifyBaseLogger; export { fastifyAdapter, fastify, logger }; // 其他业务模块.ts import { logger } from '@/common/adapters/fastify.adapter.ts' // 直接输出日志 logger.info("这是一条日志") // 或者是新实例化一个对象,保存当前调用上下文的名字,然后再使用,这样方便后期处理 const userLogger = logger.child({ context: UserService.name }); userLogger.info("用户鉴权失败");
这样的确可以使用,但是是有一些潜在的缺陷的,最根本的原因在于 Fastify 和 Nest.js 在封装上并不是一个级别的东西。Fastify 只是 Nest.js 下层依赖的一个 Web 框架,如果我们想要在 Nest.js 的生命周期事件或者是应用程序级别上打日志的话,上面的那个封装就力不从心了。
ummm,看样子最后还是偷懒不成的 = =。
好在,去 npm 上看了一下,发现有一个包叫做 nestjs-pino,看了一下 README 也比较符合需求,直接拿过来用就行了,然后将原本服务中的 logger 命令改一下,完事。
下面说一下我使用 nest-pino 这个包的大致流程。
首先,
pnpm add nestjs-pino
安装依赖,然后去app.module.ts
中引入,// app.module.ts import { LoggerModule } from 'nestjs-pino'; import { loggerOptions } from './processors/logger/logger.config'; @Module({ imports: [ // 这里导入并使用了一个自定义的配置项 LoggerModule.forRoot({ pinoHttp: loggerOptions }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
然后,在 logger.config.ts 中,写入自定义的配置
import pino from 'pino'; import { Options } from 'pino-http'; /** 日志保存的路径 */ const LOG_PATH = './logs/log.log'; /** 开发环境下的配置选项 */ const devOptions: Options = { /** 对每个请求/响应的自动日志设置上下文的值 */ // customProps: (req, res) => ({ context: 'HTTP' }), transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', }, }, // 输出到控制台,或者设置成如下,同时输出到控制台和文件中: // pino.multistream([pino.destination(LOG_PATH), process.stdout]) stream: pino.destination(process.stdout), }; /** 生产环境下的配置选项 */ const proOptions: Options = { stream: pino.destination(LOG_PATH), }; /** 根据环境切换日志配置 */ const envLogger = { development: devOptions, production: proOptions, }; /** Pino日志配置 */ export const loggerOptions: Options = envLogger[process.env.NODE_ENV];
上面的代码,主要是区分开发环境和生产环境,对日志做了不同的处理:在开发环境下,日志直接输出到控制台,生产环境下,日志输出到自定义的路径下。如果项目中有统一的配置管理,应该将这个日志路径的配置和其它的配置项放在一起。
额外解释一下,nestjs-pino 是 基于 http-pino 进行封装的,它的一个特性就是可以自动将请求相关的数据绑定到日志上。但是默认绑定的日志是没有上下文的,所以为了方便后期进行分析或处理,可以使用
customProps: (req, res) => ({ context: 'HTTP' }),
这个配置打上上下文的标记。💡 上面的配置项依赖于 pino、http-pino、pino-pretty,需要安装对应的依赖。
nestjs-pino 依赖于 http-pino,而 http-pino 依赖于 pino,可以看作是一个整体的包。pino-pretty 只需要安装为开发依赖即可,使用它主要是为了在开发环境中将 JSON 格式化为人类更易读的格式。最后,我们在 main.ts 中引入
import { AppModule } from './app.module'; import { fastifyAdapter } from './common/adapters/fastify.adapter'; import { Logger } from 'nestjs-pino'; import { NestFactory } from '@nestjs/core'; import { NestFastifyApplication } from '@nestjs/platform-fastify'; async function bootstrap() { const app = await NestFactory.create<NestFastifyApplication>( AppModule, fastifyAdapter, { cors: true, bufferLogs: true }, ); app.useLogger(app.get(Logger)); await app.listen(PORT, ADDRESS); } bootstrap();
这样就可以在项目中使用了,还是默认的日志记录器的方法
// 注意:这里是从@nestjs/common包里导入的Nest.js内置日志记录器 // 但是它内部由Pino.js驱动 import { Logger } from '@nestjs/common'; export class UserService { // 这里传入当前服务的名字,是为了之后方便判断日志是由哪个业务模块打印的 private readonly logger = new Logger(UserService.name); constructor() {} async getUserById(uId: number) { // 主要注意的一点是Pino.js的`trace`和`info`方法,映射为了`verbose`和`log`方法 this.logger.error('巴拉巴拉巴拉'); } }
除了这种方式,还可以使用 Nest.js 常见的依赖注入的方式来使用,可参考 官方文档
import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'; export class MyService { // 例1 constructor(private readonly logger: PinoLogger) { this.logger.setContext(MyService.name); } // 例2 constructor( @InjectPinoLogger(MyService.name) private readonly logger: PinoLogger, ) {} foo() { this.logger.trace({ foo: 'bar' }, 'baz %s', 'qux'); this.logger.debug('foo %s %o', 'bar', { baz: 'qux' }); this.logger.info('foo'); } }
最后要说的一点就是使用日志对服务性能会有较大的影响,但是这一部分的开销是无法节省的,只能尽可能的优化。
Node.js 部署的项目,一般都会在前面套一个 Nginx 来配合使用,起到一个反向代理或负载均衡的作用。而 Nginx 本身也是带有 HTTP 请求的日志记录功能的,所以注意一下 Nginx 和项目的日志记录不要重复。
可以使用 Nginx 来记录 HTTP 日志,而将 nestjs-pino 的请求记录关闭,在应用内手动在关键的地方打日志。
如果觉得这样麻烦,也可以反过来,关闭 Nginx 的日志,所有的日志都由项目自身来处理。