NestJS 日志

2023-09-08 17:41 更新

Nest 附帶一個(gè)默認(rèn)的內(nèi)部日志記錄器實(shí)現(xiàn),它在實(shí)例化過程中以及在一些不同的情況下使用,比如發(fā)生異常等等(例如系統(tǒng)記錄)。這由 @nestjs/common 包中的 Logger 類實(shí)現(xiàn)。你可以全面控制如下的日志系統(tǒng)的行為:

  • 完全禁用日志
  • 指定日志系統(tǒng)詳細(xì)水平(例如,展示錯(cuò)誤,警告,調(diào)試信息等)
  • 覆蓋默認(rèn)日志記錄器的時(shí)間戳(例如使用 ISO8601 標(biāo)準(zhǔn)作為日期格式)
  • 完全覆蓋默認(rèn)日志記錄器
  • 通過擴(kuò)展自定義默認(rèn)日志記錄器
  • 使用依賴注入來簡化編寫和測試你的應(yīng)用

你也可以使用內(nèi)置日志記錄器,或者創(chuàng)建你自己的應(yīng)用來記錄你自己應(yīng)用水平的事件和消息。

更多高級的日志功能,可以使用任何 Node.js 日志包,比如Winston,來生成一個(gè)完全自定義的生產(chǎn)環(huán)境水平的日志系統(tǒng)。

基礎(chǔ)自定義

要禁用日志,在(可選的)Nest 應(yīng)用選項(xiàng)對象中向 NestFactory.create() 傳遞第二個(gè)參數(shù)設(shè)置 logger 屬性為 false 。

const app = await NestFactory.create(ApplicationModule, {
  logger: false,
});
await app.listen(3000);

你也可以只啟用特定日志級別,設(shè)置一個(gè)字符串形式的 logger 屬性數(shù)組以確定要顯示的日志水平,如下:

const app = await NestFactory.create(ApplicationModule, {
  logger: ['error', 'warn'],
});
await app.listen(3000);

數(shù)組中的字符串可以是以下字符串的任意組合: log , error , warn , debug 和 verbose 。

你可以通過設(shè)置 NO_COLOR 環(huán)境變量為非空字符串來禁用默認(rèn)日志信息的顏色

自定義應(yīng)用

你可以提供一個(gè)自定義日志記錄器應(yīng)用,并由 Nest 作為系統(tǒng)記錄使用,這需要設(shè)置logger 屬性到一個(gè)滿足 LoggerService 接口的對象。例如,你可以告訴 Nest 使用內(nèi)置的全局 JavaScript console 對象(其實(shí)現(xiàn)了 LoggerService 接口),如下:

const app = await NestFactory.create(ApplicationModule, {
  logger: console,
});
await app.listen(3000);

應(yīng)用你的自定義記錄器很簡單。只要簡單實(shí)現(xiàn)以下 LoggerService 接口中的每個(gè)方法就可以:

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

export class MyLogger implements LoggerService {
  /**
   * Write a 'log' level log.
   */
  log(message: any, ...optionalParams: any[]) {}

  /**
   * Write an 'error' level log.
   */
  error(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'warn' level log.
   */
  warn(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'debug' level log.
   */
  debug?(message: any, ...optionalParams: any[]) {}

  /**
   * Write a 'verbose' level log.
   */
  verbose?(message: any, ...optionalParams: any[]) {}
}

你可以通過 logger 屬性為 Nest 應(yīng)用的選項(xiàng)對象提供一個(gè) MyLogger 實(shí)例:

const app = await NestFactory.create(ApplicationModule, {
  logger: new MyLogger(),
});
await app.listen(3000);

這個(gè)技術(shù)雖然很簡單,但是沒有為 MyLogger 類應(yīng)用依賴注入。這會(huì)帶來一些挑戰(zhàn),尤其在測試方面,同時(shí)也限制了 MyLogger 的重用性。更好的解決方案參見如下的依賴注入部分。

擴(kuò)展內(nèi)置的日志類

很多實(shí)例操作需要?jiǎng)?chuàng)建自己的日志。你不必完全重新發(fā)明輪子。只需繼承內(nèi)置 ConsoleLogger 類以部分覆蓋默認(rèn)實(shí)現(xiàn),并使用 super 將調(diào)用委托給父類。

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

export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    // add your tailored logic here
    super.error.apply(this, arguments);
  }
}

