Techniques

日志记录器

学习如何在 NestJS 应用程序中使用内置日志记录器和自定义日志记录器实现

日志记录器

Nest 自带一个基于文本的内置日志记录器,在应用程序引导过程和其他一些情况下使用,比如显示捕获的异常(即系统日志记录)。此功能通过 @nestjs/common 包中的 Logger 类提供。您可以完全控制日志记录系统的行为,包括以下任何功能:

  • 完全禁用日志记录
  • 指定日志详细级别(例如,显示错误、警告、调试信息等)
  • 配置日志消息的格式(原始、json、彩色等)
  • 覆盖默认日志记录器中的时间戳(例如,使用 ISO8601 标准作为日期格式)
  • 完全覆盖默认日志记录器
  • 通过扩展来自定义默认日志记录器
  • 利用依赖注入来简化应用程序的组合和测试

您还可以使用内置日志记录器,或创建自己的自定义实现,来记录您自己的应用程序级事件和消息。

如果您的应用程序需要与外部日志记录系统集成、自动基于文件的日志记录或将日志转发到集中式日志记录服务,您可以使用 Node.js 日志记录库实现完全自定义的日志记录解决方案。一个流行的选择是 Pino,以其高性能和灵活性而闻名。

基本自定义

要禁用日志记录,请在传递给 NestFactory.create() 方法的第二个参数(可选的 Nest 应用程序选项对象)中将 logger 属性设置为 false

const app = await NestFactory.create(AppModule, {
  logger: false,
});
await app.listen(process.env.PORT ?? 3000);

要启用特定的日志记录级别,请将 logger 属性设置为指定要显示的日志级别的字符串数组,如下所示:

const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});
await app.listen(process.env.PORT ?? 3000);

数组中的值可以是 'log''fatal''error''warn''debug''verbose' 的任意组合。

要禁用彩色输出,请传递将 colors 属性设置为 falseConsoleLogger 对象作为 logger 属性的值。

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    colors: false,
  }),
});

要为每个日志消息配置前缀,请传递设置了 prefix 属性的 ConsoleLogger 对象:

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    prefix: 'MyApp', // 默认是 "Nest"
  }),
});

下表列出了所有可用选项:

选项描述默认值
logLevels启用的日志级别。['log', 'error', 'warn', 'debug', 'verbose']
timestamp如果启用,将打印当前和上一条日志消息之间的时间戳(时间差)。注意:当启用 json 时不使用此选项。false
prefix用于每条日志消息的前缀。注意:当启用 json 时不使用此选项。Nest
json如果启用,将以 JSON 格式打印日志消息。false
colors如果启用,将以彩色打印日志消息。如果禁用 json,默认为 true,否则为 false。true
context日志记录器的上下文。undefined
compact如果启用,将在单行中打印日志消息,即使它是具有多个属性的对象。如果设置为数字,只要所有属性都适合 breakLength,最多 n 个内部元素就会在单行中合并。短数组元素也会组合在一起。true
maxArrayLength指定格式化时要包含的 Array、TypedArray、Map、Set、WeakMap 和 WeakSet 元素的最大数量。设置为 null 或 Infinity 以显示所有元素。设置为 0 或负数以不显示任何元素。当启用 json、禁用颜色且 compact 设置为 true 时忽略,因为它产生可解析的 JSON 输出。100
maxStringLength指定格式化时要包含的最大字符数。设置为 null 或 Infinity 以显示所有元素。设置为 0 或负数以不显示任何字符。当启用 json、禁用颜色且 compact 设置为 true 时忽略,因为它产生可解析的 JSON 输出。10000
sorted如果启用,将在格式化对象时对键进行排序。也可以是自定义排序函数。当启用 json、禁用颜色且 compact 设置为 true 时忽略,因为它产生可解析的 JSON 输出。false
depth指定格式化对象时递归的次数。这对于检查大型对象很有用。要递归到最大调用堆栈大小,请传递 Infinity 或 null。当启用 json、禁用颜色且 compact 设置为 true 时忽略,因为它产生可解析的 JSON 输出。5
showHidden如果为 true,对象的不可枚举符号和属性将包含在格式化结果中。WeakMap 和 WeakSet 条目以及用户定义的原型属性也会包含在内。false
breakLength输入值跨多行分割的长度。设置为 Infinity 以将输入格式化为单行(与 "compact" 设置为 true 结合使用)。当 "compact" 为 true 时默认为 Infinity,否则为 80。当启用 json、禁用颜色且 compact 设置为 true 时忽略,因为它产生可解析的 JSON 输出。Infinity

JSON 日志记录

JSON 日志记录对于现代应用程序的可观察性和与日志管理系统的集成至关重要。要在您的 NestJS 应用程序中启用 JSON 日志记录,请配置 ConsoleLogger 对象,将其 json 属性设置为 true。然后,在创建应用程序实例时,将此日志记录器配置作为 logger 属性的值提供。

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    json: true,
  }),
});

