Chapter 30

techniques-logger

kisihara.c
kisihara.c
2021.04.23に更新

ロガー

Nestにはテキストベースのロガーが内蔵されている。アプリケーションの起動時やキャッチした例外の表示(システムロギング)などに使用される。この機能は@nestjs/commonパッケージのLoggerクラスで提供されている。ロギングシステムの動作は、以下のように完全に制御可能。

  • ログを完全に無効にする
  • ログの詳細レベルの指定(例:エラー、警告、デバッグ情報の表示等)
  • デフォルトのロガーのタイムスタンプをオーバーライドする(例:ISO8601企画を日付フォーマットとして使用等)
  • デフォルトのロガーを完全にオーバーライドする
  • デフォルトのロガーを拡張してカスタマイズする
  • 依存性インジェクションを利用して、アプリケーションの構成とテストを簡素化する

また、組み込みのロガーや独自のカスタムロガーを使ってアプリケーションレベルのイベントやメッセージを記録する事もできる。

より高度なロギング機能のためには、WinstonのようなNode.jsのロギングパッケージを利用して、完全にカスタム化されたプロダクショングレードのロギングシステムを実装する事ができる。

基本的なカスタマイズ

ロギングを無効にするには、NestFactory.create()メソッドの第二引数として渡される(省略可能な)Nestアプリケーションオプションオブジェクトのloggerプロパティをfalseに設定する。

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

特定のログレベルを有効にするには、以下のようにloggerプロパティに対し、表示するログレベルを指定した文字列の配列を設定する。

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

配列は'log''error''warn''debug''verbose'の組み合わせの値になります。

HINT
デフォルトのロガーのメッセージの色を無効にするには、環境変数NO_COLORを設定する。

カスタム実装

loggerプロパティの値をLoggerServiceインターフェイスを満たすオブジェクトとして設定する事で、Nestがシステムロギングに使用するカスタムロガーの実装を作る事ができる。例えば以下のように組み込みのグローバルJavaScript(LoggerServiceインターフェイスを実装している)を使用できる。

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

独自のカスタムロガーの実装は簡単だ。以下に示すようにLoggerServiceインターフェイスの各メソッドを実装するだけだ。

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

export class MyLogger implements LoggerService {
  log(message: string) {
    /* あなたの実装 */
  }
  error(message: string, trace: string) {
    /* あなたの実装 */
  }
  warn(message: string) {
    /* あなたの実装 */
  }
  debug(message: string) {
    /* あなたの実装 */
  }
  verbose(message: string) {
    /* あなたの実装 */
  }
}

次に、Nestアプリケーションオプションオブジェクトのloggerプロパティを介してMyLoggerのインスタンスを提供できる。

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

この記法はシンプルだが、MyLoggerクラスの依存性注入を使っていない。これは特にテストの際に課題となり、MyLoggerの再利用性を制限してしまう。以下の依存性注入の項を参照してほしい。

組み込みロガーの拡張

ゼロからロガーを書くのではなく、ビルトインのLoggerクラスを拡張して、デフォルトの実装の一部の動作をオーバーライドする事で十分かもしれない。

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

export class MyLogger extends Logger {
  error(message: string, trace: string) {
    // オリジナルのロジックをここに追加
    super.error(message, trace);
  }
}

拡張ロガーは後述の「アプリケーションロギング」の項で説明するように機能モジュールで使用可能。

Nestにシステムロギング用の拡張ロガーを使用させるには、(上記の「カスタム実装」のセクションで示したように)アプリケーションオプションオブジェクトのloggerプロパティを介してそのインスタンスを渡すか、以下の「依存性インジェクション」の項で示す手法を使用する。その場合は上記のサンプルコードのようにsuperを呼び出して、特定のlogメソッドの呼び出しを親(組み込み)クラスに委譲し、Nestが期待する組み込み機能を利用できるようにしてほしい。

依存性インジェクション

より高度なロギング機能の実現の為、依存性インジェクションを活用すると良い。例えばConfigServiceをロガーにインジェクションしてカスタマイズし、さらにカスタムロガーを他のコントローラやプロバイダにインジェクションする事ができる。カスタムロガーの依存性インジェクションを有効にするには、LoggerServiceを実装したクラスを作成し、そのクラスをモジュールのプロバイダとして登録する。例えばこう進められる:

  1. 前の項のように組み込みのLoggerを拡張するか、完全にオーバーライドするMyLoggerクラスを定義してみよう。必ず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())は、あらゆるモジュールのコンテキストの外で行われる為、初期化時の通常の依存性インジェクションフェーズには参加しない。そのため、少なくとも1つのアプリケーションモジュールにLoggerModuleをインポートさせて、NestがMyLoggerクラスのシングルトンインスタンスをインスタンス化するようにしなければならない。そして次のコードによって、Nestに単一のMyLoggerシングルトンを使用させられる。

