NestJS 日志进阶:Winston & Pino 打造结构化日志记录体系
为什么需要结构化日志?
NestJS 内置日志
Winston:灵活强大的日志库
安装 Winston
基本使用
在 NestJS 中使用 Winston
Winston 的高级配置
Pino:高性能日志库
安装 Pino
基本使用
在 NestJS 中使用 Pino
Pino 的高级配置
日志的最佳实践
总结
作为一名 NestJS 开发者,你肯定遇到过这样的场景:应用出问题了,却苦于没有详细的日志信息来定位问题,或者日志信息杂乱无章,难以分析。别担心,今天咱们就来聊聊如何在 NestJS 中使用 Winston 和 Pino 这两个强大的日志库,构建一个结构化、可配置的日志记录体系,让你的应用调试和问题排查事半功倍。
为什么需要结构化日志?
在传统的开发模式中,我们可能习惯于使用 console.log()
来输出一些信息。这种方式虽然简单,但在生产环境中却存在很多问题:
- 信息杂乱:
console.log()
输出的信息通常是纯文本,缺乏结构,难以解析和分析。 - 难以过滤:无法根据日志级别(如 debug、info、error)进行过滤。
- 无法持久化:
console.log()
的输出通常只在控制台中显示,无法持久化到文件或数据库中。 - 性能问题:在生产环境中大量使用
console.log()
可能会影响应用性能。
结构化日志则可以解决这些问题。它将日志信息以结构化(如 JSON)的形式记录,包含时间戳、日志级别、消息内容、上下文信息等,方便后续的分析、查询和可视化。
NestJS 内置日志
NestJS 内置了一个简单的日志系统 Logger
。你可以通过 Logger
类来记录日志:
import { Logger } from '@nestjs/common'; const logger = new Logger('MyService'); logger.log('This is a log message.'); logger.error('This is an error message.'); logger.warn('This is a warning message.'); logger.debug('This is a debug message.'); logger.verbose('This is a verbose message.');
NestJS 的内置 Logger 提供了基本的日志功能,但在实际项目中,我们通常需要更强大的日志解决方案,例如:
- 自定义日志格式:例如 JSON 格式,方便日志收集和分析工具处理。
- 多样的输出目标:例如控制台、文件、远程日志服务等。
- 更灵活的日志级别控制:例如根据环境(开发、测试、生产)设置不同的日志级别。
- 性能优化:例如异步日志写入,避免阻塞主线程。
这时候,我们就需要引入第三方日志库,例如 Winston 或 Pino。
Winston:灵活强大的日志库
Winston 是一个非常流行的 Node.js 日志库,它提供了丰富的功能和灵活的配置选项。
安装 Winston
npm install winston
基本使用
import * as winston from 'winston'; const logger = winston.createLogger({ level: 'info', // 设置日志级别 format: winston.format.json(), // 设置日志格式为 JSON transports: [ new winston.transports.Console(), // 输出到控制台 new winston.transports.File({ filename: 'combined.log' }), // 输出到文件 new winston.transports.File({ filename: 'error.log', level: 'error' }),//只记录error级别 ], }); logger.info('This is an info message.'); logger.error('This is an error message.');
在这个例子中,我们创建了一个 Winston logger,并配置了:
level
:日志级别为info
,表示只记录info
及以上级别的日志(info
、warn
、error
)。format
:日志格式为 JSON。transports
:定义了两个输出目标:Console
:输出到控制台。File
:输出到文件combined.log
。File
:输出到文件error.log
,只记录error级别
在 NestJS 中使用 Winston
要在 NestJS 中使用 Winston,我们可以创建一个自定义的 logger 服务。
// logger.service.ts import { Injectable, LoggerService } from '@nestjs/common'; import * as winston from 'winston'; @Injectable() export class WinstonLogger implements LoggerService { private logger: winston.Logger; constructor() { this.logger = winston.createLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'combined.log' }), ], }); } log(message: string, context?: string) { this.logger.info(message, { context }); } error(message: string, trace?: string, context?: string) { this.logger.error(message, { trace, context }); } warn(message: string, context?: string) { this.logger.warn(message, { context }); } debug(message: string, context?: string) { this.logger.debug(message, { context }); } verbose(message: string, context?: string) { this.logger.verbose(message, { context }); } }
在这个例子中:
- 我们创建了一个
WinstonLogger
类,实现了 NestJS 的LoggerService
接口。 - 在构造函数中,我们创建了一个 Winston logger,并根据环境变量
NODE_ENV
设置了日志级别。 - 我们实现了
LoggerService
接口的各个方法,将日志信息传递给 Winston logger。 - 使用了
winston.format.combine
组合了多种日志格式
接下来,我们需要在 NestJS 模块中提供这个自定义的 logger 服务。
// app.module.ts import { Module } from '@nestjs/common'; import { WinstonLogger } from './logger.service'; @Module({ providers: [ { provide: LoggerService, // 使用 LoggerService 作为提供者 useClass: WinstonLogger, // 使用 WinstonLogger 作为实现 }, ], }) export class AppModule {}
现在,我们就可以在 NestJS 应用的任何地方注入 LoggerService
并使用它了。
// my.service.ts import { Injectable, LoggerService } from '@nestjs/common'; @Injectable() export class MyService { constructor(private readonly logger: LoggerService) {} doSomething() { this.logger.log('Doing something...', MyService.name); // ... } }
Winston 的高级配置
Winston 提供了很多高级配置选项,可以满足各种复杂的日志需求。
- 自定义格式:除了 JSON 格式,Winston 还支持其他格式,例如
winston.format.simple()
、winston.format.printf()
等。你也可以自定义格式。 - 添加元数据:你可以在日志信息中添加自定义的元数据,例如请求 ID、用户信息等。
- 使用 transports:Winston 支持多种 transports,例如
Console
、File
、Http
、Stream
等。你也可以自定义 transports。 - 日志轮换:可以使用
winston-daily-rotate-file
进行日志轮换。
Pino:高性能日志库
Pino 是另一个流行的 Node.js 日志库,它以高性能著称。Pino 的设计理念是尽可能减少日志记录对应用性能的影响。
安装 Pino
npm install pino pino-pretty
pino-pretty
是一个可选的工具,可以将 Pino 输出的 JSON 日志格式化为更易读的格式。
基本使用
import pino from 'pino'; const logger = pino({ level: 'info', // 设置日志级别 transport:{ target: 'pino-pretty', options:{ colorize:true } } }); logger.info('This is an info message.'); logger.error('This is an error message.');
在 NestJS 中使用 Pino
与 Winston 类似,我们也可以创建一个自定义的 logger 服务来在 NestJS 中使用 Pino。
// logger.service.ts import { Injectable, LoggerService } from '@nestjs/common'; import pino from 'pino'; @Injectable() export class PinoLogger implements LoggerService { private logger: pino.Logger; constructor() { this.logger = pino({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', transport:{ target: 'pino-pretty', options:{ colorize:true } } }); } log(message: string, context?: string) { this.logger.info({ context }, message); } error(message: string, trace?: string, context?: string) { this.logger.error({ context, trace }, message); } warn(message: string, context?: string) { this.logger.warn({ context }, message); } debug(message: string, context?: string) { this.logger.debug({ context }, message); } verbose(message: string, context?: string) { this.logger.verbose({ context }, message); } }
然后,在 AppModule
中提供这个自定义的 logger 服务:
// app.module.ts import { Module } from '@nestjs/common'; import { PinoLogger } from './logger.service'; @Module({ providers: [ { provide: LoggerService, useClass: PinoLogger, }, ], }) export class AppModule {}
Pino 的高级配置
- 自定义日志级别:Pino 允许你自定义日志级别。
- 添加钩子:你可以在日志记录的不同阶段添加钩子,例如在记录日志前修改日志信息。
- 子 logger:你可以创建子 logger,继承父 logger 的配置,并添加额外的元数据。
- 异步模式:Pino 默认使用异步模式,将日志写入操作放在事件循环的下一个 tick 中执行,避免阻塞主线程。 你也可以使用
pino.transport
进行配置。
日志的最佳实践
- 选择合适的日志级别:根据不同的环境(开发、测试、生产)设置不同的日志级别。在开发环境中,可以使用较低的日志级别(如
debug
)来记录更详细的信息;在生产环境中,使用较高的日志级别(如info
或error
)来减少日志量。 - 记录有用的信息:日志应该记录足够的信息来帮助你定位问题,但也要避免记录过多的无用信息。例如,可以记录请求的 URL、参数、响应状态码、错误堆栈等。
- 使用结构化日志:使用 JSON 格式记录日志,方便后续的分析和查询。
- 添加上下文信息:在日志信息中添加上下文信息,例如请求 ID、用户信息等,方便追踪和关联日志。
- 日志轮换:对于文件日志,要定期轮换日志文件,避免单个日志文件过大。
- 集中式日志管理:在生产环境中,建议使用集中式日志管理系统(如 ELK Stack、Graylog、Splunk 等)来收集、存储和分析日志。
总结
通过本文,相信你已经对如何在 NestJS 中使用 Winston 和 Pino 构建结构化日志记录体系有了更深入的了解。选择哪个日志库取决于你的具体需求,Winston 功能更丰富,Pino 性能更高。记住,良好的日志记录是应用可维护性和可观察性的基石,希望你能充分利用这些工具,让你的 NestJS 应用更上一层楼!