カルテット開発部の後藤です。
弊社ではアプリケーションの開発にSymfonyを使っており、ロギングはSymfonyに標準でバンドルされているMonologを使っています。
使っている期間はそこそこ長いのですが、ログのための設定を記述する頻度が少ないことや、どうもMonologの設定の概念がつかめていないためか、いつもいろいろ迷いながら設定しています。
いつも迷って困るので、今後のために概念などを整理しました。ついでに記事にして公開します。
Monologの基礎概念
Monologはロギングのためのライブラリですが、物理的なログ出力機構に関する詳細をアプリケーションから意識することなく透過的にロギングできるように、多少抽象化されています。 Monolog内に登場する概念と役割は、次のようになっています。
ロガー
アプリケーションから利用するエントリポイントオブジェクトです。PSR-3のLoggerInterfaceを実装しています。
チャンネル
Monolog内では、ロガーをインスタンス化する際に付与する識別名がチャンネルと呼ばれます。つまり、ロガーインスタンスの1つ1つがチャンネルに相当します。アプリケーションからロガーに対して直接ロギングするのではなく、チャンネルに対してロギングするというニュアンスからこうなっているのだと思われます。
ハンドラー
物理的なログ出力処理を担当します。様々なハンドラーが用意されています。1つのロガーに、複数のハンドラーを登録できます。また、複数のハンドラーを束ねて特殊な機能を付加する抽象ハンドラーもあります。
プロセッサー
ロガーやハンドラーに対して、ログ行ごとの付加情報の処理を担当します。ロガーおよびハンドラー1つに対して、複数のプロセッサーを登録できます。
フォーマッター
ログ行を出力先へ書き出す際のフォーマット処理を担当します。フォーマッターは、1つのハンドラーにつき1つだけ設定できます。
複数のチャンネル(ロガー)を構成する場合
アプリケーションの標準のログ、特定のバッチ処理専用のログというように、複数のチャンネルを構成する場合は、Monolog的には異なるロガーインスタンスを作るということになります。
それぞれのチャンネル(=ロガー)に対して、出力先の設定をハンドラーで構成し、ロガーに渡します。
以下はMonologのドキュメントに記載されている、複数のチャンネルを構成するコード例です。my_logger
と security
という2つのチャンネルを構成していますが、どちらもまったく同じハンドラーを登録しています。
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
// Create some handlers
$stream = new StreamHandler(__DIR__.'/my_app.log', Logger::DEBUG);
$firephp = new FirePHPHandler();
// Create the main logger of the app
$logger = new Logger('my_logger');
$logger->pushHandler($stream);
$logger->pushHandler($firephp);
// Create a logger for the security-related stuff with a different channel
$securityLogger = new Logger('security');
$securityLogger->pushHandler($stream);
$securityLogger->pushHandler($firephp);
// Or clone the first one to only change the channel
$securityLogger = $logger->withName('security');
ハンドラー個別の内容
よく使われるハンドラーや、特殊な動きをするハンドラーもあるので、いくつかを紹介します。
StreamHandler
シンプルなファイル出力用のハンドラーです。
RotatingFileHandler
StreamHandlerを継承したハンドラーです。ログを日付別のファイルに分割し、一定数より古いものは自動的に削除します。
FingersCrossedHandler
任意の1つのハンドラーをラップして使う抽象ハンドラーで、特定の条件(指定したレベル以上のログが記録された等)を満たさない限り何もせず、条件を満たした際に、それまで発生したログを含めて内部のハンドラーを実行します。FingersCrossedハンドラーの handle()
メソッドのコードは次のようになっています。ログをバッファリングする機構が入っていることが分かりますね。
$this->activate()
の箇所で、バッファリングしたログレコードすべてに対して handle()
が呼び出されます。
// Monolog/Handler/FingersCrossedHandler.php
public function handle(array $record)
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
}
if ($this->buffering) {
$this->buffer[] = $record;
if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
array_shift($this->buffer);
}
if ($this->activationStrategy->isHandlerActivated($record)) {
$this->activate();
}
} else {
$this->handler->handle($record);
}
return false === $this->bubble;
}
WhatFailureGroupHandler
任意の1つ以上のハンドラーをラップして使う抽象ハンドラーで、配下の複数のハンドラーを、ハンドラー内のエラー発生を無視してすべて実行します。WahtFailureGroupハンドラーの handle()
メソッドのコードは次のようになっています。What failure? がどのようなことを指しているのか、コードを見ればすぐに分かりますね。
// Monolog/Handler/WhatFailureGroupHandler.php
public function handle(array $record)
{
if ($this->processors) {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
}
foreach ($this->handlers as $handler) {
try {
$handler->handle($record);
} catch (\Exception $e) {
// What failure?
} catch (\Throwable $e) {
// What failure?
}
}
return false === $this->bubble;
}
Symfonyでの設定
SymfonyでロギングにMonologを利用する場合、ほとんどMonologに用意されている概念をそのままSymfonyの設定ファイルで記述することになります。
設定ファイルに記述した内容は、MonologBundleのエクステンションでMonologのLoggerオブジェクトを起点とするオブジェクトグラフに変換され、DIコンテナに登録されます。
アプリケーションコードからロギングするには、@logger
サービスをクラスにインジェクトするなり、サービスコンテナから取り出すなりして使います。
Symfonyでの複数チャンネルの設定
デフォルトでは @logger
だけが用意されており、つまり、チャンネルも1つのみです。
複数のロガーを構成する場合は、コンフィギュレーションで明示的にチャンネルを用意します(monolog.channels
)。さらにハンドラー側の channels
にて、「どのチャンネルに結びつけるのか」を指定します。ハンドラー側に channels
を何も指定しない場合は、すべてのチャンネルに対するロギングで使われるハンドラーになります。
// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
channels: ['foo', 'bar']
handlers:
special_log:
type: rotating_file
path: '%kernel.logs_dir%/special.log'
level: info
max_files: 10
channels: [foo] # このハンドラーは、fooチャンネル用ロガーで使う
このように設定すると、SymfonyのDIコンテナ内に @monolog.logger.foo
というサービスが登録され、foo
チャンネル専用のロガーとして使えるようになります。foo
チャンネルへのロギングを行いたいクラスには、このように特定チャンネル専用のサービスを注入して利用します。
SymfonyでのFingersCrossedHandlerの設定
Monologに用意されているFingersCrossedHandlerをSymfonyでそのまま利用できます。fingers_crossed
を利用する場合、まず記録したいログのハンドラー(下の例では nested
)を普通に記述しておき、その上にかぶせる形で fingers_crossed
を設定します(下の例では main
)。fingers_crossed
ハンドラーの action_level
には、ログを実際に出力するトリガーとなるエラーレベルを指定します。
// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: '%kernel.logs_dir%/app.log'
level: info
SymfonyでのWhatFailureGroupHandlerの設定
ログのハンドラーによっては、ログ記録処理でエラーや例外が発生してしまうケースがあります(例えば外部へ通信を行うSlackHandlerなどの場合)。ログ記録処理で発生する例外を単に無視する場合に、whatfailuregroup
ハンドラーを利用します。members
に、このハンドラーに紐付ける子ハンドラーを指定します(配列形式)。
// app/config/config_prod.yml または config/packages/prod/monolog.yaml
monolog:
handlers:
main:
type: whatfailuregroup
members:
- nested
nested:
type: stream
path: '%kernel.logs_dir%/app.log'
level: info
参考資料
Laravelでの設定
参考のために、Symfony以外のフレームワークでのMonologの使われ方として、Laravelを調べてみました。
Laravelの場合は、Monologの構造をそのまま使うのではなく、フレームワークが用意したログ用のクラスとコンフィギュレーションで設定を行えるようになっています。Monologは、フレームワークのロガーが利用するドライバーの内部実装として使われているという位置づけです。
参考:Laravelのログ
おわりに
Monologの基礎概念と、Symfonyでの設定方法の概要、およびLaravelでの使われ方を簡単に紹介しました。 一度概念的に整理しておけば、設定する際もスッキリと記述していけますよね。 Monologは非常に小さなライブラリなので、コードを読んで構造や概念を掴むのにさほど時間を必要としません。 積極的にライブラリのコードを読んでいきたいですね。