
你有没有遇到过这种情况Prometheus 的监控规则配了一大堆服务器 CPU 飙到 95%、接口错误率直线上升可你是在第二天上班打开 Grafana 看曲线时才发现的。告警确实触发了也写了邮件通知但邮件躺在收件箱里根本没人看。后来我把告警全部接到了企业微信群里手机一路狂奔那种体验才真正结束。这篇文章就讲清楚一件事如何把 Prometheus 的告警稳定地推到企业微信。我会把整条链路拆开讲——从 Alertmanager 配置、企业微信机器人申请、到自建 Webhook 接收器、再到告警模板和避坑经验。适合正在用 Prometheus、想给团队配一套靠谱告警通知的运维、SRE 和开发同学。内容不绕弯子都是可以直接复制的方案。1. 为什么告警必须“绕一圈”才能到企业微信1.1 先看清 Prometheus 告警的完整走向很多刚接触 Prometheus 的人以为配置了告警规则就能直接把消息推到企业微信。实际上标准链路分三段Prometheus 根据 rules 文件里的阈值表达式做周期评估状态进入 firing 后把告警推给 AlertmanagerAlertmanager 负责聚合、去重、路由和静默最后由 Alertmanager 调用 receiver也就是通知渠道把消息发出去。企业微信在这里只是最末端的一个 receiver真正干活的枢纽是 Alertmanager。那能不能让 Prometheus 绕过 Alertmanager直接在 rule 里配 webhook技术上可行Prometheus 的 alerting 配置里确实支持 webhook_configs 直接转发但我不建议在生产环境这么干。原因很实际你失去了 Alertmanager 的**分组group**能力。举个例子一台机器挂了上面跑了十几个 service每个 service 都有告警规则一小时内会连续触发十几条告警。如果直接发群里马上被刷屏。而 Alertmanager 可以把相同标签的告警合并成一条等 30 秒再统一发出去效果完全不一样。1.2 企业微信在告警链路里的角色企业微信在这条链路里扮演的角色非常纯粹触达。它不像邮件那样容易石沉大海也不像短信那样要钱更关键的是大多数团队本来就用它做内部沟通告警推到群里天然有人看。我见过不少团队把邮件、短信、企业微信同时配上结果还是漏告警。问题往往不在渠道而在路由和重复通知。企业微信的价值是让你在手机端快速看到、快速处理但前提是 Alertmanager 把消息组织得足够干净。所以这篇文章的重心其实不是在“如何发一条消息”而是围绕着“如何稳定、有节制地发消息”。2. 企业微信机器人准备三种消息模式与安全设置2.1 创建群聊和机器人拿 Webhook 地址第一步很简单在企业微信里拉一个群建议是专门的告警群不要拉大群然后在群设置里找到“群机器人”点“添加机器人”。创建完成后会得到一个 Webhook 地址长这样https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key你的密钥这个 key 就是你的群机器人身份凭证。任何人拿到这个地址都能往群里发消息所以它本质上是敏感信息。我见过有人把 Webhook 地址写进代码仓库还带注释结果被爬虫扫到群里被塞了一堆垃圾消息。正确的做法是存到配置系统或者环境变量里别提交到明文的 git 仓库。群机器人创建时还可以设置头像和名称推荐把名称改成类似“生产环境告警机器人”这种消息里再带集群名避免多个环境搞混。2.2 三种消息模式怎么选企业微信机器人支持三种消息类型text、markdown、news。我直接说结论告警通知用 markdown少用 textnews 基本不用。text 是指文本消息发送格式简单但显示效果比较朴素纯文本一坨。markdown 可以加标题、加粗、引用、换行还能标颜色在官方支持的语法范围内信息层级清楚很多。news 是图文卡片形式可以跳链接但一条告警做一张卡片的操作成本太高而且消息一多就刷屏不适合批量告警推送。三种类型的发送 JSON 结构不同markdown 长这样{ msgtype: markdown, markdown: { content: ## 生产环境告警\nCPU使用率超过90% } }2.3 安全设置关键词、IP 白名单、加签企业微信机器人有三个安全设置项每个都有坑我逐个说。自定义关键词。机器人可以设置若干个关键词发送的消息内容必须包含至少一个关键词否则接口返回错误码 93000。比如你设了关键词“告警”那 markdown 的 content 里必须出现“告警”两个字。这个看起来很合理但有一个坑很多人会把内容写得花里胡哨比如用“CPU High”这种英文标题结果完全不含关键词消息被企业微信拒收。建议关键词直接设成你们内部最常用的中文告警词或者干脆在内容模板里固定加上“【告警】”前缀。IP 白名单。填了之后只有白名单内的 IP 可以调这个 Webhook。看起来安全但如果你所在的办公网络出口 IP 经常变或者告警服务部署在云上但没绑固定 IP就会莫名奇妙地收不到消息。我个人建议不要轻易开 IP 白名单除非你的 Prometheus、Alertmanager 和 Webhook 服务都在一个稳定的网段里。加签。开启后调用 Webhook 时需要额外计算一个签名参数避免地址泄露后被人滥用。加签模式对代码开发有直接影响后面第 4 章我会给出具体的 Python 实现这里的重点是加签不是可选项如果机器人建在多人共用的群里最好都开因为群成员并不都是可信的。3. Alertmanager 路由配置控制消息流向与频率3.1 一份可以直接抄的 Alertmanager 配置拿到企业微信 Webhook 地址之后第一件事不是写代码而是把 Alertmanager 的路由先配好。下面是一份我在测试环境验证过的配置注释都在关键位置global: resolve_timeout: 5m route: group_by: [alertname, instance] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: wechat-webhook routes: - matchers: - severity critical receiver: wechat-webhook continue: true receivers: - name: wechat-webhook webhook_configs: - url: http://127.0.0.1:8080/wechat send_resolved: true http_config: timeout: 5s这段配置把所有告警都交给 wechat-webhook接收地址指向本地 8080 端口的 Webhook 服务。生产环境如果要区分业务线可以在 routes 里按标签拆分比如 team payment 走 payment 群team infra 走 infra 群这是后面要展开的内容。3.2 四个时间参数决定你被轰炸的频率Alertmanager 里最影响消息数量的四个参数是group_wait、group_interval、repeat_interval、group_by。很多团队告警事故的体验差问题全出在这四个参数上。group_by 决定按什么标签分组。比如 group_by 里包含 alertname那同一类告警会合并成一条。如果只按 instance 分组一台机器上所有告警会合并成一条。这个要根据你的告警场景取舍。服务多的时候我建议 group_by 至少包含 alertname 和 instance这样既能看到故障面又不会太碎。group_wait 是同一组内的告警在第一次发送前等待的时间默认 30 秒。它的作用是等同一批告警凑齐了再发避免一条一条往外蹦。如果你发现半夜告警是一条条来的可以把 group_wait 调到 1 到 2 分钟体验会好很多。group_interval 是当一个组内新增告警时多久再发一次更新。默认 5 分钟基本够用。repeat_interval 是最关键的一个同一个组里没有新告警时Alertmanager 会隔多久重新发送一次。默认 4 小时意味着如果告警一直没恢复每 4 小时会在群里重复出现一次。这个参数设得太短比如 10 分钟群里会被未恢复的旧告警反复轰炸。设得太长又容易让值班同学忘记还有故障没处理。我的习惯是工作日白天 2 小时、夜间 4 小时可以通过不同路由配置实现但大多数团队不需要搞这么复杂4 小时是个稳妥的起点。4. 自己写 Webhook 接收器把 Alertmanager 告警翻译成企业微信消息4.1 Alertmanager Webhook 请求体到底长什么样Alertmanager 的 webhook_configs 会把告警数据以 POST JSON 形式发给指定 URL。请求体结构大致如下{ receiver: wechat-webhook, status: firing, alerts: [ { status: firing, labels: { alertname: HighCpuUsage, instance: 10.0.0.1:9100, severity: critical }, annotations: { summary: CPU 使用率超过 90%, description: Instance 10.0.0.1:9100 CPU 使用率 95% 超过阈值 90% }, startsAt: 2024-01-15T03:00:00Z, endsAt: 0001-01-01T00:00:00Z, generatorURL: http://prometheus/graph?g0.expr... } ], groupLabels: { alertname: HighCpuUsage }, commonLabels: {}, commonAnnotations: {}, externalURL: http://alertmanager:9093 }这些字段对后面写模板、写格式化函数都很关键。status 有两种firing 和 resolved。startsAt 是触发时间endsAt 如果是 0001-01-01T00:00:00Z 表示尚未恢复恢复后才是真实时间。generatorURL 可以拼成 Grafana 或 Prometheus 的跳转链接值班同学点开就能看到当时的查询条件。4.2 用 Python Flask 实现转发服务接下来写一个最简可用的转发服务。它接收 Alertmanager 的 POST 请求解析 alerts 数组拼成企业微信 markdown 消息再调 Webhook 推送到群里。import hashlib import base64 import hmac import json import time from flask import Flask, request, jsonify import requests app Flask(__name__) WEBHOOK_URL https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyYOUR_KEY SECRET # 如果开启加签填入机器人密钥 def build_url(): if not SECRET: return WEBHOOK_URL timestamp str(int(time.time())) string_to_sign f{timestamp}\n{SECRET} hmac_code hmac.new(string_to_sign.encode(utf-8), digestmodhashlib.sha256).digest() sign base64.b64encode(hmac_code).decode(utf-8) return f{WEBHOOK_URL}timestamp{timestamp}sign{sign} def format_alert(alert): labels alert.get(labels, {}) annotations alert.get(annotations, {}) status alert.get(status, firing) alertname labels.get(alertname, unknown) instance labels.get(instance, ) severity labels.get(severity, ) if status firing: title f【告警】{alertname} else: title f【恢复】{alertname} lines [ f### {title}, f 级别{severity}, f 实例{instance}, f 描述{annotations.get(description, annotations.get(summary, ))}, ] return \n.join(lines) def format_payload(payload): alerts payload.get(alerts, []) blocks [format_alert(alert) for alert in alerts] return \n\n.join(blocks) app.route(/wechat, methods[POST]) def wechat(): payload request.get_json(forceTrue, silentTrue) if not payload: return jsonify({error: bad request}), 400 content format_payload(payload) msg { msgtype: markdown, markdown: {content: content}, } try: resp requests.post(build_url(), jsonmsg, timeout5) return jsonify({wechat_status: resp.status_code, wechat_body: resp.text}), 200 except Exception as exc: return jsonify({error: str(exc)}), 500 if __name__ __main__: app.run(host0.0.0.0, port8080)这段代码做了三件事接收 JSON、格式化 markdown、转发到企业微信。如果机器人没开加签SECRET 留空就行开了加签build_url 会计算签名并拼到地址上。4.3 处理加签、超时和失败重试加签的逻辑别看代码只有几行里面有个容易被坑的地方计算签名用的字符串是“时间戳 换行 密钥”时间戳必须是发送请求时的秒级时间企业微信会校验时间戳与服务器时间差超过几分钟就拒绝。所以不要缓存这个 URL 复用每次发送前现算。我踩过一次为了省事把签名后的 URL 缓存了 10 分钟结果超过 5 分钟就被拒。requests.post 里我加了一个 timeout5这个很重要。企业微信接口偶尔会变慢如果不设超时Alertmanager 侧可能等很久后面的告警全部排队最终导致通知延迟。如果企业内部网络访问公网还要走代理记得给 requests 配置 proxies 或者在部署机器上设置好 HTTP_PROXY 环境变量否则会卡在连接阶段。失败重试方面我的建议是转发服务不一定要自己实现重试让 Alertmanager 负责重试更省心。Alertmanager 的 webhook_configs 支持配置 retry 机制。你可以再加个队列把发送失败的消息落盘但大多数场景没必要因为告警高峰时的失败往往是系统性问题重试只会加剧压力。5. 告警模板精华让消息可读性上一个台阶5.1 模板语法与时间格式化这个大坑如果只是把上面的 Python 服务跑起来消息已经能用了但可读性问题不少时间显示的是 UTC 原始格式一堆告警标签全堆在消息里值班同学得自己猜哪条是根因。升级可读性的核心在 Alertmanager 的模板系统。它使用 Go 模板语法可以在 Alertmanager 里配置自定义模板文件然后在 receiver 的 webhook_configs 里引用。这里我先把最值得掌握的模板语法讲清楚。Go 模板的时间格式化是出了名的反直觉它不是用 YYYY-MM-DD 这类占位符而是用 Go 的诞生时间作参考。2006代表年01代表月02代表日15代表小时04代表分钟05代表秒。我第一次用的时候写成了YYYY-MM-DD页面直接报错花了十分钟才反应过来。时区也得单独处理。Alertmanager 里的 startsAt 是 UTC 时间如果直接输出国内用户看到的比实际时间晚 8 小时。要先用.Add 28800e9加上 8 小时的纳秒数再格式化。这个 28800e9 是 8 * 3600 * 1e9 的纳秒量写模板的时候直接抄这个值。5.2 一份可直接落地的模板文件下面是一份我自己在用的模板文件文件名叫 wechat.tmpl放在 Alertmanager 配置目录下然后在 Alertmanager.yml 的 global 里声明templates: [/etc/alertmanager/wechat.tmpl]{{ define wechat.content }} {{- $alertCount : len .Alerts -}} {{- if eq .Status firing -}} **当前有 {{ $alertCount }} 条活跃告警** {{- else -}} 告警已恢复 {{- end }} {{- range .Alerts }} {{- if eq .Status firing }} ## {{ .Labels.alertname }} 等级{{ .Labels.severity }} 实例{{ .Labels.instance }} 开始时间{{ (.StartsAt.Add 28800e9).Format 2006-01-02 15:04:05 }} {{ .Annotations.description }} [查看图表]({{ .GeneratorURL }}) {{- else }} ## 已恢复{{ .Labels.alertname }} 恢复时间{{ (.EndsAt.Add 28800e9).Format 2006-01-02 15:04:05 }} {{- end }} {{ end }} {{ end }}在 Alertmanager receiver 里这么引用receivers: - name: wechat-webhook webhook_configs: - url: http://127.0.0.1:8080/wechat send_resolved: true http_config: timeout: 5s如果要把模板内容传过去可以把 Webhook 服务升级为接收 Alertmanager 渲染后的文本。更简单的方法是直接把模板内容放进 Python 服务的 format_alert 里两个方案效果一样。我个人推荐能用 Python 拼字符串就别在 Alertmanager 里写复杂模板因为 Go 模板测试起来比较麻烦而 Python 可以本地直接跑。6. 上线三个月的真实避坑记录限流、风暴与多集群6.1 企业微信限流与告警风暴企业微信群机器人有一个硬限制每个机器人每分钟最多发送 20 条消息。超过之后接口会返回错误码 45009消息直接丢弃。这意味着如果你的 Alertmanager 在短时间内发出大量独立消息超出的部分就没了。我第一次上线时没意识到这个问题。一个节点宕机触发了 20 多条告警Alertmanager 逐条发给 Webhook 服务群里瞬间涌进来 20 条消息然后第 21 条开始被企业微信拒收。从用户视角看群不但被刷屏了还有一批告警没看到。解决思路有两条一是靠 Alertmanager 的 group_by 和 group_wait 把多条告警合并成一条二是在 Webhook 服务里加一个简单的限流器比如令牌桶每秒钟最多发 1 到 2 条。这两招配合使用才能既保留告警细节又不触发企业微信限流。企业微信接口的错误码用的时候可以记一下错误码含义典型原因93000请求不合法content 长度超限、关键词不匹配、消息格式不对45009频率超限每分钟超过 20 条需要合并或降速40013不合法 keyWebhook 地址里的 key 被改或失效6.2 多集群、多部门共用一套 Alertmanager 怎么路由如果公司有多个 Kubernetes 集群或多套环境每个 Prometheus 都独立配自己的 Alertmanager 会非常难管。更常见的做法是多个 Prometheus 都往同一个 Alertmanager 上报告警然后在 Alertmanager 里通过路由树分发到不同企业微信群。每个 Prometheus 上报时带一个 external_labels比如 clusterprod-sh、clusterprod-bj。然后在 Alertmanager 的 routes 里按这个标签拆分route: receiver: default-wechat routes: - matchers: - cluster prod-sh receiver: sh-wechat - matchers: - cluster prod-bj receiver: bj-wechat这里要注意 matchers 的匹配语法。新版 Alertmanager 推荐用匹配旧配置里用match的写法已经废弃了。如果配置文件里混用导致告警没有进入预期的路由排查起来很费劲。另一个容易忽略的事情是企业微信的群聊数量是有限的一个人能加入的群数量也有上限。如果每个业务线都建一个告警群可能发着发着就到上限了。我的建议是不要按业务线拆太多群按运维职责拆一两个群就够了真正细分靠告警消息里的标签字段这样即使某个后端兄弟不在群里也能通过 提醒被拉到正确的人。6.3 告警没送达的排查顺序告警半个小时没响先别急着改配置。我每次排查都有固定顺序省了不少时间。第一步用 curl 直接调企业微信 Webhook 地址发一条测试消息。如果 curl 能通说明机器人本身没问题。curl 不通检查关键词、加签、IP 白名单这三项。curl https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyYOUR_KEY \ -H Content-Type: application/json \ -d {msgtype:text,text:{content:测试消息}}第二步看 Alertmanager 的日志。Alertmanager 在日志里会打出每条告警的分组和 receiver 信息确认它确实调用了 webhook 地址。常用命令journalctl -u alertmanager -f第三步看 Webhook 接收服务的访问日志。确认 Alertmanager 的 POST 请求有没有到、返回了什么。如果接收服务里加了异常日志能看到 requests 调企业微信时的具体错误码。最后才是看 Prometheus 的规则状态。因为很多时候告警没送达根本原因是 Alertmanager 没收到告警问题出在规则没匹配上或者 Prometheus 到 Alertmanager 的网络不通。网络上告警这条链路的排查和业务接口一样的逻辑先底层后上层一层层打日志确认。我个人在实际运行中还有一个习惯每半小时给告警群发一条心跳消息例如“监控心跳正常”。一旦群消息停了说明链路有问题比告警本身更早暴露故障。这个小技巧救了我一次——某个晚上 Webhook 服务所在机器磁盘满了心跳消息没了值班组在无告警的情况下发现了系统异常避免了第二天才发现监控网彻底断掉的问题。Prometheus 对接企业微信这条路核心其实不在配置项而在把消息的组织方式想清楚合并、降噪、路由、限流每一样都比“把消息发出来”重要。链路跑通只是起点能稳定、克制地把该看到的消息送到该看到的人面前才算真正落地。