NestJS 日志记录终极指南:从入门到生产级实践
为什么要重视日志记录?
NestJS 内置日志模块:Logger
基本用法
日志级别
进阶:使用第三方日志库
Winston
安装
基本用法
Pino
安装
基本用法
NestJS 日志最佳实践
1. 使用有意义的日志消息
2. 选择合适的日志级别
3. 包含上下文信息
4. 处理敏感信息
5. 日志格式化
6. 日志轮转
安装
使用
7. 日志聚合和分析
总结
“哎,老哥,你这 NestJS 项目的日志是不是有点乱啊?”
“啊?有吗?我觉得还行吧,能 console.log
就行了呗。”
“console.log
大法好是好,但真出了问题,你这漫山遍野的 console.log
,找起来不跟大海捞针一样?”
相信不少 NestJS 开发者都遇到过类似上面这样的对话。在开发阶段,console.log
确实方便快捷,但到了生产环境,它就显得力不从心了。一个健壮的 NestJS 应用,必须要有完善的日志记录系统。今天,咱们就来聊聊 NestJS 日志记录的那些事儿,从基础概念到最佳实践,再到生产环境下的各种骚操作,保证让你一次性搞懂 NestJS 日志!
为什么要重视日志记录?
在深入探讨 NestJS 日志之前,咱们先来明确一下,为什么要重视日志记录?它可不仅仅是为了“记录”而“记录”,而是有着实实在在的价值:
- 故障排查: 这是日志最直接的作用。当系统出现问题时,详细的日志信息可以帮助你快速定位问题根源,就像福尔摩斯探案一样,从蛛丝马迹中找到真凶。
- 性能监控: 通过分析日志中的请求时间、响应时间、数据库查询时间等信息,你可以了解系统的性能瓶颈,从而进行针对性的优化。
- 安全审计: 日志可以记录用户的操作行为、登录信息、权限变更等,为安全审计提供依据,帮助你发现潜在的安全风险。
- 业务分析: 通过分析用户行为日志,你可以了解用户的喜好、习惯,从而优化产品功能,提升用户体验。
- 合规性要求: 某些行业(如金融、医疗)对日志记录有严格的合规性要求,必须记录特定的操作和数据。
总而言之,日志记录是系统开发和运维中不可或缺的一环。一个好的日志系统,可以让你事半功倍,而一个糟糕的日志系统,则会让你焦头烂额。
NestJS 内置日志模块:Logger
NestJS 内置了一个简单的日志模块 Logger
,它提供了基本的日志记录功能。咱们先来看看如何使用它。
基本用法
import { Logger } from '@nestjs/common'; export class MyService { private readonly logger = new Logger(MyService.name); async doSomething() { this.logger.log('开始执行 doSomething 方法...'); try { // ... 执行一些操作 ... this.logger.debug('操作成功!'); } catch (error) { this.logger.error('执行 doSomething 方法出错:', error.stack); } this.logger.verbose('doSomething 方法执行完毕。'); } }
在这个例子中,我们首先导入了 Logger
类,然后在 MyService
类中创建了一个 Logger
实例。Logger
构造函数接收一个可选参数,用于指定日志的上下文(context),通常我们会使用类的名称作为上下文。这样,在日志输出中,我们就可以看到这条日志是由哪个类产生的。
Logger
类提供了以下几个方法来记录不同级别的日志:
log
:普通日志,用于记录一般信息。error
:错误日志,用于记录错误信息和异常。warn
:警告日志,用于记录可能存在问题的警告信息。debug
:调试日志,用于记录调试信息,通常只在开发环境中使用。verbose
:详细日志,用于记录更详细的信息,通常只在开发环境中使用。
在上面的例子中,我们分别使用了 log
、debug
、error
和 verbose
方法来记录不同级别的日志。在 error
方法中,我们还传入了 error.stack
,这样可以打印出完整的错误堆栈信息,方便我们定位问题。
日志级别
日志级别用于区分日志的重要性。NestJS 内置的 Logger
支持以下五个级别(从低到高):
verbose
debug
log
warn
error
默认情况下,Logger
会输出 log
、warn
和 error
级别的日志。你可以通过设置 logLevel
选项来改变日志级别:
import { Logger, LogLevel } from '@nestjs/common'; const logger = new Logger('MyContext', { logLevel: 'debug' });
或者,你也可以通过设置环境变量 LOG_LEVEL
来改变日志级别:
LOG_LEVEL=debug nest start
通常,在开发环境中,我们会将日志级别设置为 debug
或 verbose
,以便输出更详细的日志信息。在生产环境中,我们会将日志级别设置为 log
或 warn
,以减少日志输出量,提高系统性能。
进阶:使用第三方日志库
NestJS 内置的 Logger
虽然简单易用,但功能比较有限。在实际项目中,我们通常会使用更强大的第三方日志库,如 winston
、pino
等。
Winston
winston
是一个非常流行的 Node.js 日志库,它提供了丰富的功能和灵活的配置选项。
安装
npm install winston
基本用法
// logger.service.ts import { Injectable, LoggerService } from '@nestjs/common'; import * as winston from 'winston'; @Injectable() export class WinstonLogger implements LoggerService { private readonly logger: winston.Logger; constructor() { this.logger = winston.createLogger({ level: 'info', // 设置日志级别 format: winston.format.combine( winston.format.timestamp(), // 添加时间戳 winston.format.printf(({ level, message, timestamp, context }) => { return `${timestamp} [${context}] ${level}: ${message}`; }) ), transports: [ new winston.transports.Console(), // 输出到控制台 // 可以添加其他 transports,如文件、数据库等 ], }); } log(message: string, context?: string) { this.logger.log('info', message, { context }); } error(message: string, trace?: string, context?: string) { this.logger.error(message, { context, trace }); } 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 }); } }
// app.module.ts import { Module, Logger } from '@nestjs/common'; import { WinstonLogger } from './logger.service'; import { MyService } from './my.service'; @Module({ imports: [], controllers: [], providers: [MyService, WinstonLogger, { provide: Logger, useClass: WinstonLogger }], }) export class AppModule {}
在这个例子中:
- 我们创建了一个
WinstonLogger
类,实现了LoggerService
接口,这样我们就可以在 NestJS 中使用winston
了。 - 在
WinstonLogger
的构造函数中,我们使用winston.createLogger
创建了一个winston
实例,并配置了日志级别、格式和 transports。 - 在
AppModule
中,我们将WinstonLogger
设置为了全局的日志记录器。
现在,你就可以在你的服务中注入Logger
,并使用它来记录日志,和之前Nest内置的logger用法一致。
Pino
Pino 是另一个流行的 Node.js 日志库,它以高性能著称。Pino 的核心理念是尽可能减少日志记录对应用程序性能的影响。
安装
npm install pino pino-pretty
pino-pretty
是一个可选的工具,它可以将 Pino 输出的 JSON 格式日志美化成人类可读的格式。
基本用法
// logger.service.ts import { Injectable, LoggerService } from '@nestjs/common'; import * as pino from 'pino'; @Injectable() export class PinoLogger implements LoggerService { private readonly logger: pino.Logger; constructor() { this.logger = pino({ level: 'info', // 设置日志级别 prettyPrint: { levelFirst: true, translateTime: 'SYS:standard', // 美化时间格式 }, // 启用 prettyPrint }); } 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); } }
// app.module.ts import { Module, Logger } from '@nestjs/common'; import { PinoLogger } from './logger.service'; import { MyService } from './my.service'; @Module({ imports: [], controllers: [], providers: [MyService, PinoLogger, { provide: Logger, useClass: PinoLogger }], }) export class AppModule {}
和 Winston 类似,创建一个 PinoLogger
并且在 AppModule
中设置为全局Logger。
Pino 输出的是 JSON 格式的日志,如果你想在开发环境中查看美化后的日志,可以在启动命令中添加 | pino-pretty
:
nest start | pino-pretty
NestJS 日志最佳实践
掌握了日志库的基本用法后,咱们再来看看在 NestJS 项目中,如何更好地记录日志。
1. 使用有意义的日志消息
日志消息应该清晰、简洁、易于理解。避免使用含糊不清、过于技术化的词语。好的日志消息应该能够回答以下几个问题:
- 发生了什么?
- 什么时候发生的?
- 在哪里发生的?
- 为什么会发生?
- 谁触发的? (如果涉及到用户操作)
例如,下面这条日志消息就比较糟糕:
this.logger.log('Something happened.');
它没有提供任何有价值的信息。我们可以把它改成这样:
this.logger.log('Failed to create user. Email already exists.', 'UserController');
这条消息就清晰多了,它告诉我们:
- 发生了什么:创建用户失败
- 为什么会发生:邮箱已存在
- 在哪里发生的:
UserController
2. 选择合适的日志级别
选择合适的日志级别非常重要。过多的低级别日志会淹没重要的信息,而过少的高级别日志则可能导致你错过关键的错误。以下是一些建议:
error
:用于记录严重的错误,这些错误会导致应用程序无法正常运行。warn
:用于记录潜在的问题,这些问题可能会导致错误,但目前还没有发生。log
:用于记录一般的信息,如请求处理、数据库查询等。debug
:用于记录调试信息,如变量的值、函数的调用等。只在开发环境中使用。verbose
:用于记录更详细的信息,如请求的完整内容、响应的完整内容等。只在开发环境中使用。
3. 包含上下文信息
在日志消息中包含上下文信息,可以帮助你更快地定位问题。上下文信息可以包括:
- 类名、方法名
- 请求 ID
- 用户 ID
- IP 地址
- 时间戳
在 NestJS 中,你可以使用 Logger
的 context
参数来设置上下文信息,或者在日志消息中手动添加。
4. 处理敏感信息
日志中可能会包含敏感信息,如密码、密钥、令牌等。这些信息不能直接记录到日志中,否则会造成安全风险。你可以采取以下几种方式来处理敏感信息:
- 脱敏: 将敏感信息替换成 * 或其他字符。
- 加密: 对敏感信息进行加密,只有授权的人才能解密。
- 不记录: 完全不记录敏感信息。
5. 日志格式化
日志格式化是指将日志信息按照一定的格式输出。一个好的日志格式应该易于阅读和解析。常见的日志格式有:
- 文本格式: 人类可读的格式,适合在开发环境中查看。
- JSON 格式: 机器可读的格式,适合在生产环境中收集和分析。
在 NestJS 中,你可以使用 winston
或 pino
等日志库来配置日志格式。
6. 日志轮转
日志文件会随着时间的推移而不断增长,如果不进行管理,最终可能会耗尽磁盘空间。日志轮转是指定期创建新的日志文件,并将旧的日志文件归档或删除。你可以使用 winston-daily-rotate-file
等库来实现日志轮转。
安装
npm install winston-daily-rotate-file
使用
// logger.service.ts import { Injectable, LoggerService } from '@nestjs/common'; import * as winston from 'winston'; import 'winston-daily-rotate-file'; @Injectable() export class WinstonLogger implements LoggerService { private readonly logger: winston.Logger; constructor() { this.logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.printf(({ level, message, timestamp, context }) => { return `${timestamp} [${context}] ${level}: ${message}`; }) ), transports: [ new winston.transports.Console(), new winston.transports.DailyRotateFile({ filename: 'application-%DATE%.log', // 文件名格式 datePattern: 'YYYY-MM-DD-HH', zippedArchive: true, // 是否压缩 maxSize: '20m', // 最大文件大小 maxFiles: '14d', // 最多保留文件数 dirname: 'logs', // 日志文件目录 }), ], }); } // ... 其他方法 ... }
7. 日志聚合和分析
在生产环境中,你可能需要将日志收集到一起,进行集中管理和分析。你可以使用 ELK Stack(Elasticsearch、Logstash、Kibana)或 Graylog 等工具来实现日志聚合和分析。
总结
日志记录是 NestJS 应用开发中非常重要的一环。一个好的日志系统可以帮助你快速定位问题、监控系统性能、保障系统安全。希望本文能够帮助你更好地理解 NestJS 日志记录,并在实际项目中应用这些知识。
记住,日志不是越多越好,而是要恰到好处。你需要根据实际情况,选择合适的日志级别、格式和存储方式,并定期对日志进行管理和分析。只有这样,才能让日志真正发挥它的价值。
“老哥,这次你的 NestJS 项目日志,我给满分!”