此配置以结构化 JSON 格式输出日志,使其更容易与外部系统(如日志聚合器和云平台)集成。例如,AWS ECS(弹性容器服务)等平台原生支持 JSON 日志,启用高级功能,如:

  • 日志过滤:根据日志级别、时间戳或自定义元数据等字段轻松缩小日志范围。
  • 搜索和分析:使用查询工具分析和跟踪应用程序行为的趋势。

此外,如果您使用 NestJS Mau,JSON 日志记录简化了以良好组织的结构化格式查看日志的过程,这对于调试和性能监控特别有用。

注意json 设置为 true 时,ConsoleLogger 会通过将 colors 属性设置为 false 来自动禁用文本着色。这确保输出保持有效的 JSON,没有格式化伪影。但是,出于开发目的,您可以通过显式将 colors 设置为 true 来覆盖此行为。这会添加彩色 JSON 日志,这可以使日志条目在本地调试期间更易读。

启用 JSON 日志记录时,日志输出将如下所示(在单行中):

{
  "level": "log",
  "pid": 19096,
  "timestamp": 1607370779834,
  "message": "Starting Nest application...",
  "context": "NestFactory"
}

您可以在此 Pull Request 中看到不同的变体。

使用日志记录器进行应用程序日志记录

我们可以结合上述几种技术,在 Nest 系统日志记录和我们自己的应用程序事件/消息日志记录之间提供一致的行为和格式。

一个好的做法是在我们的每个服务中从 @nestjs/common 实例化 Logger 类。我们可以在 Logger 构造函数中提供我们的服务名称作为 context 参数,如下所示:

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

@Injectable()
class MyService {
  private readonly logger = new Logger(MyService.name);

  doSomething() {
    this.logger.log('Doing something...');
  }
}

在默认日志记录器实现中,context 打印在方括号中,如下面示例中的 NestFactory

[Nest] 19096   - 12/08/2019, 7:12:59 AM   [NestFactory] Starting Nest application...

如果我们通过 app.useLogger() 提供自定义日志记录器,它实际上会被 Nest 内部使用。这意味着我们的代码保持实现无关,而我们可以通过调用 app.useLogger() 轻松地将默认日志记录器替换为我们的自定义日志记录器。

这样,如果我们按照上一节的步骤调用 app.useLogger(app.get(MyLogger)),从 MyServicethis.logger.log() 的以下调用将导致对 MyLogger 实例的 log 方法的调用。

这应该适用于大多数情况。但如果您需要更多自定义(如添加和调用自定义方法),请转到下一节。

带时间戳的日志

要为每个记录的消息启用时间戳日志记录,您可以在创建日志记录器实例时使用可选的 timestamp: true 设置。

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

@Injectable()
class MyService {
  private readonly logger = new Logger(MyService.name, { timestamp: true });

  doSomething() {
    this.logger.log('Doing something with timestamp here ->');
  }
}

这将产生以下格式的输出:

[Nest] 19096   - 04/19/2024, 7:12:59 AM   [MyService] Doing something with timestamp here +5ms

注意行末的 +5ms。对于每个日志语句,计算与上一条消息的时间差并显示在行末。

自定义实现

您可以提供自定义日志记录器实现供 Nest 用于系统日志记录,方法是将 logger 属性的值设置为满足 LoggerService 接口的对象。例如,您可以告诉 Nest 使用内置的全局 JavaScript console 对象(它实现了 LoggerService 接口),如下所示:

const app = await NestFactory.create(AppModule, {
  logger: console,
});
await app.listen(process.env.PORT ?? 3000);

实现您自己的自定义日志记录器很简单。只需实现 LoggerService 接口的每个方法,如下所示。

import { LoggerService, Injectable } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
  /**
   * 写入 'log' 级别日志。
   */
  log(message: any, ...optionalParams: any[]) {}

  /**
   * 写入 'fatal' 级别日志。
   */
  fatal(message: any, ...optionalParams: any[]) {}

  /**
   * 写入 'error' 级别日志。
   */
  error(message: any, ...optionalParams: any[]) {}

  /**
   * 写入 'warn' 级别日志。
   */
  warn(message: any, ...optionalParams: any[]) {}

  /**
   * 写入 'debug' 级别日志。
   */
  debug?(message: any, ...optionalParams: any[]) {}

  /**
   * 写入 'verbose' 级别日志。
   */
  verbose?(message: any, ...optionalParams: any[]) {}
}

然后您可以通过 Nest 应用程序选项对象的 logger 属性提供 MyLogger 的实例。

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});
await app.listen(process.env.PORT ?? 3000);

这种技术虽然简单,但不利用 MyLogger 类的依赖注入。这可能会带来一些挑战,特别是对于测试,并限制 MyLogger 的可重用性。有关更好的解决方案,请参阅下面的依赖注入部分。

扩展内置日志记录器

与其从头编写日志记录器,您可能能够通过扩展内置的 ConsoleLogger 类并覆盖默认实现的选定行为来满足您的需求。

import { ConsoleLogger } from '@nestjs/common';

