Monolog 真正难点在于配置决策与线上行为预判:RotatingFileHandler 不自动创建目录需手动确保路径存在;LineFormatter 需设第三、四参数为 true 和 false 才显示 trace 与 context;$bubble 控制日志是否继续传递;Logger 应复用而非循环新建;$maxFiles=7 指保留7天日志,依赖文件名日期且受系统时间回拨影响。

RotatingFileHandler 为什么日志文件名带日期却不自动创建目录?
RotatingFileHandler 只负责轮转,不负责 mkdir。如果 __DIR__.'/logs/app.log' 中的 logs/ 目录不存在,它会直接抛出 RuntimeException,且在 web 环境下常静默为 500 错误。
- 初始化前必须手动确保路径存在:
is_dir(__DIR__.'/logs') || mkdir(__DIR__.'/logs', 0755, true) - 别依赖
touch()或写空文件来“触发创建”——RotatingFileHandler 不检查父目录可写性,只检查最终路径是否可 open - 线上部署脚本里要显式加
mkdir -p /var/log/myapp并chown www-data:www-data /var/log/myapp
LineFormatter 怎么让 trace 和 context 真正显示出来?
默认 LineFormatter 实例(无参构造)会丢掉 context 和异常堆栈,只输出时间+级别+消息。关键在后两个布尔参数:
- 第三个参数
$includeStacktraces:设为true才解析exception键并输出完整 trace - 第四个参数
$ignoreEmptyContextAndExtra:设为false才把context数组展开进日志行(否则空数组或 null 就被跳过) - 示例:
new LineFormatter(null, null, true, false)—— 这才是开发期能查上下文的配置 - 注意:
RotatingFileHandler默认使用LineFormatter,但你必须显式setFormatter(),它不会自动继承
为什么 pushHandler 两次,只有最后一个生效?
不是“只有最后一个生效”,而是 handler 栈按 LIFO 顺序执行,且每个 handler 的 $bubble 默认为 true,意味着它处理完还会继续往下传。但常见错误是没意识到级别过滤发生在 handler 内部,不是全局拦截:
-
StreamHandler(__DIR__.'/error.log', Logger::ERROR)只会写 ERROR 及以上,INFO 消息到它这儿就终止了,不会进下一个 handler - 想让所有日志进文件、仅 ERROR 进邮件,得把
NativeMailerHandler放前面,并设$bubble = false,否则 ERROR 会先发邮件、再写文件,而你可能只想发邮件不写文件 - 检查 handler 是否生效:在
pushHandler()后加var_dump($logger->getHandlers()),确认数量和类型
Logger 实例能不能复用?循环里 new 一次行不行?
可以复用,而且必须复用。每次 new Logger() 开销不大,但 handler 初始化(尤其是 RotatingFileHandler 构造时的 stat() 和 fopen())有真实系统调用成本。
立即学习“PHP免费学习笔记(深入)”;
- Web 请求生命周期内,全局单例或 DI 容器注入一个
$logger实例即可 - 绝对不要在 for 循环里
new StreamHandler()—— 每次都打开/关闭文件描述符,容易触发Too many open files - 如果要用不同 channel(如
'auth'和'payment'),也该复用 handler,只换 Logger 实例:$authLogger->pushHandler($sharedFileHandler) - 测试时容易忽略:CLI 脚本若长时运行(如队列 worker),复用 Logger + RotatingFileHandler 会导致轮转判断失效(基于首次初始化时间),此时应改用
WhatFailureGroupHandler包一层
$maxFiles = 7 是指 7 个文件,还是 7 天?答案是后者,但它只在每次写日志时比对当前日期与文件名中的日期字符串——如果服务器时间回拨,旧日志可能被误删。这种细节,文档不写,报错不提示,只能踩过才信。



