你可以按如下使用應(yīng)用記錄器來記錄部分所述,從你的特征模塊中使用擴(kuò)展記錄器。

你可以把你的擴(kuò)展日志記錄器的實(shí)例傳遞到應(yīng)用選項(xiàng)對象的 logger 屬性來讓 Nest 使用你的日志記錄器記錄系統(tǒng)日志(如自定義應(yīng)用所述),也可以按照如下的依賴注入部分。如果你這樣做,你在調(diào)用 super 時(shí)要小心,如上述代碼示例,要委托一個(gè)特定的日志方法,調(diào)用其父(內(nèi)置)類,以便 Nest 可以依賴需要的內(nèi)置特征。

依賴注入

你可能需要利用依賴注入的優(yōu)勢來使用高級的日志記錄功能。例如,你可能想把 ConfigService 注入到你的記錄器中來對它自定義,然后把自定義記錄器注入到其他控制器和/或提供者中。要為你的自定義記錄器啟用依賴注入,創(chuàng)建一個(gè)實(shí)現(xiàn) LoggerService 的類并將其作為提供者注冊在某些模塊中,例如,你可以:

  1. 定義一個(gè) MyLogger 類來繼承內(nèi)置的 ConsoleLogger 或者完全覆蓋它,如前節(jié)所述。注意一定要實(shí)現(xiàn) LoggerService 接口。
  2. 創(chuàng)建一個(gè) LoggerModule 如下所示,從該模塊中提供 MyLogger 。
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service.ts';

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

通過這個(gè)結(jié)構(gòu),你現(xiàn)在可以提供你的自定義記錄器供其他任何模塊使用。因?yàn)槟愕?nbsp;MyLogger 類是模塊的一部分,它也可以使用依賴注入(例如,注入一個(gè) ConfigService )。提供自定義記錄器供使用還需要一個(gè)技術(shù),即 Nest 的系統(tǒng)記錄(例如,供 bootstrapping 和 error handling )。

由于應(yīng)用實(shí)例化( NestFactory.create() )在任何模塊上下文之外發(fā)生,它不能參與初始化時(shí)正常的依賴注入階段。因此我們必須保證至少一個(gè)應(yīng)用模塊導(dǎo)入了 LoggerModule 來觸發(fā) Nest ,從而生成一個(gè)我們的 MyLogger 類的單例。

我們可以在之后按照下列知道來告訴 Nest 使用同一個(gè) MyLogger 實(shí)例。

