日志无报错堆栈因Log::init()覆盖默认处理器或log_level未包含error;须保留原生异常捕获逻辑、配置log_level含error/critical、继承think\log\driver\File重写save()。

日志里找不到报错堆栈?检查 Log::init() 是否覆盖了默认处理器
ThinkPHP 默认用 FileHandler 记录错误,但一旦你调用过 Log::init() 并传入自定义配置,它会完全替换掉原生处理器——包括对异常堆栈的捕获逻辑。常见现象是:线上 500 错误发生后,日志里只有「[error]」字样,没有 Trace、没有文件行号、甚至没 Exception 类名。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 确认是否在
common.php或app.php中主动调用了Log::init() - 若必须自定义,不要直接传空数组或只配
type,至少保留'format' => '[{date}][{level}]{message}'和关键上下文字段 - 优先用
Log::driver('file')->write(...)替代全局重置,避免影响框架底层异常捕获链
自定义 Handler 必须继承 think\log\driver\File 而非直接实现 think\log\LoggerInterface
很多人自己写日志处理器时,图省事直接实现接口,结果发现 think\Exception 报错时根本进不到你的 save() 方法里——因为 TP 的异常处理流程依赖父类中对 $this->config['level']、$this->config['realtime_write'] 等字段的预判和拦截逻辑。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 从
think\log\driver\File继承,重写save()即可,不用动check()或write() - 在
save()开头加if (strpos($log['message'], 'exception') !== false || $log['level'] === 'error')快速过滤关键日志 - 别在
save()里做耗时操作(如远程 HTTP 请求),TP 日志是同步阻塞执行的
Log::record() 不会触发自定义 Handler 的 save()?那是没调用 Log::save()
手动记录日志时,Log::record('xxx', 'error') 只是把日志压进内存队列;真正落地到文件/数据库,得靠 Log::save()。而这个方法默认只调用当前激活的 Handler 的 save()——如果你的自定义 Handler 没被正确注册为默认驱动,它就永远不会被执行。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 确保自定义类已通过
Log::setDriver('mylogger', MyLogger::class)注册,并在配置中设'default' => 'mylogger' - 在
App::after()或Hook::listen('app_end')中显式调用Log::save() - 调试时可在
save()开头加file_put_contents('/tmp/log_debug', print_r($log, 1), FILE_APPEND)验证是否进入
线上环境 display_error 关闭后,error_log 仍为空?检查 log_level 配置项
TP 的日志级别不是 PHP 的 error_reporting,而是独立控制的。即使 PHP 报了 Fatal error,如果配置里 'log_level' => ['notice'],那 error 级别日志会被静默丢弃——连 FileHandler 原生实现都不会写。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 生产环境配置中必须包含
'error'和'critical':'log_level' => ['error', 'critical', 'alert', 'emergency'] - 不要依赖
LOG_LEVEL环境变量自动映射,TP 6.0+ 对该变量支持不一致,老老实实写死数组更稳 - 用
Log::getLevel()在控制器里临时输出,确认当前生效的级别是否符合预期
真正卡住排查的,往往不是 Handler 写得有多复杂,而是 Log::init() 覆盖了异常捕获入口,或者 log_level 把 error 消息无声吞掉了——这两个点在线上最不好复现,得靠配置快照和日志开关状态交叉验证。



















