
Eloquent 默认的 listen() 机制仅捕获成功执行的 SQL 查询,无法捕获抛出异常的失败查询;本文详解通过 Laravel/Illuminate 的异常处理机制、DB 事件监听与自定义异常处理器,实现对所有失败查询(如语法错误、连接超时、约束冲突等)的完整日志记录。
eloquent 默认的 `listen()` 机制仅捕获成功执行的 sql 查询,无法捕获抛出异常的失败查询;本文详解通过 laravel/illuminate 的异常处理机制、db 事件监听与自定义异常处理器,实现对所有失败查询(如语法错误、连接超时、约束冲突等)的完整日志记录。
在 Slim + Eloquent 架构中,$connection->listen() 确实只触发于查询成功完成后(即 PDO 执行无异常),因此当 SQL 语法错误、外键冲突、连接中断或超时时,异常会直接向上抛出,listen 回调根本不会执行——这正是你遇到的问题根源。
要真正捕获失败查询,需从异常源头介入。推荐采用以下双重保障方案:
✅ 方案一:全局异常处理器 + 查询上下文捕获(推荐)
在 Slim 中注册自定义异常处理器,并结合 DB::enableQueryLog()(仅开发环境)或 DB::pretend()(调试用)辅助定位,但更健壮的方式是利用 Illuminate 的 Connection 异常钩子:
use Illuminate\Database\Connection;
use Illuminate\Database\QueryException;
use Psr\Log\LoggerInterface;
// 在初始化 Eloquent 后,为每个连接注册异常监听
$capsule->getConnection()->getPdo()->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// 拦截 QueryException(最常见失败类型)
$capsule->getConnection()->setExceptionHandler(function (QueryException $e) use ($headers, $logger) {
$sql = $e->getSql() ?: 'Unknown query';
$bindings = $e->getBindings() ?: [];
$logData = [
'page' => $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'],
'query' => $sql,
'bindings' => json_encode($bindings),
'user_id' => $headers[HeaderKey::USER_ID] ?? null,
'ip_address' => $headers[HeaderKey::IP_ADDRESS] ?? null,
'sys_key' => $headers[HeaderKey::SYS_KEY] ?? null,
'error_message'=> $e->getMessage(),
'error_code' => $e->getCode(),
'db_name' => $e->getDatabaseName() ?? $capsule->getConnection()->getDatabaseName(),
'db_host' => $capsule->getConnection()->getConfig('host'),
'db_user' => $capsule->getConnection()->getConfig('username'),
'timestamp' => date('Y-m-d H:i:s'),
];
// 写入日志表(建议使用异步或队列避免阻塞主流程)
$tableName = "`SysDbLog_" . date('Y-m-d') . "`";
DB::connection('logdb')->insert(
"INSERT DELAYED INTO $tableName
(page, query, SerializedParam, UserID, IPAddress, SysCode, ErrorMessage, ErrorCode, DBName, DBUser, DBHost, WebServer, CreatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, INET_ATON(SUBSTRING_INDEX(SUBSTRING_INDEX(USER(), '@', 2), '@', -1)), ?)",
[
$logData['page'],
$logData['query'],
json_encode($logData['bindings']),
$logData['user_id'],
$logData['ip_address'],
$logData['sys_key'],
$logData['error_message'],
$logData['error_code'],
$logData['db_name'],
$logData['db_user'],
$logData['timestamp']
]
);
// 同时写入 PSR-3 日志(便于集中监控)
$logger->error('Eloquent Query Failed', $logData);
});
⚠️ 注意:setExceptionHandler() 是 Illuminate v9+ 提供的方法;若使用较旧版本(如 v5.8–v8),需改用 try/catch 包裹关键查询,或监听 illuminate.database.query.failed 事件(需启用事件系统)。
✅ 方案二:启用数据库查询事件(Laravel ≥ 9.26+)
若项目已集成 Laravel Event Dispatcher,可监听原生失败事件:
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\Events\QueryFailed;
// 注册失败事件监听器(需在服务提供者或启动脚本中)
Event::listen(QueryFailed::class, function (QueryFailed $event) use ($headers) {
$values = [
$_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'],
$event->sql,
json_encode($event->bindings),
$headers[HeaderKey::USER_ID] ?? null,
$headers[HeaderKey::IP_ADDRESS] ?? null,
$headers[HeaderKey::SYS_KEY] ?? null,
$event->exception->getMessage(),
$event->connection->getDatabaseName(),
$event->connection->getConfig('username'),
$event->connection->getConfig('host'),
date('Y-m-d H:i:s')
];
$t = "`SysDbLog_" . date('Y-m-d') . "`";
DB::connection('logdb')->insert(
"INSERT DELAYED INTO $t (page,query,SerializedParam,UserID,IPAddress,SysCode,ErrorMessage,DBName,DBUser,DBHost,CreatedAt) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
$values
);
});
? 关键注意事项
- 性能与可靠性:INSERT DELAYED 仅适用于 MyISAM 表,MySQL 8.0+ 已弃用;生产环境建议改用 INSERT IGNORE 或异步消息队列(如 Redis Queue + Worker)写日志,避免日志写入失败导致主业务中断。
- 敏感数据脱敏:$query->bindings 可能含密码、token 等,记录前务必过滤(如正则替换 'password' => '***')。
- 表结构兼容性:确保 SysDbLog_YYYY-MM-DD 表存在且字段类型匹配(如 ErrorMessage 建议 TEXT,CreatedAt 为 DATETIME)。
- 连接配置复用:logdb 连接应独立配置,避免与主库共用连接池导致雪崩。
通过上述任一方案,即可全面覆盖 QueryException、PDOException、TimeoutException 等所有数据库层失败场景,真正实现“失败即可见”,大幅提升线上问题排查效率。











