知识门户

返回

第11章 运维监控与文档交付:让脚本具备"自我报告"能力

第四篇:交付与工程化运维

views | comments

第11章 运维监控与文档交付:让脚本具备”自我报告”能力#

11.1 崩溃报警:10行代码接入企业 IM 机器人#

一个凌晨三点的电话#

凌晨三点,你被电话吵醒。产线负责人语气焦急:“测试工装的自动校准脚本从昨晚十点就挂了,五个小时的产能全废了。”

你打开电脑一看——日志里写着一行 ConnectionResetError,脚本在十点零三分就崩溃了,没有任何人知道。

你心想:如果脚本崩溃的那一秒,钉钉群里就弹出一条报警消息,这一切就不会发生。

这就是本节要解决的问题——让你的 Python 脚本具备”自我报警”能力。

11.1.1 Webhook 机器人:三分钟配置#

主流企业 IM 平台(钉钉、企业微信、飞书)都提供了”自定义机器人”功能,通过一个 Webhook URL,你的程序就能往群里发消息。

钉钉机器人的创建步骤

  1. 打开钉钉群 → 群设置 → 智能群助手 → 添加机器人
  2. 选择”自定义”类型
  3. 设置机器人名称(如”测试工装报警”)
  4. 安全设置选择”自定义关键词”,填入 报警(这样只有包含”报警”关键词的消息才会被发送,防止误发)
  5. 复制 Webhook URL,格式类似:
    https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxxxxxx
    plaintext

企业微信机器人的创建步骤

  1. 打开企业微信群 → 群设置 → 群机器人 → 添加
  2. 设置名称和头像
  3. 复制 Webhook URL,格式类似:
    https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx
    plaintext

飞书机器人的创建步骤

  1. 打开飞书群 → 设置 → 群机器人 → 添加机器人 → 自定义机器人
  2. 设置名称和描述
  3. 复制 Webhook URL,格式类似:
    https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx
    plaintext

安全提示:Webhook URL 等同于”群发消息的钥匙”,不要硬编码在代码里。建议放在配置文件(第1章讲的 JSON/INI 配置文件)或环境变量中。

11.1.2 核心实现:10行代码发一条报警#

钉钉、企业微信、飞书的 Webhook 接口格式略有不同,但本质上都是向一个 URL POST 一个 JSON。我们先从最简单的开始:

钉钉发送文本消息:

企业微信发送文本消息:

飞书发送文本消息:

三种平台的核心逻辑完全一样:构造 JSON → POST 到 Webhook URL → 检查返回值。唯一区别是 JSON 字段名称和返回码字段名略有不同。

11.1.3 统一封装:一个 AlertManager 搞定三种平台#

实际项目中,你不会只用一个平台,而且你不想在业务代码里到处写 requests.post。我们需要一个统一的报警管理器:

11.1.4 分级报警策略#

报警不能”什么都往群里发”——否则报警消息太多,真正重要的报警反而被淹没。我们需要分级策略:

┌──────────┬──────────────┬─────────────────────────────────────┐
│  等级    │  触发场景     │  报警动作                            │
├──────────┼──────────────┼─────────────────────────────────────┤
│  INFO    │  正常事件记录  │  仅写入日志文件,不发送消息            │
│  WARNING │  一般异常     │  发送群消息通知                       │
│  ERROR   │  严重异常     │  发送群消息并 @负责人                  │
│  CRITICAL│  系统崩溃     │  发送群消息 + @负责人 + 电话告警       │
└──────────┴──────────────┴─────────────────────────────────────┘
plaintext

在业务代码中使用:

11.1.5 与 logging 集成:报警自动触发#

你可能不想在业务代码里到处写 alerter.send()。更好的方式是把报警管理器集成到 logging 框架中,让日志达到特定级别时自动触发报警:

注册到 logging:

之后,业务代码只需要正常写日志,报警会自动触发:

