年初时,我司已使用开源夜莺 v8 版本构建了公司级统一监控系统,但一直遗留了一个问题:发版时告警噪声太多。
据我们观察,50% 的告警噪声是由发版导致的,如果能够把监控系统和变更系统联动,发版时自动静默相关告警,那就太棒了。
发版时,通常是有人盯着服务仪表盘的,此时服务如果有问题可以及时发现。发版时的告警通知意义不大,所以我们一直想静默掉。
本文方案概览
本文介绍的做法不是修改每一条告警规则,而是在夜莺告警事件流转阶段做处理:
- Event Update 处理器把告警事件发送给发版系统接口。
- 发版系统根据服务名、环境和最近发版记录判断当前是否处于发布窗口。
- 如果命中发布窗口,接口给告警事件追加
updating=true标签。 - Event Drop 处理器识别
updating=true,丢弃该告警事件。
这个方案的核心价值是:把“发布期是否要静默”的判断集中在事件 Pipeline 中,避免给大量 PromQL 告警规则逐条追加条件。
原始思路
起初,我们设想的是:把发版事件转成 Prometheus 指标,然后给相关告警规则都加上 and 条件,只有不发版时才会触发告警,举例:
rate(http_requests_total{service_name="gohttpd", status=~"5.."}[1m]) > 10
and on(service_name, env)
service_updating{} == 0
但我们的告警规则数量很多,上百个应用多维度告警都精确到各小组。逐条配置 and 条件的工作量很大,还要处理发版事件到 Prometheus 指标的转换,所以迟迟没有实施。
夜莺 Pipeline 新功能带来新的可能性
六月新发布的夜莺 v8.beta11 版,引入了告警事件 Pipeline 能力。简单阅读文档了解了下 Pipeline 功能,完美,正是我想要的,脑海中浮现出了一个个用法,当然首先是发版静默的实现。
下面先简单概括 Pipeline 功能,方便理解后文。具体细节可以参阅夜莺官方文档(最新版本直接在页面上就可以看到文档)。

开源版本共放出了四个 Pipeline 处理器,分别是:
- Relabel:对告警事件的标签进行各种操作,包括删除、保留、重命名、合并和修改标签值
- Callback:通过 HTTP 调用将告警事件信息发送到外部服务。用于通知外部系统或触发自动化处理流程
- Event Update:通过 HTTP 调用外部服务来动态更新告警事件的内容。当告警事件流经此处理器时,处理器会将事件数据发送到指定的 URL,并根据外部服务的响应来更新事件信息
- Event Drop:根据自定义的逻辑条件来决定是否删除特定的告警事件
Relabel 和 Callback 功能其实之前版本就有了,这里不多介绍,新版本把它们封装成了事件管道处理器,使用上更便捷灵活。
重点是 Event Update 和 Event Drop 两个处理器,此次对我帮助很大。Event Update 用于附加一些额外信息到告警事件中,比如:把事件交给 AI 分析,得到一些结论性质的信息,附加到事件中;去 CMDB 查询一些元信息,附加到事件中。Event Drop 可以通过告警事件中的字段判断是否删除告警事件。
这些处理器是可以编排使用的,所以发版降噪的实现思路就有了。
分享一条近期思考:十年磨一剑,运维监控、可观测性领域创业,拼的是产品细节和交付迭代能力
新思路

通过 Event Update 处理器把告警事件发给发版外部接口。外部接口查询服务和环境所匹配的发版事件,如果查到近期发版,就给原始 event 事件标签添加 updating=true 字段(更好的实践可能是把附加信息放到 annotations 中);然后交给 Event Drop 处理器,Event Drop 查询到 updating=true 后丢弃告警。
特别说明一下,任何发版事件都应该可追溯。Jenkins、ArgoCD 等系统可以通过接口获取变更事件;没有 CICD 系统的,也可以通过命令查询服务启动时间来推断。我们有自研发版系统,所有发版事件都已存入 MySQL,所以查询起来更方便。此处的外部接口只要能获取到发版事件,并把判断结果附加到原始 event 中返回即可。
| 环节 | 输入 | 输出 |
|---|---|---|
| Event Update | 夜莺告警事件 | 带 updating=true 标签或保持原样的事件 |
| 发版系统接口 | 服务名、环境、规则名、主机等事件信息 | 是否命中最近发版记录 |
| Event Drop | Event Update 返回后的事件 | 丢弃发布期告警,或继续流转 |
发版静默实现
上面已经介绍了实现思路,这里展开介绍下实现流程。
1. 创建 Event Update 事件管道处理器

可以设置过滤条件。我这里设置为告警规则里包含 应用监控 的事件才匹配此规则。
重点是这个 /n9e_pipeline/cd_denoiser 接口的实现,下面再展开讲下,先往下配置。
2. 创建 Event Drop 事件管道处理器

