db::listen() 是 laravel 全局监听 sql 的正确方式,需在 appserviceprovider::boot() 早期注册;db::enablequerylog() 仅限单请求内存记录,调用时机不当或连接分离时易返回空数组,且不适用于队列、命令等多上下文场景。

直接用 DB::listen(),别碰 DB::enableQueryLog() 做全局监控——它只对单次请求有效、容易漏查、生产环境一开就卡。
为什么 DB::enableQueryLog() 在 Web 请求里总返回空数组
这个方法不是“开启日志开关”,而是给当前数据库连接实例挂一个内存里的查询列表。问题出在三处:
- 它必须在查询发生前调用,比如控制器方法开头;
AppServiceProvider::boot()里调用完全无效——那时还没进请求生命周期,连接对象早被丢弃了 - 它只记录当前连接(如
mysql),如果你用了读写分离,DB::connection('read')的查询不会出现在DB::getQueryLog()里 - 队列任务、Artisan 命令、Tinker 会话各自有独立连接,
getQueryLog()拿不到其他上下文的记录
DB::listen() 回调里怎么拿到真实可执行的 SQL
$query->sql 是带 ? 占位符的模板,$query->bindings 是参数数组,两者不拼起来就是废柴。别用 vsprintf() —— 遇到 null、false 或含单引号的字符串会崩。稳妥做法是逐个替换:
$sql = $query->sql;
foreach ($query->bindings as $binding) {
if (is_string($binding)) {
$value = "'" . str_replace("'", "''", $binding) . "'";
} elseif (is_bool($binding)) {
$value = $binding ? '1' : '0';
} elseif ($binding === null) {
$value = 'NULL';
} else {
$value = $binding;
}
$sql = preg_replace('/\?/', $value, $sql, 1);
}
注意:时间类型(Carbon)通常已转成字符串,按字符串处理;大文本或 JSON 建议截断,否则日志文件会膨胀得离谱。
监听器注册位置和作用域陷阱
DB::listen() 是全局单例,注册晚了会漏掉服务提供者初始化阶段的查询(比如某些包在 boot() 里就查配置表)。但注册太早也危险:
- 放在
AppServiceProvider::boot()是常见做法,够用但非绝对安全 - 如果用了 Horizon 或自定义队列 worker,必须在
QueueServiceProvider或 worker 启动时手动再注册一次——队列进程不共享 Web 请求的监听器 - Tinker 或 Artisan 命令中监听,得自己敲一遍
DB::listen(...),它不会继承任何配置 - 重复注册会导致同一条 SQL 被记录多次,检查
config/app.php和所有服务提供者里有没有重复绑定
生产环境千万别硬编码写日志文件
用 file_put_contents() 往 storage/logs/ 写 SQL,在高并发下会锁文件、拖慢响应、甚至丢日志。更糟的是,日志里裸露用户手机号、身份证、密码哈希——审计过不了关。真正该做的:
- 只在
APP_DEBUG=true且非生产环境启用监听 - 写入 Laravel 日志通道(如
Log::channel('stack')->debug()),让它走 Monolog 的异步处理器 - 加条件过滤:只记
$query->time > 500的慢查,或跳过SELECT *、INFORMATION_SCHEMA这类无意义语句 - 真要长期观察,上
Telescope或Clockwork——它们自带采样、脱敏、Web UI 和查询分析,硬编码监听只是临时拐杖
最常被忽略的一点:监听器里禁止触发新数据库操作。比如想把慢查写进 operation_log 表,结果引发嵌套事务或死锁——监听是旁观者,不是参与者。