logger.debug("正在解析第 1024 帧数据")       # 不触发报警
logger.info("设备A已上线")                   # 不触发报警
logger.warning("温度超过阈值: 65°C")         # → 自动发送 WARNING 级别报警
logger.error("串口连接断开")                 # → 自动发送 ERROR 级别报警
logger.critical("硬盘空间不足,系统即将停止")  # → 自动发送 CRITICAL 级别报警
python

架构示意:

业务代码

  ├── logger.info()    ──→ 日志文件 + 控制台
  ├── logger.debug()   ──→ 日志文件 + 控制台

  ├── logger.warning() ──→ 日志文件 + 控制台 + 钉钉群消息
  ├── logger.error()   ──→ 日志文件 + 控制台 + 钉钉群消息(@负责人)
  └── logger.critical()──→ 日志文件 + 控制台 + 钉钉群消息 + 电话告警
plaintext

11.1.6 发送富文本消息:Markdown 卡片#

纯文本消息信息密度较低。钉钉和飞书支持 Markdown 格式的消息,可以让报警信息更加结构化:

企业微信支持 Markdown 格式的消息:

飞书 Markdown 注意事项:飞书的自定义机器人对 Markdown 的支持有特殊语法要求(如需要使用 <font> 标签而非标准 Markdown 语法),建议参考飞书开放平台的官方文档进行适配。


11.2 日志归档:用 Loguru 实现日志自动切割与清理#

11.2.1 为什么 logging 不够用?#

在第1章,我们介绍了 Python 标准库的 logging 模块,它确实是”够用”的日志方案。但在嵌入式工控机上长期运行时,logging 有一个让人头疼的问题:

它不会自动切割日志文件。

想象一下:你的测试工装脚本每天 24 小时运行,每天产生大约 200MB 的日志。一个月后,日志文件涨到了 6GB。工控机的硬盘通常只有 32GB~64GB,很快就被撑满了。然后系统报 No space left on device,脚本崩溃,产线停摆。

你当然可以用 logging.handlers.RotatingFileHandler 来做日志切割,但配置起来相当繁琐:

这段代码能用,但有几个问题:

  • 代码量大:光配置日志就要写十几行
  • 不支持压缩:切割后的旧日志还是 .log 文件,白白占用空间
  • 不支持异步写入:高频日志写入可能阻塞业务线程
  • 配置复杂RotatingFileHandlerTimedRotatingFileHandler 是两个独立的处理器,无法同时按大小和按时间切割

11.2.2 Loguru:开箱即用的日志方案#

Loguru 是一个第三方日志库,设计哲学是”日志应该开箱即用,不需要繁琐配置”。它解决了标准库 logging 的所有痛点。

安装:

pip install loguru
bash

最简单的用法——一行代码替代整个 logging 体系:

from loguru import logger

logger.debug("调试信息")
logger.info("普通信息")
logger.warning("警告信息")
logger.error("错误信息")
logger.critical("严重错误")
python

不需要创建 logger 实例、不需要配置 handler、不需要设置 formatter。from loguru import logger 这一行就够了。

11.2.3 配置日志切割:保留30天,单文件50MB,自动压缩#

这是嵌入式场景最核心的需求——在工控机有限的硬盘上,安全地保留足够的日志历史:

关键参数说明:

参数说明常用值
rotation切割条件"00:00"(每天午夜)、"50 MB"(按大小)、"1 week"(按周)
retention保留策略"30 days""10 files""2 GB"
compression压缩格式"zip""gz""tar.gz""bz2"
enqueue异步写入True(推荐,避免日志写入阻塞业务)
encoding字符编码"utf-8"(必选,否则中文乱码)
backtrace异常回溯增强True(显示完整调用栈)
diagnose变量值诊断True(在异常信息中显示变量值,调试利器)

rotation 还支持组合条件:

# 同时满足"每天"或"50MB"时切割
logger.add("logs/app.log", rotation="00:00", ...)

# 或者纯按大小
logger.add("logs/app.log", rotation="50 MB", ...)
python

11.2.4 实际效果演示#

配置好之后,你的 logs/ 目录结构会自动变成这样:

logs/
├── app_2025-07-28.log          # 今天的日志(正在写入)
├── app_2025-07-27.log.zip      # 昨天的(已压缩)
├── app_2025-07-26.log.zip      # 前天的(已压缩)
├── ...
├── error_2025-07-28.log        # 今天的错误日志
├── error_2025-07-27.log.zip    # 昨天的错误日志
└── ...(超过30天的自动删除)
plaintext

你不需要写任何”清理旧日志”的脚本——Loguru 帮你处理了一切。

11.2.5 Loguru 高级技巧#

技巧1:捕获未处理的异常#

标准库 logging 不会自动捕获未处理的异常——程序崩溃时,最后的错误信息不会被写入日志文件。Loguru 可以:

# 自动捕获所有未处理的异常并记录到日志
logger.add(
    "logs/crash.log",
    rotation="10 MB",
    retention="60 days",
    compression="zip",
    encoding="utf-8",
    backtrace=True,
    diagnose=True,        # 关键:在异常信息中显示变量值
    level="ERROR"
)
python

当程序崩溃时,crash.log 会记录完整的调用栈和每个变量的值:

2025-07-28 14:30:05.123 | ERROR    | test_station:run_calibration:156 | 未捕获的异常
Traceback (most recent call last):
  File "test_station.py", line 154, in run_calibration
    result = self.device.read_sensor(channel=3)
    │                  │                └ 3
    │                  └ <Device object at 0x7f8b8c0d4a90>
    └ <TestStation object at 0x7f8b8c0d4b50>

ConnectionError: 串口读取超时(等待 1000ms 无响应)
plaintext

注意它自动打印了 channel=3self.device 的类型和地址——这比标准 traceback 信息量大得多,排查问题时省去大量加 print 调试的时间。

技巧2:使用 @logger.catch 装饰器#

@logger.catch
def connect_device(port: str, baudrate: int):
    """连接设备 - 如果出错,自动记录详细异常信息"""
    import serial
    ser = serial.Serial(port, baudrate, timeout=1)
    return ser

# 即使这个函数抛出异常,Loguru 也会完整记录,而不是让程序无声崩溃
python

技巧3:结构化日志(JSON 格式)#

当你需要把日志导入到 ELK(Elasticsearch + Logstash + Kibana)等日志分析平台时,JSON 格式的日志更方便解析:

11.2.6 从 logging 迁移到 Loguru#

如果你的项目已经在用 logging,不需要全部重写。Loguru 提供了拦截器,可以把所有 logging 日志转发到 Loguru:

这样,pymodbuspyserial 等第三方库的日志也会自动通过 Loguru 输出,统一格式、统一管理。

11.2.7 与报警模块联动#

把 11.1 的报警模块和 Loguru 结合起来,实现”日志自动归档 + 重要日志自动报警”的完整方案:

最终效果:

  • 所有日志:自动写入文件,按天切割,30天自动清理,压缩归档
  • WARNING 及以上:除了写日志,还会自动发送钉钉群消息
  • ERROR 及以上:发送钉钉群消息 + @负责人
  • CRITICAL:发送钉钉群消息 + @负责人 + 电话告警
  • 硬盘不会撑满:Loguru 自动管理日志生命周期
  • 业务代码零侵入:只需要 logger.info() / logger.warning(),报警和归档全自动

11.3 文档交付:基于代码结构扫描的 AI 文档生成策略#

11.3.1 场景与痛点:文档永远过期#

“文档和代码不一致”是软件工程中最古老的问题之一。在嵌入式项目中,这个问题尤其严重:

  • 代码在迭代,文档停在三个月前:你改了 parse_frame() 的参数签名,但 README 里还是旧的调用示例
  • 新人看不懂代码,老员工没时间写文档:项目交付时,文档往往是最先被牺牲的
  • 手动写文档成本高、容易出错:一个有 50 个函数的模块,手动写文档要花一整天

核心矛盾:文档需要与代码保持同步,但人没有精力持续同步。

