NestJS 日志记录终极指南:从入门到生产级实践
“哎,老哥,你这 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 支持以下五个级别(从低到高):
verbosedebuglogwarnerror
默认情况下,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 项目日志,我给满分!”