OneUptime 接入 Prometheus Alertmanager:把告警通知转化为带告警升级与自动恢复的 Incident 实战指南
2026/9/18 20:58:58 网站建设 项目流程

OneUptime 接入 Prometheus Alertmanager:把告警通知转化为带告警升级与自动恢复的 Incident 实战指南

【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime

导读

本文介绍如何将 Prometheus Alertmanager 的通知接入 OneUptime,使其成为可跟踪、可升级、可自动关闭的 Incident。Prometheus 负责评估告警规则,Alertmanager 负责路由,而 OneUptime 负责记录、升级(on-call 分页)与恢复处理。读完本文,你将掌握两种接入方式(Incoming Request 监控器与 Webhook 工作流)、一个告警一条 Incident 的分组配置、恢复事件的自动闭合机制,以及针对“Prometheus 本身静默”的 dead man's switch 方案,并能定位绝大多数接入故障。

集成架构概览

这是一个**入站(inbound)**集成:数据从 Alertmanager 流向 OneUptime,由 OneUptime 承担告警的记录、去重、升级与恢复判定。链路如下:

Prometheus rule fires ──► Alertmanager webhook receiver ──► OneUptime ──► Incident + on-call

OneUptime 为这种入站集成提供了两种构建方式,适用于不同诉求:

方式适用场景
Incoming Request 监控器(推荐)你希望告警变成带 on-call 升级的 Incident、一个告警对应一个 Incident、并在恢复时自动关闭,且不想维护任何自定义逻辑。
Webhook 触发的工作流你需要 OneUptime 原生不提供的路由逻辑——调用其他系统、重塑 payload、按条件分支。

两种方式接收的是同一个 Alertmanager webhook payload,因此接入链路、测试方法与大部分排障经验可以互相复用。Grafana 的 webhook 也是同构的 Alertmanager 形状,本页的配置同样适用于 Grafana 集成。

前提条件

开始之前,需要确认以下三点均已满足:

  • 一套可编辑alertmanager.yml的 Prometheus + Alertmanager 环境;
  • Alertmanager 所在网络可以通过 HTTPS 访问你的 OneUptime 实例(自托管时即实例的入口地址);
  • 一个拥有创建监控器(或工作流)权限的 OneUptime 项目。

方式一:Incoming Request 监控器(推荐)

Incoming Request 监控器会生成一个专属 URL,所有打到该 URL 的 HTTP 请求都会按你配置的 Criteria 被评估,进而改变监控器状态、声明 Incident 并呼叫值班人员。它是把“外部告警源”变成“OneUptime Incident”的最直接通道。

Step 1 — 创建监控器

  1. 进入Monitors → Create Monitor,选择Incoming Request类型。

  2. 打开该监控器,点击左侧菜单的Documentation,复制其 URL:

    https://oneuptime.com/heartbeat/YOUR_SECRET_KEY

    自托管时请将域名替换为你自己的 OneUptime 实例地址。路径中的 Secret Key 是唯一的凭证——不需要任何 Header 或 Token。

关于这个 URL 的几点实现事实:

  • GET、POST 均可;HEAD 被视为 GET,其他方法返回 404;
  • URL 本身即秘密,任何知道它的人都能让监控器保持健康,请按密钥对待;发往该端点的 Header 会被完整存储并可被有读取权限的人看到,不要把 API Key 或 Token 放进 Header
  • OneUptime 会在做任何校验之前立即返回空200并异步入队处理,因此200不代表请求已被接受——错误的 Secret Key、已删除或已停用的监控器同样返回200,确认请求落地要看监控器自身的时间线(timeline);
  • 请求体最大 50 MB,且不要使用Content-Encoding: gzip压缩(压缩体不会被解析,路径引用将无法解析)。

Step 2 — 把 Alertmanager 指向监控器 URL

alertmanager.yml中增加一个 webhook receiver,并把默认路由指向它:

receivers: - name: oneuptime webhook_configs: - url: "https://oneuptime.com/heartbeat/YOUR_SECRET_KEY" send_resolved: true route: receiver: oneuptime group_by: ["alertname", "instance"] group_wait: 30s group_interval: 5m repeat_interval: 4h

send_resolved: true是必须的——它决定 Alertmanager 是否把恢复(resolved)通知也发给 OneUptime,而恢复通知正是 OneUptime 判断“告警已恢复”并关闭 Incident 的依据。配置修改后用下面命令热重载(或直接重启 Alertmanager):

curl -X POST http://localhost:9093/-/reload

Alertmanager 发送的Content-Type: application/json也是必须的:OneUptime 依赖它解析出 payload 中的字段。若使用其他 Content-Type(或缺失),requestBody下的所有引用都将无法解析。这一点与 Incoming Request 监控器文档 中的说明一致:application/x-www-form-urlencoded虽会被解析,但仅限扁平顶层字段;其余类型完全不解析。

