パッケージ独自のログ出力を提供したいときは、ロガーを作る仕組みと、利用アプリケーションが選ぶチャネル設定を分離します。ドライバーを登録するだけで既存のチャネルが作り直されるわけではなく、設定変更後も古いHandlerが使われる場合があります。
このページはLaravel Framework v13.39.0 の実装を確認しています。基本操作はロギング、プロバイダーの準備はパッケージ開発を参照してください。
必要な拡張ポイントを選ぶ
既存ドライバーで実現できる場合、独自ドライバーを増やす必要はありません。
via のクラスと tap のクラスはコンテナで解決されるため、コンストラクター注入も利用できます。tap はチャネル生成時の調整であり、ログを1件書き込むたびに実行されるフックではありません。
logging.channels.audit の audit はチャネル名です。その中の driver は生成方法の名前です。Log::channel('audit') で選ぶのはチャネル名なので、後でドライバーを変更しても呼び出し側の名前を維持できます。
まずcustomとviaでファクトリを公開する
架空の Acme\Audit パッケージで、ファイル出力用のファクトリを提供します。PSR-4で読み込める src/Logging/CreateAuditLogger.php に配置します。
利用アプリケーションでは、config/logging.php の既存の channels 配列に次の要素を追加します。既存の default や stack の設定は残します。
LogManager::createCustomDriver() は via のクラスをコンテナから取得し、チャネルの設定配列を唯一の引数として呼び出します。この方式だけなら、ドライバー登録用のプロバイダーは不要です。
このファクトリでは、level をStreamHandlerへ明示的に渡しています。custom のファクトリが返すロガーに対して、LogManagerが設定の level を一律に適用するわけではありません。独自の設定項目を追加した場合も、ファクトリ側で反映します。
単にファイルの出力先を変えるだけなら single ドライバーで十分です。この例はパッケージ独自のHandlerや初期化を組み込むための最小構成です。via は設定キャッシュに格納できるクラス名にし、設定配列にクロージャやロガーのインスタンスを保存しないでください。
独自ドライバー名を提供するときはextendを使う
利用側に driver: acme-audit という設定を公開したい場合は、同じファクトリを使うドライバーを登録します。src/AuditServiceProvider.php の boot() で、Laravelの log サービスを拡張します。
プロバイダーはパッケージの自動検出などで読み込みます。利用側の channels.audit は、先ほどの設定から次の設定へ置き換えます。via は不要になります。
- Laravelの
LogServiceProvider は log をsingletonとして登録します。callAfterResolving() は未解決なら待ち、解決済みならそのインスタンスにも登録を反映します。詳しくはサービス解決後フックを参照してください。
extend() は生成用クロージャを登録するだけです。チャネルの生成やログの書き込みは、このコールバックの登録時には行いません。
callCustomCreator() はクロージャへ $app とチャネル設定配列を渡します。via のファクトリに渡される引数とは異なります。
- 同じドライバー名の再登録は生成用クロージャを上書きします。
single などの組み込み名や、他パッケージと衝突する名前を使わないでください。
確認した実装では、extend() に渡した匿名クロージャはLogManagerのスコープへ再バインドされます。非staticクロージャの $this はサービスプロバイダーではありません。例のようにstaticクロージャで引数の $app を使い、プロバイダーの $this に依存しない実装にします。
パッケージはドライバーを提供し、どのチャネルやstackで使うかは利用側が選びます。プロバイダーで logging.default や既存の logging.channels 全体を書き換えない設計にしてください。
チャネルは名前ごとに保持される
LogManager::get() は解決したチャネルを $channels に保存します。同じ名前のチャネルを取得すると、設定配列を読み直さず保持済みのオブジェクトを返します。
これはファイルに保存する設定キャッシュとは別の、現在のLogManager内のオブジェクト保持です。config() の変更や extend() の再登録だけでは、作成済みのHandlerは入れ替わりません。
この例の $same は $before と同じオブジェクトです。forgetChannel('audit') がManagerの保持を解除すると、次の取得で新しい設定から $after が生成されます。
forgetChannelは既存の参照を更新しない
forgetChannel() が行うのはManager内のエントリー削除です。
$before を保持しているサービスは、その後も古いロガーを使えます。既存の参照を新しいオブジェクトへ差し替える操作ではありません。
Log::channel('stack') などで生成済みのstackは、子チャネルのHandlerやProcessorを集めた独立したMonologを持ちます。子だけを forgetChannel() しても、既存stackの構成は変わりません。必要なら、そのstackのチャネル名についても保持を解除し、取得し直します。
forgetChannel() 自体はHandlerの close() を呼びません。接続やバッファーの終了処理まで保証するAPIではありません。
通常のデプロイでは、設定キャッシュの更新と、利用している長寿命ワーカーの再起動を運用に含めます。リクエストごとに設定を書き換えて再解決する仕組みに頼らず、サービスのライフサイクルも考慮してください。
stackへの組み込みにはMonolog互換の操作も必要
独自ドライバーは Psr\Log\LoggerInterface の実装を返せますが、Laravelの createStackDriver() は子チャネルに getHandlers() と getProcessors() を呼びます。これらはPSRのインターフェースには含まれません。
任意のPSRロガーを返せばstackでも動くとは限らないため、stackへの組み込みも提供するパッケージでは、例のようにMonologを返すか、その利用範囲を明示してテストします。
モックだけでなく実際のチャネル解決を検証する
Log::shouldReceive('channel') だけでは、プロバイダーの登録、ファクトリの選択、設定の反映は確認できません。次はパッケージのテスト環境で、実際のManagerとファクトリを通す例です。
テスト用の出力先には php://memory を指定します。ここではチャネルの構築とHandlerの設定を検証し、ログの書き込み自体は行いません。
LogManager::get() はチャネルの生成中に発生した例外を捕捉し、emergency loggerを返すことがあります。「例外が出なかった」「LoggerInterfaceだった」という確認だけでは、目的のチャネルが構築できた証拠になりません。上のテストではMonologの名前、Handler、ログレベルも確認して、フォールバックを成功と誤認しないようにしています。生成後の書き込みエラーまで、同じ仕組みで必ず捕捉されるわけではありません。
長期保守で追加する検証
参照した一次情報