はじめに
TypeScript + Docker でサーバーサイド開発をしており、コードに Logger を導入したかったため、ライブラリ選定を行いました。
今回は Docker を使っており、 AWS ECS にデプロイしているため、ログを標準出力に出したいです。通常ログはファイルに出力することが多いが、 Docker を使っている場合は標準出力に出す場合が多いです。
特に、 AWS ECS を使っている場合は、標準出力に出たログを簡単に CloudWatch Logs に送ることができるメリットもあります。
CloudWatch Logs にログを送る場合、JSON 形式でログを出していれば、勝手に CloudWatch Logs でパースしてくれて、検索が便利です。
ということで、要件は以下の通り
- 標準出力にログを出せる
- JSON 形式でログを出せる
ライブラリの選択肢
標準出力にログを出力する場合、ライブラリを導入しなくても、 console.log で済ませる方法もあります。
- ライブラリを導入しない(console.log で済ませる)
- ライブラリを導入する
- winston
- log4js-node
- tslog
- その他ロガーのライブラリは多数...
この中で、winston が特に利用者数が多く、使い勝手も問題なかったため、 winston を導入することにしました。
console.log を導入するか迷いました。
ライブラリを使わず console.log を利用するメリットは、ライブラリのアップデートといった雑務がなくなることですが、
- JSON フォーマッタなどの実装コストがかなり減ること
- ログのフォーマット等を変更するのが容易であること
- 将来的に JSON 以外のフォーマットで出したくなるかもしれないし、標準出力以外に出したくなるかもしれない
といった理由から winston を採用します。
実装
シンプルな実装
winston の logger を直接使うのではなく、一つラップしてあげて、自分たちのコードベースに合うようにしてあげると使いやすくなります。
winston の使い方は以下の通りです
- 環境変数で LOG_LEVEL を指定することで、「開発環境では debug ログを出す」といったことも可能にしています
- winston のログレベルには、 debug, info, warn, error などがあり、例えば、 level に info を指定すると info 以上のログしか出力されなくなります。debug ログは出ないようになります。
- ログレベルの使い分けは以下のようにしています
- error 緊急の対応を必要とする場合
- warn 緊急ではないが、大量に出力された場合には調査を必要とする場合など
- info 検知する必要がないが、エラーの調査時などに残っていて欲しいログ
- debug 開発環境で開発するときだけ出てれば良いログ
- transports には出力を複数指定できますが、今回は標準出力にだけ出ればいいので、 Console を指定します
import winston from "winston" class MyLogger { private logger: winston.Logger public constructor() { this.logger = winston.createLogger({ level: process.env.LOG_LEVEL ?? 'info', transports: [ new winston.transports.Console() ], }) } /** * 開発環境でのみ出力したい内容を出力する */ public debug(message: string, extra: object = {}) { this.logger.debug(message, {extra: extra}) } /** * エラーではないが、調査の際に必要となるようなログを出力する * 本番環境でも出力される */ public info(message: string, extra: object = {}) { this.logger.info(message, {extra: extra}) } /** * アラートを鳴らすほどではないが、検知しておきたいエラーなどを出力する */ public warn(message: string, extra: object = {}) { this.logger.warn(message, {extra: extra}) } /** * アラートを鳴らすべきエラーログを出力する */ public error(message: string, extra: object = {}) { this.logger.error(message, {extra: extra}) } }
凝った実装
「ログは出していたけど userId を出力するの忘れていた!」といったことがないように、ログに出したいパラメータは、ラップしたクラスの引数で必須にしておくと、忘れることがありません。
よくあるのは、 Context クラスを作成し、それを Logger の引数で渡すことです。リクエストごとに requestId を発行して Context に埋め込んでおくなどすると、一連のログが追いやすくなり便利になります。
import winston from "winston" class MyLogger { private logger: winston.Logger public constructor() { this.logger = winston.createLogger({ level: process.env.LOG_LEVEL ?? 'info', transports: [ new winston.transports.Console() ], }) } /** * エラーではないが、調査の際に必要となるようなログを出力する * 本番環境でも出力される */ public info(context: Context, message: string, extra: object = {}) { this.logger.info(message, { requestId: context.requestId, requestUrl: context.requestUrl, userId: context.userId, extra: extra}) } } class Context { public constructor(requestUrl: string, requestId: string, userId: string) { ... } }