SQL日志与错误日志必须分开配置、分开落盘,否则高并发下关键错误易被污染或遗漏;SQL日志应监听db_query/db_execute事件,记录原始语句、绑定参数及连接信息,禁用Log::sql();错误日志须覆盖未捕获异常、PHP致命错误和404,并确保APP_DEBUG=false及runtime/log/目录权限正确。

SQL日志和错误日志必须分开配置、分开落盘,否则查问题时互相污染,尤其在高并发下容易漏掉关键错误。
SQL日志该用 db_query/db_execute 事件监听,别信 Log::sql()
Log::sql() 只是简单写一条日志,不带参数、不带执行耗时、不区分 INSERT/UPDATE/DELETE,而且只在查询构造器走通时才触发——原生 Db::query() 或事务内多条语句可能完全不进这条日志。db_query 和 db_execute 这两个系统事件才是真实覆盖所有 SQL 执行路径的钩子。
- 监听器回调的
$params['sql']是带 ? 占位符的原始语句(如"SELECT * FROM user WHERE id = ?"),$params['bind']是实际绑定值,必须分开记录,不能拼成完整 SQL 字符串(否则会丢掉 null → NULL、时间格式化等细节) - 想看“最终执行语句”仅限调试:手动用
str_replace()替换占位符,但生产环境禁用,性能损耗明显 - 务必自己用
microtime(true)包裹逻辑测耗时,$params里没有 duration 字段 - 多库场景下,
$params['connection']必须记进日志,否则分不清是哪个库出的问题
错误日志要捕获三类错误:未 catch 异常、PHP 致命错误、404
ThinkPHP 的 Log::error() 只管你主动调用的那部分;真正要命的是没被 catch 的异常、Fatal error、Parse error,它们根本不会走到 Log::error() ——得靠框架底层的 shutdown 处理机制。
- 线上环境必须设
APP_DEBUG = false(.env 文件),否则 shutdown 函数不生效,致命错误直接抛页面,不落盘 - 确认
config/exception.php中'log' => true已开启,它控制未捕获异常是否写日志 -
'ignore_404' => false要显式设,否则 404 请求不进日志,接口误配或爬虫扫路径时就抓瞎 - 别指望
try-catch包住所有错误:语法错误、扩展缺失、内存超限这些,在框架加载前就崩了,Log::error()根本执行不到
日志路径和文件名配置稍错,日志就静默丢失
runtime/log/ 目录不存在、没写权限、或配置了动态子目录(如 date('Ym')),都会导致日志写入失败且无报错提示,现象就是“明明配了日志,却找不到任何文件”。
立即学习“PHP免费学习笔记(深入)”;
- 部署时必须手动创建
runtime/log/并确保 Web 用户有写权限(Linux:chmod -R 755 runtime/;Windows:检查 IIS/Apache 用户权限) - 若需固定文件名便于监控或日志采集,改
config/log.php中'file' => 'log/error.log'(注意路径相对于runtime/) - 禁止在
'path'或'file'配置里用date()、uniqid()等函数——ThinkPHP 不自动创建多级目录,会静默失败 - 单条日志别塞太长:SQL + bind 超过 2KB 容易撑爆日志驱动,默认按天切片,grep 困难、ELK 解析失败
最易被忽略的是:SQL 日志和错误日志共用一个通道时,慢查询日志会把 ERROR 级别消息挤出当日文件;而 db_query 事件监听器若没做敏感字段过滤(比如 $params['bind'] 里的 password),上线后就是数据泄露风险点。



















