数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案

发布于 2025-10-24 17:05
浏览
0收藏

作者 | 崔皓

审校 | 重楼

整体思路

在数据库运维场景中,“慢查询报警” 往往是最让工程师头疼的问题之一 —— 就像我的运维学员所遭遇的:频繁接到告警通知,却始终难以快速定位问题根源是系统资源瓶颈、数据库配置缺陷,还是应用层 SQL 写法不规范。

为彻底解决这一 “定位难、分析繁” 的痛点,本文将通过 Fluent-Bit + Fluentd + DeepSeek 的实战方案,构建一套 “从慢查询信息采集到智能分析” 的全流程自动化体系。这套方案的核心价值在于:用工具链解决 “日志采集与处理” 的重复性工作,用大模型替代 “人工排查与分析” 的经验依赖,让 MySQL 慢查询问题从 “被动响应” 转变为 “主动诊断 + 精准解决”,大幅降低运维成本,提升数据库性能稳定性。

我们将该项目实战的整体思路整理为下图所示:

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

具体思路可拆解为三大核心环节:

日志采集:当业务应用向 MySQL 数据库发起查询请求时,若查询执行时间超出预设阈值(即触发慢查询条件),系统将先通过轻量级日志采集工具 Fluent-Bit,实时捕获慢查询的完整日志信息(含原始 SQL 语句、执行时间戳等基础数据),再将采集到的日志稳定传递至 Fluentd 进行后续处理,确保慢查询数据不丢失、不延迟。

提取信息:Fluentd 作为日志处理中枢,将对原始慢查询日志进行格式化解析:通过正则匹配与字段提取逻辑,从非结构化日志中拆解出 “查询耗时”“锁定时间”“返回行数”“扫描行数” 等核心性能指标,同时识别 SQL 语句中涉及的数据库表名,完成慢查询信息从 “杂乱文本” 到 “结构化数据” 的转化,为后续分析奠定基础。

智能分析:为避免仅依赖日志数据导致的分析片面性,方案将进一步关联 MySQL 数据库的元数据信息 —— 通过数据库连接工具获取目标表的 “表结构定义”“索引配置”“表数据量” 等关键元数据,再将 “结构化慢查询指标 + 表元数据 + 原始 SQL” 三部分信息整合为统一的分析素材,传递至 DeepSeek 大模型。大模型将基于数据库性能优化知识,自动诊断慢查询根源(如 “缺少关键索引导致全表扫描”“SQL 关联逻辑冗余” 等),并生成包含具体优化建议(索引创建语句、SQL 改写方案等)的结构化报告,最终帮助运维工程师快速定位问题类型(系统/数据库/应用层),并直接落地优化操作。

实战步骤

说完了整体思路,就到了项目实战的环节,我们通过一张图让大家快速了解要经历的实战步骤,如下图所示。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

  • 安装 MySQL 数据库,为后续的查询及慢查询日志提供准备。
  • 配置慢查询日志文件与触发条件。指定慢查询日志的存储文件,同时设定触发慢查询的阈值(比如执行时间超过多少秒的查询会被记录为慢查询)。
  • 配置 Fluent - Bit 和 FluentD。Fluent - Bit 负责从 MySQL 端采集慢查询日志,FluentD 则对采集到的日志进行过滤、解析等处理操作,它们共同构成了日志采集与初步处理的管道。
  • 安装 Fluent - Bit 与 FluentD,确保这两个工具能在环境中正常运行,为后续的日志采集和处理提供工具支持。
  • 生成日志分析程序(process_slow_log.py)。该程序会基于 FluentD 处理后的结构化日志,结合数据库的表结构、索引等元数据,进一步深入分析慢查询的原因,为后续生成报告提供分析依据。
  • 生成日志查看应用(show_slow_report.py)。这个应用主要用于将日志分析程序处理后的结果,以更直观、易读的形式展示出来,方便用户查看慢查询的分析报告。
  • 测试功能环节。通过模拟或实际的慢查询场景,验证从 MySQL 慢查询日志的产生、采集、处理,到分析程序运行、报告展示这一整个流程是否正常工作。

数据库安装与配置

介绍完毕项目的执行步骤之后,我们就开始动手实现了,首先在 MySQL 的官网下载安装文件,地址如下:

​https://dev.mysql.com/downloads/​

由于我所用的操作系统是 Windows ,所以点击下图“MySQL Installer for Windows”下载 MySQL,大家请根据自身系统情况下载对应的版本。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

接着选择“5.7.44”的 MySQL 版本。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

下载完成后,双击mysql-installer-community-5.7.44.0.msi 文件进行安装。

如下图所示,在安装时会提示更新安装程序,选择“No”。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

如下图所示,选择“Custom”可以自定义安装目录,否则默认安装到C盘。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

接着选择安装“MySQL Server 5.7.44 - X64”版本,选中后点击“Next”。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

这里可以选择安装目录,默认是C盘,也可以修改安装目录。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

选择“Execute”执行安装。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

勾选Show Advanced and Logging Options,其他配置按默认不变。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

填写root用户的密码,为了方便我直接使用“root”作为密码,大家不用效仿,自行设置密码即可。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

后面就是一顿 Next 安装,不过到了下面这个界面的时候请注意。

如下图所示,修改Slow Query Log下的FilePath和Seconds。

  • FilePath修改为mysql-slow.log(慢查询日志文件);
  • Seconds修改为3(sql执行超过3秒记录到慢查询日志中),当然也可以改成希望的时间,这个时间后面还可以通过配置文件修改。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

接着,点击“Execute”执行安装。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

最后,点击“Finish”完成安装。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

安装完成后打开MySQL 5.7 Command Line Client,按照下图操作在“Windows”中搜索“mysql”,打开 MySQL 的客户端。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

在打开的客户端中,输入在安装时就输入过的密码, 我这里输入“root”密码。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

如果看到如下图所示的内容,说明安装成功。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

设置慢查询日志权限

这里需要注意的是,我们在安装时定义过了慢查询日志文件:mysql-slow.log,如果没有更改安装目录会在c:\ProgramData\MySQL\MySQL Server 5.7\Data目录下,在后续的使用中发现需要给mysql-slow.log文件赋予权限,否则 Fluent-bit 解析日志时会报错。所以,在安装完 MySQL 之后最后手动删除这个文件,再重新启动 MySQL 的服务,该服务会自动生成mysql-slow.log,并赋予它正确的权限。

当然如果没有权限问题的小伙伴可以跳过这个步骤。

这里如下图所示,在“Windows”中搜索“服务”,并打开。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

在服务列表中选择“MySQL57”的服务,并且点击“重启此服务”。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

慢查询配置

至此,MySQL 的安装就完成了,接着再啰嗦一下慢查询日志和阈值的配置。假设 MySQL 安装在 c:\ProgramData\MySQL\MySQL Server 5.7(默认会在C盘)目录下,如下图所示我们可以打开该目录下的 my.ini 文件。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

并且,编辑文件内容如下:

# 启用慢查询的配置一定要放在[mysqld]下面
# 默认配置文件中如果有[mysqld]这里不要重复加[mysqld]
[mysqld]
# 启用慢查询日志
slow-query-log=1

# 慢查询日志名称,生成的文件路径 c:\ProgramData\MySQL\MySQL Server 5.7\Data,如果
slow_query_log_file="mysql-slow.log"

# 定义慢查询时间(秒)
long_query_time=2

该文件可以启用慢查询,保存配置后需要重启MySQL服务。

  • slow-query-log=1

当设置为 1 时,MySQL 会将执行时间超过指定阈值的查询操作记录到慢查询日志文件中;如果设置为 0,则关闭慢查询日志功能。

  • slow_query_log_file="mysql-slow.log"

设置的日志文件名为 mysql-slow.log,MySQL 会将慢查询相关的日志内容写入到这个文件中。

  • long_query_time=2

定义了慢查询的时间阈值,单位是秒。当一条 SQL 查询语句的执行时间超过这个值(为了方便演示,我设置的是 2 秒)时,这条查询就会被认定为慢查询,进而被记录到上述指定的慢查询日志文件中。也就是说,只有执行时间超过 2 秒的查询才会被 MySQL 记录到慢查询日志里,方便 DBA(数据库管理员)后续分析和优化这些执行效率低下的查询。

配置 fluent-bit

完成 MySQL 安装以及慢查询日志配置之后,就需要配置 Fluent-bit 采集日志信息。个人建议在本地创建目录用来存放 Fluent-bit 的配置文件,这个文件用来解析慢日志的内容。由于后期我们会使用 Docker 安装 Fluent-bit,所以也需要将这个目录下的配置文件与 Docker 中安装的 Fluent-bit 文件进行挂载,这样方便修改配置信息。

我在本地“d:\docker\EFK” 下面分别建立 fluent-bit 和 fluentd 两个目录,大家可以根据具体情况创建相似的目录。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

配置日志解析

通过前面内容的介绍可以得知,fluent-bit 的主要任务是采集并解析慢日志的内容,采集属于基本功能,而解析需要按照 MySQL 慢日志的格式对文本进行处理。先通过如下慢日志文本观察其结构。