const app = await NestFactory.create(AppModule, {
  logger: false,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);

ここでは、NestApplicationインスタンスのget()メソッドを使用して、MyLoggerオブジェクトのシングルトンを取得している。この手法は基本的に、Nestで使用するロガーのインスタンスをインジェクションする方法だ。app.get()呼び出しはMyLoggerのシングルトンを取得する。そして上記のように、別のモジュールで最初にインジェクションされるインスタンスに依存している。

この方法の唯一の欠点は、最初の初期化メッセージがどこにも表示されない事で、ごくたまに重要な初期化エラーを見逃す。別の方法として、デフォルトのロガーを使って最初の初期化メッセージを出力し、その後カスタムロガーに切り替える事もできる。

const app = await NestFactory.create(AppModule);
app.useLogger(app.get(MyLogger));
await app.listen(3000);

また、このMyLoggerプロバイダを機能クラスにインジェクションする事で、Nestのシステムロギングとアプリケーションロギングの両方で一貫したロギング動作を実現できる。詳細については以下のそれぞれの項を参照の事。

アプリケーションのロギングにロガーを使用する

上記のテクニックを組み合わせて、Nestシステムのロギングと独自のアプリケーションのイベント/メッセージのロギングの両方で、一貫した動作とフォーマットを提供する事ができる。

グッドプラクティスは、@nestjs/commonLoggerクラスを各サービスでインスタンス化する事だ。Loggerのコンストラクタのcontext引数には、以下のようにサービス名を指定する。

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

@Injectable()
class MyService {
  private readonly logger = new Logger(MyService.name);
  
  doSomething() {
    this.logger.log('Doing something...');
  }
}

デフォルトのロガーの実装では、以下の例のNestFactoryのように、角括弧の中にcontextが出力される。

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

app.useLogger()でカスタムロガーを指定すると、実際にNestが内部で使用する。つまりapp.userLogger()を呼び出す事でデフォルトのロガーを簡単にカスタムロガーに切り替えられる事で、コードが実装に対して独立しているといえる。

前節の手順でapp.useLogger(app.get(MyLogger))を呼び出した場合、MyServiceからthis.logger.log()を呼び出すと、MyLoggerインスタンスからlogメソッドを呼び出す事になる。

これはほとんどの場合で有用だが、もっとカスタマイズが必要な場合(カスタムメソッドの追加や呼び出し等)は次のセクションを参照の事。

カスタムロガーをインジェクションする

手始めに、以下のようなコードで組み込みのロガーを拡張する。ここではLoggerクラスの設定メタデータとしてscopeオプションを指定し、一時的なスコープを指定する事で、各機能モジュールでMyLoggerの一意のインスタンスを持つようにしている。この例では個々のLoggerメソッド(log()warn()など)を拡張していないが、必要に応じて拡張可能。

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

@Injectable({ scope: Scope.TRANSIENT })
export class MyLogger extends Logger {
  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を機能モジュールにインポートする。デフォルトのLoggerを拡張したので、setContextメソッドを使用できるようになった。コンテキストを考慮したカスタムロガーを以下のように使い始められる。

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

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

  constructor(private myLogger: MyLogger) {
    // 遷移スコープによって、CatsServiceは独自のMyLoggerインスタンスを持つ。
    // なのでここにコンテキストをセットしても他のサービスの他のインスタンスには影響がない。 
    this.myLogger.setContext('CatsService');
  }

  findAll(): Cat[] {
    // 全てのデフォルトメソッドを呼べる
    this.myLogger.warn('About to return cats!');
    // カスタムメソッドも呼べる
    this.myLogger.customLog();
    return this.cats;
  }
}

最後に、以下のようにNest内main.tsファイルでカスタムロガーのインスタンスを使用する。もちろんこの例では(log()warn()などのLoggerメソッドを拡張して)実際にロガーの動作をカスタマイズしていないので、このステップは実際には必要ない。しかし、これらのメソッドにカスタムロジックを追加し、Nestに同じ実装を使用させたい場合は必要になる。

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

NestFactory.createlogger:falseを指定した場合、useLoggerを呼び出すまで何も記録されない為、重要な初期化エラーを見逃してしまう可能性がある事に注意。初期メッセージの一部がデフォルトのloggerで記録される事を気にしないのであれば、logger:falseオプションを省略しても大丈夫。

外部ロガーの仕様

本番アプリケーションでは、高度なフィルタリング、フォーマット、集中的なロギングなど、特別なロギング要件がある場合がよくある。Nestの組み込みロガーはNestの挙動のモニタリングによく使われるし、プロダクト内独自の機能モジュールに対する、基本的なフォーマットのテキストでのロギングに有用だ。だが、製品アプリケーションではWinstonのような専用ロギングモジュールを利用することがよくある。他の標準的なNode.jsアプリケーションと同様、Nestでもそういったモジュールを最大限に活用可能だ。