解法:让 AI 扫描你的代码结构,自动生成文档。当代码变了,重新跑一遍扫描就行——文档永远跟代码同步。

11.3.2 方法论:AST 扫描 + AI 生成#

整个流程分两步:

第一步:扫描代码结构(机器做)
  Python 代码 ──→ AST 解析 / inspect 模块 ──→ 结构化数据(函数名、参数、类型、docstring)

第二步:生成文档(AI 做)
  结构化数据 ──→ AI ──→ README.md / API 文档 / 用户操作手册
plaintext

第一步 是确定性的——机器精确地读取代码结构,不会遗漏、不会出错。

第二步 是创造性的——AI 基于结构化信息,用自然语言组织成人类可读的文档。

11.3.3 实战:用 inspect 模块扫描代码结构#

Python 标准库的 inspect 模块是代码扫描的瑞士军刀。它能在运行时获取任何 Python 对象的详细信息:

使用示例:

# 扫描项目并输出结构化信息
modules = scan_project("./src")
for mod in modules:
    print(f"\n{'='*60}")
    print(f"模块: {mod.name}")
    print(f"文件: {mod.source_file}")
    print(f"文档: {mod.docstring[:100]}...")
    print(f"函数数量: {len(mod.functions)}")
    print(f"类数量: {len(mod.classes)}")
    for func in mod.functions:
        print(f"  - {func.name}{func.signature}")
    for cls in mod.classes:
        print(f"  + class {cls.name}({', '.join(cls.bases)})")
        for method in cls.methods:
            print(f"    - {method.name}{method.signature}")
python

输出示例:

11.3.4 用 AST 模块做更深度的扫描#

inspect 模块适合运行时扫描,但有时候你需要更细粒度的信息(比如函数体内的逻辑结构)。Python 的 ast(Abstract Syntax Tree,抽象语法树)模块可以直接解析 .py 源文件:

AST 扫描 vs inspect 扫描的区别:

维度inspect 模块ast 模块
工作方式运行时扫描(需要 import 模块)静态分析(只读源文件)
速度较慢(需要执行导入)极快(纯文本解析)
信息丰富度能获取运行时类型、默认值能获取代码结构、装饰器、注释
适用场景需要精确类型信息快速扫描大量文件、检查文档覆盖率
依赖要求需要所有 import 可用零依赖,纯 Python

实用建议:快速生成项目概览用 AST;生成精确的 API 文档用 inspect。

11.3.5 让 AI 基于扫描结果生成文档#

有了结构化数据,接下来就是 AI 发挥的时刻了。你需要把扫描结果喂给 AI,让它生成各种文档。

Prompt 模板:生成 README.md

Prompt 模板:生成 API 文档

Prompt 模板:生成用户操作手册

11.3.6 自动化流水线:扫描 + 生成 + 提交#

把整个过程自动化,加入到你的开发流程中:

使用流程:

# 第一步:扫描代码
python generate_docs.py --project ./src --output ./docs --format text

# 第二步:复制 docs/code_structure.txt 内容,粘贴给 AI
# 第三步:使用上面的 Prompt 模板,让 AI 生成 README.md / API 文档 / 操作手册
# 第四步:将 AI 生成的文档保存到 docs/ 目录
# 第五步:git commit + push(文档与代码一起版本管理)
bash

11.3.7 关键价值:文档与代码始终同步#

传统的文档流程:

写代码 → (手动) 写文档 → 代码迭代 → 文档过期 → (没人有时间更新) → 文档废弃
plaintext

AI 驱动的文档流程:

写代码 → 运行扫描脚本 → AI 生成文档 → 代码迭代 → 重新运行扫描 → AI 重新生成
plaintext

核心差异:文档生成的成本从”人工数小时”降低到”机器几秒钟”。当成本足够低时,你就能做到”每次代码变更都同步更新文档”——这是手动文档永远做不到的事情。


11.4 AI 协作指南#

11.4.1 让 AI 根据代码结构自动生成标准 API 文档#

前面的 11.3 节已经给出了详细的 Prompt 模板。这里补充几个实用技巧:

