归档(wecom-public-health-alert-diagnostics-and-debounce): 完成公网健康告警优化

这个提交包含在:
2026-08-23 11:03:22 +08:00
父节点 a0f0483514
当前提交 42bbbdae22
共修改 6 个文件,包含 3 行新增和 3 行删除
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,106 @@
# 企微公网健康告警防抖与诊断增强设计
## 1. Architecture and Boundaries
本任务保持现有“ARM systemd timer → 公网探测器 → 脱敏 summary → 共用企微 helper”的边界,不引入守护进程、数据库或外部状态服务。
- 探测器负责网络阶段分类、连续样本计数、恢复确认和抖动周期判定。
- 告警 helper 继续负责 summary 校验、消息文案、Webhook 安全读取和发送侧去重。
- 安装脚本继续固定 5 分钟 timer、受控路径、root 所有权和默认 disabled。
- 本地合同测试通过 stub、受控 socket/DNS 失败和测试 capture 验证,不连接真实机器人。
## 2. Failure Classification Contract
分类优先级固定为:
1. `socket.gaierror` 或等价解析失败 → `dns_resolution_failed`;
2. TCP 建连拒绝、重置、不可达及其他建连类错误 → `tcp_connection_failed`;
3. TLS 证书或握手校验失败 → `tls_validation_failed`;
4. socket/请求超时 → `request_timeout`;
5. 非 2xx HTTP → `http_unavailable`;
6. 响应超过上限 → `response_too_large`;
7. JSON、编码或 `{data: {ok: true}}` 合同不满足 → `response_contract_invalid`。
异常类型按从具体到通用的顺序匹配,避免 `OSError` 提前吞掉 DNS、TLS 或超时。对无法进一步安全细分的连接阶段异常归入 `tcp_connection_failed`,但不把异常文本写入状态、summary 或日志。
## 3. State Model
### 3.1 State schema
探测状态升级为显式版本,字段只包含:
- `phase`:`healthy`、`failure_pending`、`alerted`、`recovery_pending`、`recovery_watch`、`flapping`;
- `activeCategory` / `lastRecoveredCategory`:稳定类别 token 或 `null`;
- `consecutiveFailures` / `consecutiveSuccesses` / `flapTransitions`:有界非负整数;
- `recoveredAt` / `healthySince`:必要时存在的非负 Unix 秒时间戳。
旧版 schema 只含连续失败数和最后类别。部署时采用保守兼容迁移:有效旧状态映射为等价的 `healthy` 或 `failure_pending`,不凭旧状态补发消息;非法或未知 schema 继续 fail closed。告警 helper 自身状态 schema 保持向后兼容。
### 3.2 Normal incident flow
```text
healthy --失败1--> failure_pending --失败2--> alerted(发送公网健康告警)
alerted --成功1--> recovery_pending --成功2--> recovery_watch(发送公网健康恢复)
recovery_pending --失败--> alerted(不发送恢复;持续事件仍去重)
```
失败阈值统计持续不可用样本,告警使用达到阈值时的最新稳定类别。已告警期间相同类别不重复发送;类别发生变化时沿用现有最多一次更新通知语义。
### 3.3 Recovery watch and flapping flow
```text
recovery_watch
--60分钟内同类失败达到2次--> flapping(发送一次链路反复抖动提醒)
--不同类别失败达到2次-----> alerted(作为新事件发送普通告警)
--观察窗到期且健康--------> healthy(静默)
flapping
--任意失败/短时成功-------> flapping(只更新脱敏计数)
--连续健康满30分钟--------> healthy(静默退出)
```
“连续健康满 30 分钟”以第一条连续成功样本的时间为起点,期间任何失败都会清空 `healthySince`。状态转换实现为可直接传入当前时间的纯函数,使合同测试可使用合成时间,不依赖等待真实 30/60 分钟,也不增加生产测试参数。
## 4. Notification Contract
应用健康事件使用专用标题:
- `FAIL_APP_HEALTH`:`OA 公网健康告警`;
- `PASS_APP_HEALTH_READY`:`OA 公网健康恢复`;
- `FAIL_APP_HEALTH_FLAPPING`:`OA 公网健康链路反复抖动`。
正文只保留状态、稳定类别和操作提示。helper 根据应用健康状态选择专用文案,其他备份/dead-man/提醒状态继续使用原有通用标题。抖动事件采用独立、白名单内的稳定状态 token,防止与普通失败去重键互相覆盖。
状态转换返回“通知候选”而不是自行发送:处于普通故障、已确认恢复或抖动故障时,可在后续同类样本上重复交给 helper 评估,由 helper 的现有状态完成去重;如果上次发送失败且 helper 状态未落盘,下一次样本可以重试。抖动期的健康样本和静默退出不产生恢复候选,由此保证抖动退出不产生额外恢复消息。
## 5. Data Flow and Failure Handling
1. 校验 URL、状态目录、状态文件及阈值。
2. 读取并严格校验/迁移探测状态。
3. 执行一次 `/ready` 探测并生成成功或稳定失败类别。
4. 用当前时间和样本驱动纯状态转换,得到新状态及可选通知候选。
5. 原子写入 `0600` 探测状态;有通知候选时才写脱敏 summary 并调用 helper,由 helper 去重或重试。
6. 探测失败仍返回非零,健康样本返回零;通知发送或本地状态失败继续 fail closed。
状态先落盘再通知,保持当前“状态不可安全保存则不发送误导消息”的边界。若通知失败,命令返回失败并保留可诊断的稳定错误;后续同类样本再次提交候选,helper 仅在自身发送状态尚未落盘时重试,合同测试必须证明成功发送后不会重复。
## 6. Compatibility and Trade-offs
- 2 次恢复确认会把恢复消息延后约 5 分钟,这是减少短时成功噪声的明确代价。
- 同类抖动期不逐次通知,会降低每次瞬断的可见性;本地计数和最终稳定退出状态保留诊断能力。
- 不同类别复发仍作为新事件处理,消息可能多于“全部静默”,但避免把新根因错误吞掉。
- 不修改 `/ready` 合同、timer 周期、Webhook 文件、summary 隐私边界或非应用健康告警语义。
## 7. Rollout and Rollback
- 本地阶段仅提交代码、合同测试和必要文档,不触碰 ARM/DNS/真实企微。
- rollout 获批后先以 `install` 模式部署 disabled unit,核对文件权限、unit 内容、旧状态迁移和只读探测,再单独批准 `enable`。
- 启用后先观察分类计数和消息频率;DNS 调整只以实际 `dns_resolution_failed` 证据为依据。
- 回滚时停用 timer,恢复上一版探测器/helper/unit;保留并备份脱敏状态。若旧版不能读取新 schema,则在明确目标文件后将新状态移出状态目录,由旧版建立健康基线,避免伪造恢复通知。
## 8. Security and Privacy
- 禁止写入 URL、域名、IP、文件路径、异常文本、响应 payload、Webhook 或 Secret。
- capture 仅在既有显式测试模式下可用,测试输出权限保持 `0600`。
- 所有状态枚举和类别经过白名单/token 校验;计数与时间戳必须有类型和范围检查。
- 生产、企业微信和 DNS 写入继续受独立批准门禁控制。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,69 @@
# 企微公网健康告警防抖与诊断增强实施计划
## Preconditions
- 用户审核并明确批准本规划后,才运行 `task.py start`。
- Phase 2 开始前加载 `trellis-before-dev`,读取 operations 相关 spec、当前任务三份规划材料及引用的运维事实源。
- 本任务采用 Codex inline 实施,不整理或依赖 `implement.jsonl` / `check.jsonl`。
- 保持现有报销任务未提交改动不动,不纳入本任务 diff、校验或提交。
## Ordered Implementation Checklist
1. **探测分类**
- 在 `scripts/check-public-app-health.py` 按具体异常优先级拆分 DNS 与 TCP 类别。
- 保持 TLS、超时、HTTP、响应过大和响应合同分类稳定,日志只打印 token。
2. **状态 schema 与纯状态转换**
- 引入显式新版探测状态、严格校验、有界计数和旧 schema 保守迁移。
- 将“样本 + 当前时间 → 新状态 + 可选通知候选”提取为纯逻辑。
- 实现 2 次失败、2 次成功、60 分钟同类复发提醒、抖动期静默计数和连续健康 30 分钟静默退出。
3. **专用消息文案与兼容**
- 为应用健康普通告警、恢复和抖动事件增加明确的“OA 公网健康”标题。
- 扩展 helper 白名单及发送判断,但保持备份、dead-man 和其他既有状态文案/去重行为不变。
4. **合同测试**
- 扩充 `scripts/qa/run-app-health-monitor-contract.sh`:分别模拟 DNS、TCP、TLS、超时、HTTP、格式、过大响应。
- 使用纯状态转换或合成时间覆盖全部正常、恢复确认、同类抖动、不同类别复发、30/60 分钟边界和旧状态迁移。
- 断言消息数量、顺序、专用标题、静默退出及敏感内容禁入。
- 回归损坏状态 fail-closed、文件权限、固定路径、默认 disabled 和 5 分钟 timer。
5. **共享合同与事实源**
- 运行自动备份合同,证明共用 helper 无回归。
- 仅在形成稳定可复用规则时,更新既有 `.trellis/spec/hyb-oa/` 或 `docs/试运行与自动化验证手册.md`;不新建无必要文档。
6. **本地质量门禁**
- 逐项运行下列验证,限定敏感扫描和 diff 审核在本任务相关文件。
- 对权限/状态机变更完成主会话只读复核;inline 模式不派发 implement/check 子代理。
7. **提交与交付**
- 只暂存当前 Trellis task 和本任务实现/测试/必要事实源文件。
- 使用中文 commit message,并引用 `wecom-public-health-alert-diagnostics-and-debounce`。
- 本地完成后仅报告“本地验证通过”;未获 ARM 部署与公网检查批准前,不告知用户可在企业微信测试。
## Validation Commands
```bash
python3 -m py_compile scripts/check-public-app-health.py scripts/send-wecom-group-alert.py
bash scripts/qa/run-app-health-monitor-contract.sh
bash scripts/qa/run-automatic-backup-contract.sh
bash -n deploy/trialrun/install-offhost-app-health-monitor.sh
node scripts/qa/check-process-contract.mjs --baseline --json
node scripts/qa/audit-docs.mjs --check-registry
node scripts/qa/check-trellis-governance.mjs
python3 ./.trellis/scripts/task.py validate 08-23-wecom-public-health-alert-diagnostics-and-debounce
git diff --check
```
敏感扫描采用稳定关键词和本任务文件清单,不打印潜在 Secret 值或原始 payload。
## Risky Files and Rollback Points
- `scripts/check-public-app-health.py`:状态迁移和通知时机会改变;每一步先以合同测试锁定消息数量,再进入下一步。
- `scripts/send-wecom-group-alert.py`:被备份/dead-man 共用;任何改动必须先通过应用健康合同,再通过完整自动备份合同。
- `deploy/trialrun/install-offhost-app-health-monitor.sh`:若无需新增生产参数则尽量不改;如需改动,必须保持默认 disabled 和固定安全目标。
- 状态 schema:必须支持有效旧状态保守迁移;回滚前先停 timer,并只对已解析的精确状态文件执行保留/移出操作。
## External Approval Gates
以下动作均不属于本地实施批准,执行前必须分别再次获得明确授权:
1. ARM 文件、systemd 或生产配置写入;
2. 启用/重启公网健康 timer;
3. 向真实企业微信群发送测试或生产消息;
4. 修改 DNS、resolver 或任何网络策略;
5. 写入真实业务数据或执行恢复操作。
@@ -0,0 +1,92 @@
# 企微公网健康告警防抖与诊断增强
## Goal
让 ARM 公网健康探测准确区分 DNS、TCP、TLS、HTTP 和响应合同故障,以明确来源的企业微信文案报告真实事件,并在链路反复抖动时减少无意义的“告警—恢复”消息,同时保持约 10 分钟量级的持续 OA 不可用发现能力。
## Background and Confirmed Facts
- 上一排查任务已确认群内反复消息来自 ARM 对公网 `/ready` 的健康探测,不是备份失败或数据恢复;生产自动备份和新鲜度 watchdog 近 14 天均为 14/14 成功、0 条失败。
- 实施前基线 `986668a58762` 每 5 分钟探测,连续 2 次失败即告警、1 次成功即恢复;基线实现见该提交的 `scripts/check-public-app-health.py:220-279` 和 `deploy/trialrun/install-offhost-app-health-monitor.sh:94-113`。
- 实施前基线 `scripts/check-public-app-health.py:131-170` 将 DNS 与 TCP 故障合并为 `dns_or_connect_failed`。
- 实施前基线 `scripts/send-wecom-group-alert.py:133-140` 为所有健康恢复统一使用“内部 OA 运维恢复通知”,接收人无法直接判断这是公网健康监控。
- 当前 ARM 健康。单一静态 DNS 是可疑单点,但现有证据不足以断言历史故障均由 DNS 导致,因此本任务先增强诊断和防抖,不直接修改 DNS。
## Requirements
### R1 — 故障阶段分类
- 将 `dns_or_connect_failed` 至少拆分为 `dns_resolution_failed` 与 `tcp_connection_failed`。
- 保留并稳定区分 `tls_validation_failed`、`request_timeout`、`http_unavailable`、`response_contract_invalid` 和 `response_too_large`。
- 分类只记录稳定类别,不记录解析结果、目标地址、异常原文、响应正文或请求 payload。
### R2 — 公网健康专用文案
- `FAIL_APP_HEALTH`、`PASS_APP_HEALTH_READY` 和新增的 `FAIL_APP_HEALTH_FLAPPING` 抖动事件必须明确显示“OA 公网健康”。
- 恢复文案不得使用可能被理解为数据库、附件或备份恢复的泛化标题。
- 自动备份、异机 dead-man、季度提醒等共用告警 helper 的既有标题、发送条件和脱敏合同保持兼容。
### R3 — 均衡防抖状态机
- 保持 5 分钟探测频率和连续 2 次失败告警,持续不可用仍在约 10 分钟量级发送一次告警。
- 已发送故障告警后,必须连续 2 次成功才发送一次恢复;单次短暂成功只进入恢复待确认状态,不发送恢复。
- 正常恢复后的 60 分钟观察窗内,同一故障类别再次连续达到失败阈值时,只发送一次“OA 公网健康链路反复抖动”提醒并进入抖动状态。
- 抖动状态内后续失败、成功及同类切换只更新本地脱敏计数,不逐次发送告警或恢复。
- 抖动状态在连续健康满 30 分钟后静默退出;退出本身不另发消息。之后的新故障按新的正常周期处理。
- 恢复观察窗内出现不同故障类别时,按新的故障事件执行连续 2 次失败门槛,避免隐藏不同根因;同一持续故障期间类别变化仍按既有“类别变化最多通知一次”的兼容行为处理。
### R4 — 状态与隐私边界
- 本地状态仅允许保存 schema 版本、稳定状态/类别、连续成功或失败计数、必要的 Unix 时间戳和抖动计数。
- 状态文件继续使用原子写入、`0600` 权限和严格 schema 校验;损坏或越界状态 fail closed,并只输出稳定脱敏错误类别。
- 不保存主机名、IP、URL、路径、异常原文、Webhook、Secret 或原始响应;测试捕获不得访问真实企业微信。
### R5 — 发布门禁
- 本地实现和合同验证不授权 ARM 写入、systemd 变更、DNS 修改或真实企业微信消息测试。
- 后续 ARM rollout 必须再次取得明确批准,并按“disabled 安装 → 只读检查 → 受控启用”执行;DNS 调整必须依据增强分类后的观测证据另行决策。
## Scope
- `scripts/check-public-app-health.py` 的故障分类、恢复确认、抖动状态机和脱敏状态 schema。
- `scripts/send-wecom-group-alert.py` 的公网健康专用文案及共享行为兼容。
- `scripts/qa/run-app-health-monitor-contract.sh`、`scripts/qa/run-automatic-backup-contract.sh` 和 ARM 安装合同的相关回归。
- 必要的既有运维事实源更新,以及后续 ARM rollout/rollback 操作设计。
## Out of Scope
- 规划阶段修改产品代码、ARM 配置、systemd 或 DNS。
- 自动重启 OA、切换流量、数据库修复或任何数据恢复。
- 部署 Prometheus/Alertmanager 或引入新的消息供应商。
- 未取得证据前指定公共 DNS、覆盖主机网络策略或停用健康监控。
- 在本任务内执行生产部署、真实企微消息发送或真实数据写入。
## Acceptance Criteria
- [x] **AC1 / R1**:无外网依赖的合同测试可稳定区分 DNS、TCP、TLS、超时、HTTP、响应格式及响应过大故障。
- [x] **AC2 / R2**:应用健康告警、恢复及抖动提醒的标题均明确包含“OA 公网健康”,测试证明消息不含地址、路径、异常原文或 payload。
- [x] **AC3 / R3**:5 分钟频率与 2 次失败阈值保持不变;持续故障只发送一次告警,相同故障持续期间不重复发送。
- [x] **AC4 / R3**:状态机测试证明 1 次成功不恢复、2 次连续成功只恢复一次;恢复后 60 分钟内同类复发只提醒一次,抖动期切换静默计数,连续健康 30 分钟后静默退出。
- [x] **AC5 / R3**:恢复观察窗内的不同类别故障不会被同类抖动规则吞掉,并仍经过连续 2 次失败门槛。
- [x] **AC6 / R4**:本地状态 schema、权限、原子写入、损坏状态 fail-closed 和敏感字段禁入均有自动化覆盖。
- [x] **AC7 / R2**:自动备份、异机 dead-man 和既有共享 helper 合同全部通过,非应用健康消息行为无回归。
- [x] **AC8 / R5**:安装合同保持默认 disabled、5 分钟周期、固定安全目标及独立启用门禁;本地验证不产生真实企微或远端写入。
## Test Plan
- `python3 -m py_compile scripts/check-public-app-health.py scripts/send-wecom-group-alert.py`
- `bash scripts/qa/run-app-health-monitor-contract.sh`
- `bash scripts/qa/run-automatic-backup-contract.sh`
- `bash -n deploy/trialrun/install-offhost-app-health-monitor.sh`
- `node scripts/qa/check-process-contract.mjs --baseline --json`
- `node scripts/qa/audit-docs.mjs --check-registry`
- `node scripts/qa/check-trellis-governance.mjs`
- 针对本任务变更执行敏感信息扫描和 `git diff --check`。
- ARM 写入获批后才执行 disabled 安装、只读状态检查、受控启用和回滚验证;真实消息测试与 DNS 修改仍需分别批准。
## Local Verification Result
- `bash scripts/qa/run-app-health-monitor-contract.sh`:PASS,36 项检查。
- `bash scripts/qa/run-automatic-backup-contract.sh`:PASS,74 项检查,共享备份/dead-man 标题和去重无回归。
- Python/Shell 语法、process baseline、文档注册审计、敏感扫描、Trellis governance、task validate 和 `git diff --check` 均通过;文档审计仅保留既有的未登记文件 warning。
- 当前结论为 `LOCAL_FIX_VERIFIED`;ARM 部署、真实群消息、DNS 调整和外部行为验证仍为 `EXTERNAL_GATED`。
@@ -0,0 +1,33 @@
{
"id": "wecom-public-health-alert-diagnostics-and-debounce",
"name": "wecom-public-health-alert-diagnostics-and-debounce",
"title": "企微公网健康告警防抖与诊断增强",
"description": "细分 ARM 公网健康探测故障阶段,优化企微告警来源文案与防抖状态机,并为后续 DNS 调整建立证据门槛。",
"status": "completed",
"dev_type": null,
"scope": "operations",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-08-23",
"completedAt": "2026-08-23",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"scripts/check-public-app-health.py",
"scripts/send-wecom-group-alert.py",
"scripts/qa/run-app-health-monitor-contract.sh",
"scripts/qa/run-automatic-backup-contract.sh",
".trellis/spec/hyb-oa/deployment-trialrun.md",
"docs/试运行与自动化验证手册.md"
],
"notes": "本地实现与合同验证完成;ARM 部署、真实企微消息、DNS 调整和外部行为验证仍需独立批准。",
"meta": {}
}