WEBKT

NestJS 日志记录终极指南:从入门到生产级实践

237 0 0 0

“哎,老哥,你这 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:详细日志,用于记录更详细的信息,通常只在开发环境中使用。

在上面的例子中,我们分别使用了 logdebugerrorverbose 方法来记录不同级别的日志。在 error 方法中,我们还传入了 error.stack,这样可以打印出完整的错误堆栈信息,方便我们定位问题。

日志级别

日志级别用于区分日志的重要性。NestJS 内置的 Logger 支持以下五个级别(从低到高):

  1. verbose
  2. debug
  3. log
  4. warn
  5. error

默认情况下,Logger 会输出 logwarnerror 级别的日志。你可以通过设置 logLevel 选项来改变日志级别:

import { Logger, LogLevel } from '@nestjs/common';

const logger = new Logger('MyContext', { logLevel: 'debug' });

或者,你也可以通过设置环境变量 LOG_LEVEL 来改变日志级别:

LOG_LEVEL=debug nest start

通常,在开发环境中,我们会将日志级别设置为 debugverbose,以便输出更详细的日志信息。在生产环境中,我们会将日志级别设置为 logwarn,以减少日志输出量,提高系统性能。

进阶:使用第三方日志库

NestJS 内置的 Logger 虽然简单易用,但功能比较有限。在实际项目中,我们通常会使用更强大的第三方日志库,如 winstonpino 等。

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 {}

在这个例子中:

  1. 我们创建了一个 WinstonLogger 类,实现了 LoggerService 接口,这样我们就可以在 NestJS 中使用 winston 了。
  2. WinstonLogger 的构造函数中,我们使用 winston.createLogger 创建了一个 winston 实例,并配置了日志级别、格式和 transports。
  3. 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 中,你可以使用 Loggercontext 参数来设置上下文信息,或者在日志消息中手动添加。

4. 处理敏感信息

日志中可能会包含敏感信息,如密码、密钥、令牌等。这些信息不能直接记录到日志中,否则会造成安全风险。你可以采取以下几种方式来处理敏感信息:

  • 脱敏: 将敏感信息替换成 * 或其他字符。
  • 加密: 对敏感信息进行加密,只有授权的人才能解密。
  • 不记录: 完全不记录敏感信息。

5. 日志格式化

日志格式化是指将日志信息按照一定的格式输出。一个好的日志格式应该易于阅读和解析。常见的日志格式有:

  • 文本格式: 人类可读的格式,适合在开发环境中查看。
  • JSON 格式: 机器可读的格式,适合在生产环境中收集和分析。

在 NestJS 中,你可以使用 winstonpino 等日志库来配置日志格式。

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 项目日志,我给满分!”

日志打工人 NestJS日志Node.js

评论点评