Prometheus Alertmanager 电话告警:只为真正紧急的事响铃

Alertmanager 没有语音接收器。本文介绍如何只在 critical 级别时把 Prometheus 告警变成一通电话:webhook 配置、条件过滤,以及 Watchdog 陷阱。

更新于

目录

Alertmanager 没有语音接收器。想在 Prometheus 告警触发时接到电话,只需添加一个 webhook_configs 接收器,指向订阅类型设为来电铃声的 Echobell 频道。本文给出确切的 YAML 配置、阻止「已恢复」告警打电话的条件、按严重级别路由的方法,以及那个会让你的手机每四小时响一次、永不停歇的 Watchdog 告警。

Prometheus 是过去十年里绝大多数基础设施的默认指标方案,而 Alertmanager 在真正困难的部分上做得相当出色:告警去重、分组、维护期静默,以及在上游依赖故障时抑制下游噪音。

它唯一不会做的事,就是把人叫醒。

为什么 Alertmanager 自己打不了电话

Alertmanager 内置了邮件、Slack、PagerDuty、OpsGenie、Discord、Telegram、Pushover、Webex、MS Teams 等十几种接收器。但它们无一例外都是投递消息,而消息会被静音开关、勿扰模式和 iOS 专注模式拦下。到了凌晨三点,这意味着告警到了,却什么都没发生。

它没有 voice_configs。人们通常会落到这几个选项上:

  • PagerDuty / OpsGenie / Splunk On-Call —— 它们确实能打电话,但都是完整的事件管理平台,价格也按席位收。如果你需要排班和升级策略,它们是正确答案;如果你只是想让手机响起来,就太重了。(OpsGenie 正在逐步停止服务,这也是眼下这么多团队在重新评估这一层的原因。)
  • 短信桥接,比如 Sachet —— 你要多跑一个服务,按条向网关付费,而短信本质上仍是一条消息。在 iOS 上,除非发件人在允许列表里,短信并不会突破专注模式。
  • 基于 Twilio 自建 —— 写一个小的 webhook 接收服务、买一个号码、按通话付费,然后你就多了一块生产基础设施,而它唯一的职责是让手机响铃。

通用的 webhook 接收器才是那个出口。它会向任意 URL POST 一份有明确文档的 JSON,而这已经足够了。

