跳转到主要内容
当包需要提供自己的日志输出时,应将创建 logger 的机制与使用方应用选择的通道配置分离开来。仅注册驱动并不会重新创建已有的通道,即使配置发生变更,也可能仍在使用旧的 Handler。 本页面基于对 Laravel Framework v13.39.0 实现的确认。基本操作请参阅日志记录,服务提供者的准备请参阅包开发。

选择所需的扩展点

如果现有驱动能够实现需求,就无需新增自定义驱动。 via 的类和 tap 的类都通过容器解析,因此也可以使用构造函数注入。tap 是在通道生成时进行的调整,并不是每写入一条日志就执行一次的钩子。
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 工厂返回的 logger,LogManager 并不会统一应用配置中的 level。如果添加了自定义配置项,也需要在工厂中自行处理。
如果只是更改文件的输出位置,使用 single 驱动就足够了。这个示例是用于嵌入包专属 Handler 或初始化逻辑的最小结构。via 应使用可以存入配置缓存的类名,不要在配置数组中保存闭包或 logger 实例。

提供自定义驱动名称时使用 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 的服务之后仍可继续使用旧的 logger。这并不是将已有引用替换为新对象的操作。
  • 通过 Log::channel('stack') 等方式生成的 stack 持有一个独立的 Monolog,其中汇集了子通道的 Handler 和 Processor。仅对子通道调用 forgetChannel(),不会改变已有 stack 的结构。如有需要,也要解除该 stack 通道名称的保留并重新获取。
  • forgetChannel() 本身不会调用 Handler 的 close()。它并不是保证连接或缓冲区完成收尾处理的 API。
在常规部署中,应将更新配置缓存和重启所使用的长时间运行 worker 纳入运维流程。不要依赖每次请求都改写配置并重新解析的机制,同时也请考虑服务的生命周期。

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

自定义驱动可以返回 Psr\Log\LoggerInterface 的实现,但 Laravel 的 createStackDriver() 会对子通道调用 getHandlers() 和 getProcessors()。这些方法并不包含在 PSR 接口中。 返回任意 PSR logger 并不一定能在 stack 中正常工作。因此,对于同时支持加入 stack 的包,应像示例那样返回 Monolog,或明确说明其适用范围并进行测试。

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

仅使用 Log::shouldReceive('channel') 无法确认服务提供者的注册、工厂的选择以及配置的生效。下面是在包的测试环境中,经过实际的 Manager 和工厂的示例。 测试用的输出位置指定为 php://memory。这里验证通道的构建和 Handler 的配置,并不实际写入日志。
LogManager::get() 可能会捕获通道生成过程中发生的异常,并返回 emergency logger。仅确认”没有抛出异常”或”是 LoggerInterface”,并不能证明目标通道已成功构建。上面的测试还确认了 Monolog 的名称、Handler 和日志级别,以免将回退误认为成功。生成之后的写入错误,并不一定会被同一机制捕获。

长期维护中应补充的验证

参考的一手资料

最后修改于 2026年10月28日