export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    // 在这里添加您的定制逻辑
    super.error(...arguments);
  }
}

您可以在功能模块中使用这样的扩展日志记录器,如下面的使用日志记录器进行应用程序日志记录部分所述。

您可以通过应用程序选项对象的 logger 属性传递其实例(如上面的自定义实现部分所示),或使用下面的依赖注入部分中显示的技术,告诉 Nest 将您的扩展日志记录器用于系统日志记录。如果您这样做,您应该注意调用 super,如上面的示例代码所示,将特定的日志方法调用委托给父(内置)类,以便 Nest 可以依赖它期望的内置功能。

依赖注入

对于更高级的日志记录功能,您需要利用依赖注入。例如,您可能希望将 ConfigService 注入到您的日志记录器中以自定义它,然后将您的自定义日志记录器注入到其他控制器和/或提供者中。要为您的自定义日志记录器启用依赖注入,请创建一个实现 LoggerService 的类,并将该类注册为某个模块中的提供者。例如,您可以

  1. 定义一个 MyLogger 类,该类要么扩展内置的 ConsoleLogger,要么完全覆盖它,如前面部分所示。确保实现 LoggerService 接口。
  2. 创建如下所示的 LoggerModule,并从该模块提供 MyLogger
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}

通过这种构造,您现在为任何其他模块提供了您的自定义日志记录器以供使用。因为您的 MyLogger 类是模块的一部分,它可以使用依赖注入(例如,注入 ConfigService)。还需要一种技术来提供此自定义日志记录器供 Nest 用于系统日志记录(例如,用于引导和错误处理)。

因为应用程序实例化(NestFactory.create())发生在任何模块的上下文之外,它不参与初始化的正常依赖注入阶段。所以我们必须确保至少有一个应用程序模块导入 LoggerModule 以触发 Nest 实例化我们的 MyLogger 类的单例实例。

然后我们可以指示 Nest 使用以下构造使用 MyLogger 的相同单例实例:

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(app.get(MyLogger));
await app.listen(process.env.PORT ?? 3000);

注意 在上面的示例中,我们将 bufferLogs 设置为 true,以确保所有日志都将被缓冲,直到附加自定义日志记录器(在这种情况下为 MyLogger)并且应用程序初始化过程完成或失败。如果初始化过程失败,Nest 将回退到原始的 ConsoleLogger 来打印任何报告的错误消息。此外,您可以将 autoFlushLogs 设置为 false(默认 true)以手动刷新日志(使用 Logger.flush() 方法)。

在这里,我们使用 NestApplication 实例上的 get() 方法来检索 MyLogger 对象的单例实例。这种技术本质上是一种"注入"日志记录器实例供 Nest 使用的方法。app.get() 调用检索 MyLogger 的单例实例,并依赖于该实例首先在另一个模块中注入,如上所述。

您还可以在功能类中注入此 MyLogger 提供者,从而确保 Nest 系统日志记录和应用程序日志记录之间的一致日志记录行为。有关更多信息,请参阅使用日志记录器进行应用程序日志记录和下面的注入自定义日志记录器

注入自定义日志记录器

首先,使用如下代码扩展内置日志记录器。我们为 ConsoleLogger 类提供 scope 选项作为配置元数据,指定瞬态作用域,以确保我们在每个功能模块中都有 MyLogger 的唯一实例。在此示例中,我们不扩展单个 ConsoleLogger 方法(如 log()warn() 等),尽管您可以选择这样做。

import { Injectable, Scope, ConsoleLogger } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class MyLogger extends ConsoleLogger {
  customLog() {
    this.log('Please feed the cat!');
  }
}

接下来,创建一个 LoggerModule,其构造如下:

import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}

接下来,将 LoggerModule 导入到您的功能模块中。由于我们将日志记录器声明为瞬态的,每个功能模块都将有自己的 MyLogger 实例。

import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { LoggerModule } from '../logger/logger.module';

@Module({
  imports: [LoggerModule],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

最后,在功能类中注入 MyLogger 实例,如下所示:

import { Injectable } from '@nestjs/common';
import { MyLogger } from '../logger/my-logger.service';

@Injectable()
export class CatsService {
  constructor(private myLogger: MyLogger) {
    // 由于瞬态作用域,CatsService 有自己的 MyLogger 实例,
    // 因此设置上下文不会影响其他服务中的其他实例
    this.myLogger.setContext(CatsService.name);
  }

  findAll(): string {
    // 您可以调用所有默认方法
    this.myLogger.log('About to return cats!');
    // 以及您的自定义方法
    this.myLogger.customLog();
    return 'This action returns all cats';
  }
}

使用外部日志记录器

生产应用程序通常有特定的日志记录要求,包括高级功能,如日志轮换、格式化、远程日志记录等。虽然 Nest 的内置日志记录器对于 Nest 系统日志记录和基本的应用程序级日志记录在开发中很有用,但生产应用程序可能希望利用专用的日志记录包,如 Winston。与任何标准的 Node.js 应用程序一样,您可以在 Nest 中充分利用此类包。