当前位置:首页 > 技术 > 正文内容

基于Python的Spring Boot慢SQL日志分析工具

访客 技术 2026年8月7日 1

自动化慢SQL检测与分析系统

本工具专为Java Spring Boot应用中的MyBatis日志设计,可自动识别执行耗时超过阈值的数据库操作,并生成结构化报告。

核心功能特性

  • 支持标准Spring Boot MyBatis日志格式解析
  • 动态配置慢查询触发阈值(毫秒级)
  • 多线程环境下的SQL执行记录关联匹配
  • 生成可导入表格的CSV结果文件
  • 实时进度反馈与性能统计摘要

输入日志格式要求

14:23:15.456 [task-thread-7] DEBUG UserMapper.selectById - ==>  Preparing: SELECT id, name FROM users WHERE id = ?
14:23:15.458 [task-thread-7] DEBUG UserMapper.selectById - ==> Parameters: 123(Integer)
14:23:18.902 [task-thread-7] DEBUG UserMapper.selectById - <==      Total: 1

快速使用指南

直接运行脚本并指定日志路径:

python analyze_slow_sql.py /var/log/app/application.log -t 3000 -o findings.csv

命令行参数说明

参数描述默认值
log_file待分析的日志文件路径必需
-o, --output输出CSV文件位置auto-generated
-t, --threshold慢查询判定阈值(毫秒)5000

输出内容详解

控制台将展示如下信息:

============================================================
慢SQL分析汇总
============================================================
总执行次数: 45,231 次
慢查询数量: 1,347 次
占比: 2.98%
最高耗时: 87,654.00ms
最慢语句: SELECT * FROM orders o JOIN customers c ON o.cust_id = c.id WHERE o.status = ? AND c.region = ? ...

导出的CSV包含以下字段:

  • 线程标识:执行上下文名称
  • Mapper类名:数据访问接口全限定名
  • 原始SQL:完整查询语句
  • 绑定参数:实际传入的变量值
  • 返回行数:查询结果集大小
  • 执行时长:从准备到完成的时间差
  • 起止时间戳:精确到毫秒
  • 阈值基准:设定的判断标准
  • 日志行号:原始日志中对应行的位置

底层工作流程

  1. 逐行扫描:以流式方式读取大文件,避免内存溢出
  2. 正则匹配:通过预编译模式识别三类日志事件
  3. 上下文聚合:根据线程名建立同一请求的完整生命周期
  4. 时间计算:基于开始和结束时间点精确测算耗时
  5. 智能筛选:仅保留超出阈值的记录进行持久化

最佳实践建议

  • 确保日志级别设置为DEBUG或TRACE
  • 确认日志编码为UTF-8,避免乱码问题
  • 对于超大日志(>100MB),建议分段处理
  • 使用-t参数根据业务场景调整敏感度

故障排查提示

  • 若无结果,请检查日志是否包含完整的PreparingTotal链路
  • 出现编码异常时,可用文本编辑器转换为UTF-8格式
  • 如需验证正则匹配逻辑,可在代码中临时启用调试打印

扩展能力预留

当前架构支持后续增强:

  • 批量处理多个日志文件
  • 按时间段过滤特定窗口内的操作
  • 按语句类型(SELECT/INSERT/UPDATE)分类统计
  • 生成时间趋势图用于性能监控

源码实现概览

以下是关键部分的重构版本:

import re
import csv
from datetime import datetime
from argparse import ArgumentParser

class QueryAnalyzer:
    def __init__(self, threshold_ms=5000):
        self.threshold = threshold_ms
        self.records = {}
        self.slow_queries = []

    def parse_timestamp(self, ts_str):
        try:
            return datetime.strptime(ts_str, '%H:%M:%S.%f')
        except:
            return None

    def extract_event(self, line):
        # 统一正则表达式处理不同类型的日志条目
        patterns = [
            (r'(\d{2}:\d{2}:\d{2}\.\d{3})\s+\[([^\]]+)\]\s+(\w+)\s+([^\s]+)\.(\w+)\s+-\s*==>\s*Preparing:\s*(.+)',
             'PREPARE'),
            (r'(\d{2}:\d{2}:\d{2}\.\d{3})\s+\[([^\]]+)\]\s+(\w+)\s+([^\s]+)\.(\w+)\s+-\s*==>\s*Parameters:\s*(.+)',
             'PARAMS'),
            (r'(\d{2}:\d{2}:\d{2}\.\d{3})\s+\[([^\]]+)\]\s+(\w+)\s+([^\s]+)\.(\w+)\s+-\s*<==\s+Total:\s*(\d+)',
             'RESULT')
        ]
        
        for pattern, event_type in patterns:
            match = re.match(pattern, line.strip())
            if match:
                return {
                    'type': event_type,
                    'time': match.group(1),
                    'thread': match.group(2),
                    'level': match.group(3),
                    'mapper': match.group(4),
                    'method': match.group(5),
                    'content': match.group(6) if len(match.groups()) > 6 else ''
                }
        return None

    def process_log(self, log_path):
        with open(log_path, 'r', encoding='utf-8') as f:
            for line_num, line in enumerate(f, start=1):
                parsed = self.extract_event(line)
                if not parsed:
                    continue
                
                thread_key = f"{parsed['thread']}_{parsed['mapper']}_{parsed['method']}"
                
                if parsed['type'] == 'PREPARE':
                    self.records[thread_key] = {'prepare': parsed}
                elif parsed['type'] == 'PARAMS':
                    if thread_key in self.records:
                        self.records[thread_key]['params'] = parsed
                elif parsed['type'] == 'RESULT':
                    if thread_key in self.records and 'prepare' in self.records[thread_key]:
                        prep_time = self.parse_timestamp(self.records[thread_key]['prepare']['time'])
                        result_time = self.parse_timestamp(parsed['time'])
                        
                        duration = (result_time - prep_time).total_seconds() * 1000
                        
                        if duration >= self.threshold:
                            self.slow_queries.append({
                                'sql': self.records[thread_key]['prepare']['content'],
                                'duration': duration,
                                'rows': int(parsed['content']),
                                'thread': parsed['thread'],
                                'timestamp': result_time.strftime('%H:%M:%S.%f')[:-3]
                            })
                            
                        del self.records[thread_key]

    def export_results(self, output_path):
        if not self.slow_queries:
            print("未发现慢查询")
            return
        
        with open(output_path, 'w', encoding='utf-8-sig', newline='') as f:
            writer = csv.DictWriter(f, fieldnames=['SQL', '耗时(ms)', '返回行数', '线程', '时间'])
            writer.writeheader()
            for q in sorted(self.slow_queries, key=lambda x: x['duration'], reverse=True):
                writer.writerow(q)

def main():
    parser = ArgumentParser(description='Spring Boot慢查询分析工具')
    parser.add_argument('log_file', help='日志文件路径')
    parser.add_argument('-t', '--threshold', type=int, default=5000)
    parser.add_argument('-o', '--output', default='slow_queries.csv')
    
    args = parser.parse_args()
    
    analyzer = QueryAnalyzer(threshold_ms=args.threshold)
    analyzer.process_log(args.log_file)
    analyzer.export_results(args.output)

if __name__ == '__main__':
    main()

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。