Nightingale(n9e)告警订阅规则(alert_subscribe)实战指南:跨团队抄送、告警升级与排障
【免费下载链接】nightingaleNightingale is to monitoring and alerting what Grafana is to visualization.项目地址: https://gitcode.com/GitHub_Trending/ni/nightingale
Nightingale 的告警订阅(alert_subscribe)是在通知阶段对告警事件做"复制 + 二次路由"的机制:它按条件筛选事件、克隆一份副本,并交给关联的通知规则继续派发,从而实现跨团队抄送(CC)、告警升级(N 分钟未处理再通知他人)、跨业务组收告警、把零散规则的事件聚合到统一出口等典型场景。本文基于仓库内 SKILL.md 及其配套的 reference.md、troubleshooting.md、http-api.md,并结合引擎源码与数据模型,完整讲解订阅规则的配置字段、新旧两版通知路由、五个可落地的配置示例、创建/编辑/排障工作流以及底层实现原理,读完即可在 n9e 中独立完成订阅规则的搭建与问题排查。
一、先建立正确的"心智模型":订阅发生在通知阶段
订阅规则不是一个独立的告警源,它依附在已有告警事件之上,属于通知阶段的能力。在 alert/dispatch/dispatch.go 中,Dispatch在完成事件本身的派发后,会调用handleSubs处理订阅:
- 原始事件照常走它自己的通知链路,不受订阅影响;
- 每一条匹配的订阅都会克隆一份事件副本,按订阅配置改写后,再走一遍通知链路(dispatch.go#L723-L729 的
if !isSubscribe { e.handleSubs(event) }体现了这一点); - 因此订阅是加法语义:不拦截、不替换原始通知。如果同一个人同时被配在两条链路上,会收到两遍,这是设计使然。
从引擎匹配链(dispatch.go#L752-L789 的subMatches)可以确认,订阅的匹配条件是全部 AND,并按固定顺序检查:enabled → datasource → prod → cate → tags → 业务组名 → 持续时长 → 严重级别,任何一个门不通过,该订阅即被跳过。
还有三个容易被误解的点,先明确下来:
group_id只是管理归属(权限),不参与事件匹配。订阅天然可以接收跨业务组的事件;想"只订阅某个业务组的事件",要使用busi_groups过滤条件。- 新版路由(
notify_version=1):克隆事件的出口被改写为订阅指定的notify_rule_ids;克隆事件的callbacks 默认被清空(见 models/alert_subscribe.go#L450-L477 的ModifyEvent),防止再次命中原规则的回调。 - 配置变更最长 9 秒内生效:内存缓存轮询周期为 9 秒。在 memsto/alert_subscribe_cache.go#L110-L118 的
loopSyncAlertSubscribes中可以看到duration := time.Duration(9000) * time.Millisecond的轮询实现,无需重启服务。
二、核心配置结构:一个 JSON 对象说清楚
订阅规则的配置是一个单一 JSON 对象(不是数组),核心结构如下:
{ "name": "Subscription rule name", "note": "Notes", "disabled": 0, "prod": "", "cate": "", "datasource_ids": [], "cluster": "0", "rule_ids": [], "severities": [1, 2, 3], "for_duration": 0, "tags": [], "busi_groups": [], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [] }逐字段的完整说明见 reference.md,核心要点如下:
severities必填:新旧版本校验都会强制要求非空(models/alert_subscribe.go#L116-L118 中if len(s.SeveritiesJson) == 0 { return errors.New("severities is required") });[1,2,3]表示全部严重级别。- 新版必须
notify_version=1+ 非空notify_rule_ids:先用list_notify_rules拿通知规则 ID;若还没有合适的通知规则,先通过create_notify_rule创建再回来。注意:版本 1 下,旧版的 redefine 类字段(redefine_severity/redefine_channels/webhooks等)会被校验主动清空(见下文新旧版本对比)。 rule_ids为空 = 订阅所有告警规则的事件;只想订阅特定规则时,用list_alert_rules拿 ID。for_duration(秒)= 仅当告警持续时长超过该值时转发,是"告警升级"的开关;0= 不限。- 多个
tags/busi_groups条目之间全部是 AND;busi_groups匹配的是事件的业务组名称(key 约定写成"groups",实际匹配由 func/value 驱动)。 prod/cate参与匹配但语义较弱:prod非空时为精确匹配;cate只有填"host"时才有真实过滤效果,其它值等价于不过滤。拿不准时两者都留空字符串。datasource_ids空数组 = 所有数据源;cluster固定为"0"(V5 遗留字段)。
三、过滤条件的完整字段表
tags/busi_groups的元素结构统一为:
{ "key": "tag name", "func": "match operator", "value": "match value" }支持的运算符(func):
| func | 含义 | value 示例 |
|---|---|---|
== | 精确匹配 | "web01" |
!= | 不相等 | "web01" |
=~ | 正则匹配 | "web.*" |
!~ | 正则不匹配 | "web.*" |
in | 在列表中(空格分隔) | "web01 web02 web03" |
not in | 不在列表中(空格分隔) | "web01 web02" |
常用 tag:ident(机器标识)、rulename(告警规则名)、__name__(指标名)以及自定义业务标签。
严重级别取值:
| 值 | 含义 |
|---|---|
| 1 | 一级告警(Critical) |
| 2 | 二级告警(Warning) |
| 3 | 三级告警(Info) |
完整字段表(数据模型见 models/alert_subscribe.go 的AlertSubscribe):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 订阅规则名 |
note | string | 否 | 备注 |
disabled | int | 否 | 0=启用(默认),1=禁用。禁用的订阅在缓存层直接被过滤,完全不参与匹配 |
group_id | int64 | 是 | 管理归属业务组(决定谁能查看/编辑);不参与事件匹配 |
prod | string | 否 | 产品类型,非空时对事件的 RuleProd精确匹配;空 = 不过滤。常见值"metric"/"logging"/"host" |
cate | string | 否 | 数据源类型,当前实现只有"host"有真实过滤效果;空 = 不过滤 |
datasource_ids | int[] | 否 | 数据源 ID 列表;空数组或含0= 全部(工具会自动归一化为[0]);事件无数据源(dsId=0)时跳过此过滤 |
cluster | string | 否 | 恒为"0"(V5 遗留字段) |
rule_ids | int[] | 否 | 订阅的告警规则 ID 列表;空 = 订阅所有告警规则的事件(全局订阅) |
severities | int[] | 是 | 订阅的严重级别,新旧版本均校验非空;[1,2,3]= 全部 |
for_duration | int64 | 否 | 秒。仅当告警持续时长(trigger_time - first_trigger_time)超过该值才转发——用于告警升级(如300= 持续 5 分钟未恢复才转发);0= 不限 |
tags | array | 否 | 事件标签过滤,多条 AND |
busi_groups | array | 否 | 按事件的业务组名称过滤,多条 AND;元素{"key":"groups","func":"=~","value":"production.*"},key 按约定写"groups"(实现中 key 不参与匹配,func/value 匹配事件的 GroupName) |
四、通知配置:新版 vs 旧版
| 字段 | 类型 | 说明 |
|---|---|---|
notify_version | int | 1=新版(推荐,通过通知规则转发);0=旧版(直接填用户组 + 渠道) |
notify_rule_ids | int[] | 新版必填非空:克隆事件的出口被改写为这些通知规则 |
新版(notify_version=1)校验会清空所有旧版字段:user_group_ids、redefine_channels/new_channels、redefine_webhooks/webhooks、redefine_severity/new_severity。这一点在 models/alert_subscribe.go#L120-L133 的Verify中有直接实现:版本 1 下这几个字段被逐一置零/置空。换句话说,改写严重级别/渠道是旧版能力;新版要改级别/渠道,请在通知规则层做路由(对应 notify-rule-copilot)。
旧版(notify_version=0)字段,仅在维护存量配置时才会遇到:
| 字段 | 说明 |
|---|---|
user_group_ids | 接收用户组 ID,空格分隔字符串(如"1 2");指定了就必须同时指定new_channels |
redefine_severity/new_severity | 为 1 时,克隆事件的严重级别改为 new_severity |
redefine_channels/new_channels | 为 1 时,克隆事件的通知渠道改为 new_channels(空格分隔) |
redefine_webhooks/webhooks | 为 1 时,克隆事件的回调改为 webhooks(JSON 数组);未启用时克隆事件的 callbacks 会被清空(防止再次命中原始回调)——这条对新版同样适用 |
extra_config | 扩展配置 JSON 对象,引擎没有固定用途,照抄{}即可 |
五、五个可直接落地的完整示例
示例 1:订阅所有 Critical 告警并转发到值班通知规则(最常见)
{ "name": "Subscribe to all Critical alerts", "note": "CC all critical alerts to the on-call chain", "disabled": 0, "prod": "", "cate": "", "datasource_ids": [], "cluster": "0", "rule_ids": [], "severities": [1], "for_duration": 0, "tags": [], "busi_groups": [], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [1] }示例 2:按标签订阅特定机器组的告警
{ "name": "Subscribe to web cluster alerts", "note": "CC alerts from all web-prefixed machines to the web team", "disabled": 0, "prod": "metric", "cate": "", "datasource_ids": [], "cluster": "0", "rule_ids": [], "severities": [1, 2, 3], "for_duration": 0, "tags": [ {"key": "ident", "func": "=~", "value": "web.*"} ], "busi_groups": [], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [1] }示例 3:告警升级——指定规则持续 5 分钟未恢复后通知二线
{ "name": "CPU alert escalation", "note": "CPU-related alerts that persist unrecovered for 5 minutes, escalate to second-line", "disabled": 0, "prod": "metric", "cate": "", "datasource_ids": [1], "cluster": "0", "rule_ids": [10, 11], "severities": [1, 2], "for_duration": 300, "tags": [], "busi_groups": [], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [2] }示例 4:跨业务组订阅——只接收 "production" 业务组下的数据库告警
{ "name": "Subscribe to production database alerts", "note": "CC database-related alerts under the production business group to the DBA", "disabled": 0, "prod": "metric", "cate": "", "datasource_ids": [], "cluster": "0", "rule_ids": [], "severities": [1, 2], "for_duration": 0, "tags": [ {"key": "rulename", "func": "=~", "value": ".*database.*|.*MySQL.*|.*Redis.*"} ], "busi_groups": [ {"key": "groups", "func": "=~", "value": "production.*"} ], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [2, 3] }示例 5:聚合告警事件,统一转发到工单系统
{ "name": "Forward alerts to the ticketing system", "note": "Warning-and-above alerts are uniformly forwarded to the internal ticketing system via a notification rule", "disabled": 0, "prod": "", "cate": "", "datasource_ids": [], "cluster": "0", "rule_ids": [], "severities": [1, 2], "for_duration": 0, "tags": [], "busi_groups": [], "extra_config": {}, "notify_version": 1, "notify_rule_ids": [5] }六、工作流一:创建订阅规则
- 确定业务组(管理归属):用
list_busi_groups拿group_id。如果用户已点名业务组,或前端已弹出业务组表单,直接使用其 ID,不必再问。 - 确定关联通知规则:用
list_notify_rules拿notify_rule_ids(新版路由的通知出口)。 - (可选)限定订阅范围:只想订阅某些告警规则,用
list_alert_rules拿rule_ids;想按标签/业务组/严重级别/时长收窄,填对应过滤字段。 - 调用
create_alert_subscribe:把第 1 步的业务组作为group_id传入(也可以放进 config 里),config传单个 JSON 对象字符串(不是数组)。如果省略group_id,工具会自动弹出业务组选择表单,用户选完后恢复本次创建。 - 汇报结果:工具返回
{id, name, group_id, disabled, notify_rule_ids, url},简要汇报订阅条件和通知出口即可;把规则名以内链形式展示:<name>(url 是返回的/alert-subscribes/edit/<id>),用户可直接点击进入配置页核对。
七、工作流二:编辑与排障
- 用
list_alert_subscribes/get_alert_subscribe_detail拿到规则 ID 和当前状态,确认要改什么。 - 调用
update_alert_subscribe(基于提案(proposal):调用后立即向用户展示变更清单并暂停,用户确认后系统自动持久化——确认步骤不经过你):id必填,config只含要改的字段(增量补丁:未指定的字段保持原值;数组字段如 tags/severities/rule_ids/notify_rule_ids/busi_groups/datasource_ids提供即整体替换——先拿 detail 里的现有数组,在其基础上构造完整的新数组再传入)。常见操作:- 临时禁用= 传 config
{"disabled":1}(缓存层直接过滤,立即完全失效);恢复 ={"disabled":0}; - 调整升级阈值=
{"for_duration":600};切换通知出口={"notify_rule_ids":[...]}(先用list_notify_rules确认 ID); - 业务组(管理归属)不可变更;删除没有站内工具——让用户在 UI 完成(告警管理 → 订阅规则)。
- 临时禁用= 传 config
- 当用户说"订阅了但没收到任何东西"时,按 troubleshooting.md 中的链路逐门检查,并主动指出最可能失败的门(常见:for_duration 设太大、busi_groups 名称对不上、notify_rule_ids 指向的通知规则本身配置不对);确认是某个门的配置问题后,可直接用
update_alert_subscribe修复。
八、排障链路:"订阅了但没收到任何东西"
引擎匹配链在 alert/dispatch/dispatch.go 的handleSub/subMatches中按以下顺序求值,任何一个门失败即跳过该订阅,请按序检查:
| # | 检查项 | 常见失败原因 |
|---|---|---|
| 0 | 缓存同步 | 刚改完规则,内存缓存最多滞后9 秒 |
| 1 | disabled | 规则被禁用(disabled=1,缓存层直接过滤) |
| 2 | 数据源 | datasource_ids不是"全部"且事件datasource_id不在列表中 |
| 3 | prod | prod非空且不等于事件的 RuleProd(注意是精确匹配) |
| 4 | cate | 填了"host"但事件不是主机类型(其它值不会导致不匹配) |
| 5 | 标签 | 多条tags是 AND;in值写成逗号分隔会导致不匹配(必须空格分隔) |
| 6 | 业务组名称 | busi_groups匹配事件的业务组名称——按名称硬绑的条件在业务组改名后会失去目标;正则没覆盖全名也会失配 |
| 7 | 时长 | for_duration大于事件已持续时长(trigger_time - first_trigger_time)——刚触发的告警必然不匹配,必须等持续足够长后的下一轮通知周期 |
| 8 | 严重级别 | severities未包含事件的严重级别 |
| 9 | 下游出口 | 以上全过、克隆事件已产生,但notify_rule_ids指向的通知规则本身配置不对(渠道/接收人/适用属性不匹配)→ 转交 notify-rule-copilot 排障 |
另外先确认源头:原始告警事件必须真的产生——订阅不会凭空变出事件;如果事件被静默(在求值阶段被拦截),它根本到不了订阅这一步。
九、行为语义速查(回答用户问题时可引用)
| 行为 | 语义 |
|---|---|
| 订阅会替换原始通知吗 | 不会。原始事件照常走自己的通知链路;订阅克隆一份副本额外转发。如果同一个人被配在两条链路上会收到两遍——这是设计使然 |
| 会再次命中回调(webhooks)吗 | 不会。克隆事件的 callbacks默认清空(models/alert_subscribe.go#L460-L467 的ModifyEvent);只有旧版显式redefine_webhooks=1时才携带订阅自己的 webhooks |
| 订阅范围受 group_id 限制吗 | 不受。group_id只是管理归属(权限);订阅天然接收所有业务组的事件,用busi_groups/rule_ids/tags收窄 |
for_duration如何生效 | 比较事件的trigger_time - first_trigger_time。事件首次触发时差值为 0 必然不匹配;只有告警反复通知到第 N 次、差值超过阈值后,那次事件的克隆才被转发——所以"升级"依赖告警规则本身配置了重复通知 |
| 新版能改写严重级别/渠道吗 | 不能。notify_version=1时Verify会清空 redefine_* 字段;请在通知规则层做级别/渠道路由 |
| 恢复事件也会被订阅吗 | 走同样的匹配链;恢复通知是否发送取决于下游通知规则的配置(如is_recovered属性过滤) |
| 变更多久生效 | 缓存每 9 秒轮询一次,最多 9 秒,无需重启 |
| 怎么判断事件是否被订阅转发 | 克隆事件携带sub_rule_id(订阅规则 ID),在通知记录/事件详情中可见 |
十、其他常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
创建报severities is required | 新旧版本都必填 | 至少填[1,2,3] |
创建报no notify rules selected | notify_version=1但notify_rule_ids为空 | 先用list_notify_rules拿 ID;没有就先创建通知规则 |
创建报new_channels is required | 旧版指定了user_group_ids但没填new_channels | 补new_channels,或改用新版 |
| 保存后 redefine 字段全没了 | 新版 Verify 主动清空旧版字段 | 预期行为,不是数据丢失 |
| 升级订阅从不触发 | 告警规则没有配置重复通知(notify_repeat_step=0),事件不会第二次来 | 让用户检查告警规则的重复通知间隔 |
| 全局订阅事件量爆炸 | rule_ids/tags/busi_groups 全空 = 复制所有事件 | 至少加一层过滤;或在下游通知规则收窄 |
| 业务组改名后订阅失配 | busi_groups按名称匹配 | 改用=~正则,或改名时同步订阅 |
十一、验证方法
- 站内:
get_alert_subscribe_detail核对字段;get_notify_rule_detail核对下游出口。 - HTTP(给用户命令):
POST /api/n9e/alert-subscribe/alert-subscribes-tryrun用历史事件 ID + 订阅草稿试运行,显示在哪个步骤匹配失败;新版甚至会执行通知规则的真实测试发送。先试跑、再保存。 - 真实验证:触发一条匹配的告警,然后去"历史告警 → 详情"查看是否出现携带
sub_rule_id的克隆事件及其通知记录。
十二、HTTP API(供外部 A2A Agent / 给用户 curl 命令时使用)
站内 AI 助手不得使用这些端点(应使用内置 FC 工具)。认证:
Authorization: Bearer <token>。路由定义在 center/router/router.go。
| 操作 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 列表(跨业务组) | GET | /api/n9e/busi-groups/alert-subscribes | 当前用户可见业务组下的订阅 |
| 列表(单业务组) | GET | /api/n9e/busi-group/:id/alert-subscribes | |
| 详情 | GET | /api/n9e/alert-subscribe/:sid | |
| 创建 | POST | /api/n9e/busi-group/:id/alert-subscribes | Body 为单个AlertSubscribe JSON 对象;group_id 取自 URL |
| 更新 | PUT | /api/n9e/busi-group/:id/alert-subscribes | Body 为数组[{...}](与创建相反);按显式字段列表更新,但仍建议先 GET 详情、改完整对象再 PUT |
| 删除 | DELETE | /api/n9e/busi-group/:id/alert-subscribes | Body:{"ids":[1,2,3]} |
| 试跑 | POST | /api/n9e/alert-subscribe/alert-subscribes-tryrun | Body:{"event_id":<历史事件 ID>,"config":{...订阅草稿...}};逐门校验匹配,新版还会执行通知规则的真实测试发送 |
权限:创建/更新/删除需要业务组读写权限(bgrw)+ 对应的/alert-subscribes/*菜单权限;列表只需只读权限。
直接改库(最后手段):表alert_subscribe,其中tags/busi_groups/webhooks/extra_config/notify_rule_ids等是 JSON/序列化字段;内存缓存约 9 秒自动重载,无需重启;改前先备份。
十三、源码级原理:一次订阅转发的完整链路
把文档中的流程落到源码上,订阅的完整生命周期是:
- 求值产出事件:告警规则求值产生
AlertCurEvent,进入 alert/dispatch/dispatch.go 的Dispatch派发流程。 - 原始通知照常发送:
HandleEventNotify用NotifyGroupDispatch、GlobalWebhookDispatch、EventCallbacksDispatch等 handler 合并出通知目标并go e.Send(...)异步发送(dispatch.go#L706-L728)。 - 收集订阅:非订阅事件会调用
handleSubs,通过collectSubscribes从alertSubscribeCache取出"规则维度订阅"(key=RuleId)和"全局订阅"(key=0)两类候选(dispatch.go#L738-L750)。 - 逐门匹配:
subMatches依次检查IsDisabled → MatchCluster(数据源) → MatchProd → MatchCate → MatchTags → MatchGroupsName → ForDuration 时长 → SeveritiesJson 严重级别,全过才继续(dispatch.go#L752-L789)。 - 克隆与改写:
handleSub以值传递拷贝事件副本(避免污染原事件),调用sub.ModifyEvent(&event)改写副本(清空 callbacks、写入 notify_rule_ids / notify_groups 等),打上event.SubRuleId = sub.Id标记,记录 subscribe 日志后再次进入HandleEventNotify(&event, true)走通知链路(dispatch.go#L791-L803)。isSubscribe=true时 handler 集合只剩NotifyGroupDispatch与EventCallbacksDispatch(dispatch.go#L706-L710)。 - 缓存热加载:订阅表由 memsto/alert_subscribe_cache.go 的
SyncAlertSubscribes启动,后台loopSyncAlertSubscribes每 9 秒全量同步一次(先查统计、有变化才重载),因此配置改动最长 9 秒生效、无需重启。
理解这条链路后,"订阅只是通知阶段的加法路由"这一心智模型就有了代码级支撑:它不干预告警求值,只在事件已经产生之后做"复制 + 二次派发",这也是为什么升级、跨组抄送、聚合转发这些场景都由它承载。
【免费下载链接】nightingaleNightingale is to monitoring and alerting what Grafana is to visualization.项目地址: https://gitcode.com/GitHub_Trending/ni/nightingale
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考