告警应让读者知道第一步该查什么,而不只是展示一个红色标记。它需要说明观察到的条件、受影响范围、持续时间、负责团队,以及接下来要看的证据。这些内容属于规则的可观察行为,应当和触发条件一起测试,不能等事故发生以后才把通知文案当作装饰补上。

把观察与解释分开

一次 scrape 失败,说明 Prometheus 没有成功收集目标的指标,并不能独立证明公共 API 已经不可用。服务可能仍能处理用户请求,但指标处理器失败;也可能指标采集正常,业务操作却一直报错。应该组合面向用户症状的探测与内部证据,并在告警里准确说明本次到底是哪一种观察触发,避免把采集失败直接写成业务停机。

最近发布是有价值的排查线索,却不是因果证明。证书到期、依赖故障、资源短缺或网络异常,都可能发生在没有部署的时刻。把相关变更记录和状态、日志、用户侧指标放在一起比较;在没有建立关联之前,不要把最近一次提交自动标记为根因。好的上下文帮助缩小范围,而不是提前替调查下结论。

保持身份稳定,让上下文可用

Prometheus 根据表达式返回的每组标签识别告警实例。服务、团队等稳定路由属性可以放进 labels;当前数值、解释文字和 runbook 链接放进 annotations。如果把不断变化的时间戳或测量值塞进标签,告警身份也会不断改变,pending 状态可能始终等不到持续时间结束。注解可以随情况变化提供解释,而不重新定义这是否还是同一条告警。

runbook 开头应给出可访问、只读的检查:先看目标的具体采集错误,再比较指标端点和用户请求路径,最后关联相关日志与变更记录。同时写清正常时应看到什么、看不到时进入哪个分支、什么时候升级求助。通知链接不要带凭据或敏感查询参数,还要实际确认值班读者有权访问目标页面。链接存在但读者无法打开,同样不能算有效的排查入口。

坏掉的样子

规则可能正确触发,却没有路由到任何接收人;也可能链接的文档已被删除,或者聚合时丢失了本应定位实例的标签。噪声阈值则会为不需要决策的短暂现象反复打断人。只有确实需要及时人工判断时才使用 page,其他情况应进入合适的看板、工单或自动处理。Alertmanager 的分组和抑制,与 Prometheus 判断规则是否 firing,是两段需要分别验证的流程。

可选的 for 要求表达式在连续评估中持续保持 active,达到指定时长后才进入 firing。它不是滑动平均窗口,也没有包含通知发送的等待时间。如果表达式不再产生该标签集合,本例规则就会停止活跃。尤其要注意,stale 或完全缺失的序列不是显式的零,因此告警安静不等于业务恢复;采集链路和预期目标清单需要独立监控,不能把“看不见”解释为健康。

快速验证

使用 Prometheus 发行包中的 promtool;下面实验已用 3.14.0 执行。在一个空的本地目录里,把接下来的两个代码块分别保存成 alerts.yml 和 alerts.test.yml。规则故意只描述指标采集可用性,不代表 API 可用性 SLO。severity 和 team 是测试标签,执行测试不会发送通知或改动任何现有告警配置。

groups:
  - name: api-scrape
    rules:
      - alert: APIScrapeUnavailable
        expr: up{job="api"} == 0
        for: 2m
        labels:
          severity: page
          team: platform
        annotations:
          summary: 'Cannot scrape {{ $labels.job }} on {{ $labels.instance }}'
          description: 'up == 0 for 2m. Inspect the target error and /metrics handler; scrape failure alone does not prove API downtime.'
          runbook_url: https://www.xbcatm.com/posts/a-purpose-built-health-check-is-worth-keeping/

测试使用一分钟间隔的人工样本。零分钟的失败先进入 pending,到第二分钟才 firing;第三分钟恢复后,后面再次失败仍须等待新的持续周期。预期注解不仅核对有没有告警,还核对展开后的目标名称、排查文字和 runbook URL,防止规则通过而通知内容已经失去用途。

rule_files:
  - alerts.yml
evaluation_interval: 1m
tests:
  - name: sustained failure, recovery, and recurrence
    interval: 1m
    input_series:
      - series: 'up{job="api",instance="api.example:9090"}'
        values: '0 0 0 1 0 0 0'
    alert_rule_test:
      - eval_time: 0m
        alertname: APIScrapeUnavailable
        exp_alerts: []
      - eval_time: 1m
        alertname: APIScrapeUnavailable
        exp_alerts: []
      - eval_time: 2m
        alertname: APIScrapeUnavailable
        exp_alerts: &firing
          - exp_labels:
              job: api
              instance: api.example:9090
              severity: page
              team: platform
            exp_annotations:
              summary: 'Cannot scrape api on api.example:9090'
              description: 'up == 0 for 2m. Inspect the target error and /metrics handler; scrape failure alone does not prove API downtime.'
              runbook_url: https://www.xbcatm.com/posts/a-purpose-built-health-check-is-worth-keeping/
      - eval_time: 3m
        alertname: APIScrapeUnavailable
        exp_alerts: []
      - eval_time: 5m
        alertname: APIScrapeUnavailable
        exp_alerts: []
      - eval_time: 6m
        alertname: APIScrapeUnavailable
        exp_alerts: *firing
    promql_expr_test:
      - expr: 'ALERTS{alertname="APIScrapeUnavailable",alertstate="pending"}'
        eval_time: 1m
        exp_samples:
          - labels: 'ALERTS{alertname="APIScrapeUnavailable",alertstate="pending",instance="api.example:9090",job="api",severity="page",team="platform"}'
            value: 1
  - name: transient failure does not fire
    interval: 1m
    input_series:
      - series: 'up{job="api",instance="api.example:9090"}'
        values: '1 0 1 1'
    alert_rule_test:
      - eval_time: 2m
        alertname: APIScrapeUnavailable
        exp_alerts: []
      - eval_time: 3m
        alertname: APIScrapeUnavailable
        exp_alerts: []
  - name: stale series is not an explicit zero
    interval: 1m
    input_series:
      - series: 'up{job="api",instance="api.example:9090"}'
        values: '0 stale _ _'
    alert_rule_test:
      - eval_time: 2m
        alertname: APIScrapeUnavailable
        exp_alerts: []
    promql_expr_test:
      - expr: 'ALERTS{alertname="APIScrapeUnavailable"}'
        eval_time: 2m
        exp_samples: []

在同一目录执行:

promtool --version
promtool check rules alerts.yml
promtool test rules alerts.test.yml

规则检查和单元测试都应报告 SUCCESS。用例覆盖持续失败、pending 状态、恢复、再次触发、短暂失败,以及在触发前变为 stale 的目标。stale 用例刻意证明检测盲区,而不是把缺失样本冒充健康。全部预期标签和注解都必须相符;例如实例标签丢失或 runbook 地址变化,测试都不应默默通过。

再做一次反例检查:只修改规则里的 runbook URL,不改测试期望,测试应当失败;恢复原值后重新通过。把 for 从两分钟改为一分钟,应让第一分钟的断言失败;更小的改动可能在这个评估间隔下不可见。这样的验证能证明规则评估和说明性上下文确实受检,但不证明真实 scrape、文档访问权限、Alertmanager 分组或呼叫设备送达。生产启用前还要沿这些真实链路分别演练,不能把本地 SUCCESS 当作整条值班链路已经验收。

延伸阅读

来源