C:\Program Files\MySQL\MySQL Server 5.7\bin\mysqld.exe, Version: 5.7.44-log (MySQL Community Server (GPL)). started with:
TCP Port: 3306, Named Pipe: (null)
Time                 Id Command    Argument
# Time: 2025-09-30T10:35:46.687011Z
# User@Host: root[root] @ localhost [::1]  Id:     2
# Query_time: 4.063656  Lock_time: 0.017691 Rows_sent: 100694  Rows_examined: 1814434
use after_sale;
SET timestamp=1759228546;
SELECT 
u.UserId, u.Name, u.Email,
i.IssueId, i.Description, i.Status,
cs.CustomerServiceId, cs.Name,
s.SolutionId, s.SolutionDescription,
si.ActionTime
FROM SupportIssue si
JOIN Issue i ON si.IssueId = i.IssueId
JOIN User u ON i.UserId = u.UserId
JOIN CustomerService cs ON si.CustomerServiceId = cs.CustomerServiceId
JOIN Solution s ON si.SolutionId = s.SolutionId
WHERE s.IsValid = 1
AND i.Status = 'unsolve'
ORDER BY si.ActionTime DESC;

虽然,日志的内容比较多,可以从如下几个方面进行分析,慢查询日志由 5 部分组成,呈现 “起始行 + 元数据行 + SQL 语句行” 的多行结构:

1. 起始行:# Time: 2025-09-30T10:35:46.687011Z以# Time: 开头,包含精确到微秒的时间戳,是每条慢查询日志的唯一起始标识。

2. 用户与连接信息行:# User@Host: root[root] @ localhost [::1] Id: 2记录执行查询的用户、主机和连接 ID,是日志的元数据部分。

3. 性能指标行:# Query_time: 4.063656 Lock_time: 0.017691 Rows_sent: 100694 Rows_examined: 1814434包含查询耗时(4.06 秒)、锁定时间、返回行数、扫描行数等核心性能数据,是后续分析的关键指标。

4. 环境与时间设置行:use after_sale; 和 SET timestamp=1759228546;记录查询执行的数据库环境(切换到after_sale库)和时间戳设置,属于 SQL 执行的前置操作。

5. SQL 语句行:从SELECT到最后的;完整的查询语句,包含多表关联(JOIN)、条件筛选(WHERE)和排序(ORDER BY),是慢查询分析的核心对象。

接着,我们就需要根据日志“特征”对其进行解析。先来一波分析,如下:

第一步:找到 “唯一起始标志”—— 确定一条日志的开端

所有结构化日志(包括 MySQL 慢查询日志)都有一个共同特点:每条完整日志都有唯一的 “开头标识”,这是区分不同日志的关键。在示例慢查询日志中,# Time: 2025-09-30T10:35:46.687011Z 就是典型的起始标志 —— 它以固定前缀 # Time: 开头,后跟标准化的时间戳(年 - 月 - 日 T 时:分: 秒。毫秒 Z),且每条慢查询日志必然以此行开始。因此,第一步要做的就是:用正则表达式精准匹配这个起始标志,告诉 Fluent Bit“遇到这样的行,就意味着一条新的慢查询日志开始了”。因此需要用正则表达式 /^# Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/ :^ 锚定行首,\d{4}-\d{2}-\d{2}T... 匹配时间戳格式,确保只命中起始行。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

第二步:定义 “延续行规则”—— 合并日志的后续内容

