开源夜莺监控DIY发版时告警静默

基于夜莺监控 Pipeline、Event Update 和 Event Drop,把发版系统与告警事件联动,实现发布期间自动静默相关告警。

作者 网友投稿

年初时,我司已使用开源夜莺 v8 版本构建了公司级统一监控系统,但一直遗留了一个问题:发版时告警噪声太多。

据我们观察,50% 的告警噪声是由发版导致的,如果能够把监控系统和变更系统联动,发版时自动静默相关告警,那就太棒了。

发版时,通常是有人盯着服务仪表盘的,此时服务如果有问题可以及时发现。发版时的告警通知意义不大,所以我们一直想静默掉。

本文方案概览

本文介绍的做法不是修改每一条告警规则,而是在夜莺告警事件流转阶段做处理:

  1. Event Update 处理器把告警事件发送给发版系统接口。
  2. 发版系统根据服务名、环境和最近发版记录判断当前是否处于发布窗口。
  3. 如果命中发布窗口,接口给告警事件追加 updating=true 标签。
  4. 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 中;实际选择取决于后续处理器如何读取字段。

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

标签 夜莺监控
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云