> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# パッケージのログチャネル拡張と再解決

> Laravel 13のcustom・viaとLogManager::extend()を使い分け、チャネルの保持、forgetChannel()、フォールバックを考慮してログ機能をテストします。

パッケージ独自のログ出力を提供したいときは、**ロガーを作る仕組み**と、**利用アプリケーションが選ぶチャネル設定**を分離します。ドライバーを登録するだけで既存のチャネルが作り直されるわけではなく、設定変更後も古いHandlerが使われる場合があります。

このページはLaravel Framework `v13.39.0` の実装を確認しています。基本操作は[ロギング](https://kawax.biz/jp/logging.md)、プロバイダーの準備は[パッケージ開発](https://kawax.biz/jp/advanced/package-development.md)を参照してください。

## 必要な拡張ポイントを選ぶ

既存ドライバーで実現できる場合、独自ドライバーを増やす必要はありません。

| やりたいこと | 選択肢 | 拡張する場所 |
| - | - | - |
| 既存チャネルのフォーマッターなどを調整する | `tap` | 生成された `Illuminate\Log\Logger` |
| MonologのHandlerを設定で選ぶ | `driver: monolog` | `handler`、`handler_with` などの設定 |
| パッケージのファクトリでロガーを組み立てる | `driver: custom` と `via` | `__invoke(array $config)` を持つクラス |
| パッケージ固有のドライバー名を公開する | `LogManager::extend()` | アプリケーションと設定配列を受け取るクロージャ |

`via` のクラスと `tap` のクラスはコンテナで解決されるため、コンストラクター注入も利用できます。`tap` はチャネル生成時の調整であり、ログを1件書き込むたびに実行されるフックではありません。

> [!NOTE]
>
> `logging.channels.audit` の `audit` は**チャネル名**です。その中の `driver` は**生成方法の名前**です。`Log::channel('audit')` で選ぶのはチャネル名なので、後でドライバーを変更しても呼び出し側の名前を維持できます。

## まずcustomとviaでファクトリを公開する

架空の `Acme\Audit` パッケージで、ファイル出力用のファクトリを提供します。PSR-4で読み込める `src/Logging/CreateAuditLogger.php` に配置します。

```php theme={null}
<?php

namespace Acme\Audit\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;

final class CreateAuditLogger
{
    public function __invoke(array $config): Logger
    {
        return new Logger(
            $config['name'] ?? 'acme-audit',
            [new StreamHandler($config['path'], $config['level'] ?? 'info')],
        );
    }
}
```

利用アプリケーションでは、`config/logging.php` の既存の `channels` 配列に次の要素を追加します。既存の `default` や `stack` の設定は残します。

```php theme={null}
'audit' => [
    'driver' => 'custom',
    'via' => \Acme\Audit\Logging\CreateAuditLogger::class,
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

`LogManager::createCustomDriver()` は `via` のクラスをコンテナから取得し、チャネルの設定配列を唯一の引数として呼び出します。この方式だけなら、ドライバー登録用のプロバイダーは不要です。

```php theme={null}
use Illuminate\Support\Facades\Log;

Log::channel('audit')->info('注文を確定しました。', ['order_id' => 42]);
```

このファクトリでは、`level` をStreamHandlerへ明示的に渡しています。**`custom` のファクトリが返すロガーに対して、LogManagerが設定の `level` を一律に適用するわけではありません**。独自の設定項目を追加した場合も、ファクトリ側で反映します。

> [!TIP]
>
> 単にファイルの出力先を変えるだけなら `single` ドライバーで十分です。この例はパッケージ独自のHandlerや初期化を組み込むための最小構成です。`via` は設定キャッシュに格納できるクラス名にし、設定配列にクロージャやロガーのインスタンスを保存しないでください。

## 独自ドライバー名を提供するときはextendを使う

利用側に `driver: acme-audit` という設定を公開したい場合は、同じファクトリを使うドライバーを登録します。`src/AuditServiceProvider.php` の `boot()` で、Laravelの `log` サービスを拡張します。

```php theme={null}
<?php

namespace Acme\Audit;

use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\LogManager;
use Illuminate\Support\ServiceProvider;
use Monolog\Logger as Monolog;

class AuditServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('log', static function (LogManager $manager): void {
            $manager->extend('acme-audit', static function (Application $app, array $config): Monolog {
                return $app->make(CreateAuditLogger::class)($config);
            });
        });
    }
}
```

プロバイダーは[パッケージの自動検出](https://kawax.biz/jp/advanced/package-discovery.md)などで読み込みます。利用側の `channels.audit` は、先ほどの設定から次の設定へ置き換えます。`via` は不要になります。

```php theme={null}
'audit' => [
    'driver' => 'acme-audit',
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

* Laravelの `LogServiceProvider` は `log` をsingletonとして登録します。`callAfterResolving()` は未解決なら待ち、解決済みならそのインスタンスにも登録を反映します。詳しくは[サービス解決後フック](https://kawax.biz/jp/advanced/package-service-resolution.md)を参照してください。
* `extend()` は生成用クロージャを登録するだけです。チャネルの生成やログの書き込みは、このコールバックの登録時には行いません。
* `callCustomCreator()` はクロージャへ `$app` とチャネル設定配列を渡します。`via` のファクトリに渡される引数とは異なります。
* 同じドライバー名の再登録は生成用クロージャを上書きします。`single` などの組み込み名や、他パッケージと衝突する名前を使わないでください。

> [!WARNING]
>
> 確認した実装では、`extend()` に渡した匿名クロージャはLogManagerのスコープへ再バインドされます。非staticクロージャの `$this` はサービスプロバイダーではありません。例のようにstaticクロージャで引数の `$app` を使い、プロバイダーの `$this` に依存しない実装にします。

パッケージはドライバーを提供し、どのチャネルやstackで使うかは利用側が選びます。プロバイダーで `logging.default` や既存の `logging.channels` 全体を書き換えない設計にしてください。

## チャネルは名前ごとに保持される

`LogManager::get()` は解決したチャネルを `$channels` に保存します。同じ名前のチャネルを取得すると、設定配列を読み直さず保持済みのオブジェクトを返します。

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"auditは解決済み？"}
    B -->|Yes| C["保持済みのLoggerを返す"]
    B -->|No| D["channels.auditのdriverを選択"]
    D --> E["viaまたはextendのファクトリ"]
    E --> F["Illuminate Loggerでラップ<br>tapと共有コンテキストを適用"]
    F --> G["auditの名前で保持"]
```

これはファイルに保存する設定キャッシュとは別の、**現在のLogManager内のオブジェクト保持**です。`config()` の変更や `extend()` の再登録だけでは、作成済みのHandlerは入れ替わりません。

```php theme={null}
use Illuminate\Support\Facades\Log;

$before = Log::channel('audit');

config(['logging.channels.audit.level' => 'error']);

$same = Log::channel('audit');

Log::forgetChannel('audit');

$after = Log::channel('audit');
```

この例の `$same` は `$before` と同じオブジェクトです。`forgetChannel('audit')` がManagerの保持を解除すると、次の取得で新しい設定から `$after` が生成されます。

### forgetChannelは既存の参照を更新しない

`forgetChannel()` が行うのはManager内のエントリー削除です。

* `$before` を保持しているサービスは、その後も古いロガーを使えます。既存の参照を新しいオブジェクトへ差し替える操作ではありません。
* `Log::channel('stack')` などで生成済みのstackは、子チャネルのHandlerやProcessorを集めた独立したMonologを持ちます。子だけを `forgetChannel()` しても、既存stackの構成は変わりません。必要なら、そのstackのチャネル名についても保持を解除し、取得し直します。
* `forgetChannel()` 自体はHandlerの `close()` を呼びません。接続やバッファーの終了処理まで保証するAPIではありません。

通常のデプロイでは、[設定キャッシュの更新](https://kawax.biz/jp/advanced/package-config-merging.md)と、利用している長寿命ワーカーの再起動を運用に含めます。リクエストごとに設定を書き換えて再解決する仕組みに頼らず、[サービスのライフサイクル](https://kawax.biz/jp/advanced/package-service-lifetimes.md)も考慮してください。

### stackへの組み込みにはMonolog互換の操作も必要

独自ドライバーは `Psr\Log\LoggerInterface` の実装を返せますが、Laravelの `createStackDriver()` は子チャネルに `getHandlers()` と `getProcessors()` を呼びます。これらはPSRのインターフェースには含まれません。

任意のPSRロガーを返せばstackでも動くとは限らないため、stackへの組み込みも提供するパッケージでは、例のようにMonologを返すか、その利用範囲を明示してテストします。

## モックだけでなく実際のチャネル解決を検証する

`Log::shouldReceive('channel')` だけでは、プロバイダーの登録、ファクトリの選択、設定の反映は確認できません。次は[パッケージのテスト環境](https://kawax.biz/jp/advanced/package-testing.md)で、実際のManagerとファクトリを通す例です。

テスト用の出力先には `php://memory` を指定します。ここではチャネルの構築とHandlerの設定を検証し、ログの書き込み自体は行いません。

```php theme={null}
<?php

namespace Acme\Audit\Tests;

use Acme\Audit\AuditServiceProvider;
use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger as Monolog;
use Orchestra\Testbench\TestCase;

class AuditLoggingTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [AuditServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('logging.channels.audit', [
            'driver' => 'acme-audit',
            'path' => 'php://memory',
            'level' => 'warning',
        ]);

        $app['config']->set('logging.channels.emergency.path', 'php://memory');
    }

    public function test_the_package_driver_creates_the_configured_logger(): void
    {
        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    public function test_configuration_changes_require_a_new_channel(): void
    {
        $before = Log::channel('audit');

        config(['logging.channels.audit.level' => 'error']);

        $this->assertSame($before, Log::channel('audit'));
        $this->assertAuditChannel($before, Level::Warning);

        Log::forgetChannel('audit');

        $after = Log::channel('audit');

        $this->assertNotSame($before, $after);
        $this->assertAuditChannel($after, Level::Error);
        $this->assertAuditChannel($before, Level::Warning);
    }

    public function test_the_same_factory_can_be_used_with_via(): void
    {
        config([
            'logging.channels.audit.driver' => 'custom',
            'logging.channels.audit.via' => CreateAuditLogger::class,
        ]);

        Log::forgetChannel('audit');

        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    private function assertAuditChannel(Logger $channel, Level $level): void
    {
        $logger = $channel->getLogger();

        $this->assertInstanceOf(Monolog::class, $logger);
        $this->assertSame('acme-audit', $logger->getName());
        $this->assertCount(1, $logger->getHandlers());

        $handler = $logger->getHandlers()[0];

        $this->assertInstanceOf(StreamHandler::class, $handler);
        $this->assertSame($level, $handler->getLevel());
    }
}
```

> [!WARNING]
>
> `LogManager::get()` はチャネルの生成中に発生した例外を捕捉し、emergency loggerを返すことがあります。「例外が出なかった」「LoggerInterfaceだった」という確認だけでは、目的のチャネルが構築できた証拠になりません。上のテストではMonologの名前、Handler、ログレベルも確認して、フォールバックを成功と誤認しないようにしています。生成後の書き込みエラーまで、同じ仕組みで必ず捕捉されるわけではありません。

## 長期保守で追加する検証

| 変更や利用方法 | 確認すること |
| - | - |
| `tap` の追加 | 実際のファクトリが返すロガーに対して、期待する調整が適用される |
| stackへの組み込み | 子チャネルのHandlerとProcessorが組み込まれ、再解決時の構成も正しい |
| 出力先やフォーマットの変更 | 実際の書き込み内容、権限、ローテーションなどを別の検証環境で確認する |
| 登録時点の変更 | `log` がすでに解決済みでもドライバーが登録され、チャネルを生成できる |
| Laravelの更新 | コールバックの引数とスコープ、保持の解除、emergency loggerへのフォールバックを継続確認する |

## 参照した一次情報

* [Laravel公式ドキュメント: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: LogManagerの生成・拡張・保持解除](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: Loggerのラップとメソッド転送](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: LogServiceProviderのsingleton登録](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogServiceProvider.php)
* [Framework v13.39.0: ServiceProvider::callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Framework v13.39.0: RebindsCallbacksToSelf](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/RebindsCallbacksToSelf.php)
* [Laravel公式テスト: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [ロギング](https://kawax.biz/jp/logging.md)
- [パッケージのカスタム通知チャネルと配信検証](https://kawax.biz/jp/advanced/package-notification-channels.md)
- [パッケージのStorageドライバー登録と再解決](https://kawax.biz/jp/advanced/package-filesystem-drivers.md)
- [パッケージのBlade拡張とコンパイルキャッシュ](https://kawax.biz/jp/advanced/package-blade-extensions.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.