monolog多通道日志分流关键在于handler绑定方式与上下文结构一致性:必须为每个通道创建独立logger实例并绑定专属handler,或在单logger中用filterhandler严格过滤级别,同时统一使用jsonformatter和结构化上下文,确保elasticsearch可检索、文件系统按级分离。

Monolog 多通道日志分流到 Elasticsearch 和文件系统,关键不在“能不能”,而在「Handler 绑定方式」和「上下文结构一致性」——直接复用同一个 Logger 实例推多个 Handler,会导致所有通道混写;必须按通道拆分 Logger 或用 Processor 过滤,否则 ES 里查不到 error.log 对应的堆栈,文件里又塞满 debug 日志。
多个 Logger 实例 vs 单 Logger + 多 Handler:别踩共享实例的坑
常见错误是创建一个 $logger = new \Monolog\Logger('app'),然后连续 pushHandler() 文件 Handler 和 Elasticsearch Handler。结果是:ERROR 级日志既写进 error.log,又发往 ES,但 DEBUG 日志也一并进了 ES,根本没法按通道做权限隔离或检索过滤。
- 正确做法是为每个通道新建独立
Logger实例,比如$errorLogger = new \Monolog\Logger('error')、$accessLogger = new \Monolog\Logger('access') - 每个
Logger只绑定对应 Handler:$errorLogger->pushHandler($esHandler)+$errorLogger->pushHandler($rotatingFileHandler),这样 ERROR 日志才真正只走这两路 - 若坚持单
Logger,必须配合FilterHandler包裹目标 Handler,并设置minLevel和maxLevel,否则级别过滤在 Handler 层就失效了
RotatingFileHandler 与 DailyRotatingFileHandler 的轮转逻辑差异
RotatingFileHandler 按大小切分(如单文件 10MB),而 DailyRotatingFileHandler(v2.0+)按日期生成新文件(app-2026-07-09.log)。两者底层都依赖 rename() 和 file_exists(),但在 Swoole 或多进程环境下,可能因文件句柄未释放导致归档失败。
- 生产环境务必设置
$handler->setFilenameFormat('{filename}-{date}', 'Y-m-d'),否则DailyRotatingFileHandler默认用Y-m-d但不带前缀,容易和旧日志混淆 -
RotatingFileHandler的第二个参数是保留归档数(如5),不是总文件数——它会删掉最老的.1~.5,而不是清空目录 - Linux 下注意
/var/log/app/目录权限:PHP 进程用户(如www-data)必须有写+执行权限,否则轮转时mkdir()报Permission denied
Elasticsearch Handler 必须结构化输出,否则字段全丢
直接用 StreamHandler 或 SocketHandler 发原始文本到 Logstash,ES 里只能当 message 字段全文检索,trace_id、user_id 全变成字符串子串,无法聚合或筛选。
- 必须用支持 JSON 的 Formatter,例如
new \Monolog\Formatter\JsonFormatter(),且禁用appendNewline(Logstash 需要每行一个 JSON 对象) - ES Handler 推荐用
ruflin/elastica的ElasticaHandler,或 Monolog 官方ElasticSearchHandler(需 v3.0+),避免自己拼curl - 关键上下文必须显式传入:
$logger->error('DB timeout', ['trace_id' => $traceId, 'sql' => $sql]),不能依赖全局变量——Swoole Worker 里$_SERVER是共享的,协程间会串
通道名(channel)不是装饰,它决定 ES 索引路由和文件路径生成
Monolog 的 channel 参数不只是日志头里的字符串,它是 Handler 路由和索引命名的实际依据。比如配置 monolog.yaml 时写 channels: ['payment', 'auth'],ES 索引名可设为 log-%channel%-%date%,文件路径也可动态插值为 /var/log/%channel%.log。
- 不要用硬编码字符串当 channel 名,如
new \Monolog\Logger('order_service_v2')—— 后期改名就得改代码+ES mapping - 建议统一用小写 + 下划线,如
'payment_gateway',避免 ES 索引名含大写或特殊字符报错 - channel 名会作为
context['channel']自动注入每条日志,可在 Kibana 里直接channel: "payment_gateway"过滤
最易被忽略的是上下文快照时机:在 Swoole onRequest 回调里注入 trace_id,但异常发生在 Redis callback 里,此时 $logger->error() 已拿不到请求上下文。必须用 Processor 在日志生成瞬间捕获,而不是靠中间件“存起来再取”。