一条完整的慢查询日志除了起始行,还包含用户信息(# User@Host: ...)、性能指标(# Query_time: ...)、SQL 语句等多行内容。这些行的共同特点是:它们不是 “新日志的起始行”,而是当前日志的一部分。因此,第二步的思路是:用 “否定逻辑” 匹配所有 “不是起始行” 的内容,告诉 Fluent Bit“只要不是新日志的开头,就都归到当前日志里”。因此,需要通过正则表达式: /^(?!\# Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z).*/ 采用了正则的 “否定前瞻” 语法(?!),意思是 “匹配所有不以 # Time: 时间戳 开头的行”。无论这些行是# User@Host这样的元数据,还是SELECT ...这样的 SQL 语句(哪怕 SQL 跨多行),都会被合并到当前日志中。

第三步:设置 “超时兜底”—— 避免日志截断或阻塞

实际场景中,日志可能因系统延迟、写入中断等原因出现 “不完整” 的情况(比如一条日志写到一半突然停了)。这时需要一个 “超时机制”:如果一段时间内没有新内容加入,就强制结束当前日志的合并,避免数据一直卡在缓冲区。示例中的 Flush_Timeout 5000 就是这个作用(5000 毫秒 = 5 秒),确保即使日志不完整,也能在 5 秒后输出已收集的内容,平衡数据完整性和处理效率。

好了,有了上面的分析之后,我们就可以在 fluent-bit 目录下面创建 etc 目录,同时创建“parsers_mysql_slow.conf” 文件,它用来对慢查询日志进行解析。如下:

[MULTILINE_PARSER]
# 多行解析器名称,用于在输入插件中引用
Name          mysql_slow
# 解析器类型,regex 表示使用正则表达式匹配
Type          regex
# 超时时间(毫秒),当多行日志间隔超过此时间时强制刷新缓冲区
Flush_Timeout 5000

# 第一条规则:定义多行日志的起始行
# "start_state" - 初始状态名称
# 正则表达式匹配以 "# Time:" 开头的行(慢查询日志的开始)
# "cont" - 匹配后跳转到的下一个状态
Rule          "start_state" "/^# Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/" "cont"

# 第二条规则:定义多行日志的延续行
# "cont" - 当前状态名称(与上一条规则的跳转状态对应)
# 正则表达式匹配不以 "# Time:" 开头的行(即延续行)
# "cont" - 匹配后保持当前状态,继续收集多行内容
Rule          "cont"       "/^(?!\# Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z).*/" "cont"

这个文件是用来解析慢日志内容的,遵循了几个原则:

  • 找 “开头”:观察日志格式,找到每条日志唯一的起始标志(如特定前缀、固定格式的时间戳等),用正则精准匹配;
  • 定 “延续”:用 “否定起始标志” 的逻辑,匹配所有属于当前日志的后续行,确保不遗漏任何内容;
  • 设 “超时”:添加超时配置,应对日志不完整的极端情况。

这里我们介绍解析日志的部分,是希望大家遇到类似的日志时,具备基本的分析能力。当然,还有一个弯道超车的方法,就是丢给大模型让它帮你分析日志,自动生成MULTILINE_PARSER 的内容。

引用日志解析

在“\fluent-bit\etc”目录下创建fluent-bit.conf 文件, 加入如下内容:

##############################################
# Fluent Bit 配置文件 (INI格式)
# 注意:注释必须独占一行,不能与配置项同行
##############################################

[SERVICE]
# 指定自定义解析器配置文件路径
# 该文件包含专门的多行日志解析规则(如mysql慢查询)
Parsers_File  /fluent-bit/etc/parsers_mysql_slow.conf
##############################################
# 输入插件配置(收集日志)
##############################################
# 输入源:采集mysql慢查询日志(支持多行处理)
[INPUT]
# 使用 tail 插件监控文件变化
Name                 tail
# 自定义标签,标识为mysql慢查询日志
Tag                  mysql.slow
# mysql慢查询日志文件路径
Path                 /var/log/mysql-slow.log
Read_from_Head       On
# 跳过超长行,避免解析错误导致进程崩溃
Skip_Long_Lines      On
# 检查文件变化的间隔时间(秒)
Refresh_Interval     10
# 指定多行解析器名称(需在parsers_mysql_slow.conf中定义)
# 用于mysql慢查询跨多行的日志条目
multiline.parser     mysql_slow


##############################################
# 输出插件(转发到 Fluentd)
##############################################
[OUTPUT]
# 使用 forward 插件将日志转发到Fluentd聚合器
Name            forward
# 匹配所有标签的日志(* 是通配符,表示所有输入源)
Match           *
# Fluentd 服务地址(使用Docker Compose服务名进行服务发现)
Host            fluentd
# Fluentd 监听端口(forward插件的默认端口)
Port            24224
# 网络故障时的最大重试次数,防止无限重试消耗资源
Retry_Limit     10

# 输出源2:同时输出到控制台(用于调试和监控)
[OUTPUT]
# 使用 stdout 插件在控制台打印日志
Name          stdout
# 匹配所有标签的日志
Match         *
# 注意:生产环境通常应注释或移除此输出,避免日志重复和性能开销

该文件配置聚焦 MySQL 慢查询日志的采集与解析,通过 [SERVICE] 模块加载存放慢查询多行解析规则的parsers_mysql_slow.conf文件,再以 [INPUT] 模块的 tail 插件,监控/var/log/mysql-slow.log路径下的慢查询日志文件,同时启用mysql_slow多行解析器确保跨多行日志完整合并,最后通过 [OUTPUT] 模块的 forward 插件,将标记为mysql.slow的日志转发到 Fluentd(地址为 fluentd,端口 24224)做进一步处理,全程仅保留慢查询采集与解析的关键配置。

配置 Fluentd

Fluent-bit 的配置完成之后,就轮到 Fluentd 了, 前者主要任务是日志的采集和解析,后者则需要完成日志结构化、重要信息提取以及调用智能报告分析服务等功能。

在“\fluentd\conf” 创建新文件fluent.conf

文件内容比较多,我们聚焦如下几个内容:

核心,通过 4 个filter插件对原始慢查询日志进行 “解析→提取→转换→精简”,最终输出结构化数据,仅针对mysql.slow标签的日志生效:

第 1 个 filter:解析原始日志(parser)

<filter mysql.slow>
  @type parser               # 日志解析插件
  key_name log               # 待解析的字段(Fluent Bit转发的原始日志存放在"log"字段中)
  reserve_data true          # 保留原始数据,避免解析后丢失信息
  <parse>
    @type regexp             # 用正则表达式解析
    # 正则提取:日志头部(时间、用户、查询耗时等)和SQL主体
    expression /^(?<header># Time: (?<time>.+)\n# User@Host: (?<user_host>.+)\n# Query_time: (?<query_time>\d+\.\d+)\s+Lock_time: (?<lock_time>\d+\.\d+)\s+Rows_sent: (?<rows_sent>\d+)\s+Rows_examined: (?<rows_examined>\d+))(?<sql_body>[\s\S]*)$/
  </parse>
</filter>

将非结构化的 “log” 字段拆分为结构化字段,最终生成time(慢查询时间)、user_host(执行用户与主机)、query_time(查询耗时)、lock_time(锁定时间)、rows_sent(返回行数)、rows_examined(扫描行数)、sql_body(完整 SQL 语句)等字段,为后续处理打基础。

第 2 个 filter:提取涉及的表名(record_transformer)

<filter mysql.slow>
  @type record_transformer   # 日志字段转换插件
  enable_ruby true           # 启用Ruby脚本,实现复杂逻辑
  <record>
    # Ruby脚本:从sql_body中提取SQL涉及的表名(支持带反引号/带库名的表名格式)
    tables ${ sql = record["sql_body"].to_s; pattern = /(?:FROM|JOIN)\s+(?:(?<schema1>[a-zA-Z_][a-zA-Z0-9_]*)\.(?<table1>[a-zA-Z_][a-zA-Z0-9_]*)|(?<schema2>`[^`]+`)\.(?<table2>`[^`]+`)|(?<schema3>[a-zA-Z_][a-zA-Z0-9_]*)\.(?<table3>`[^`]+`)|(?<schema4>`[^`]+`)\.(?<table4>[a-zA-Z_][a-zA-Z0-9_]*)|(?<table5>[a-zA-Z_][a-zA-Z0-9_]*)|(?<table6>`[^`]+`))(?:\s|$)/; matches = sql.scan(pattern); tables = []; matches.each { |m| schema1, table1, schema2, table2, schema3, table3, schema4, table4, table5, table6 = m; if schema1 && table1; tables << "#{schema1}.#{table1}"; elsif schema2 && table2; tables << "#{schema2}.#{table2}"; elsif schema3 && table3; tables << "#{schema3}.#{table3}"; elsif schema4 && table4; tables << "#{schema4}.#{table4}"; elsif table5 && !["si","i","u","cs","s"].include?(table5); tables << table5; elsif table6; tables << table6; end }; tables.uniq }
  </record>
</filter>

作用:通过 Ruby 脚本和正则,从sql_body的FROM/JOIN关键字后提取涉及的表名(支持db.table、`db`.`table`等多种格式),去重后生成tables字段(表名数组,如["after_sale.SupportIssue", "after_sale.Issue"]),方便后续关联表结构元数据。

第 3 个 filter:衍生字段与类型转换(record_transformer)

<filter mysql.slow>
  @type record_transformer
  enable_ruby true
  <record>
    tables_str ${ record["tables"].join(",") }  # 将表名数组转为字符串(如"table1,table2")
    table_count ${ record["tables"].size }      # 计算涉及的表数量
    # 类型转换:将字符串格式的数值转为浮点数/整数(便于后续分析计算)
    query_time_float ${ record["query_time"].to_f }
    lock_time_float ${ record["lock_time"].to_f }
    rows_sent_int ${ record["rows_sent"].to_i }
    rows_examined_int ${ record["rows_examined"].to_i }
  </record>
</filter>

作用:基于已有字段生成衍生信息(表名字符串、表数量),并将 “查询耗时”“行数” 等字符串字段转为数值类型(避免后续分析时因类型错误导致问题)。

第 4 个 filter:精简字段(record_transformer)

<filter mysql.slow>
  @type record_transformer
  remove_keys sql_body,header  # 删除无用字段(sql_body、header已完成使命,精简数据)
</filter>

作用:删除解析过程中生成的临时字段(sql_body、header),减少后续输出的数据量,避免冗余。

四个过滤器之后接着的是“match”,将结构化后的慢查询日志分两路输出:

<match *.**>                 # 匹配所有标签的日志(此处主要匹配mysql.slow)
  @type copy                 # 复制插件:将同一份日志同时发送到多个目的地
  # 输出目标1:控制台(调试用)
  <store>
    @id output
    @type stdout             # 输出到Fluentd的控制台
  </store>
  # 输出目标2:HTTP接口(对接慢查询分析服务)
  <store>
    @id http_output
    @type http               # HTTP输出插件
    # 发送到宿主机的5001端口(host.docker.internal是Docker专属地址,指向宿主机)
    endpoint http://host.docker.internal:5001/analyze-slow-query
    http_method post         # 用POST方法发送
    <format>
      @type json             # 日志格式转为JSON(便于后端API解析)
    </format>
    <buffer>
      # 缓冲配置:平衡实时性与可靠性
      flush_interval 2s      # 每2秒刷新一次缓冲区(即使数据未满)
      retry_type exponential_backoff  # 指数退避重试(避免网络故障时频繁重试)
      retry_wait 1s          # 首次重试等待1秒
      retry_max_interval 30s # 最大重试间隔30秒(避免等待过久)
      retry_timeout 10m      # 10分钟后放弃重试(防止无限重试)
    </buffer>
  </store>
</match>

作用:

控制台输出:用于调试(查看日志是否正确处理),生产环境可关闭;

HTTP 接口输出:将 JSON 格式的结构化日志发送到宿主机的analyze-slow-query接口(后续提供该接口实现),调用大模型生成优化报告。

安装 Fluent-Bit 和 FluentD

由于本案例需要安装 fluent-bit、fluentd等应用,为了方便安装与调试,计划使用 docker 方式对他们进行安装。于是 docker compose 的安装方式就成了最佳选择,它可以用于定义和管理多容器 Docker 应用的 YAML 配置文件,能将多个关联的容器(如应用服务、数据库、缓存等)的配置(镜像、端口映射、数据卷、环境变量、依赖关系等)集中整合,通过 docker compose 命令一键实现多容器的创建、启动、停止、重启等操作。其核心益处在于简化了多容器应用的部署与管理流程。选择​​Fluentd官网的docker-compose​​ 文件,并在其基础上进行修改,从而适应安装需求。

​https://docs.fluentd.org/container-deployment/docker-compose#step-0-create-docker-compose.yml​

我们可以直接下载并修改 yml 文件,在文件中找到服务名fluent-bit增加挂载配置volumes,添加如下内容:

# 数据卷挂载:将宿主机的目录或文件挂载到容器内,实现数据持久化或配置注入
    volumes:
      # Fluent Bit 会监控这个目录下的日志文件变化,并收集新产生的日志。
      - C:\ProgramData\MySQL\MySQL Server 5.7\Data\mysql-slow.log:/var/log/mysql-slow.log
      # 将宿主机的自定义解析器配置文件挂载到容器内,用于解析特定格式的日志(如 mysql慢查询日志)。
      - D:\docker\EFK\fluent-bit\etc\parsers_mysql_slow.conf:/fluent-bit/etc/parsers_mysql_slow.conf
      # 将宿主机的 Fluent Bit 主配置文件挂载到容器内,替代镜像内的默认配置。
      # 这个文件定义了数据输入(Input)、处理(Parser, Filter)和输出(Output)的规则。
      - D:\docker\EFK\fluent-bit\etc\fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf

在配置中找到服务名fluentd(在services下一级)增加挂载配置volumes,要在container_name同级。

volumes:
      # 将宿主机上的 Fluentd 配置目录挂载到容器内,使配置变更无需重新构建镜像。
      - D:\docker\EFK\fluentd\conf\fluent.conf:/fluentd/etc

需要注意的是,这里的 MySQL 慢日志文件目录、Fluent-bit 以及 FluentD 配置文件目录都在前面的配置中出现过了,按照你本地的配置填写就好了,其目的就是方便修改 Docker 容器中的配置文件以及对日志文件的监听。

如下图所示,就是我的目录结构,在 EFK 目录下面放的就是 docker-compose.yml 文件。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

配置完成来到 docker-compose.yml 文件所在目录,执行如下命令安装容器:

docker compose -f  docker-compose.yml up -d

此时打开 docker desktop,如下图所示,可以看到 fluent-bit、fluent 都启动了,另外还有 kibana 和 es 也启动了,本例中后面两个容器用不到,可以关闭节省资源。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

编写“生成日志分析程序”

好了,到这里我们已完成整个实例 50% 以上的工作了,回到大图看看进展。如下图所示,在安装 Fluent-Bit 和 FluentD 之后,就要编写日志分析程序了。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

在编写程序之前执行 pip 命令安装必要依赖如下:

pip install dotenv "flask[async]" openai

接着在工作目录下创建环境变量文件 .env 文件,我的工作目录如下图所示,我在 D 盘的 docker 目录下创建了 mysql 目录,将所有与本次实践相关的代码和配置都放这里了。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

.env 的具体文件内容如下:

# DeepSeek API配置
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 数据库配置
DB_USER=root
DB_PASSWORD=root

需要注意的是,确保 .env 文件与 Python 脚本在同一目录即可。

好了,再创建process_slow_log.py文件用来处理慢查询日志,这个文件的代码比较长,其实一句话就可以概括:把慢日志基本信息、 SQL、表结构和记录数都丢给大模型(DeepSeek),让它分析并产生解决方案,最后生成报表。

具体代码如下:

import os
import re
import json
import mysql.connector
import threading
import asyncio
from datetime import datetime
from dataclasses import dataclass
from typing import Optional, Dict, Any, List

from flask import Flask, request, Response
from openai import AsyncOpenAI, APIError, APIConnectionError, Timeout

# 应用配置 - 非敏感配置集中管理
@dataclass
class AppConfig:
    # 服务器配置
    HOST: str = "0.0.0.0"
    PORT: int = 5001
    DEBUG: bool = False
    
    # 路径配置
    CURRENT_DIR: str = os.path.dirname(os.path.abspath(__file__))
    LOG_DIR: str = os.path.join(CURRENT_DIR, "logs")
    REPORT_DIR: str = os.path.join(CURRENT_DIR, "reports")
    SLOW_REPORT_PATH: str = os.path.join(REPORT_DIR, "slow_report.json")  # 统一报告文件路径
    
    # 日志文件路径
    OPERATION_LOG_PATH: str = os.path.join(LOG_DIR, "api_operation.log")
    ERROR_LOG_PATH: str = os.path.join(LOG_DIR, "api_error.log")
    
    # LLM配置
    LLM_BASE_URL: str = "https://api.deepseek.com"
    LLM_MODEL: str = "deepseek-chat"
    LLM_TEMPERATURE: float = 0.7
    LLM_MAX_TOKENS: int = 2000
    
    # 数据库连接配置
    DB_CONNECT_TIMEOUT: int = 10
    DB_CHARSET: str = "utf8mb4"
    DEFAULT_DB_SCHEMA: str = "after_sale"  # 无schema时使用的默认数据库
    
    # 内容类型配置
    SUPPORTED_CONTENT_TYPE: str = "application/x-ndjson"

# 初始化配置与目录
config = AppConfig()
os.makedirs(config.LOG_DIR, exist_ok=True)
os.makedirs(config.REPORT_DIR, exist_ok=True)

# 初始化报告文件(如果不存在)
if not os.path.exists(config.SLOW_REPORT_PATH):
    with open(config.SLOW_REPORT_PATH, 'w', encoding='utf-8') as f:
        json.dump([], f, ensure_ascii=False, indent=2)

# 初始化Flask应用与LLM客户端(延迟初始化)
app = Flask(__name__)
llm_client: Optional[AsyncOpenAI] = None


def init_llm_client() -> AsyncOpenAI:
    """初始化LLM客户端,检查API密钥"""
    global llm_client
    if llm_client is None:
        api_key = os.getenv("DEEPSEEK_API_KEY")
        if not api_key:
            raise ValueError("未找到DEEPSEEK_API_KEY环境变量,请配置")
        
        llm_client = AsyncOpenAI(
            api_key=api_key,
            base_url=config.LLM_BASE_URL
        )
    return llm_client


def get_db_config(schema: Optional[str] = None) -> Dict[str, Any]:
    """从环境变量获取数据库配置,支持指定schema"""
    config_dict = {
        'host': os.getenv('DB_HOST', 'localhost'),
        'user': os.getenv('DB_USER', 'root'),
        'password': os.getenv('DB_PASSWORD', ''),
        'port': int(os.getenv('DB_PORT', 3306)),
        'connect_timeout': config.DB_CONNECT_TIMEOUT,
        'charset': config.DB_CHARSET,
        'autocommit': True
    }
    
    # 如果指定了schema,则添加到配置
    if schema:
        config_dict['database'] = schema
        
    return config_dict


def write_operation_log(message: str, request_id: Optional[str] = None) -> None:
    """记录操作日志,包含时间戳和请求ID"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    request_id = request_id or "N/A"
    log_entry = f"[{timestamp}] [REQUEST={request_id}] OPERATION: {message}\n"
    try:
        with open(config.OPERATION_LOG_PATH, 'a', encoding='utf-8') as f:
            f.write(log_entry)
        print(log_entry.strip())
    except Exception as e:
        print(f"[日志系统错误] 写入操作日志失败: {str(e)} | 原始消息: {message}")


def write_error_log(error_message: str, request_id: Optional[str] = None) -> None:
    """记录错误日志,包含时间戳和请求ID"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
    request_id = request_id or "N/A"
    log_entry = f"[{timestamp}] [REQUEST={request_id}] ERROR: {error_message}\n"
    try:
        with open(config.ERROR_LOG_PATH, 'a', encoding='utf-8') as f:
            f.write(log_entry)
        print(log_entry.strip())
    except Exception as e:
        print(f"[日志系统错误] 写入错误日志失败: {str(e)} | 原始错误: {error_message}")


def clean_table_name(table_name: str) -> tuple[str, str]:
    """
    清理表名中的反引号和提取数据库前缀
    优化点:无schema时使用配置的默认数据库
    """
    # 移除反引号
    cleaned = re.sub(r'`', '', table_name)
    # 分割数据库和表名(如果存在)
    parts = cleaned.split('.')
    if len(parts) == 2:
        return parts[0].strip(), parts[1].strip()  # (数据库名, 表名)
    # 无schema时使用默认数据库
    return config.DEFAULT_DB_SCHEMA, cleaned.strip()  # (默认数据库, 表名)


def get_table_info(table_name: str, request_id: str) -> Dict[str, Any]:
    """获取表的 DDL 和行数,优化无schema表名的处理"""
    db_name, clean_name = clean_table_name(table_name)
    write_operation_log(f"开始获取表 {table_name} 的信息 (清理后: {db_name}.{clean_name})", request_id)
    
    try:
        # 获取数据库配置,指定数据库名
        db_config = get_db_config(db_name)
        write_operation_log(f"使用数据库配置: host={db_config['host']}, port={db_config['port']}, database={db_name}, user={db_config['user']}", request_id)
        
        conn = mysql.connector.connect(**db_config)
        cursor = conn.cursor(dictionary=True)
        
        # 获取表的 DDL
        cursor.execute(f"SHOW CREATE TABLE `{clean_name}`")
        create_table = cursor.fetchone()
        ddl = create_table['Create Table'] if create_table else None
        write_operation_log(f"成功获取表 {db_name}.{clean_name} 的DDL信息", request_id)
        
        # 获取表的行数(快速近似值)
        cursor.execute(f"SHOW TABLE STATUS LIKE '{clean_name}'")
        table_status = cursor.fetchone()
        row_count = table_status['Rows'] if table_status else None
        write_operation_log(f"成功获取表 {db_name}.{clean_name} 的行数信息: {row_count}", request_id)
        
        result = {
            'table_name': table_name,
            'cleaned_name': f"{db_name}.{clean_name}",
            'database': db_name,
            'table': clean_name,
            'ddl': ddl,
            'row_count': row_count,
            'error': None
        }
        
        cursor.close()
        conn.close()
        write_operation_log(f"完成表 {table_name} 的信息获取", request_id)
        return result
        
    except mysql.connector.Error as e:
        # 更详细的数据库错误处理
        error_msg = f"数据库错误 (代码: {e.errno}): {e.msg}"
        write_error_log(f"获取表 {db_name}.{clean_name} 信息失败: {error_msg}", request_id)
        return {
            'table_name': table_name,
            'cleaned_name': f"{db_name}.{clean_name}",
            'database': db_name,
            'table': clean_name,
            'error': error_msg,
            'ddl': None,
            'row_count': None
        }
    except Exception as e:
        error_msg = f"获取表信息失败: {str(e)}"
        write_error_log(f"获取表 {db_name}.{clean_name} 信息失败: {error_msg}", request_id)
        return {
            'table_name': table_name,
            'cleaned_name': f"{db_name}.{clean_name}",
            'database': db_name,
            'table': clean_name,
            'error': error_msg,
            'ddl': None,
            'row_count': None
        }


def generate_report_title(log_data: Dict[str, Any]) -> str:
    """生成报告标题,包含关键信息"""
    query_time = log_data.get('query_time', '未知')
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    tables_str = log_data.get('tables_str', '多个表')
    
    # 提取SQL中的主要操作(SELECT/UPDATE/DELETE等)
    sql_body = log_data.get('log', '').upper()
    operation = '查询'
    if 'UPDATE' in sql_body:
        operation = '更新'
    elif 'DELETE' in sql_body:
        operation = '删除'
    elif 'INSERT' in sql_body:
        operation = '插入'
    
    return f"{timestamp} - 慢{operation}分析报告(耗时: {query_time}秒,涉及表: {tables_str[:50]})"


def generate_prompt(log_data: Dict[str, Any], table_info_list: List[Dict[str, Any]], request_id: str) -> str:
    """生成发送给大模型的提示词"""
    write_operation_log("开始生成提示词", request_id)
    
    prompt = f"""请分析以下MySQL慢查询日志,并提供优化建议。

慢查询日志详情:
{log_data['log']}

查询时间: {log_data['query_time']} 秒
锁定时间: {log_data['lock_time']} 秒
返回行数: {log_data['rows_sent']}
扫描行数: {log_data['rows_examined']}

涉及表结构及行数:
"""
    for table_info in table_info_list:
        if table_info['error']:
            prompt += f"- 表 {table_info['cleaned_name']}: 错误 - {table_info['error']}\n"
        else:
            prompt += f"- 表 {table_info['cleaned_name']} (约 {table_info['row_count']} 行):\n"
            prompt += f"  DDL: {table_info['ddl'][:500]}...\n\n"
    
    prompt += """
请基于以上信息,分析该慢查询的性能问题原因,并提供具体的优化建议,包括但不限于:
1. 索引优化建议
2. SQL语句改写建议
3. 表结构优化建议
4. 其他可能的性能改进方案

请提供详细且可操作的建议,避免泛泛而谈。
"""
    
    write_operation_log(f"提示词生成完成,长度: {len(prompt)}字符", request_id)
    return prompt


async def call_deepseek_api(prompt: str, request_id: str) -> str:
    """调用DeepSeek API生成分析报告"""
    write_operation_log("开始调用DeepSeek API", request_id)
    
    try:
        client = init_llm_client()
        response = await client.chat.completions.create(
            model=config.LLM_MODEL,
            messages=[
                {"role": "system", "content": "你是一位数据库性能优化专家,擅长分析MySQL慢查询并提供优化建议。"},
                {"role": "user", "content": prompt}
            ],
            temperature=config.LLM_TEMPERATURE,
            max_tokens=config.LLM_MAX_TOKENS,
            stream=False
        )
        
        write_operation_log(f"API调用成功,响应ID: {response.id}", request_id)
        write_operation_log(
            f"Token使用情况: 输入={response.usage.prompt_tokens}, 输出={response.usage.completion_tokens}",
            request_id
        )
        
        return response.choices[0].message.content
        
    except APIError as e:
        error_msg = f"LLM API错误 (状态码: {e.status_code}): {e.message}"
        write_error_log(error_msg, request_id)
        return f"分析失败: {error_msg}"
    except APIConnectionError as e:
        error_msg = f"LLM连接错误: {str(e)}"
        write_error_log(error_msg, request_id)
        return f"分析失败: {error_msg}"
    except Timeout as e:
        error_msg = f"LLM调用超时: {str(e)}"
        write_error_log(error_msg, request_id)
        return f"分析失败: {error_msg}"
    except Exception as e:
        error_msg = f"LLM调用未知错误: {str(e)}"
        write_error_log(error_msg, request_id)
        return f"分析失败: {error_msg}"


def append_to_report(report_data: Dict[str, Any], request_id: str) -> bool:
    """将报告数据追加到slow_report.json数组中"""
    write_operation_log(f"开始将报告追加到 {config.SLOW_REPORT_PATH}", request_id)
    
    try:
        # 读取现有报告
        existing_reports = []
        if os.path.exists(config.SLOW_REPORT_PATH):
            try:
                with open(config.SLOW_REPORT_PATH, 'r', encoding='utf-8') as f:
                    existing_reports = json.load(f)
                write_operation_log(f"成功读取现有报告,共{len(existing_reports)}条记录", request_id)
            except json.JSONDecodeError:
                write_error_log("报告文件损坏,将重新初始化", request_id)
                existing_reports = []
        
        # 添加新报告
        existing_reports.append(report_data)
        
        # 写入更新后的报告
        with open(config.SLOW_REPORT_PATH, 'w', encoding='utf-8') as f:
            json.dump(existing_reports, f, ensure_ascii=False, indent=2)
        
        write_operation_log(f"报告已成功追加到 {config.SLOW_REPORT_PATH}", request_id)
        return True
        
    except Exception as e:
        error_msg = f"追加报告到文件失败: {str(e)}"
        write_error_log(error_msg, request_id)
        return False


def extract_valid_log_records(ndjson_data: List[str], request_id: str) -> List[Dict[str, Any]]:
    """解析NDJSON数据并提取有效的日志记录"""
    write_operation_log(f"开始解析NDJSON数据,共{len(ndjson_data)}行", request_id)
    records = []
    
    for i, line in enumerate(ndjson_data):
        line = line.strip()
        if not line:
            write_operation_log(f"第{i+1}行是空白行,已忽略", request_id)
            continue
            
        try:
            record = json.loads(line)
            if isinstance(record, dict) and 'log' in record and 'tables' in record:
                records.append(record)
                write_operation_log(f"第{i+1}行NDJSON解析成功,包含有效日志记录", request_id)
            else:
                missing_fields = []
                if 'log' not in record:
                    missing_fields.append('log')
                if 'tables' not in record:
                    missing_fields.append('tables')
                write_operation_log(
                    f"第{i+1}行NDJSON缺少必要字段: {', '.join(missing_fields)},已忽略", 
                    request_id
                )
        except json.JSONDecodeError as e:
            error_msg = f"第{i+1}行NDJSON解析失败: {str(e)}"
            write_error_log(error_msg, request_id)
    
    write_operation_log(f"NDJSON解析完成,共获取{len(records)}条有效日志记录", request_id)
    return records


async def process_analysis_background(log_records: List[Dict[str, Any]], request_id: str) -> None:
    """后台处理分析流程"""
    try:
        write_operation_log(f"开始后台分析流程,共处理{len(log_records)}条记录", request_id)
        
        for record_idx, log_data in enumerate(log_records):
            write_operation_log(f"开始处理第{record_idx+1}/{len(log_records)}条记录", request_id)
            
            # 1. 生成报告标题
            report_title = generate_report_title(log_data)
            write_operation_log(f"生成报告标题: {report_title}", request_id)
            
            # 2. 获取所有表的信息
            table_info_list = []
            for table_name in log_data.get('tables', []):
                table_info = get_table_info(table_name, request_id)
                table_info_list.append(table_info)
            
            # 3. 生成提示词并调用API
            prompt = generate_prompt(log_data, table_info_list, request_id)
            analysis_report = await call_deepseek_api(prompt, request_id)
            
            # 4. 准备报告数据
            report_data = {
                "id": f"{request_id}-record-{record_idx+1}",
                "title": report_title,
                "request_id": request_id,
                "record_index": record_idx + 1,
                "total_records": len(log_records),
                "timestamp": datetime.now().isoformat(),
                "original_data": {
                    "query_time": log_data.get('query_time'),
                    "lock_time": log_data.get('lock_time'),
                    "rows_sent": log_data.get('rows_sent'),
                    "rows_examined": log_data.get('rows_examined'),
                    "log": log_data.get('log'),
                    "tables": log_data.get('tables')
                },
                "table_info": table_info_list,
                "analysis_report": analysis_report
            }
            
            # 5. 追加到报告文件
            if append_to_report(report_data, request_id):
                write_operation_log(f"第{record_idx+1}条记录分析报告已追加到汇总文件", request_id)
            else:
                write_error_log(f"第{record_idx+1}条记录分析报告追加失败", request_id)
        
        write_operation_log("所有记录分析完成", request_id)
        
    except Exception as e:
        write_error_log(f"后台分析流程失败: {str(e)}", request_id)


def run_async_task(log_records: List[Dict[str, Any]], request_id: str) -> None:
    """线程包装器,运行异步任务"""
    try:
        write_operation_log("启动异步处理线程", request_id)
        asyncio.run(process_analysis_background(log_records, request_id))
    except Exception as e:
        write_error_log(f"异步任务执行失败: {str(e)}", request_id)


@app.route('/analyze-slow-query', methods=['POST'])
def analyze_slow_query():
    """分析慢查询日志的API端点"""
    # 生成唯一请求ID
    request_id = datetime.now().strftime("%Y%m%d%H%M%S") + f"-{os.urandom(4).hex()}"
    write_operation_log("收到慢查询分析请求", request_id)
    
    try:
        # 记录请求基本信息
        call_timestamp = datetime.now().isoformat()
        write_operation_log(
            f"请求信息 - 方法: {request.method}, 客户端IP: {request.remote_addr}, Content-Type: {request.content_type}",
            request_id
        )
        
        # 验证内容类型
        if request.content_type != config.SUPPORTED_CONTENT_TYPE:
            error_msg = f"不支持的Content-Type: {request.content_type},仅接受{config.SUPPORTED_CONTENT_TYPE}"
            write_error_log(error_msg, request_id)
            return Response(
                json.dumps({
                    "status": "error",
                    "message": error_msg,
                    "request_id": request_id
                }, ensure_ascii=False, indent=2),
                mimetype="application/json",
                status=415
            )
        
        # 解析NDJSON数据
        write_operation_log("开始读取并解析NDJSON数据", request_id)
        try:
            ndjson_data = request.data.decode('utf-8').splitlines()
            write_operation_log(f"成功读取{len(ndjson_data)}行NDJSON数据", request_id)
        except UnicodeDecodeError as e:
            error_msg = f"数据解码失败: {str(e)}"
            write_error_log(error_msg, request_id)
            return Response(
                json.dumps({
                    "status": "error",
                    "message": error_msg,
                    "request_id": request_id
                }, ensure_ascii=False, indent=2),
                mimetype="application/json",
                status=400
            )
        
        # 提取有效日志记录
        log_records = extract_valid_log_records(ndjson_data, request_id)
        
        # 未找到有效日志时返回正常响应
        if not log_records:
            write_operation_log("未找到有效日志记录,返回正常响应", request_id)
            return Response(
                json.dumps({
                    "status": "completed",
                    "message": "未找到需要处理的有效日志记录",
                    "request_id": request_id,
                    "total_lines_received": len(ndjson_data),
                    "valid_records_found": 0
                }, ensure_ascii=False, indent=2),
                mimetype="application/json",
                status=200
            )
        
        # 有有效日志时启动后台处理
        thread = threading.Thread(
            target=run_async_task,
            args=(log_records, request_id),
            daemon=True
        )
        thread.start()
        write_operation_log("后台处理线程已启动", request_id)
        
        # 返回接受响应
        response_data = {
            "status": "accepted",
            "message": "请求已接收,正在后台处理",
            "request_id": request_id,
            "timestamp": call_timestamp,
            "total_records": len(log_records),
            "total_lines_received": len(ndjson_data)
        }
        
        # write_operation_log("请求处理完成,返回响应", request_id)
        return Response(
            json.dumps(response_data, ensure_ascii=False, indent=2),
            mimetype="application/json",
            status=200
        )
        
    except Exception as e:
        # 仅逻辑错误返回错误响应
        error_msg = f"请求处理失败: {str(e)}"
        write_error_log(error_msg, request_id)
        return Response(
            json.dumps({
                "status": "error",
                "message": error_msg,
                "request_id": request_id
            }, ensure_ascii=False, indent=2),
            mimetype="application/json",
            status=500
        )


@app.route('/health', methods=['GET'])
def health_check():
    """健康检查接口"""
    request_id = f"health-{datetime.now().strftime('%Y%m%d%H%M%S')}"
    write_operation_log("收到健康检查请求", request_id)
    
    # 检查LLM客户端
    llm_status = "healthy"
    llm_details = "未初始化"
    try:
        init_llm_client()
        llm_status = "healthy"
        llm_details = "客户端已初始化"
    except Exception as e:
        llm_status = "degraded"
        llm_details = f"初始化失败: {str(e)}"
    
    # 检查数据库连接
    db_status = "healthy"
    db_details = "连接成功"
    try:
        conn = mysql.connector.connect(** get_db_config())
        conn.close()
    except Exception as e:
        db_status = "degraded"
        db_details = f"连接失败: {str(e)}"
    
    # 检查报告文件
    report_status = "healthy"
    report_details = "文件正常"
    try:
        if not os.path.exists(config.SLOW_REPORT_PATH):
            report_status = "warning"
            report_details = "报告文件不存在,将在首次分析后创建"
        else:
            with open(config.SLOW_REPORT_PATH, 'r') as f:
                pass  # 仅检查文件是否可读取
    except Exception as e:
        report_status = "degraded"
        report_details = f"报告文件访问失败: {str(e)}"
    
    # 整体状态
    overall_status = "healthy"
    if llm_status != "healthy" or db_status != "healthy" or report_status == "degraded":
        overall_status = "degraded"
    
    response = {
        "status": overall_status,
        "services": {
            "llm_client": {
                "status": llm_status,
                "details": llm_details
            },
            "database": {
                "status": db_status,
                "details": db_details
            },
            "report_file": {
                "status": report_status,
                "details": report_details,
                "path": config.SLOW_REPORT_PATH
            }
        },
        "supported_content_type": config.SUPPORTED_CONTENT_TYPE,
        "default_schema": config.DEFAULT_DB_SCHEMA,
        "timestamp": datetime.now().isoformat(),
        "request_id": request_id
    }
    
    write_operation_log("健康检查完成", request_id)
    return Response(
        json.dumps(response, ensure_ascii=False, indent=2),
        mimetype="application/json"
    )


if __name__ == '__main__':
    print("慢查询分析API启动中...")
    print(f"服务器配置: {config.HOST}:{config.PORT} (调试模式: {'开启' if config.DEBUG else '关闭'})")
    print(f"内容类型支持: {config.SUPPORTED_CONTENT_TYPE}")
    print(f"默认数据库schema: {config.DEFAULT_DB_SCHEMA}")
    print(f"报告文件路径: {config.SLOW_REPORT_PATH}")
    print(f"LLM配置: 模型={config.LLM_MODEL}, 基础URL={config.LLM_BASE_URL}")
    print(f"日志目录: {config.LOG_DIR}")
    print("环境变量检查:")
    print(f"  - DEEPSEEK_API_KEY: {'已配置' if os.getenv('DEEPSEEK_API_KEY') else '未配置'}")
    print(f"  - 数据库配置: HOST={os.getenv('DB_HOST', 'localhost')}, USER={os.getenv('DB_USER', 'root')}")
    print("可用接口:")
    print("  - POST /analyze-slow-query - 分析慢查询日志")
    print("  - GET  /health - 健康检查")
    
    app.run(host=config.HOST, port=config.PORT, debug=config.DEBUG)

这里我把代码中重要的部分做一个总结性讲解:

  • 数据库交互模块

A.提供数据库配置获取函数,支持指定数据库 Schema,默认使用预设的after_sale库。

B.实现表信息提取功能:清理表名中的反引号、拆分库表结构,通过SHOW CREATE TABLE获取表 DDL、SHOW TABLE STATUS获取表行数,同时处理数据库连接错误、表不存在等异常。

  • 大模型交互模块

A.封装 DeepSeek API 调用逻辑:初始化客户端时校验 API 密钥,支持设置模型参数(温度、最大 tokens),处理 API 错误(连接超时、状态码异常等)并返回友好提示。

B.提供提示词生成函数:整合慢查询详情(执行时间、扫描行数等)、表结构元信息,生成结构化提示,引导大模型输出索引优化、SQL 改写等可操作建议。

  • 报告生成与管理模块

A.自动生成报告标题:包含查询类型(SELECT/UPDATE 等)、执行耗时、涉及表名和时间戳,确保报告辨识度。

B.实现报告持久化:将分析结果(原始慢查询数据、表元信息、大模型报告)追加到 JSON 格式的报告文件,支持读取现有报告并处理文件损坏的情况。

  • API 接口模块

A.慢查询分析接口(POST /analyze-slow-query):接收 NDJSON 格式的结构化慢查询数据,验证内容类型,解析并过滤有效记录(需包含log和tables字段),启动后台线程异步处理分析流程,避免前端等待超时。

编写“生成日志查看应用”

这里依旧使用我们的老朋友 streamlit 来充当 Web UI 界面,如下命令安装它:

pip install streamlit

在工作目录创建,创建show_slow_report.py文件,方便查看生成的慢查询分析报告,代码如下:

import streamlit as st
import json
import os
from datetime import datetime

# 页面基础配置
st.set_page_config(
    page_title="慢查询分析报告查看器",
    page_icon="📊",
    layout="wide"
)

# 自定义CSS - 按钮样式和间隔
st.markdown("""
<style>
    .report-btn {
        width: 100%;
        text-align: left;
        margin-bottom: 8px;
        padding: 8px 12px;
        border-radius: 4px;
        border: none;
        cursor: pointer;
        transition: all 0.2s ease;
    }
    .report-btn:not(.selected) {
        background-color: #f0f2f6;
        color: #333;
    }
    .report-btn:not(.selected):hover {
        background-color: #e6e9ed;
    }
    .report-btn.selected {
        background-color: #0066cc;
        color: white;
    }
    .report-container {
        max-height: 600px;
        overflow-y: auto;
        padding-right: 8px;
    }
</style>
""", unsafe_allow_html=True)

# 报告文件路径
REPORT_PATH = os.path.join("reports", "slow_report.json")

# ----------------------
# 回调函数 - 用于即时更新选中状态
# ----------------------
def update_selected_report(report_id):
    """更新选中的报告ID(回调函数)"""
    st.session_state.selected_report_id = report_id

# ----------------------
# 工具函数
# ----------------------
def load_reports():
    """加载并排序报告(按时间倒序),处理文件异常"""
    try:
        if not os.path.exists(REPORT_PATH):
            st.warning(f"报告文件不存在:{REPORT_PATH}")
            return []
        
        with open(REPORT_PATH, 'r', encoding='utf-8') as f:
            reports = json.load(f)
        
        # 按时间戳倒序排序(最新报告在前)
        reports.sort(
            key=lambda x: x.get('timestamp', ''),
            reverse=True
        )
        return reports
    
    except json.JSONDecodeError:
        st.error("报告文件格式错误,无法解析(可能是JSON损坏)")
        return []
    except PermissionError:
        st.error(f"无权限访问报告文件:{REPORT_PATH}")
        return []
    except Exception as e:
        st.error(f"加载报告失败:{str(e)}")
        return []

def format_timestamp(timestamp_str):
    """格式化ISO时间戳为易读格式"""
    try:
        dt = datetime.fromisoformat(timestamp_str)
        return dt.strftime("%Y-%m-%d %H:%M:%S")
    except:
        return timestamp_str

def search_reports(reports, query):
    """根据搜索词过滤报告(支持标题、表名、内容)"""
    if not query:
        return reports
        
    query_lower = query.lower()
    return [
        report for report in reports
        if (
            query_lower in report.get('title', '').lower() or
            any(query_lower in str(table).lower() for table in report.get('original_data', {}).get('tables', [])) or
            query_lower in report.get('analysis_report', '').lower() or
            query_lower in report.get('original_data', {}).get('log', '').lower()
        )
    ]

# ----------------------
# 主逻辑
# ----------------------
reports = load_reports()
st.title("📊 慢查询分析报告查看器")

# 初始化会话状态
if 'selected_report_id' not in st.session_state:
    if reports:
        st.session_state.selected_report_id = reports[0]['id']
    else:
        st.session_state.selected_report_id = None

# 侧边栏 - 报告列表和搜索
with st.sidebar:
    st.header("报告列表")
    
    # 搜索框
    search_query = st.text_input("搜索报告", placeholder="输入标题、表名或内容关键词...")
    filtered_reports = search_reports(reports, search_query)
    
    # 显示统计信息
    st.caption(f"显示 {len(filtered_reports)} 份报告(共 {len(reports)} 份)")
    
    # 报告列表
    if filtered_reports:
        # 确保选中的ID在当前过滤列表中
        valid_ids = [r['id'] for r in filtered_reports]
        if (st.session_state.selected_report_id is None or 
            st.session_state.selected_report_id not in valid_ids):
            st.session_state.selected_report_id = valid_ids[0]
        
        # 报告容器(带滚动)
        st.markdown('<div class="report-container">', unsafe_allow_html=True)
        
        # 逐个显示报告按钮,使用回调函数更新状态
        for report in filtered_reports:
            report_id = report['id']
            timestamp = format_timestamp(report['timestamp'])
            title = report['title']
            
            # 按钮状态判断
            is_selected = (report_id == st.session_state.selected_report_id)
            
            # 使用回调函数确保状态即时更新
            st.button(
                label=f"[{timestamp}] {title}",
                key=f"btn_{report_id}",
                use_container_width=True,
                type="primary" if is_selected else "secondary",
                on_click=update_selected_report,
                args=(report_id,)
            )
        
        st.markdown('</div>', unsafe_allow_html=True)
        
        # 获取选中的报告
        selected_report = next(
            (r for r in filtered_reports if r['id'] == st.session_state.selected_report_id),
            None
        )
    else:
        st.info("没有找到匹配的报告")
        selected_report = None

# 主内容区 - 显示报告详情
if selected_report:
    st.subheader(selected_report['title'])
    
    # 基本信息卡片
    col1, col2, col3 = st.columns(3)
    with col1:
        st.info(f"**报告ID**\n\n{selected_report['id']}")
    with col2:
        st.info(f"**生成时间**\n\n{format_timestamp(selected_report['timestamp'])}")
    with col3:
        st.info(f"**请求ID**\n\n{selected_report['request_id']}")
    
    # 性能指标
    st.subheader("查询性能指标")
    original_data = selected_report.get('original_data', {})
    metrics_col1, metrics_col2, metrics_col3, metrics_col4 = st.columns(4)
    with metrics_col1:
        st.metric("查询时间", f"{original_data.get('query_time', 'N/A')} 秒")
    with metrics_col2:
        st.metric("锁定时间", f"{original_data.get('lock_time', 'N/A')} 秒")
    with metrics_col3:
        st.metric("返回行数", original_data.get('rows_sent', 'N/A'))
    with metrics_col4:
        st.metric("扫描行数", original_data.get('rows_examined', 'N/A'))
    
    # 涉及表名
    st.subheader("涉及表名")
    tables = original_data.get('tables', [])
    if tables:
        for table in tables:
            st.text(f"- {table}")
    else:
        st.text("无表信息")
    
    # 原始SQL
    with st.expander("查看原始SQL", expanded=False):
        st.code(original_data.get('log', '无SQL日志'), language="sql")
    
    # 表结构信息
    st.subheader("表结构详情")
    table_info_list = selected_report.get('table_info', [])
    for table_info in table_info_list:
        with st.expander(f"表: {table_info.get('cleaned_name', '未知表')}"):
            if table_info.get('error'):
                st.error(f"获取表信息失败: {table_info['error']}")
            else:
                st.text(f"预估行数: {table_info.get('row_count', '未知')}")
                st.code(table_info.get('ddl', '无DDL信息'), language="sql")
    
    # 分析报告
    st.subheader("优化建议")
    st.markdown(selected_report.get('analysis_report', '无分析内容'))

else:
    if reports:
        st.info("请从侧边栏选择一份报告查看详情")
    else:
        st.info("暂无报告数据,请先运行分析任务生成报告")

至此,所有准备工作已经完成,我们开始测试功能。

功能测试

由于是对慢查询记录进行采集、处理、分析、生成报告。所以测试的基本思路是:创建 MySQL 数据库表,插入数据,启动 Fluent-Bit,FluentD 服务准备采集慢查询日志,启动 process_slow_log.py 生成分析报告,然后执行一条 SQL 语句触发慢查询日志的生成,最后通过show_slow_report.py 查看分析报告的内容。

创建数据库

在mysql5.7下执行sql生成对应的表,打开MySQL 5.7 Command Line Client,输入如下 sql 并执行。

-- 创建数据库
CREATE DATABASE IF NOT EXISTS after_sale
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_general_ci;

USE after_sale;

-- 删除现有表,如果存在
DROP TABLE IF EXISTS SupportIssue;
DROP TABLE IF EXISTS CustomerService;
DROP TABLE IF EXISTS Solution;
DROP TABLE IF EXISTS Issue;
DROP TABLE IF EXISTS User;

-- 创建用户表
CREATE TABLE User
(
    UserId INTEGER PRIMARY KEY AUTO_INCREMENT, -- 用户ID
    Name VARCHAR(60) NOT NULL, -- 姓名
    Email VARCHAR(60) NOT NULL, -- 电子邮件
    Phone VARCHAR(24) -- 电话
);

-- 创建问题表
CREATE TABLE Issue
(
    IssueId INTEGER PRIMARY KEY AUTO_INCREMENT, -- 问题ID
    UserId INTEGER NOT NULL, -- 用户ID
    Description TEXT NOT NULL, -- 问题描述
    Status VARCHAR(20) NOT NULL, -- 状态
    FOREIGN KEY (UserId) REFERENCES User (UserId)
        ON DELETE NO ACTION ON UPDATE NO ACTION
);

-- 创建解决方案表
CREATE TABLE Solution
(
    SolutionId INTEGER PRIMARY KEY AUTO_INCREMENT, -- 解决方案ID
    SolutionDescription TEXT NOT NULL, -- 解决方案描述
    IsValid TINYINT -- 是否有效(MySQL中用TINYINT表示布尔值,1为真,0为假)
);

-- 创建客服表
CREATE TABLE CustomerService
(
    CustomerServiceId INTEGER PRIMARY KEY AUTO_INCREMENT, -- 客服ID
    Name VARCHAR(40) NOT NULL, -- 姓名
    Email VARCHAR(60), -- 电子邮件
    Phone VARCHAR(24) -- 电话
);

-- 创建客服问题关联表
CREATE TABLE SupportIssue
(
    SupportIssueId INTEGER PRIMARY KEY AUTO_INCREMENT, -- 客服问题关联ID
    IssueId INTEGER NOT NULL, -- 问题ID
    CustomerServiceId INTEGER NOT NULL, -- 客服ID
    SolutionId INTEGER NOT NULL, -- 解决方案ID
    ActionTime DATETIME NOT NULL, -- 操作时间
    FOREIGN KEY (IssueId) REFERENCES Issue (IssueId)
        ON DELETE NO ACTION ON UPDATE NO ACTION,
    FOREIGN KEY (CustomerServiceId) REFERENCES CustomerService (CustomerServiceId)
        ON DELETE NO ACTION ON UPDATE NO ACTION,
    FOREIGN KEY (SolutionId) REFERENCES Solution (SolutionId)
        ON DELETE NO ACTION ON UPDATE NO ACTION
);

插入测试数据

数据库和表结构有了,接着插入一些测试数据,为后面慢查询提供条件。为了保证数据量,我这里利用 python 脚本生成数据,先执行如下命令安装数据库连接依赖,方便 python 访问数据库。

pip install mysql-connector-python

然后,在工作目录创建 generate_data.py 文件,插入数据。

import mysql.connector
from faker import Faker
import random
from datetime import datetime, timedelta

# 数据库连接配置 - 请修改为你的数据库信息
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',
    'password': 'root',
    'database': 'after_sale',
    'autocommit': True
}

# 生成数据量 (可根据需要调整)
NUM_USERS = 100000          # 10万用户
NUM_ISSUES = 500000         # 50万问题
NUM_SOLUTIONS = 10000       # 1万解决方案
NUM_CUSTOMERSERVICE = 500   # 500客服
NUM_SUPPORTISSUES = 1000000 # 100万客服问题关联记录

# 初始化Faker
fake = Faker('zh_CN')

def get_db_connection():
    """获取数据库连接"""
    try:
        conn = mysql.connector.connect(**DB_CONFIG)
        return conn
    except mysql.connector.Error as err:
        print(f"数据库连接错误: {err}")
        raise

def generate_users():
    """生成用户数据"""
    print(f"开始生成 {NUM_USERS} 条用户数据...")
    conn = get_db_connection()
    cursor = conn.cursor()

    # 批量插入大小
    batch_size = 1000
    insert_query = """
    INSERT INTO User (Name, Email, Phone)
    VALUES (%s, %s, %s)
    """

    try:
        for i in range(0, NUM_USERS, batch_size):
            batch_data = []
            for _ in range(min(batch_size, NUM_USERS - i)):
                name = fake.name()
                email = fake.email()
                phone = fake.phone_number()
                batch_data.append((name, email, phone))

            cursor.executemany(insert_query, batch_data)
            print(f"已插入 {i + len(batch_data)} 条用户数据")
    except mysql.connector.Error as err:
        print(f"插入用户数据错误: {err}")
        raise
    finally:
        cursor.close()
        conn.close()
    print(f"用户数据生成完成,共 {NUM_USERS} 条")

def generate_solutions():
    """生成解决方案数据 - 修复了字符串格式化问题"""
    print(f"开始生成 {NUM_SOLUTIONS} 条解决方案数据...")
    conn = get_db_connection()
    cursor = conn.cursor()

    # 重新设计解决方案模板,确保每个模板的占位符数量明确
    solution_templates = [
        ["检查%s并重启%s", 2],  # 包含2个占位符
        ["更新%s至最新版本", 1],  # 包含1个占位符
        ["清除%s缓存并重新尝试", 1],
        ["联系%s获取技术支持", 1],
        ["检查%s设置是否正确", 1],
        ["卸载并重新安装%s", 1],
        ["重启%s后再试", 1],
        ["检查网络连接并确保%s可访问", 1],
        ["使用%s工具进行诊断", 1],
        ["恢复%s至默认设置", 1]
    ]

    objects = ["网络", "系统", "应用", "设备", "软件", "驱动", "账户", "服务"]

    batch_size = 1000
    insert_query = """
    INSERT INTO Solution (SolutionDescription, IsValid)
    VALUES (%s, %s)
    """

    try:
        for i in range(0, NUM_SOLUTIONS, batch_size):
            batch_data = []
            for _ in range(min(batch_size, NUM_SOLUTIONS - i)):
                # 随机选择一个模板及其占位符数量
                template, param_count = random.choice(solution_templates)

                # 根据占位符数量提供相应数量的参数
                if param_count == 1:
                    obj = random.choice(objects)
                    description = template % obj
                elif param_count == 2:
                    obj1 = random.choice(objects)
                    obj2 = random.choice(objects)
                    description = template % (obj1, obj2)
                else:
                    # 默认为1个参数的情况
                    obj = random.choice(objects)
                    description = template % obj
                    
                is_valid = random.choice([0, 1])
                batch_data.append((description, is_valid))
            
            cursor.executemany(insert_query, batch_data)
            print(f"已插入 {i + len(batch_data)} 条解决方案数据")
    except mysql.connector.Error as err:
        print(f"插入解决方案数据错误: {err}")
        raise
    except Exception as e:
        print(f"生成解决方案描述时出错: {e},模板: {template}, 参数数量: {param_count}")
        raise
    finally:
        cursor.close()
        conn.close()
    print(f"解决方案数据生成完成,共 {NUM_SOLUTIONS} 条")

def generate_customer_service():
    """生成客服数据"""
    print(f"开始生成 {NUM_CUSTOMERSERVICE} 条客服数据...")
    conn = get_db_connection()
    cursor = conn.cursor()
    
    batch_size = 100
    insert_query = """
    INSERT INTO CustomerService (Name, Email, Phone)
    VALUES (%s, %s, %s)
    """
    
    try:
        for i in range(0, NUM_CUSTOMERSERVICE, batch_size):
            batch_data = []
            for _ in range(min(batch_size, NUM_CUSTOMERSERVICE - i)):
                name = fake.name()
                email = f"{name.replace(' ', '')}.{random.randint(100, 999)}@support.com"
                phone = fake.phone_number()
                batch_data.append((name, email, phone))
            
            cursor.executemany(insert_query, batch_data)
            print(f"已插入 {i + len(batch_data)} 条客服数据")
    except mysql.connector.Error as err:
        print(f"插入客服数据错误: {err}")
        raise
    finally:
        cursor.close()
        conn.close()
    print(f"客服数据生成完成,共 {NUM_CUSTOMERSERVICE} 条")

def generate_issues():
    """生成问题数据"""
    print(f"开始生成 {NUM_ISSUES} 条问题数据...")
    conn = get_db_connection()
    cursor = conn.cursor()
    
    # 获取最大用户ID
    cursor.execute("SELECT MAX(UserId) FROM User")
    max_user_id = cursor.fetchone()[0]
    if not max_user_id:
        raise Exception("请先生成用户数据")
    
    # 问题描述模板
    issue_templates = [
        ["%s无法正常工作", 1],
        ["无法%s", 1],
        ["%s出现错误", 1],
        ["%s时遇到问题", 1],
        ["%s功能异常", 1],
        ["关于%s的问题", 1],
        ["%s失败", 1],
        ["%s导致系统异常", 1]
    ]
    
    issue_objects = ["电脑", "手机", "应用", "软件", "网络", "账户", "订单", "支付", "登录", "文件"]
    actions = ["连接网络", "发送消息", "上传文件", "下载数据", "安装软件", "卸载程序", "更新系统", "登录账户"]
    statuses = ["未解决", "处理中", "已解决", "已关闭", "重新打开"]
    
    batch_size = 1000
    insert_query = """
    INSERT INTO Issue (UserId, Description, Status)
    VALUES (%s, %s, %s)
    """
    
    try:
        for i in range(0, NUM_ISSUES, batch_size):
            batch_data = []
            for _ in range(min(batch_size, NUM_ISSUES - i)):
                user_id = random.randint(1, max_user_id)
                template, param_count = random.choice(issue_templates)
                
                if "无法%s" in template:
                    desc = template % random.choice(actions)
                else:
                    desc = template % random.choice(issue_objects)
                    
                status = random.choice(statuses)
                batch_data.append((user_id, desc, status))
            
            cursor.executemany(insert_query, batch_data)
            print(f"已插入 {i + len(batch_data)} 条问题数据")
    except mysql.connector.Error as err:
        print(f"插入问题数据错误: {err}")
        raise
    finally:
        cursor.close()
        conn.close()
    print(f"问题数据生成完成,共 {NUM_ISSUES} 条")

def generate_support_issues():
    """生成客服问题关联数据"""
    print(f"开始生成 {NUM_SUPPORTISSUES} 条客服问题关联数据...")
    conn = get_db_connection()
    cursor = conn.cursor()
    
    # 获取各表最大ID
    cursor.execute("SELECT MAX(IssueId) FROM Issue")
    max_issue_id = cursor.fetchone()[0]
    cursor.execute("SELECT MAX(CustomerServiceId) FROM CustomerService")
    max_cs_id = cursor.fetchone()[0]
    cursor.execute("SELECT MAX(SolutionId) FROM Solution")
    max_solution_id = cursor.fetchone()[0]
    
    if not all([max_issue_id, max_cs_id, max_solution_id]):
        raise Exception("请先生成所有基础表数据")
    
    # 生成时间范围:过去1年
    end_date = datetime.now()
    start_date = end_date - timedelta(days=365)
    
    batch_size = 1000
    insert_query = """
    INSERT INTO SupportIssue (IssueId, CustomerServiceId, SolutionId, ActionTime)
    VALUES (%s, %s, %s, %s)
    """
    
    try:
        for i in range(0, NUM_SUPPORTISSUES, batch_size):
            batch_data = []
            for _ in range(min(batch_size, NUM_SUPPORTISSUES - i)):
                issue_id = random.randint(1, max_issue_id)
                cs_id = random.randint(1, max_cs_id)
                solution_id = random.randint(1, max_solution_id)
                
                # 随机生成时间
                time_delta = end_date - start_date
                random_days = random.randint(0, time_delta.days)
                random_seconds = random.randint(0, 86399)  # 一天的秒数
                action_time = start_date + timedelta(days=random_days, seconds=random_seconds)
                
                batch_data.append((issue_id, cs_id, solution_id, action_time.strftime('%Y-%m-%d %H:%M:%S')))
            
            cursor.executemany(insert_query, batch_data)
            print(f"已插入 {i + len(batch_data)} 条客服问题关联数据")
    except mysql.connector.Error as err:
        print(f"插入客服问题关联数据错误: {err}")
        raise
    finally:
        cursor.close()
        conn.close()
    print(f"客服问题关联数据生成完成,共 {NUM_SUPPORTISSUES} 条")

if __name__ == "__main__":
    # 按顺序生成数据(有依赖关系)
    try:
        generate_users()
        generate_solutions()
        generate_customer_service()
        generate_issues()
        generate_support_issues()
        print("所有数据生成完成!")
    except Exception as e:
        print(f"生成数据时出错: {str(e)}")

由于在代码中使用到了假数据,所以需要安装 faker 组件,执行如下代码:

pip install faker
python generate_data.py

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

确保 Fluent-Bit 与 FluentD 执行

在docker desktop 中可以看到容器运行状态。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

启动 “生成日志分析程序”

工作目录执行如下命令:

python Process_slow_log.py

执行慢查询 SQL

MySQL 5.7 Command Line Client执行如下 sql。

use after_sale;

SELECT 
    u.UserId, u.Name, u.Email,
    i.IssueId, i.Description, i.Status,
    cs.CustomerServiceId, cs.Name,
    s.SolutionId, s.SolutionDescription,
    si.ActionTime
FROM SupportIssue si
JOIN Issue i ON si.IssueId = i.IssueId
JOIN User u ON i.UserId = u.UserId
JOIN CustomerService cs ON si.CustomerServiceId = cs.CustomerServiceId
JOIN Solution s ON si.SolutionId = s.SolutionId
WHERE s.IsValid = 1
AND i.Status = 'unsolve'
ORDER BY si.ActionTime DESC;

返回结果如下:

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

查看fluent-bit容器日志,容器日志中会显示采集到的内容。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

启动“生成日志查看应用”

执行如下命令:

streamlit run show_slow_report.py

启动后的控制台会显示调用日志和生成报告。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

此时通过浏览器页面可以查看慢查询日志分析报告,如下图所示。

数据库慢查询破局指南:从日志采集到智能诊断的全流程自动化方案-AI.x社区

作者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。

收藏
回复
举报
回复
相关推荐