const app = await NestFactory.create(ApplicationModule, {
  bufferLogs: true,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);

在上面的例子中,我們把 bufferLogs 設(shè)置為 true 以確保所有的日志都會(huì)被放入緩沖區(qū)直到一個(gè)自定義的日志記錄器被接入(在上面的例子中是 MyLogger )并且應(yīng)用初始化成功或者失敗。如果初始化失敗,Nest 會(huì)回退到原始的 ConsoleLogger 以打印出錯(cuò)誤信息。你也可以將 autoFlushLogs 設(shè)置為 false (默認(rèn)為 true )來手動(dòng)刷新日志緩沖區(qū)(使用 Logger#flush() 方法)。

在這里我們在 NestApplication 實(shí)例中用了 get() 方法以獲取 MyLogger 對象的單例。這個(gè)技術(shù)在根本上是個(gè)“注入”一個(gè)日志記錄器的實(shí)例供 Nest 使用的方法。 app.get() 調(diào)用獲取 MyLogger 單例,并且像之前所述的那樣依賴于第一個(gè)注入到其他模塊的實(shí)例。

你也可以在你的特征類中注入這個(gè) MyLogger 提供者,從而保證 Nest 系統(tǒng)記錄和應(yīng)用記錄行為一致。參考為應(yīng)用記錄使用記錄器注入一個(gè)自定義日志記錄器章節(jié)以獲取更多信息。

為應(yīng)用記錄使用記錄器

我們可以組合上述幾種技術(shù)來提供一致性的行為和格式化以保證我們的應(yīng)用事件/消息記錄和 Nest 系統(tǒng)記錄一致。

一個(gè)很好的實(shí)踐是在每個(gè)提供者內(nèi)實(shí)例化 @nestjs/common 內(nèi)的 Logger 類。我們可以將提供者的名字當(dāng)作 context 參數(shù)傳入 Logger 的構(gòu)造函數(shù),就像這樣:

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

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

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

在默認(rèn)的日志記錄器實(shí)現(xiàn)中, context 是包裹在方括號中被打印出來,就像下面例子中的 NestFactory :

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

如果我們通過 app.useLogger() 提供一個(gè)自定義日志記錄器,那么它會(huì)在 Nest 內(nèi)部被使用。這就意味著我們的代碼可以保持與實(shí)現(xiàn)無關(guān),因?yàn)槲覀兛梢院唵蔚卣{(diào)用 app.useLogger() 用默認(rèn)日志記錄器來代替自定義的那個(gè)。

如果我們跟著前面章節(jié)一步步做下來并且調(diào)用了 app.useLogger(app.get(MyLogger)) ,那么接下來在 MyService 中對 this.logger.log() 的調(diào)用會(huì)造成對 MyLogger 實(shí)例中方法 log 的調(diào)用。

這個(gè)應(yīng)該在大多數(shù)情況下都適用。但是你如果想要更深入的自定義(比如增加或者調(diào)用自定義方法),請看下一章節(jié)。

注入自定義日志記錄器

通過像下面一樣的擴(kuò)展內(nèi)置的日志記錄器來開始這一章節(jié)。我們傳入 scope 選項(xiàng)來配置 ConsoleLogger 的元數(shù)據(jù),通過指定瞬態(tài)作用域來保證在每個(gè)模塊內(nèi)都有獨(dú)一無二的 MyLogger 的實(shí)例。在下面的例子中,我們沒有擴(kuò)展每個(gè)單獨(dú)的 ConsoleLogger 方法(比如 log() , warn() 之類),盡管你可能會(huì)選擇去這么做。

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

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

接下來,我們采用如下結(jié)構(gòu)創(chuàng)建一個(gè) LoggerModule 。

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

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

然后,在你的模塊中導(dǎo)入 LoggerModule 。因?yàn)槲覀兝^承了默認(rèn)的 Logger ,所以我們可以很方便地調(diào)用 setContext 方法。之后我們就可以像下面一樣開始使用這個(gè)包含了上下文的日志記錄器了。

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  constructor(private myLogger: MyLogger) {
    // 因?yàn)樗矐B(tài)作用域的原因, CatService 有屬于自己的獨(dú)一無二的 MyLogger 實(shí)例,
    // 所以在這里設(shè)置上下文不會(huì)影響到其他提供者的實(shí)例
    this.myLogger.setContext('CatsService');
  }

  findAll(): Cat[] {
    // 你可以調(diào)用所有的默認(rèn)方法
    this.myLogger.warn('About to return cats!');
    // 當(dāng)然還有你的自定義方法
    this.myLogger.customLog();
    return this.cats;
  }
}

最后,和下面一樣在你的 main.ts 文件中讓 Nest 使用自定義日志記錄器的實(shí)例。當(dāng)然這只是個(gè)例子,我們還沒有真正自定義日志記錄器的行為(通過擴(kuò)展像 log() , warn() 這些 Logger 的方法),所以這一步并不一定需要。但是如果你給這些方法增加了自定義的邏輯而且你想讓 Nest 去使用這個(gè)實(shí)現(xiàn),那么你還是會(huì)需要這一步。

const app = await NestFactory.create(ApplicationModule, {
  bufferLogs: true,
});
app.useLogger(new MyLogger());
await app.listen(3000);

除了把 bufferLogs 設(shè)置為true,你也可以聲明 logger: false 來臨時(shí)禁用日志記錄器。需要注意的是如果你給 NestFactory.create 提供了 logger: false ,在你調(diào)用 useLogger 以前沒有東西會(huì)被記錄進(jìn)日志,所以你可能會(huì)錯(cuò)過一些重要的初始化錯(cuò)誤。如果你不在意一些初始化信息會(huì)使用默認(rèn)日志記錄器來記錄,那你可以直接設(shè)置 logger: false 。

使用外部記錄器

生產(chǎn)環(huán)境應(yīng)用通常包括特定的記錄需求,包括高級過濾器,格式化和中心化記錄。Nest 的內(nèi)置記錄器用于監(jiān)控 Nest 系統(tǒng)狀態(tài),在開發(fā)時(shí)也可以為你的特征模塊提供實(shí)用的基礎(chǔ)的文本格式的記錄,但生產(chǎn)環(huán)境可能更傾向于使用類似Winston的模塊,這是一個(gè)標(biāo)準(zhǔn)的 Node.js 應(yīng)用,你可以在 Nest 中體驗(yàn)到類似模塊的優(yōu)勢。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號