
作者来自 Elastic Jeffrey Rengifo分组键决定一个 incident 会产生多少个 alerts而由于 Kibana 会为每个 alert document 添加 rule revision你可以针对最初导致这些 alerts 的同一个 incident衡量 alert 噪声的减少程度。在用户发现问题之前就发现问题。从 synthetic monitoring 和 SLO 文档 开始。你也可以立即开始 Elastic Cloud 免费试用。一次 payments 平台上的糟糕部署在 7 分钟内开启了 18 个 alert episodes而它们全部来自同一个 incident。一个校准不当的 rule 在仅 7 个 alert instances 中产生了其中 14 个 episodes。episode 持续时间的中位数为 1 分钟因此其中大多数在任何人能够打开页面之前就已经恢复了。这是一次通过查询完成的 alert 噪声减少练习。Kibana 会将每个 alert 写入一个隐藏 index并在 alert 恢复后继续保留在那里因此我们使用 ES|QL 向这个 alert history 提出三个问题这是一个 incident 还是多个 incidents它从哪里开始以及哪个 rule 产生的噪声多于信号然后我们更改产生噪声的 rule 的 grouping key并重新执行相同的故障此时相同的查询返回一个持续 7 分钟的 episode。Kibana 会为每个 alert document 添加 rule revision因此这次调优更改前后的数据都位于同一个 index 中。最后一个 Agent Builder agent 会针对相同的 alert history 回答相同的问题。下面的所有内容都在 Elastic Cloud 上的 Elasticsearch 和 Kibana 9.5.2以及 OpenTelemetry Collector Contrib 0.157.0 上进行了测试。前置条件Elasticsearch 和 Kibana 9.5 或更高版本运行在 Elastic Cloud 或 自托管 环境中。具有创建和编辑 Elasticsearch query rules 和 Observability custom threshold rules 的 Kibana 权限以及对.alerts-*indices 的读取权限。OpenTelemetry Collector Contrib 0.157.0 或更高版本以及一个具有 OTLP endpoint ingest 权限的 Elasticsearch API key。已启用 Agent Builder仅最后一节需要。此前的所有内容都是纯 ES|QL无需 Agent Builder 即可运行。为什么 alert history 胜过 alert notifications通知回答的是一个短暂的问题现在谁应该关注这个问题而大多数能够让 alerting 变得更好的问题都会在之后出现并且它们都是关于数据的问题18 个 alert episodes 代表 18 个问题还是从 18 个角度评估的同一个 incident哪个 service 最先出现症状故障是否沿着依赖链传播哪些 rules 会产生没人能够采取行动的短暂、重复 episodes上个月调整 threshold 后是否真的减少了噪声在 Elastic 内部这种思路有一个名称alerts as data。其理念是每个 alert 和每次状态转换都是时间中的一个事实以明确的语义进行存储因此 correlation 可以通过跨 streams 的查询来完成而不需要专门开发的功能。这也符合实际运维渠道中出现的情况一次基础设施故障可以在几秒钟内开启数十个 alerts而阈值校准不当导致的 alerts flapping 也是造成页面疲劳的常见来源。这两个问题都很难通过 notifications 解决但通过保留下来的 alert history 进行研究则很直接。Kibana alerting 在每个 alert document 中存储的内容Kibana alerting rules 会将 alerts-as-data documents 写入隐藏 indices你可以像查询任何其他 index 一样对它们进行查询。本文使用的两个 aliases 分别是.alerts-stack.alerts-default用于 Elasticsearch query rules和.alerts-observability.threshold.alerts-default用于 Observability custom threshold rule。我们将存储单元称为 episode一个 document覆盖一个 alert instance 从变为 active 到恢复的整个过程。调查时需要关注的字段字段它告诉你的信息kibana.alert.instance.id此 episode 所属的 grouping-key 值kibana.alert.start、kibana.alert.endepisode 开始和恢复的时间kibana.alert.duration.us它保持 active 的时长kibana.alert.statusactive或recoveredkibana.alert.flapping、kibana.alert.flapping_historyKibana 是否将其标记为状态快速变化kibana.alert.rule.name、kibana.alert.rule.revision使用的是哪个 rule以及哪个版本的 rulekibana.alert.grouping结构化的 grouping 值例如service.namekibana.alert.reason触发时人类可读的条件kibana.alert.url返回 rule 数据视图的深层链接有两个属性让本文其余部分成为可能。首先同一个 instance 的新 episode 会创建一个新的 document因此统计 documents 就是在统计 episodes。alert documents 和 notifications 是解耦的一个 episode 是否也会向某人发送 page取决于附加到 rule 的 actions 以及它们的 frequency可以是每次检查时、仅在状态发生变化时或者按照自定义 interval 发送 summary。我们的三个 rules 都没有附加 actions因此在实验过程中没有人收到 page从这里开始我们统计的所有内容都是 alert document。其次具有 ECS 名称的 grouping fields例如service.name会被复制到 alert document 的顶层这意味着 alert history 可以与其他 indices 进行 join。在开始查询之前还有一个注意事项alert indices 很宽。此部署中的 alerts index 大约映射了 1,400 个 fields因此本文中的每个 ES|QL 查询都会使用KEEP投影出一组较窄的 columns你也应该这样做尤其是在将结果提供给 LLM 的 tools 中。由于这些是隐藏的 system indicesDiscover editor 并不总能检查到它们的 fields因此它可能会为查询显示 warning甚至显示 error badge同时仍然执行查询并返回 rows。请相信工具栏中的结果数量而不是 badge。还有一点需要说明下面的 Kibana screenshots 会使用浏览器的 local time 渲染 timestamps本实验中为 UTC-5而正文中的 query result tables 使用 UTC。实验一次糟糕的部署如何产生 alert storm为了生成一个真实的 storm我们需要的是一组以链式方式发生故障的 services而不是一次单独的 threshold breach。场景是这样的fraud-scorer2.3.0 由于 model loading 缓慢而上线payments queue 开始堆积payment-gatewayrequests 开始超时而客户在payments-web上看到 checkout 失败。每个 service 都会写入结构化 JSON logs并且一个 OpenTelemetry Collector 使用filelogreceiver 对这些 logs 进行 tail然后通过 OTLP/HTTP 使用 API key 将它们发送到部署的 Elastic OpenTelemetry endpoint。receivers: filelog/payment_gateway: include: - ${env:LAB_DIR}/events/payment-gateway.jsonl start_at: end resource: service.name: payment-gateway service.namespace: payments service.version: 5.1.2 cloud.region: us-central1 operators: - id: parse_json type: json_parser parse_from: body - id: parse_severity type: severity_parser parse_from: attributes.severity_text - id: use_message_as_body type: move from: attributes.message to: body exporters: otlphttp/elasticsearch: endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT} headers: Authorization: ApiKey ${env:ES_API_KEY}一个 transform processor 会将所有内容路由到一个 data streamlogs-payments.stormlab-default。这些 records 最终包含service.name、log.level、labels.*下的自定义字符串 attributes以及numeric_labels.*下的数值 attributes。三个 services 的完整 collector 配置以及一个可以复现整个实验的 notebook都位于companion repository。我们还会加载一个稍后将发挥很大作用的小型 indexservice catalog。PUT payments_service_catalog { settings: { index.mode: lookup }, mappings: { properties: { service.name: { type: keyword }, team: { type: keyword }, tier: { type: keyword }, depends_on: { type: keyword } } } }三个 documents 将每个 service 映射到其负责的 team、tier 和上游 dependencyfraud-scorer依赖payments-queuepayment-gateway依赖fraud-scorer而payments-web依赖payment-gateway。POST payments_service_catalog/_bulk { index: {} } { service.name: fraud-scorer, team: risk-ml, tier: backend, depends_on: payments-queue } { index: {} } { service.name: payment-gateway, team: payments-core, tier: edge, depends_on: fraud-scorer } { index: {} } { service.name: payments-web, team: storefront, tier: frontend, depends_on: payment-gateway }index.mode: lookup使我们稍后能够从 ES|QL 执行LOOKUP JOIN。grouping keys 如何决定你会获得多少个 alerts这个 storm 由三个 alerting rules 进行观测而它们的 grouping keys 才是本文真正关注的主题。RuleRule typeGrouping keyWindowThresholdgateway-5xx-per-endpoint-statusElasticsearch query ruleES|QL 模式labels.endpoint、labels.status_code、cloud.region1 分钟3 个或更多 errorsfraud-scorer-queue-lagElasticsearch query ruleES|QL 模式service.name、cloud.region1 分钟最大 lag 为 30 秒或更长payments-error-rate-per-serviceObservability custom threshold ruleservice.name2 分钟超过 5 个ERRORdocuments第一个 rule 是故意校准不当的采用了一种我们都曾在某个时候上线过的配置在 1 分钟的 window 内按照 endpoint 和 status code 对 gateway errors 进行分组并设置了较低的 threshold。FROM logs-payments.stormlab* | WHERE service.name payment-gateway AND log.level ERROR | STATS error_count COUNT(*) BY labels.endpoint, labels.status_code, cloud.region | WHERE error_count 3它作为 ES|QL 模式下的 Elasticsearch query rule 每分钟运行一次并且每个返回的 row 都会成为一个 alert instance因此/api/payments,503,us-central1和/api/payments/confirm,504,us-central1会分别触发 alert。第二个 rule 直接监控第一个多米诺骨牌fraud queue 上的 consumer lag由 service 通过numeric_labels.queue_lag_seconds报告。FROM logs-payments.stormlab* | WHERE service.name fraud-scorer AND numeric_labels.queue_lag_seconds IS NOT NULL | STATS max_lag_seconds MAX(numeric_labels.queue_lag_seconds) BY service.name, cloud.region | WHERE max_lag_seconds 30第三个 rule 是一个 Observability custom threshold rule在两分钟内超过 5 个ERRORdocuments并按照service.name进行分组。它代表合理的默认配置每个受影响的 service 保持一个稳定的 alert instance。它还演示了不同类型的 rules 可以为同一个调查提供数据因为它们都会写入 alerts-as-data documents。三个 rules 都带有alerts-as-data-v2标签这就是下面每个查询隔离它们的方式。Rules 上的 tags 并不是装饰性的kibana.alert.rule.tags会存储在每个 alert document 中因此一个 tag 实际上就是 alert history 中一个可查询的数据集标签。重放 incident7 分钟内产生 18 个 alert episodes我们重放这个 incident3 分钟的健康基线、2.3.0 部署、queue lag 超过 threshold、持续 6 分钟不断轮换的 gateway 5xx bursts、用户可见的 errors然后回滚并恢复。storm 期间的 Alerts 页面看起来就像 pager 当时感受到的那样。注意这个表格而不仅仅是数量。相同的 instance IDs 会在几分钟内同时以Active和Recovered出现这就是你可以看到、但还无法衡量的 churn。当一切尘埃落定后保留下来的 history 可以在 Discover 的 ES|QL 模式中进行查询。storm 的原始材料也就是 error logs 本身证实了这种级联形态FROM logs-payments.stormlab* | WHERE labels.incident_id payments-brownout-20260722 AND log.level ERROR | STATS errors COUNT(*) BY minute DATE_TRUNC(1 minutes, timestamp), service.name | SORT minute![每分钟按 service 统计 error 数量的 Discover ES|QL 视图首先是 fraud-scorer errors然后是 payment-gateway 每分钟 7 个最后是 payments-web]3现在我们不再关注 logs而只分析 alert history。如何判断一次 alert storm 是一个 incident 还是多个 incidentsFROM .alerts-* | WHERE kibana.alert.rule.tags alerts-as-data-v2 | STATS episodes COUNT(*), alert_instances COUNT_DISTINCT(kibana.alert.instance.id), first_episode MIN(kibana.alert.start), last_episode MAX(kibana.alert.start) BY rule kibana.alert.rule.name | SORT episodes DESCruleepisodesalert_instancesfirst_episodelast_episodegateway-5xx-per-endpoint-status14716:36:3816:42:38payments-error-rate-per-service3316:36:4116:37:41fraud-scorer-queue-lag1116:35:3516:35:3518 个 episodes、一个 incident而且每一个都在 7 分钟的时间窗口内开启。仅 gateway rule 就产生了 18 个 episodes 中的 14 个同时只跟踪了 7 个不同的 instances这意味着其中一半的 episodes 都是在某个刚刚恢复的 instance 上重新开启的。这一行数据将一个通常停留在主观描述层面的抱怨量化了这个 rule 触发得比它提供的帮助更多。如何使用 ES|QL 找出哪个 service 最先发生故障由于 grouping fields 使用的是 ECS 名称每个 episode 都会在顶层携带service.name因此我们可以将 alert history 与 service catalog 进行 join。FROM .alerts-* | WHERE kibana.alert.rule.tags alerts-as-data-v2 AND service.name IS NOT NULL | STATS first_symptom MIN(kibana.alert.start) BY service.name | LOOKUP JOIN payments_service_catalog ON service.name | KEEP first_symptom, service.name, team, tier, depends_on | SORT first_symptomfirst_symptomservice.nameteamtierdepends_on16:35:35fraud-scorerrisk-mlbackendpayments-queue16:36:41payment-gatewaypayments-coreedgefraud-scorer16:37:41payments-webstorefrontfrontendpayment-gatewayalert 的顺序与 dependency chain 完全一致第一个 symptom 属于 risk-ml team66 秒后 edge 出现 degraded又过了 1 分钟客户才发现问题。从这个表格中还可以得出一个更微妙的结论。最早产生 alert 的 service 依赖于payments-queue而payments-queue从未产生 alert因此最可能的真正 origin 位于第一个可见 symptom 的上游一跳而这正是 post-incident review 应该开始查看 logs 的地方。还要注意这个分析中缺少哪个 rule按 endpoint 和 status code 分组的 noisy gateway rule因此它的 episodes 不包含service.name无法参与 topology join。Grouping key 是你未来进行调查时的数据契约。如何评估哪个 alerting rule 的噪声最大一个 noise scorecard 查询可以将 rule quality 汇总到一个表格中。FROM .alerts-* | WHERE kibana.alert.rule.tags alerts-as-data-v2 | EVAL minutes_active COALESCE(kibana.alert.duration.us, 0) / 60000000.0 | STATS episodes COUNT(*), alert_instances COUNT_DISTINCT(kibana.alert.instance.id), flapping_episodes SUM(CASE(kibana.alert.flapping true, 1, 0)), median_minutes_active MEDIAN(minutes_active) BY rule kibana.alert.rule.name, revision kibana.alert.rule.revision | SORT episodes DESCrulerevisionepisodesinstancesflappingmedian minutes activegateway-5xx-per-endpoint-status014701.0payments-error-rate-per-service03307.0fraud-scorer-queue-lag01109.0先看最后一列。1 分钟的 median active time 意味着 gateway rule 的典型 episode 在 on-call 甚至来得及登录之前就已经自行恢复而两个 grouping 合理的 rules 产生的 episodes 持续时间足够长可以代表实际的 incident。flapping列是 Kibana 针对同一个问题的处理方式而它属于事后补救。真正的修复发生在上游也就是 rule definition 中而 alert history 刚刚已经准确告诉我们需要修改什么grouping key 制造了 instances而 1 分钟的 window 制造了重新开启。减少 alert 噪声修复 rule 并验证它是否生效rule 自己的 detail page 已经总结了这个问题24 小时内有 14 个 alerts它们全部来自同一个 incident而产生这些 alerts 的正是右侧显示的 definition。我们更新 rule而不是替换它将 grouping 更改为service.name将 window 扩大到 5 分钟并提高 threshold。FROM logs-payments.stormlab* | WHERE service.name payment-gateway AND log.level ERROR | STATS error_count COUNT(*) BY service.name, cloud.region | WHERE error_count 10以原地方式更新 rule 很重要原因只有一个Kibana 会为每个 alert document 添加kibana.alert.rule.revision因此调优更改前后的数据可以在同一个 index 中进行区分而无需我们自己做任何记录。然后我们重放同一个故障的压缩版本相同的部署、相同的 lag 上升过程、相同的 gateway bursts 轮换、相同的客户影响在 7 分钟后回滚。scorecard 查询无需修改现在 revision 列体现出了它的价值rulerevisionepisodesinstancesflappingmedian minutes activegateway-5xx-per-endpoint-status014701.0payments-error-rate-per-service06306.0fraud-scorer-queue-lag02108.0gateway-5xx-per-endpoint-status11107.0未发生更改的 rules 现在会在各自的 rows 中同时显示两个 incidents即 6 个和 2 个 episodes这正是稳定的 rules 应该表现出的情况。gateway rule 被拆分成两行因为 revision 发生了变化而这种对比用 4 个数字就完整呈现了本文的核心论点。相同的故障形态从 14 个 episodes 变成了 1 个 episode而现在 episode 的中位持续时间从 1 分钟变成了 7 分钟因此它覆盖了整个 incident而不再只是其中的一小部分。这正是 alerting 工作中通常依赖经验判断的部分而在这里它变成了一个查询结果我们使用相同的 ES|QL在相同的 index 中针对相同的 incident 模式验证了这次调优更改。这个循环中的任何部分都不是我们的 lab rule 所特有的。任何为其 alerts 添加 tag 的 rule 都可以通过这种方式进行评估、调优和重新评估从而将 alert 调优从一种主观意见转变为一种可量化的测量。使用 Agent Builder agent 查询 alert history上面的每个查询都是确定性的因此非常适合作为 AI agent 的 tools。我们在 Agent Builder 中注册 3 个 ES|QL tools每个 tool 都是本文某个查询的参数化版本并带有明确的KEEP投影然后将它们连接到一个名为 Alert Historian 的 agent。POST kbn:/api/agent_builder/tools { id: alert_noise_scorecard, type: esql, description: Noise scorecard per alerting rule and revision: episodes, distinct instances, flapping episodes, median minutes active., configuration: { query: FROM .alerts-* | WHERE kibana.alert.rule.tags \alerts-as-data-v2\ AND DATE_DIFF(\hours\, kibana.alert.start, NOW()) ?lookback_hours | EVAL minutes_active COALESCE(kibana.alert.duration.us, 0) / 60000000.0 | STATS episodes COUNT(*), alert_instances COUNT_DISTINCT(kibana.alert.instance.id), flapping_episodes SUM(CASE(kibana.alert.flapping true, 1, 0)), median_minutes_active MEDIAN(minutes_active) BY rule kibana.alert.rule.name, revision kibana.alert.rule.revision | KEEP rule, revision, episodes, alert_instances, flapping_episodes, median_minutes_active | SORT episodes DESC, params: { lookback_hours: { type: integer, description: How many hours of alert history to analyze, for example 6 } } } }agent 的 instructions 定义了术语一个 episode 就是一个 alert document、方法先看 timeline然后看 propagation最后看 scorecard以及诚实性规则报告 tool output 中的 numbers 和 timestamps绝不编造 alerts。我们向它提出一个问题过去 3 小时发生了什么这是一个 incident 还是多个 incidents第一个 symptom 由哪个 team 负责以及 gateway rule 的调优是否减少了噪声agent 调用了全部 3 个 tools并正确理解了数据结构它将 history 分成两个中间存在安静间隔的 storms每个 storm 对应一轮故障而不是将它们混合成一个 event。它将第一个 symptom 归因于 risk-ml team复现了关于从未产生 alert 的 queue 的上游推断然后通过 revision table 回答了调优问题。它对这次修复的结论读起来就像一条有数据支撑的 review commentrevision 0 有 14 个 episodes而 revision 1 只有 1 个稳定的 episode同时 episode 的中位持续时间从 1 分钟变成了 7 分钟。agent 并没有做任何我们手动无法完成的事情。这正是关键所在由于 alerts 是具有明确语义的持久化结构化数据language model 可以针对 patterns 和 histories 进行推理而不是对单个 notification 做出反应并且它陈述的每一个数字都可以追溯到一个可以重新运行的 tool call。在生产环境中运行 alert history 查询在生产环境中依赖 alert history 之前需要考虑以下几点。Retention 是一个学习窗口的决策Alert indices 遵循 ILM policy默认保留的数据远多于本实验所需的几个小时但如果你希望分析不同季度之间的 rule quality trends就应该有意识地让 policy 与这一目标保持一致。严格限制 alert index 的读取权限范围Alert documents 中包含 rule parameters、reasons 和 deep links因此应像对待其他 operational datasets 一样对待.alerts-*的读取权限并授予能够满足需求的最窄 index patterns例如仅授予 observability alert aliases。Grouping keys 是数据契约在BY子句中只要存在 ECS field names 就使用它们保持 cardinality 有界并且绝不要将 secrets 或 personal data 放入 grouping values因为它们会成为kibana.alert.instance.id并在 retention window 内持续存在。使用KEEP积极投影 columnsAlert indices 中大约映射了 1,400 个 fields因此不进行 projection 的 query 对人类来说很难处理在 LLM tools 中则会造成实际危害所以每个可复用的 query 都应该以KEEP结束。Alert storms 也会给 automation 带来压力如果一个 rule 每个 alert 都触发一次 workflow那么 14 个 episodes 就意味着 14 次执行相同工作的 executions这也是应该让 automation 面向聚合后的 alert dataset而不是面向每个 notification 的另一个理由。从你自己噪声最大的 rule 开始本文中的实验是有意设计得很小但其中每一个部分都可以推广。使用 scorecard query 对你实际噪声最大的 rule 进行评估修复它的 grouping 或 window然后让kibana.alert.rule.revision告诉你这次修复是否保持有效。将你的 alert history 与现有的任何 catalog 进行 joinlookup index 中的一份 CMDB export 就足够了而 propagation order 就可以通过 query 得到。如果你使用 Agent Builder可以将最常用的两三个 investigation queries 封装成 tools那么下一次 storm review 就可以从一次对话开始而不是面对一整面 notification。companion notebook构建了本文使用的全部内容catalog index、log data stream、三个 rules、两次 incident replays、queries以及 Agent Builder tools 和 agent。整个过程大约需要 25 分钟因为 rules 每分钟评估一次而 episode durations 就是我们要获取的数据。Notification 会告诉你出了问题。Alert history 会告诉你 detection 的表现如何而它已经存在于一个 index 中正等待被查询。原文https://www.elastic.co/observability-labs/blog/esql-alert-noise-reduction