应统一监听db_query和db_execute事件,因其在sql执行前后触发且覆盖所有数据库操作;通过$params['sql']和var_export($params['bind'], true)分离记录,避免拼接风险,并补充trace_id关联业务上下文。

数据库操作日志该往哪儿写
直接写进业务代码里?不行。每次 Db::table()->insert() 都手动加日志,漏一次就丢一条,维护成本高还容易被删掉。ThinkPHP 的日志入口得统一收口到框架底层钩子上——用 db_query 和 db_execute 这两个系统事件最稳,它们在 SQL 实际执行前后触发,不依赖模型、查询构造器或事务状态。
监听器里怎么拿到 SQL 和参数
监听器回调函数的第二个参数是 $params,里面藏着真实要执行的 SQL 和绑定参数。但注意:$params['sql'] 是带问号占位符的原始语句(如 "INSERT INTO user (name) VALUES (?)"),$params['bind'] 才是实际值。别直接拼接日志,否则会漏掉参数类型转换(比如 null 变成 NULL)、时间格式化等细节。
实操建议:
- 用
Log::info()记录时,把$params['sql']和var_export($params['bind'], true)分开存,避免 SQL 注入式拼接 - 如果想看“最终执行的语句”,得手动替换占位符——但仅用于调试,生产环境禁用,性能损耗明显
-
$params里没有执行耗时,需自己用microtime(true)包裹监听器逻辑来计算
为什么不能只监听 model_after_write
这个行为事件只在模型调用 save()、update() 等方法后触发,绕过查询构造器(如 Db::name('user')->where(...)->update())或原生 query() 就完全不触发。更麻烦的是,它不区分 INSERT/UPDATE/DELETE,也不暴露原始 SQL,只给个 $result 对象——你根本不知道底层发了什么语句。
常见错误现象:
- 日志里只有“用户更新成功”,但没记录到底是
UPDATE user SET status=1 WHERE id=5还是UPDATE user SET status=2 WHERE id=5 - 批量更新、软删除、原生 SQL 查询全无痕迹
- 事务内多条语句只记最后一条,因为事件只在模型方法退出时触发一次
日志格式和落盘要注意啥
别用 Log::write() 直接塞大段字符串,ThinkPHP 日志驱动默认按天切片,单条日志超长(比如带完整 SQL 和 100 行 bind)会导致文件膨胀、grep 困难、ELK 解析失败。
实操建议:
- 字段精简:只留
sql、bind、time、duration、connection(多库场景下必须) - 敏感字段过滤:检查
$params['bind']里是否有密码、token 类字段,用unset()或打码处理 - 异步写入?别试。监听器在请求主流程里,用协程或队列反而增加复杂度,且可能丢日志。真要降压,改用 Swoole 的
defer或写入本地缓冲再定时刷盘
真正难的不是监听哪个事件,而是保证每条 SQL 都能对应回业务上下文——比如分页查询里 LIMIT 0,10 和 LIMIT 10000,10 性能差百倍,但日志里看不出请求来源。这时候得靠 trace_id 关联请求链路,而 ThinkPHP 默认不透出这个 ID 到 DB 事件里,得自己从 Request::header('x-request-id') 拿,还要考虑 CLI 场景兜底。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