你需要准备

  • 一套运行中的 Prometheus + Alertmanager,以及修改 alertmanager.yml 的权限
  • 安装好 Echobell(App Store / Google Play
  • 十分钟

本文基于 Alertmanager 0.31 撰写。webhook 载荷多年来一直是 version: "4",所以 0.2x 版本的行为完全一致。

你的 Alertmanager 需要能出站访问 hook.echobell.one,但不需要从公网可达——因此集群内、VPC 内或家庭实验室里的 Alertmanager 都没问题。

第一步 —— 创建一个会呼叫你的频道

在 Echobell 中创建一个频道,命名为 Prometheus Critical 之类。把它的订阅通知类型设为来电铃声。这个设置才是关键:来电类告警会以来电界面的形式出现,能穿透 iOS 专注模式和勿扰模式,而普通推送做不到。

把模板设置为直接读取 Alertmanager 的载荷:

Title: 🔴 {{commonLabels.alertname}} on {{commonLabels.instance}}
Body: {{commonAnnotations.summary}}
{{commonAnnotations.description}}

再在高级设置里配置一个链接模板,让通知记录直接跳转到对应的图表:

{{alerts[0].generatorURL}}

然后复制频道的 Webhook URL

https://hook.echobell.one/t/<channel-token>

把这个 URL 当作密钥对待——任何拿到它的人都能让你的手机响起来。

第二步 —— 添加 webhook 接收器

alertmanager.yml 中:

route:
  group_by: ["alertname", "cluster", "service"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: echobell-critical

receivers:
  - name: echobell-critical
    webhook_configs:
      - url: "https://hook.echobell.one/t/<channel-token>"
        send_resolved: false

curl -X POST http://localhost:9093/-/reloadSIGHUP 重载配置。

注意 send_resolved: falsewebhook 接收器的默认值是 true,这一点和 Alertmanager 的大多数其他接收器不同。所以如果不写这一行,服务故障时你的手机会响,服务自愈时还会再响一次。而第二通电话,正是让人开始忽略第一通的原因。第四步会讲如何保留恢复通知、但不带铃声。

第三步 —— 搞清楚到底收到了什么

Alertmanager 会先分组,然后每组 POST 一份载荷:

{
  "version": "4",
  "groupKey": "{}:{alertname=\"HighErrorRate\"}",
  "truncatedAlerts": 0,
  "status": "firing",
  "receiver": "echobell-critical",
  "groupLabels": { "alertname": "HighErrorRate" },
  "commonLabels": { "alertname": "HighErrorRate", "severity": "critical" },
  "commonAnnotations": { "summary": "Error rate above 5% for 10m" },
  "externalURL": "http://alertmanager.internal:9093",
  "alerts": [
    {
      "status": "firing",
      "labels": { "alertname": "HighErrorRate", "instance": "api-7d9f:8080" },
      "annotations": { "summary": "Error rate above 5% for 10m" },
      "startsAt": "2026-09-04T02:41:07.351Z",
      "endsAt": "0001-01-01T00:00:00Z",
      "generatorURL": "http://prometheus:9090/graph?g0.expr=...",
      "fingerprint": "a1b2c3d4e5f60718"
    }
  ]
}

Echobell 会原样读取 JSON 请求体,因此上面每一个字段都能在模板和条件中使用。嵌套访问两种写法都支持——{{commonLabels.severity}}{{alerts[0].labels["instance"]}}

这份载荷有两个特性决定了下面的一切:

只要组内有任意一条告警处于 firing,顶层 status 就是 firing 只有当组内所有告警都恢复后,它才变成 resolved。这让它成为一个非常干净的过滤依据。

commonLabels 只包含组内所有告警共有的标签。 这是最常见的意外。如果 group_by 足够宽泛,以致一次 webhook 携带了三个不同实例上的 HighErrorRate,那么 commonLabels.instance 就不存在,{{commonLabels.instance}} 会渲染成空字符串。后文有专门一节讲如何应对。

第四步 —— 把恢复通知降级为安静推送

你仍然想知道服务什么时候恢复了——只是不想为此接电话。再创建一个 Echobell 频道 Prometheus Recovered,通知类型设为普通,模板如下:

Title: ✅ {{commonLabels.alertname}} resolved
Body: {{commonAnnotations.summary}}

并在高级设置中填入这个条件

status == "resolved"

条件是在投递任何内容之前求值的表达式。如果表达式为假,Echobell 会接受这个请求,但什么都不发送。

然后让同一个接收器同时指向两个频道——一个接收器可以包含多个 webhook_configs

receivers:
  - name: echobell-critical
    webhook_configs:
      # 让手机响铃。只在 firing 时发送。
      - url: "https://hook.echobell.one/t/<calling-channel-token>"
        send_resolved: false
      # 安静推送。频道条件会丢弃 firing 的那一半。
      - url: "https://hook.echobell.one/t/<recovery-channel-token>"
        send_resolved: true

恢复频道会同时收到 firing 和 resolved 两种载荷,并丢弃 firing 的那些。结果就是:故障响铃,恢复变成一条你早上再看的推送。

第五步 —— 按严重级别路由,而不是全都发

把所有告警都发到来电频道的兜底路由,是一台批量生产「被忽略的电话」的机器。应该在 Alertmanager 里按 severity 分流——路由树本来就该待在那儿:

route:
  group_by: ["alertname", "cluster", "service"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: echobell-warning

  routes:
    # Watchdog 永远不该到达人类。见第六步。
    - matchers:
        - alertname = "Watchdog"
      receiver: "null"

    - matchers:
        - severity = "critical"
      receiver: echobell-critical
      group_wait: 10s
      repeat_interval: 1h

receivers:
  - name: "null"

  - name: echobell-critical
    webhook_configs:
      - url: "https://hook.echobell.one/t/<calling-channel-token>"
        send_resolved: false

  - name: echobell-warning
    webhook_configs:
      - url: "https://hook.echobell.one/t/<normal-channel-token>"
        send_resolved: true

路由自上而下求值,第一个匹配者胜出——continue 默认为 false。所以顺序很重要:Watchdog 那条路由必须放在任何可能吞掉它的规则之上。

如果你更希望只用一个频道、在 Echobell 侧过滤,等价的条件是:

status == "firing" && commonLabels.severity == "critical"

通常在 Alertmanager 里做更好,因为这样 severity 还能顺带驱动 group_waitrepeat_interval。而当你今天没法合并一个配置变更时,在 Echobell 里做更实际。

第六步 —— Watchdog 陷阱

如果你在用 kube-prometheus-stack,你就有一条名为 Watchdog 的告警,表达式是 vector(1)。它被设计成永远触发——它存在的意义,是让外部系统能察觉 Prometheus 本身已经停摆。默认配置会把它路由到一个 null 接收器。

一旦你把兜底路由指向来电频道却没有排除它,Watchdog 就会每隔一个 repeat_interval 给你打一次电话,从此不停,而且立刻开始。这是人们得出「电话告警根本没法用」这个结论的头号原因。

保留第五步里的 null 路由。然后,可选地,用它做一件真正有用的事:把 Watchdog 变成一个真正的死人开关(dead man's switch)。

    - matchers:
        - alertname = "Watchdog"
      receiver: deadmansswitch
      group_wait: 0s
      group_interval: 1m
      repeat_interval: 50s

receivers:
  - name: deadmansswitch
    webhook_configs:
      - url: "https://hc-ping.com/<your-check-uuid>"
        send_resolved: false

Echobell 本身不能充当死人开关——它在请求到达时告警,而不是在请求停止时告警。所以把 Watchdog 的心跳发给专门做静默检测的服务(Healthchecks.io、Cronitor、Dead Man's Snitch),再把那个服务的「检查失联」webhook 指向你的 Echobell 来电频道。这样一来,一通电话就意味着「监控系统本身死了」——这是你最希望被叫醒的那条告警,也是几乎没人配置的那条。

调参,让它不至于「狼来了」

Alertmanager 里有三个设置承担了大部分工作,Prometheus 里还有一个:

设置位置作用
for:告警规则条件必须持续多久才真正触发。这是抵御两秒抖动的第一道防线。
group_wait路由首次通知前等待更多告警的时间。默认 30s;critical 可降到 10s
group_interval路由就已有分组中的告警再次通知前的最小间隔。默认 5m。
repeat_interval路由未恢复的告警多久重复通知一次。默认 4h——所以一次夜间故障会在凌晨三点打给你,七点再打一次。

repeat_interval 是最值得琢磨的一个。四小时对一个坏掉的东西来说太长了;二十分钟则是一台专门让你关掉这个频道的机器。critical 用一小时是个合理的起点。

如果你希望未接来电立刻重试、而不是等到下一个 repeat_interval,请在 Echobell 的应用设置中开启重试失败的通话

只在非工作时间响铃

工作时间里你多半已经盯着仪表盘了。Echobell 的系统时间变量(全部为 UTC)让一个频道能按小时表现不同,而不需要在 Alertmanager 里再加一条路由:

status == "firing" && (hour >= 17 || hour < 9)

这样只会在 UTC 09:00–17:00 之外呼叫你。再把一个普通类型的频道指向相反的时段,用于白天的推送:

status == "firing" && hour >= 9 && hour < 17

加上 dayOfWeek >= 1 && dayOfWeek <= 5,就能把周末也算作非工作时间。记住这些值始终按 UTC 计算——请按你自己的时区做偏移。用 UTC 条件实现时间窗通知里有更完整的讲解。

处理 commonLabels 为空的问题

当一个分组包含来自多个实例的告警时,commonLabels.instance 会消失,你的通知标题就变成了 🔴 HighErrorRate on

三种解法,按推荐程度排序:

  1. 把该标签放进 group_by 如果 group_by 包含 instance,那么同组内每条告警都共享它,commonLabels.instance 就一定存在。代价是通知变多——从每个告警名一条变成每个实例一条。
  2. 改读第一条告警。 {{alerts[0].labels.instance}} 一定有值。但它只是可能存在的多条中的一条,所以配上一个计数:{{alerts[0].labels.instance}}(共 {{alerts.length}} 条)
  3. 把标签设计成即使为空也读得通。 Echobell 没有默认值运算符——{{a || "unknown"}} 会渲染出字面量 true,而不是回退值——所以把 Instance: {{commonLabels.instance}} 单独写成一行,这样空值一眼就是空值,而不是一句破碎的话。

控制载荷大小

一个覆盖上百个 Pod 的分组会产生很大的 JSON 请求体,而 Echobell 会以 HTTP 413 拒绝超过 1 MiB 的触发请求。在 Alertmanager 里限制它:

      - url: "https://hook.echobell.one/t/<channel-token>"
        send_resolved: false
        max_alerts: 20

Alertmanager 随后最多发送 20 条告警,并把被丢弃的数量写入 truncatedAlerts,你可以把它显示在正文里:

Body: {{commonAnnotations.summary}}
Alerts: {{alerts.length}}(另有 {{truncatedAlerts}} 条被截断)

与团队共享告警

Echobell 频道可以通过订阅链接分享,每位订阅者各自选择通知类型。因此同一条路由既能呼叫值班工程师,又能以普通推送落到其他所有人那里——不按席位收费,Alertmanager 里也不用加额外的路由规则。

这也契合许多团队一开始自托管 Prometheus 的理由:指标和告警规则留在你自己的基础设施上,而 Echobell 把通知内容和历史记录保存在设备上,而非它的服务器上。

这套方案不提供什么

把边界讲清楚,能让你以后少做一次糟糕的迁移。Echobell 是投递层,不是事件管理平台。它没有:

  • 值班排班表或跨时区交接
  • 第一个人未接听时自动呼叫第二个人的升级策略
  • 事件时间线、确认追踪或事后复盘工具

如果你的团队需要这些,那你需要的是 PagerDuty、Grafana Cloud IRM 之类的产品。这套方案覆盖的是 Alertmanager 留下的那个具体缺口:把一条触发的告警变成一部真正响起来的手机。对独立运维者、小团队和家庭实验室来说,这通常就是全部需求。

排查

完全收不到东西。 先看 Alertmanager 自己的日志(level=error component=dispatcher),再确认路由确实落到了你的接收器上——amtool config routes test severity=critical alertname=HighErrorRate 能在不等真实告警的情况下告诉你某条告警会进入哪个接收器。

Echobell 返回 HTTP 404。 频道 token 错误,或频道已被删除。未知 token 返回的是 404,而不是静默成功。

Echobell 返回 200,且 "notificationTriggered": false 你的条件求值为假。响应体里还带着 "conditionsMet": false,这是区分「我的条件写错了」和「我的 webhook 根本没到」最快的方式。请对照 Alertmanager 实际发送的内容检查 status == "firing"——注意是顶层 status,不是 alerts[0].status

HTTP 413。 载荷超过 1 MiB。按上文设置 max_alerts

HTTP 405。 频道启用了 POST Only,而某个来源发的是 GET。Alertmanager 用的是 POST,所以这通常意味着你在浏览器里试了这个 URL。

标题中间空了一块。 该分组的 commonLabels 里没有这个标签。见上文对应小节。

通知到了,但不响铃。 该订阅的通知类型是普通或时效性,而不是来电铃声。通知类型是每位订阅者各自选择的,所以请在那台不响的设备上检查。

在不影响生产的前提下测试。 添加一条 expr: vector(1)、使用独特 alertname、带 severity: critical 的规则,让它触发一次后删掉。或者手动发一条:

curl -X POST http://localhost:9093/api/v2/alerts -H 'Content-Type: application/json' -d '[
  {"labels":{"alertname":"EchobellTest","severity":"critical"},
   "annotations":{"summary":"Testing the phone call path"}}
]'

常见问题

Prometheus Alertmanager 原生能打电话吗?

不能。Alertmanager 有邮件、Slack、PagerDuty、OpsGenie 等许多接收器,但没有语音或短信接收器。要实现电话告警,需要把通用 webhook 接收器路由到一个能拨打电话的服务(比如 Echobell),或者付费使用事件管理平台。

电话告警能绕过勿扰模式吗?

可以。Echobell 的来电铃声通知类型以来电形式呈现,能穿透 iOS 专注模式和勿扰模式。细节和相关设置见如何让关键告警绕过 iOS 专注模式

防火墙后面或 Kubernetes 里的 Alertmanager 能用吗?

可以。这个 webhook 是从 Alertmanager 发出的出站 HTTPS 请求,只需要能访问 hook.echobell.one。你的 Alertmanager 不需要公网地址,也不需要 Ingress。

怎样让告警恢复时不再打电话?

在指向来电频道的那个 webhook 配置上设置 send_resolved: false。webhook 接收器的默认值是 true,这一点与 Alertmanager 的大多数其他接收器不同,所以这是「选择退出」而非「选择加入」。若仍想安静地收到恢复通知,可再建一个频道,条件设为 status == "resolved"

为什么同一条告警每四小时就让我的手机响一次?

那是 repeat_interval,默认值 4h。Alertmanager 会按这个节奏对仍在触发的告警重复通知。请按路由分别设置——critical 用 1h 是常见选择。如果这些电话是在你加了兜底路由之后立刻开始的,那罪魁祸首更可能是永远触发的 Watchdog 告警,见第六步。

同一条告警能呼叫多个人吗?

可以。把频道分享给同事,每位订阅者选择自己的通知类型。所有订阅了来电频道的人都会被呼叫,且不按席位收费。

严重级别过滤应该放在 Alertmanager 还是 Echobell 条件里?

优先放在 Alertmanager:在那里路由还能让你为不同 severity 设置各自的 group_waitrepeat_interval,而且路由树会和其余配置一起纳入版本控制。当你改不了 Alertmanager 配置时,或者要做 Alertmanager 没有对应概念的过滤(比如按一天中的时段)时,再用 Echobell 条件。

小结

整套配置就是一个接收器、一行 send_resolved: false,以及一棵把 severity: critical 以外的一切都挡在来电频道之外的路由树。它完全不改动你现有的告警规则、分组、静默和抑制配置,却补上了「Prometheus 发现了」和「有人发现了」之间的那道缝。

在 iPhone 上下载 Echobell在 Google Play 获取,然后先发一次上面那条 EchobellTest 告警,再把任何真正重要的事情托付给这条链路。


相关内容