技巧1:分模块喂给 AI

当项目较大时,不要一次性把整个项目的扫描结果都丢给 AI——太长了,AI 可能遗漏细节。建议一个模块一个模块地喂:

# 不推荐:一次喂整个项目(可能几万字)
[粘贴 50 个模块的扫描结果]

# 推荐:一个模块一个模块来
[粘贴 protocol_parser 模块的扫描结果]
→ 生成该模块的 API 文档
→ 审查、修正
→ 继续下一个模块
plaintext

技巧2:让 AI 根据函数签名推断功能

当函数没有 docstring 时,AI 可以根据函数名、参数名、类型注解推断功能:

以下函数没有 docstring:

def parse_frame(frame: bytes) -> dict: ...
def build_frame(cmd: int, data: bytes, version: int = 1) -> bytes: ...
def validate_checksum(data: bytes, expected: int) -> bool: ...

请根据函数名和参数名,为每个函数推断功能描述,生成中文 docstring。
要求:
- 描述函数的输入、处理逻辑、输出
- 包含 Args、Returns、Raises 三部分
- 保持简洁(每个函数描述不超过 5 行)
plaintext

技巧3:Docstring 格式选择

嵌入式项目推荐使用 Google 风格的 docstring,因为它对 AI 和人类都最友好:

11.4.2 让 AI 编写 Webhook 报警模块的 Prompt#

11.4.3 让 AI 帮你写 Loguru 配置的 Prompt#

11.4.4 审查清单#

当 AI 生成代码后,你需要重点审查:

审查项关注点
Webhook URL 安全性不应硬编码在代码中,应从配置文件或环境变量读取
超时设置所有 HTTP 请求必须有超时,否则网络故障时程序会卡死
异常处理报警发送失败不应导致业务程序崩溃
限频机制必须有防报警风暴的限频策略,否则一个 bug 可能在 1 秒内发出 1000 条消息
日志切割路径确认工控机上有足够的磁盘空间存放日志
编码设置日志中的中文必须使用 UTF-8 编码
异步写入enqueue=True 确保日志写入不阻塞业务线程
日志敏感信息日志中不应出现密码、密钥等敏感信息

本章小结#

知识点解决的工程痛点
Webhook 机器人接入崩溃时无人知晓,错失最佳修复时间
AlertManager 统一封装一套代码支持钉钉/企微/飞书三种平台
分级报警策略避免报警风暴,重要信息不被淹没
logging + AlertLogHandler报警自动触发,业务代码零侵入
Loguru 日志切割防止日志撑爆工控机硬盘
Loguru 异步写入高频日志不阻塞业务线程
Loguru 异常捕获程序崩溃时完整记录现场
inspect 模块扫描精确获取函数签名和类型信息
AST 模块扫描快速扫描大量文件的代码结构
AI 生成文档文档成本从”人工数小时”降到”机器几秒”
扫描→生成→提交文档与代码始终同步,告别”文档过期”

一句话总结:报警让脚本”会喊救命”,日志归档让脚本”有记忆”,AI 文档生成让脚本”会自述”——三者合在一起,你的脚本就从一个”沉默的程序”变成了一个”有自我报告能力的工程化产品”。

下一章,我们将讨论最后一公里的工程化问题——如何把你的 Python 脚本打包成可交付的产品:Windows 下用 PyInstaller 打包为 .exe,Linux 工控机上用 Docker 容器化部署,以及如何优雅地处理打包过程中遇到的各种”幽灵错误”。


动手练习

  1. 在钉钉/企业微信/飞书中创建一个自定义机器人,用本章的代码成功发送一条测试消息
  2. 为你的项目配置 Loguru 日志,设置按天切割、保留 7 天、自动压缩
  3. 运行 code_scanner.py 扫描你当前的项目,把输出结果粘贴给 AI,让它生成一份 README.md
  4. 挑战题:把 AlertLogHandler 注册到你的项目中,让 logger.error() 自动触发钉钉报警
Comment seems to stuck. Try to refresh?✨