Step 3 — 配置 Criteria(条件)

打开监控器的Criteria,编辑第一条 criteria。

Filter(过滤条件)

  • Filter Type(类型)JavaScript Expression
  • Filter Condition(条件)Evaluates To True
  • Value(值)"{{requestBody.status}}" === "firing"

占位符两侧的引号是必须的:JavaScript Expression 的求值机制会先把{{var}}替换成实际值再执行脚本,所以字符串比较必须带引号,数字比较则不需要(详见 JavaScript Expressions 文档)。如果不习惯表达式,也可以使用Request Body / Contains /"status":"firing"的过滤器。注意 Request Body 的匹配是区分大小写的子串匹配,且对象体会被序列化为无空格的紧凑 JSON——所以必须写成"status":"firing",从格式化文档里复制的"status": "firing"永远不会命中。

Actions(动作)

  • 打开When filters match, change monitor status,状态设为Offline(或 Degraded);
  • 打开When filters match, declare an incident,设置TitleSeverity,以及需要被呼叫的On-Call Policies
  • 在该 Incident 表单的Advanced Options下,打开Auto Resolve Incident没有它,恢复通知会被忽略,Incident 将永远保持打开状态。

Settings → Group incidents and alerts by a payload field

打开此开关,让一个端点可以同时容纳多个并发 Incident——每个告警一条,而不是每封通知只有一条 Incident:

字段
Open a separate incident for each…requestBody.alerts[*].fingerprint
Field that signals recoveryrequestBody.alerts[*].status
Value that means recoveredresolved
Max incidents per request100

[*]会在 Alertmanager 的alerts数组上扇出(fan out):对数组中的每个元素各提取一个值,为每个不同的提取值各开一条 Incident。由于分组路径和恢复路径都用了[*],恢复判定是逐告警进行的——当一个 payload 里一条告警已 resolved、另外两条仍 firing 时,只有已恢复的那条被关闭。

警告:分组必须选真正对每个告警唯一的字段。Alertmanager 的fingerprint是告警完整标签集的哈希,天然唯一。普通标签只在“通知内部有差异”时才能用于分组——而路由group_by中列出的任何标签在通知内都不会有差异,因为它正是聚合分组的依据。以上述group_by: ["alertname", "instance"]为例,若按requestBody.alerts[*].labels.alertname分组,payload 中每个告警提取出的值都一样,所有告警会塌缩成一条 Incident。更糟的是,重复值只保留第一次出现——如果 payload 里第一条告警是resolved,它会关闭那条 Incident,而其余告警仍在 firing。

分组底层实现(可参考 IncomingRequestIncidentGrouping.ts):

  • 分组配置由 IncidentGroupingConfig 定义:groupByJSONPathresolvedWhenJSONPathresolvedWhenValuemaxKeysPerPayload(默认 100),即上面表格四个字段的模型映射;
  • 每个提取出的键值会被哈希成seriesFingerprint(与指标监控器 per-series Incident 相同的去重机制),实现“创建 + 去重”复用;
  • extractItems[*]前缀处把 payload 拆成数组逐元素处理:元素后缀存在时从元素内深挖取值,否则数组元素本身即键值;同一 payload 内相同键只保留第一次出现;超过maxKeysPerPayload后其余键被忽略并打 warn 日志;
  • 恢复分类是事件驱动的:只有当 payload 明确把某个键标记为resolved时才关闭对应 Incident(collectResolvedFingerprints),绝不因为键“缺席”而关闭——webhook 只描述当前 payload 内的告警,不像快照模型那样代表全局状态。

Step 4 — 编写 Incident 标题与描述

分组键会以路径最后一段命名的变量形式暴露给模板:requestBody.alerts[*].fingerprint对应{{fingerprint}}。但 fingerprint 是哈希值,不适合展示给值班人员——标题应使用通知中共享的标签commonLabels携带你路由group_by中的全部标签,因此上面的配置里alertnameinstance都可用:

  • Title(标题){{requestBody.commonLabels.alertname}} on {{requestBody.commonLabels.instance}}

  • Description(描述)

    {{requestBody.commonAnnotations.summary}} {{requestBody.commonAnnotations.description}} Severity: {{requestBody.commonLabels.severity}} Alertmanager: {{requestBody.externalURL}}

commonLabelscommonAnnotations保存的是通知内共享的字段,天然与分组键同义;而requestBody.alerts[0].annotations.summary这类逐告警路径永远读取 payload 中的第一条告警,不一定是你这条 Incident 所对应的那一条——所以想让每条 Incident 携带各自独立的注释文本,就必须让group_by足够“紧”。另外,解析不到的路径会原样打印(连花括号一起)而不是留空。完整的变量清单见 Incident & Alert Dynamic Templating。

