ThinkPHP 6.x 的 File 日志驱动在写入失败时静默丢弃,不重试也不报错;应继承并重写 write() 方法添加重试与降级逻辑,通过配置替换默认驱动,同时配合外部日志轮转工具防范磁盘满等问题。

日志写入失败时直接静默丢弃?think\log\driver\File 默认不重试
ThinkPHP 6.x 的 File 日志驱动在磁盘满、权限不足或目录被删时,会静默跳过写入,连 error_log() 都不触发。这不是 bug,是设计取舍:避免日志逻辑拖垮主业务。但线上出问题时,你根本不知道「本该记下的错误」其实没记上。
实操建议:
- 不要依赖默认行为,必须主动接管异常路径
- 在
app\common\listener\LogWriteFailed这类自定义监听器中捕获think\exception\LogLevelException(注意:TP6 不抛该异常,需自己包装) - 真正可行的切入点是重写
think\log\driver\File::write(),并在 try/catch 中做降级
怎么给 File::write() 加重试和降级?别改 vendor,用继承+配置替换
直接改 vendor/topthink/framework/src/think/log/driver/File.php 是反模式:升级就丢。正确做法是继承原类,覆盖关键方法,并通过配置让框架用你的实现。
实操建议:
- 新建
app\extend\log\driver\RetryFile.php,继承think\log\driver\File - 重写
write()方法:外层加for ($i = 0; $i 循环,每次失败后usleep(50000)(50ms)再试 - 三次全失败后,降级写到
/tmp/thinkphp_fallback.log(确保该路径绝对可写,且不在 webroot 下) - 在
config/log.php中把'default' => 'file'对应的驱动类改成app\extend\log\driver\RetryFile::class
示例片段(非完整类):
error_log()
- 降级路径必须用正斜杠 + 绝对路径,例如
C:/temp/fallback.log,避免\转义问题 - 用
fopen($file, 'a')+flock()+fwrite()替代error_log(),确保原子写入 - 手动补
\n,否则多条日志挤在同一行 - 如果项目开了
mbstring.func_overload,fwrite()可能乱码,降级前先mb_internal_encoding('UTF-8')
重试不是万能的:磁盘满和 inotify 监控失效时,得靠外部告警兜底
重试最多解决瞬时 IO 失败,但若磁盘持续 100%、日志目录被 chattr +i 锁死、或 inotify watch 数超限导致 File 驱动的轮转检测失效,重试只会反复失败并掩盖根因。
实操建议:
- 在重试循环里加入
disk_free_space()检查,剩余 - 用
think\facade\Cache::remember('log_disk_check', 300, fn() => disk_free_space(...))避免每条日志都查磁盘 - 真正的防线不在 PHP 层:部署时必须配
logrotate或systemd-journald做外部轮转,而不是只靠 TP 自带的max_files
最常被忽略的一点:TP 的 max_files 是按天轮转,但如果你的日志量极大,单日文件可能超 2GB —— 此时 fopen('a') 在某些 Linux 内核下会失败,而这个错误不会进任何日志,只能靠外部监控发现。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











