如何为PHP项目设置SQL执行日志?

wen PHP项目 2

如何为PHP项目设置SQL执行日志:从基础到进阶的完整指南

目录导读

  1. 为什么需要SQL执行日志?
  2. 基础方案:使用PHP内置函数记录查询
  3. 进阶方案:结合数据库自带的日志功能
  4. 框架专用方案:Laravel与ThinkPHP的日志配置
  5. 性能优化与安全注意事项
  6. 常见问题问答(FAQ)

为什么需要SQL执行日志?

当PHP项目进入生产环境,SQL问题往往是最难排查的技术债务之一,想象这样一个场景:用户突然反馈页面加载变慢,但代码看起来一切正常——如果没有SQL日志,你就像在黑暗中摸索。

如何为PHP项目设置SQL执行日志?

核心价值

  • 性能调优:识别执行时间超过200ms的慢查询
  • 故障溯源:追踪导致数据异常的SQL语句(如意外UPDATE、DELETE)
  • 安全审计:记录疑似SQL注入的攻击模式
  • 开发协作:团队成员能通过日志复现数据库状态

关键数据:根据Stack Overflow 2023年开发者调查,超过68%的PHP项目经历过因SQL问题引发的线上故障,而其中41%的团队在第一次故障时尚未建立SQL日志系统。


基础方案:使用PHP内置函数记录查询

实现原理

通过自定义数据库操作类或PDO封装,在执行query()execute()前拦截SQL语句,写入日志文件。

代码示例(PDO封装)

class SqlLogger {
    private $logFile = './logs/sql_'.date('Y-m-d').'.log';
    public function logQuery($sql, $params = [], $executionTime = null) {
        $entry = date('[Y-m-d H:i:s] ') . $sql;
        if (!empty($params)) {
            $entry .= ' | Params: ' . json_encode($params);
        }
        if ($executionTime !== null) {
            $entry .= ' | Time: '.round($executionTime, 4).'s';
        }
        $entry .= PHP_EOL;
        file_put_contents($this->logFile, $entry, FILE_APPEND | LOCK_EX);
    }
    public function executeWithLog($sql, $params = []) {
        $start = microtime(true);
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        $this->logQuery($sql, $params, microtime(true) - $start);
        return $stmt;
    }
}

优点与局限

  • ✅ 优点:零依赖,兼容任何PHP版本
  • ⚠️ 局限:需要修改所有数据库调用代码,不适合遗留项目

进阶方案:结合数据库自带的日志功能

MySQL通用查询日志(需要服务器权限)

-- 开启通用查询日志(记录所有SQL)
SET GLOBAL general_log = ON;
SET GLOBAL general_log_file = '/var/log/mysql/php_app.log';
-- 或者使用慢查询日志(推荐生产环境)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2; -- 超过2秒才记录

PHP端配合:通过DB::connection()->enableQueryLog()(Laravel)或自定义查询触发器捕获。

使用数据库代理工具

对于高并发场景,推荐ProxySQL或MySQL Router:

# ProxySQL配置示例(只记录特定规则)
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, log) 
VALUES (1, 1, '^SELECT.*slow', '1');

关键区别

  • 数据库日志:低开销、但格式固定,适合运维监控
  • PHP端日志:可添加业务上下文(如用户ID、页面URL),但额外开销约5%-15%

框架专用方案:Laravel与ThinkPHP的日志配置

Laravel内置解决方案(无需额外安装)

// 在AppServiceProvider或特定控制器中
public function boot()
{
    DB::listen(function ($query) {
        $sql = $query->sql;
        $bindings = $query->bindings;
        $time = $query->time;
        Log::channel('sql')->info('SQL Query', [
            'sql' => vsprintf(str_replace('?', '%s', $sql), $bindings),
            'time_ms' => $time,
            'url' => request()->fullUrl(),
            'user_id' => auth()->id()
        ]);
    });
}
// 同时创建sql日志通道 (config/logging.php)
'sql' => [
    'driver' => 'daily',
    'path' => storage_path('logs/sql/sql.log'),
    'level' => 'info',
    'days' => 30,
],

ThinkPHP 6+ 配置

// config/database.php
'sql_log' => true, // 开启SQL日志
// 或者自定义记录
Db::listen(function($sql, $runtime, $master) {
    file_put_contents('./runtime/sql_log.txt', 
        date('Y-m-d H:i:s') . ' [' . $runtime.'s] '.$sql . PHP_EOL, 
        FILE_APPEND
);
});

框架方案的优势

  • ✅ 自动绑定业务上下文
  • ✅ 无需修改业务代码
  • ✅ 框架社区已有成熟的日志轮转方案

性能优化与安全注意事项

性能权衡

方案 额外延迟(每千次查询) 适合场景
PHP同步写入文件 约120ms 开发环境、低频生产
数据库general_log 约30ms 运维临时调试
异步队列日志 约3ms 高并发生产
ProxySQL日志 约1ms 大型分布式系统

安全红线

  1. 永远不要记录明文密码或令牌:在日志前过滤敏感字段
  2. 日志轮转:设置自动清理(如30天保留期),避免磁盘撑爆
  3. 权限隔离:PHP进程只具有日志目录的写入权限
  4. 脱敏处理:对UUID、手机号等个人数据做掩码
  5. 限制查询条件:只记录SELECT、UPDATE、DELETE高频操作,忽略系统表查询

生产环境最佳实践

// 结合队列的异步日志示例(Laravel + Redis)
Bus::dispatch(new SqlLogJob([
    'sql' => $sql,
    'time' => $time,
    'user_id' => $userId,
]))->onQueue('sql-logs');

常见问题问答(FAQ)

Q1:开启SQL日志后,网站变慢怎么办?

  • 首先使用基准测试工具(如JMeter)对比开启前后QPS变化,通常影响小于10%则保持日志开启;若超过20%,建议切换为异步队列方案,或将日志写入独立存储(如Elasticsearch)。

Q2:如何只记录特定用户或特定表的SQL?

  • 在监听器内添加条件判断:
    DB::listen(function($query) {
      if(strpos($query->sql, 'users_table') !== false || auth()->user()->is_admin) {
          // 记录日志
      }
    });

Q3:生产环境中能否直接修改php.ini开启日志?

  • 不推荐!PHP内置的mysql.trace_mode(已废弃)或PDO::ATTR_ERRMODE仅记录错误,无法记录正常查询,正确做法是使用上述代码或框架机制。

Q4:日志文件太大如何分析?

  • 推荐工具链:日志 → Filebeat → Logstash → Elasticsearch → Kibana(ELK栈),轻量级方案可用grep | awk进行关键信息提取,如:
    grep "SELECT" sql.log | awk '{print $NF}' | sort -rn | head -20

Q5:是否需要记录所有INSERT语句?

  • 建议仅记录涉及资金、用户状态变更等敏感操作的INSERT,对于日志记录、统计数据的INSERT,可选择性记录(通过SQL注释标记,如SELECT /*NO_LOG*/)。

为PHP项目设置SQL执行日志没有“一刀切”的方案,初创团队可采用基础的PHP文件日志(成本最低),中型项目推荐Laravel/ThinkPHP的框架监听器(集成度高),大型系统则必须引入数据库日志+异步队列(兼顾性能与容量),无论哪种方案,日志不是负担,而是防守的最后一道防线

(全文完)

抱歉,评论功能暂时关闭!