> ## 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() 和回退的前提下测试日志功能。

当包需要提供自己的日志输出时，应将**创建 logger 的机制**与**使用方应用选择的通道配置**分离开来。仅注册驱动并不会重新创建已有的通道，即使配置发生变更，也可能仍在使用旧的 Handler。

本页面基于对 Laravel Framework `v13.39.0` 实现的确认。基本操作请参阅[日志记录](https://kawax.biz/zh-CN/logging.md)，服务提供者的准备请参阅[包开发](https://kawax.biz/zh-CN/advanced/package-development.md)。

## 选择所需的扩展点

如果现有驱动能够实现需求，就无需新增自定义驱动。

| 目的 | 选项 | 扩展位置 |
| - | - | - |
| 调整现有通道的格式化器等 | `tap` | 生成的 `Illuminate\Log\Logger` |
| 通过配置选择 Monolog 的 Handler | `driver: monolog` | `handler`、`handler_with` 等配置 |
| 用包的工厂组装 logger | `driver: custom` 与 `via` | 拥有 `__invoke(array $config)` 的类 |
| 公开包专属的驱动名称 | `LogManager::extend()` | 接收应用实例和配置数组的闭包 |

`via` 的类和 `tap` 的类都通过容器解析，因此也可以使用构造函数注入。`tap` 是在通道生成时进行的调整，并不是每写入一条日志就执行一次的钩子。

> [!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` 工厂返回的 logger，LogManager 并不会统一应用配置中的 `level`**。如果添加了自定义配置项，也需要在工厂中自行处理。

> [!TIP]
>
> 如果只是更改文件的输出位置，使用 `single` 驱动就足够了。这个示例是用于嵌入包专属 Handler 或初始化逻辑的最小结构。`via` 应使用可以存入配置缓存的类名，不要在配置数组中保存闭包或 logger 实例。

## 提供自定义驱动名称时使用 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/zh-CN/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/zh-CN/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` 的服务之后仍可继续使用旧的 logger。这并不是将已有引用替换为新对象的操作。
* 通过 `Log::channel('stack')` 等方式生成的 stack 持有一个独立的 Monolog，其中汇集了子通道的 Handler 和 Processor。仅对子通道调用 `forgetChannel()`，不会改变已有 stack 的结构。如有需要，也要解除该 stack 通道名称的保留并重新获取。
* `forgetChannel()` 本身不会调用 Handler 的 `close()`。它并不是保证连接或缓冲区完成收尾处理的 API。

在常规部署中，应将[更新配置缓存](https://kawax.biz/zh-CN/advanced/package-config-merging.md)和重启所使用的长时间运行 worker 纳入运维流程。不要依赖每次请求都改写配置并重新解析的机制，同时也请考虑[服务的生命周期](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)。

### 加入 stack 还需要兼容 Monolog 的操作

自定义驱动可以返回 `Psr\Log\LoggerInterface` 的实现，但 Laravel 的 `createStackDriver()` 会对子通道调用 `getHandlers()` 和 `getProcessors()`。这些方法并不包含在 PSR 接口中。

返回任意 PSR logger 并不一定能在 stack 中正常工作。因此，对于同时支持加入 stack 的包，应像示例那样返回 Monolog，或明确说明其适用范围并进行测试。

## 不仅使用 mock，还要验证实际的通道解析

仅使用 `Log::shouldReceive('channel')` 无法确认服务提供者的注册、工厂的选择以及配置的生效。下面是在[包的测试环境](https://kawax.biz/zh-CN/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` | 对实际工厂返回的 logger 应用了预期的调整 |
| 加入 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/zh-CN/advanced/index.md)
- [日志记录](https://kawax.biz/zh-CN/logging.md)
- [包的 Storage 驱动注册与重新解析](https://kawax.biz/zh-CN/advanced/package-filesystem-drivers.md)
- [包的 Blade 扩展与编译缓存](https://kawax.biz/zh-CN/advanced/package-blade-extensions.md)
- [包的服务解析后钩子](https://kawax.biz/zh-CN/advanced/package-service-resolution.md)


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