Event Drop 的判断逻辑使用 go template 语法。如果最后渲染为 true,就会在此环节丢弃 event。
我这里的语法含义就是把带有 updating=true 标签的事件丢弃:
{{ if eq $labels.updating "true" }}true{{ else }}false{{ end }}
3. 修改或创建通知规则,关联事件管道处理器

在事件处理那栏添加事件处理,选择上面添加的 Event Update 和 Event Drop 处理器即可。夜莺会按顺序执行处理器。
4. 告警规则里选上调整后的通知规则

哪些应用层面的告警规则想要关联这个通知规则,就直接在告警规则里选择上面的通知规则。到这里,夜莺侧配置就完成了。
新版夜莺的“通知规则”、“通知媒介”、“消息模板”等一系列概念,使用很灵活便捷,理解之后能轻松配置各种想要的通知方式。但没有银弹,缺点就是对新手不友好,需要花时间理解下这里的流转逻辑,本文不作介绍,可参考官方文档。
5. Event Update 外部接口实现
下面具体讲下 Event Update 外部接口的实现。
Event Update 通过给原始 event 事件附加信息来更新事件,所以要先知道原始 event 长什么样。外部接口接收夜莺的 Event Update 事件,打印出 body 信息即是原始 event,如下:
{
"id": 4107,
"cate": "prometheus",
"cluster": "prometheus-skywalking",
"datasource_id": 2,
"group_id": 13,
"group_name": "🅰️应用监控|上游融资",
"hash": "ea724a85d3c27d17e006c3a837046151",
"rule_id": 1992,
"rule_name": "【应用监控|receive-server】JVM-GC次数大于100",
"rule_note": "",
"rule_prod": "metric",
"rule_algo": "",
"severity": 3,
"prom_for_duration": 180,
"prom_ql": "instance_jvm_young_gc_count{service='receive-server', layer='GENERAL'} > 100",
"rule_config": {
"algo_params": null,
"inhibit": false,
"prom_ql": "",
"queries": [
{
"prom_ql": "instance_jvm_young_gc_count{service='receive-server', layer='GENERAL'} > 100",
"recover_config": {
"judge_type": 0,
"recover_exp": ""
},
"severity": 3,
"unit": "milliseconds"
}
],
"severity": 0
},
"prom_eval_interval": 60,
"callbacks": [],
"runbook_url": "",
"notify_recovered": 1,
"target_ident": "SaaS-PRO",
"target_note": "",
"trigger_time": 1749722880,
"trigger_value": "106",
"trigger_values": "",
"trigger_values_json": {
"values_with_unit": {
"v": {
"value": 106,
"unit": "ms",
"text": "106.00 ms",
"stat": 106
}
}
},
"tags": [
"__name__=instance_jvm_young_gc_count",
"ident=SaaS-PRO",
"layer=GENERAL",
"rulename=【应用监控|receive-server】JVM-GC次数大于100",
"scope=ServiceInstance",
"service=receive-server",
"service_instance=instance1"
],
"tags_map": {
"__name__": "instance_jvm_young_gc_count",
"ident": "SaaS-PRO",
"layer": "GENERAL",
"rulename": "【应用监控|receive-server】JVM-GC次数大于100",
"scope": "ServiceInstance",
"service": "receive-server",
"service_instance": "instance1"
},
"annotations": {},
"is_recovered": false,
"last_eval_time": 1749722982,
"last_sent_time": 1749722982,
"notify_cur_number": 1,
"first_trigger_time": 1749722880,
"status": 0,
"claimant": "",
"sub_rule_id": 0,
"extra_info": null,
"rule_hash": "15efd67aa7a6c305a159f4b847ab256d",
"extra_info_map": null,
"notify_rule_ids": [
57
],
"notify_version": 0,
"notify_rules": null
}
可以看到,event 中有告警信息的完整详情。接下来要做的事情就很清晰:把关注的信息补充到 event 中,让告警继续发出,或供夜莺的后续事件处理器使用。类似思路也可以用于补充 CMDB 元信息、AI 分析结果等。
我这里做的就是查询发版系统对应服务对应环境的发版记录,5分钟内有发版则添加 updating=true 标签,认为需要静默。由于发版系统是Django写的,所以 Event Update 接口 /n9e_pipeline/cd_denoiser 也直接写在了发版系统的 Django 代码里,如下:
def cd_denoiser(request):
event = json.loads(request.body.decode('utf-8'))
logger.info("Received event: %s", request.body.decode('utf-8'))
# 提取n9e告警事件的信息,就是把上面打出的evnet body字段重新组织提取出重点信息
key_info = extract_key_info(event)
# 获取报警规则名、报警主机名
rule_name = key_info.get('rule_name', 'Unknown')
hostname = key_info.get('hostname', 'Unknown')
# 获取不到环境信息时,直接返回原事件
if hostname == 'Unknown':
return JsonResponse(event)
env = HOST_ENV_MAP.get(hostname, 'Unknown')
if env == 'Unknown':
return JsonResponse(event)
# 查询发版系统5分钟内相应发版事件
job_count = 0
if 'Dubbo掉线' not in rule_name:
# 提取出应用名称,如receive-server,如未知应用则不处理。
# rule_name格式如:【应用监控|receive-server】JVM-GC时间大于10s
app_name = rule_name.split('|')[1].split('】')[0] if '|' in rule_name else 'Unknown'
if app_name == 'Unknown':
return JsonResponse(event)
# 对应app_name,对应env,5分钟之内的发版数量
start_time = datetime.now() - timedelta(minutes=5)
job_count = tw_jobs.objects.filter(
job_servicename=app_name,
job_envno=env,
job_started__gte=start_time
).count()
else:
# dubbo掉线告警,查询下此环境5分钟内全部发版,有任何发版就丢掉
start_time = datetime.now() - timedelta(minutes=5)
job_count = tw_jobs.objects.filter(
job_envno=env,
job_started__gte=start_time
).count()
# 查询结果为0,说明无发版,直接返回原事件
if job_count == 0:
return JsonResponse(event)
# 查询结果不为0,说明有发版,打上标签
if 'tags_map' not in event:
event['tags_map'] = {}
event['tags_map']['updating'] = 'true'
if 'tags' not in event:
event['tags'] = []
event['tags'].append('updating=true')
logger.info(f"发现近期发版事件,标签更新为: {event['tags_map']}")
return JsonResponse(event)
其中这段代码直接查询发版系统的 MySQL,并根据不同告警规则做了一些定制化查询。因为我司所有发版记录都已经记录在 MySQL 里,所以可以直接查询。其他团队也可以对接 Jenkins、ArgoCD 等接口获取发版历史。
# 查询发版系统5分钟内相应发版事件
job_count = 0
if 'Dubbo掉线' not in rule_name:
# 提取出应用名称,如receive-server,如未知应用则不处理。
# rule_name格式如:【应用监控|receive-server】JVM-GC时间大于10s
app_name = rule_name.split('|')[1].split('】')[0] if '|' in rule_name else 'Unknown'
if app_name == 'Unknown':
return JsonResponse(event)
# 对应app_name,对应env,5分钟之内的发版数量
start_time = datetime.now() - timedelta(minutes=5)
job_count = tw_jobs.objects.filter(
job_servicename=app_name,
job_envno=env,
job_started__gte=start_time
).count()
else:
# dubbo掉线告警,查询下此环境5分钟内全部发版,有任何发版就丢掉
start_time = datetime.now() - timedelta(minutes=5)
job_count = tw_jobs.objects.filter(
job_envno=env,
job_started__gte=start_time
).count()
6. 实现效果
观察日志,发现外部接口服务打印出如下日志,说明已经附加了 'updating': 'true' 标签。该事件流转给 Event Drop 处理器后,发布期间的相关告警事件被成功静默。
2025-06-12 18:09:42,171 - n9e_pipeline - INFO - 发现近期发版事件,标签更新为: {'__name__': 'instance_jvm_young_gc_count', 'ident': 'SaaS-PRO', 'layer': 'GENERAL', 'rulename': '【应用监控|receive-server】JVM-GC次数大于100', 'scope': 'ServiceInstance', 'service': 'receive-server', 'service_instance': 'instance1', 'updating': 'true'}
如上便是整个发版时静默的实现逻辑。这个方案的关键点不是“删除告警”,而是在告警事件流转过程中识别发布上下文,让发布期噪声不再打扰值班人员。
FAQ
Q1:为什么不直接在 PromQL 里加发布状态条件? A:如果告警规则数量少,这样做可以。但在多团队、多应用、规则数量多的场景中,逐条修改 PromQL 成本高,也容易遗漏。Pipeline 方案把逻辑集中到了事件流转阶段。
Q2:Event Update 接口必须接发版系统吗? A:不一定。只要能判断当前服务和环境是否处于发布窗口即可,可以来自自研发版系统、Jenkins、ArgoCD,或者其他可追溯的变更记录。
Q3:updating=true 应该放在 labels 还是 annotations?
A:本文示例放在标签里,便于 Event Drop 判断。原文也提到,更好的实践可能是把附加信息放到 annotations 中;实际选择取决于后续处理器如何读取字段。