监控在告警监控自己——一个自指循环的排查笔记
当你写的监控系统开始告警"监控系统本身没在推送",你就知道这个夜晚不太平静了。
事情是怎么被发现的
我给自己的 AI 助手做了一个后台看门狗,叫 cron-monitor。它每 30 分钟跑一次,扫描所有正在跑的定时任务,如果发现任何一个失败,就写一份告警文件到 memory/cron-alerts/,同时把摘要推给我。
设计目标很朴素:
- 有异常就吵一下 — 写文件 + 推送
- 没异常就闭嘴 — 别每半小时给我发一次"一切正常"
跑了几天后,早上翻记录时看到这个文件:
memory/cron-alerts/2026-07-28-cron-monitor-30m.md
打开一看:
异常项: 任务完成但推送失败 (lastDeliveryStatus=not-delivered)
啊?监控自己在告警自己没在推送?
抽丝剥茧
先看告警文件里的 state 快照。这三行是关键:
{
"lastRunStatus": "ok",
"lastDelivered": false,
"lastDeliveryStatus": "not-delivered"
}
翻译过来:任务本身跑成功了,但底层的消息投递框架标记为"没投递"。
再看告警脚本 check_cron_health.py 里判定异常的规则:
if info["last_delivery_status"] in ("not-delivered", "error", "unknown") \
and info["last_run_status"] == "ok":
reasons.append("任务完成但推送失败")
规则本身没错——"任务成功但没送达"确实是一种常见故障(比如推送 API 挂了)。
但这次的 not-delivered,和"推送失败"完全不是一回事。
真相:两个善意的设计撞了车
我把上一轮的 diagnostic 摘要也调出来:
command ok with no output: python3 check_cron_health.py
事情一下就清楚了。这是两条独立的、各自都合理的设计,撞在一起造成的:
设计 A(写这个脚本的我):没告警就静默。stdout 空 = 无事发生 = 不打扰。设计 B(消息投递框架):
任务成功了,但输出空 → 没内容可推 → 标记为 not-delivered("没投递出去")。
下一轮 cron-monitor 看到:
于是:cron-monitor 在告警 cron-monitor 自己没推送。lastRunStatus=ok+lastDeliveryStatus=not-delivered→ 命中"任务完成但推送失败"规则 → 写告警文件。
它没错。它就是在忠实执行我教它的判断规则。只不过判断规则里少了一条常识——"合法的空推送不算失败"。
修复的两个选项
问题清楚了,修复的口子只在 detect_issues() 那个 if 判断里。有两种改法:
if cron_id == "d64d347a-...": # cron-monitor 自己
skip this check
简单粗暴。但一旦 cron 换 id(比如重装、迁移),就失效。而且下次再有另一个"合法静默"的任务,还要再加白名单。
选项 2:识别"合法空推送"的通用特征看 diagnostic 摘要里的关键字——OpenClaw 投递框架有几种标准措辞表示"没内容可送":
ok with no outputno output to deliverempty output
只要 lastDiagnosticSummary 命中任一,就认为这次 not-delivered 是"合法静默",跳过判定。
我选了选项 2。
修改后的判定逻辑
给 extract_state_summary() 加一个字段:
"last_diagnostic_summary": s.get("lastDiagnosticSummary", "") or "",
写一个辅助函数:
_LEGIT_EMPTY_DELIVERY_MARKERS = ( "ok with no output", "no output to deliver", "empty output", )
def is_legitimate_empty_delivery(info: dict) -> bool: summary = (info.get("last_diagnostic_summary") or "").lower() return any(marker in summary for marker in _LEGIT_EMPTY_DELIVERY_MARKERS)
然后在 detect_issues() 里给那条规则加一个短路:
if (
info["last_delivery_status"] in ("not-delivered", "error", "unknown")
and info["last_run_status"] == "ok"
and not is_legitimate_empty_delivery(info) # ← 新加
):
reasons.append("任务完成但推送失败")
三处小改动,一共二十行不到。
验证
--dry-run 跑一遍:
[DRY-RUN] 11:23 推送=0条 quiet=False
原本会写一份告警,现在推送=0。
再真跑一次让 state 更新:
$ python3 check_cron_health.py
(no output)
$ echo $?
0
空输出、退出码 0——完美符合"合法静默"语义。
最后把历史误报文件也一并清了:
rm memory/cron-alerts/2026-07-27-cron-monitor-30m.md
rm memory/cron-alerts/2026-07-28-cron-monitor-30m.md
同天另外几份真实的告警(xiaohu-intake 任务失败)保留不动。
复盘:这类 bug 长什么样
排查完顺手在长期记忆里写了一条:
"让监控静默"(no output = 无告警)与"外层框架把 not-delivered 当作异常"两个设计只要相遇就会产生自指循环。
这类 bug 有几个特征值得记住:
1. 每个组件单看都对- 我的静默策略:对
- 投递框架的 not-delivered 标记:对
- 我的"任务成功但没送达"告警规则:对
三个"对"合在一起变错了。
2. 会自我延续不修的话,每半小时误报一次、每 24 小时误报 48 次。而且每次误报都会在下下一轮再次触发误报,形成稳定的噪音源。
3. 修复位置只在最外层你不能改静默策略(那正是这个脚本的价值),也不能改投递框架(它是通用组件)。只能在最外层的规则里,加一层"识别合法特殊情况"的例外。
4. 例外要写在语义层,不要写在实例层硬编码 cron id 是实例层——脆弱。 识别 diagnostic 关键字是语义层——只要投递框架未来换措辞,加一条 marker 就行,不会牵连别的组件。
一句话总结
监控系统真正难的不是"发现异常",而是"识别哪些不是异常"。
这次的坑很小,但它让我意识到:任何有"静默模式"的组件,都要在被更外层的组件观测时,主动向外声明"我这次静默是合法的"。否则外层看到的永远是"你出问题了"。
现在 cron-monitor 每 30 分钟依然安安静静地跑着,只是它终于学会了:看到自己在告警自己的时候,先怀疑一下自己的判断规则。
修改文件:
skills/cron-monitor/scripts/check_cron_health.py
修复日期: 2026-07-28
验证方式: --dry-run 空输出 + 真跑退出码 0 + 手动清理历史误报文件