从模板实现的源码角度看,这类{{...}}引用与 JavaScript Expression 共用同一套占位符替换机制,最终由VMUtil.deepFind在 payload 上做路径解析(VMAPI.ts):支持点分路径、[0]/[last]数组下标;路径解析失败时静默返回 undefined,模板层于是原样输出占位符。分组字段与模板字段还共享一个硬性约束——路径必须以字面量requestBody.开头,否则一律解析为空(静默失败)。

Step 5 — 把监控器送回 Operational(可选)

Criteria 只在命中时生效。为了让一切恢复正常后监控器不一直停留在 Offline,需要再补一条 criteria:

  • Filter TypeJavaScript ExpressionValue"{{requestBody.status}}" === "resolved"
  • 动作:Change monitor status toOperational,且不声明 Incident。

Step 6 — 测试

用一条模拟 Alertmanager 通知验证整条链路:

curl -X POST https://oneuptime.com/heartbeat/YOUR_SECRET_KEY \ -H "Content-Type: application/json" \ -d '{ "version": "4", "status": "firing", "commonLabels": { "alertname": "HighCPU", "severity": "critical" }, "commonAnnotations": { "summary": "CPU above 90% for 5m" }, "externalURL": "http://alertmanager:9093", "alerts": [ { "status": "firing", "labels": { "alertname": "HighCPU", "instance": "web-1" }, "fingerprint": "a1b2c3d4e5f60001" }, { "status": "firing", "labels": { "alertname": "HighCPU", "instance": "web-2" }, "fingerprint": "a1b2c3d4e5f60002" } ] }'

预期结果:因为 payload 中有两个不同的fingerprint,应产生两条 Incident。把两条告警的status都改为resolved重新发送,两条 Incident 都应被自动关闭。

也可以直接用amtool触发一条真实告警来测试:

amtool alert add test_alert severity=warning \ --annotation=summary="Test from Alertmanager" \ --alertmanager.url=http://localhost:9093

方式二:Webhook 触发的工作流

当需求超出“告警变成 Incident”时使用此方式——例如调用第三方系统、改造 payload、按条件分支。

  1. 打开Workflows → Create Workflow,命名为Alertmanager → Incidents,进入Builder
  2. 添加Webhook触发器并复制其 URL,把该块重命名为Alertmanager。Webhook 触发器的机制是:OneUptime 生成唯一 URL,任何打到该 URL 的请求都会启动工作流,请求的 Headers、Query Params 与 Body 会被完整传入(Webhook trigger)。
  3. 添加一个连接到触发器的Conditions块(面板中叫 If / Else):
    • Left(左值){{Alertmanager.Request Body.status}}
    • Operator(运算符)==
    • Right(右值)firing
  4. Yes分支连接Create Incident块:
    • Title(标题){{Alertmanager.Request Body.commonAnnotations.summary}}
    • Description(描述){{Alertmanager.Request Body.commonAnnotations.description}}\nAlert: {{Alertmanager.Request Body.commonLabels.alertname}}
    • Severity(严重级别):选择一个固定值,或在前面先用 Conditions 按{{Alertmanager.Request Body.commonLabels.severity}}分支映射。
  5. 保存后,把 Step 2 中webhook_configs的 URL 替换为工作流的 URL。

工作流版本的变量路径与监控器版本不同:触发器输出被绑定到块名上,因此是{{Alertmanager.Request Body.status}}而不是{{requestBody.status}};数据组件(如 Create Incident)的字段则按记录自身的列名(column)写入,例如_id是 ID 列。详见 Workflow components。

如果希望“一个告警一条 Incident”,可以添加一个Custom Code块,用 JavaScript 遍历Request Body.alerts;配合send_resolved: true,再添加第二个Conditions分支判断status == resolved,用Update Incident找到匹配的 Incident 并将其移动到已解决状态。

Dead man's switch:盯住 Prometheus 本身

以上两种方式都无法告诉你“Prometheus 自己挂了”——没有告警到达看起来和“一切正常”完全一样。常规答案是:设置一条永远在触发的告警,路由到一个按节奏期待请求的监控器。官方 kube-prometheus-stack 内置了一条名为Watchdog的此类告警;裸 Prometheus 上可以自己加一条表达式恒为真(vector(1))的告警规则。

做法:

  1. 再创建一个Incoming Request 监控器;
  2. Watchdog路由到它,并配一个较短的repeat_interval(这样一旦 Prometheus 静默,能较快暴露);
  3. 给这个监控器配置Filter Type: Incoming Request / Filter Condition: Not Recieved In Minutes的 criteria——这是唯一一种“缺失请求”条件应该出现在告警接收器上的场景。

之所以必须用独立监控器,是因为接收告警的监控器没有规律节奏,“Not Recieved In Minutes”条件放在它上面会频繁抖动;dead man's switch 必须单独成器(Incoming Request 监控器最佳实践)。

