Monolog 的 channel 是日志出口名称(如 stack、daily),handler 是执行写入的底层类(如 StreamHandler);stack channel 本质是 HandlerStack,通过 isHandling() 动态路由日志到不同 handler。

什么是 Monolog 的 channel 和 handler 关系?
在 Laravel 中,channel 是日志的“出口名称”,比如 stack、single、daily;而 handler 是真正执行写入动作的底层类(如 StreamHandler、RotatingFileHandler)。Laravel 的 stack channel 本质是 Monolog 的 HandlerStack,它支持按条件决定是否把某条日志交给某个 handler —— 这就是分渠道记录的核心机制。
如何用 Processor + Handler 实现按请求来源写不同文件?
不能只靠配置文件硬编码多个 channel,因为“渠道”(如 API / Web / CLI / Queue)往往体现在运行时上下文里。正确做法是:复用同一个 stack channel,但为不同 handler 添加 Processor 或自定义 Handler 的 isHandling() 方法来过滤。
- 在
config/logging.php中定义一个带条件 handler 的 channel,例如conditional - 创建自定义 handler 类,继承
Monolog\Handler\StreamHandler,重写isHandling() - 在
isHandling()中检查当前环境或请求特征:app()->runningInConsole()、request()->is('api/*')(需确保在 HTTP 上下文中)、app()->bound('queue') && app()->make('queue')->getName() - 注意:不要在
isHandling()里调用可能触发日志循环的方法(如Log::info()),否则会栈溢出
class ApiOnlyStreamHandler extends StreamHandler
{
public function isHandling(array $record): bool
{
if (! app()->runningInConsole() && request()->is('api/*')) {
return true;
}
return false;
}
}
为什么不能直接在 config/logging.php 里用闭包做条件判断?
Laravel 日志配置在容器启动早期就被加载并固化,此时 request()、app() 等实例尚未完全可用,闭包中引用它们会导致 Target class [request] does not exist 或 Application is not bound 错误。所有动态逻辑必须下沉到 handler 实例内部,而不是配置数组里。
- 错误写法:
'handler' => fn() => request()->is('api/*') ? new StreamHandler(storage_path('logs/api.log')) : null - 正确路径:注册自定义 handler 到容器,并在
logging.php中通过handler键指向该类名 - 注册方式示例:在
App\Providers\AppServiceProvider::register()中调用$this->app->bind('log.handlers.api', function () { return new ApiOnlyStreamHandler(...); });
如何避免多 handler 写入冲突和性能损耗?
Monolog 默认对每个 handler 都执行完整格式化流程(包括 Formatter、Processor),即使最终没写入。如果配置了 5 个条件 handler,每条日志都会被格式化 5 次,其中最多只有 1 个真正落盘。这是隐性性能陷阱。
- 解决方案一:把条件判断前移到
isHandling(),确保绝大多数 handler 快速返回false,跳过后续处理 - 解决方案二:使用单个 handler,在其
write()中做路由逻辑,手动选择目标文件路径(更可控,但失去 Monolog 原生 rotate 能力) - 关键细节:
StreamHandler的$level参数仅控制日志级别阈值,不参与业务渠道判断;渠道逻辑必须独立实现
最易被忽略的一点:Laravel 的 stack channel 默认启用 BufferHandler 包裹,它会缓存日志直到脚本结束才批量写入——这会导致条件判断时上下文已丢失(比如 request 对象被销毁)。务必在自定义 handler 中禁用 buffer,或改用 WhatFailureGroupHandler 组合方式替代 stack。