下面是 Step 2 配置合并了 watchdog 路由与 receiver 的完整版本——子路由先于父路由自身的 receiver 匹配,因此Watchdog走第二个监控器,其余告警仍走第一个:

receivers: - name: oneuptime webhook_configs: - url: "https://oneuptime.com/heartbeat/YOUR_SECRET_KEY" send_resolved: true - name: oneuptime-watchdog webhook_configs: - url: "https://oneuptime.com/heartbeat/WATCHDOG_SECRET_KEY" route: receiver: oneuptime group_by: ["alertname", "instance"] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - receiver: oneuptime-watchdog matchers: - alertname = "Watchdog" group_wait: 0s group_interval: 5m repeat_interval: 5m

注意 watchdog 路由的group_wait: 0s与较短的repeat_interval: 5m:前者让 Watchdog 触发后立即送达,后者让它在静默后尽快被“漏报”暴露。

Troubleshooting:常见故障与处置

没有任何请求到达—— 确认 Alertmanager 能访问该 URL,检查其日志中的投递错误。注意 OneUptime 在做任何校验之前就对每个请求应答空200,所以200并不代表 payload 已被接受——请以监控器的时间线(timeline)为准。

Incident 打开后永不关闭—— 依次检查:

  • Alertmanager 侧send_resolved: true是否存在;
  • criteria 上的恢复字段与恢复值(比较区分大小写);
  • IncidentAdvanced Options里的Auto Resolve Incident是否开启;
  • 两个更隐蔽的原因:其一,payload 中不同键的数量超过Max incidents per request时,超出上限的键对恢复同样不可见;其二,如果resolved通知恰好被 ingest 合并(coalescing,见下文)丢弃,Incident 会被永久困住——因为Alertmanager 会重复发送 firing 通知,却不会重复发送 resolved 通知。这类 Incident 只能手动关闭。

完全没有 Incident、监控器状态也不变—— 分组路径必须以字面量requestBody.开头,且一条路径中只有第一个[*]是通配符。这两种错误都会静默失败(源码层面,normalizePath对非requestBody.前缀的路径直接返回空串,extractItems对含多个[*]的路径只会处理第一个,见 IncomingRequestIncidentGrouping.ts)。

Incident 文本显示原始{{...}}占位符—— 路径未解析成功,而 OneUptime 的策略是保留未解析的占位符原样输出而非留空。不同告警规则设置的注释字段不同,请引用你的规则中真实存在的字段(commonAnnotations与逐告警的annotations是两回事)。

一个满是告警的 payload 只产生一条 Incident—— 你按一个在通知内部不变的标签分了组,最常见的就是该标签同时出现在路由的group_by里。改用requestBody.alerts[*].fingerprint分组。

Incident 过多—— 放宽 Alertmanager 的group_by/group_interval,让相关告警被批量聚合;调低Max incidents per request可以封顶,但超出上限的键同样对恢复不可见,属于“封顶即失明”,需权衡。

突发高峰时部分通知似乎被跳过—— 发往同一监控器的请求在 ingest 阶段会被合并(coalesce),以防单个发送方压垮监控器;当通知背靠背到达时,中间的某次 payload 可能被丢弃。增大group_waitgroup_interval可以把它们拉开。该合并由应用容器的环境变量INCOMING_REQUEST_INGEST_COALESCE_ENABLED控制,默认开启;需要每次都评估所有 payload 的自托管运维人员可在该容器上将其设为false

源码佐证:该开关定义于 App/FeatureSet/Telemetry/Config.ts(process.env["INCOMING_REQUEST_INGEST_COALESCE_ENABLED"] !== "false",即默认开启、只有显式设为"false"才关闭);实际合并在 TelemetryQueueService.ts 中通过 BullMQ 的deduplicationidincoming-request-${secretKey}keepLastIfActive: true)实现——同一监控器至多保留“一个活跃 + 一个等待”的任务且保留最新 payload,从而在入队阶段串行化同一监控器的处理,避免大量并发monitorResource()争抢 per-monitor 的 Redis 锁。

进一步阅读

  • Incoming Request Monitor —— 该监控器类型的完整文档:criteria、过滤条件、路径语法与 Incident 分组的全部细节;
  • Integrations Overview —— 入站与出站集成模式总览;
  • Grafana —— 同样的思路,换成 Grafana alerting(payload 形状一致,配置几乎完全复用);
  • Webhook trigger —— 工作流接收 URL 的工作原理;
  • Workflow components —— Conditions、Custom Code、数据组件(Create/Update Incident)的用法;
  • Incident & Alert Dynamic Templating —— 标题与描述中可用的全部变量;
  • JavaScript Expressions —— 表达式语法与引号规则。

【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询