Prometheus 告警恢复时,怎么获取恢复时的值?

Prometheus 告警恢复事件中的 `$value` 往往仍是最后一次告警触发值,而不是恢复时的最新值。原因在于 Prometheus 规则表达式带阈值时正常状态不返回数据,Alertmanager 生成 Resolved 事件时也不会反查 Prometheus。本文解释原理和两类解决方式。

作者 巴辉特

Prometheus 告警事件中的 $value 表示当前告警触发时的值,但是在告警恢复时,Resolved 事件中的 $value 仍然是最新告警时的值,并非是恢复时的值,这是什么原因和原理?是否有办法来解决呢?

不废话,先说原理。

核心摘要

  • Prometheus 告警恢复时拿不到恢复时的 $value,根因是带阈值的 PromQL 在恢复状态下通常不返回数据。
  • Resolved 事件由 Alertmanager 根据 resolve_timeout 生成,Alertmanager 会沿用上次告警的标签和注解,不会再查询 Prometheus 获取最新值。
  • 让 Alertmanager 反查 Prometheus 在职责边界、标签还原、PromQL 解析等方面都很难成立。
  • 常见解决办法有两类:在告警规则里额外配置恢复时查询用的 PromQL,或者把阈值从 PromQL 中拿掉,由上层告警引擎做阈值判定。
  • Nightingale 示例使用 recovery_promql 获取恢复值;Flashduty 示例展示了查询条件不带阈值、判定规则单独配置阈值的方式。

原理

告警规则是配置在 prometheus.yaml 中的,由 Prometheus 负责做规则判定。Prometheus 规则判定的逻辑很直接:周期性拿着 PromQL 去查询数据,如果查到了数据,并且连续多次满足 for 指定的时长,就触发告警事件;如果查不到数据,就认为指标是正常健康的状态。比如:

cpu_usage_idle < 5

如上例,告警规则的 promql 中是带有阈值(< 5)的,所以只要查到了数据,就说明当前的值小于 5,只要没查到数据,就说明当前的值大于等于 5,即当前数据是健康的状态。注意,查不到数据的时候,时序库不返回数据,换句话说,数据正常的时候,因为时序库不返回数据,上层就拿不到正常状态时的值,既然拿不到正常状态时的值,也就没法在恢复时展示当前最新的值啦。

实际上,恢复时的事件是 Alertmanager 根据 resolve_timeout 生成的,而不是 Prometheus 再生成一个携带恢复值的新事件。Alertmanager 生成恢复事件时,会把上次告警的标签和注解带过去。值呢?就是上次告警时的值,Alertmanager 不会再去查询 Prometheus 拿最新值。

Alertmanager 可以拿到恢复时的值么?

坦白讲,很难。Alertmanager 需要根据上次告警的标签和注解去查询 Prometheus 拿到上次告警时的值,Alertmanager 不会这么干的,核心是:

  • 从职能上,Alertmanager 去查询 Prometheus,就反向依赖了,Alertmanager 是告警的分发中心,不止是接收 Prometheus 推送过来的事件,还会接收其他告警源推送过来的事件,如果 Alertmanager 要去查询 Prometheus,那就耦合太过严重。
  • Prometheus 的告警规则中可以附加标签,和监控指标的标签一起,作为事件的标签集发给 Alertmanager,Alertmanager 需要根据这些标签去查询 Prometheus,拿到原始数据,这在某些场景下是不可行的。一个是要把标签中的附加标签剔除,只留数据标签,Alertmanager 没有办法做到;其次,有些 promql 查询结果压根就没有标签,根本没法查;再次,Alertmanager 需要解析 promql,把阈值的部分拿掉,而有些 promql 压根就不是数字阈值。

如果你想通过修改 Alertmanager 达成所愿,放弃吧。

有没有办法解决?

有。通常的解决办法有两种:

  • 告警规则里,顺便配置恢复时的 promql
  • 把阈值从 promql 中拿掉,promql 只用于查询原始数据,然后在上层做阈值判定,不管监控指标当前是否健康,都会查到原始数据

下面我以一些监控产品为例,具体来说明。

告警规则里配置恢复时的 promql

使用这个方法的产品,以夜莺(Nightingale)监控为例,来讲解具体做法。核心是配置两个地方:一个是在告警规则里配置恢复时的 PromQL,另一个是在告警模板里配置恢复时值的渲染。

比如我有一个告警规则用来侦测 HTTP 地址探测失败:

夜莺告警恢复时拿到恢复时的值-告警时的promql

需要在告警规则最下面的自定义字段里,增加 recovery_promql 的配置,如下:

recovery_promql

要理解这个工作逻辑,我们先来看看 http_response_result_code 这个指标的数据长什么样子:

夜莺告警恢复时拿到恢复时的值-即时查询

从上图可以看出,这个指标包含两个 series,其中 agent_hostname 和 method 字段相同,target 字段可以区分开这俩 series。告警规则 http_response_result_code != 0 如果触发,告警事件中一定会带有 target 标签。所以,告警事件恢复时,用告警时的 target 标签去查询,就可以准确查到恢复时的值。recovery_promql 的配置中引用了 target 标签,其值是变量,这个变量就是告警事件中的 target 标签值。

然后,我们在告警模板里,增加一个恢复时的值的渲染,以钉钉模板为例:

#### {{if .IsRecovered}}<font color="#008800">💚{{.RuleName}}</font>{{else}}<font color="#FF0000">💔{{.RuleName}}</font>{{end}}

---
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}
- **告警级别**: {{.Severity}}级
{{- if .RuleNote}}
- **规则备注**: {{.RuleNote}}
{{- end}}
{{- if not .IsRecovered}}
- **当次触发时值**: {{.TriggerValue}}
- **当次触发时间**: {{timeformat .TriggerTime}}
- **告警持续时长**: {{humanizeDurationInterface $time_duration}}
{{- else}}
{{- if .AnnotationsJSON.recovery_value}}
- **恢复时值**: {{formatDecimal .AnnotationsJSON.recovery_value 4}}
{{- end}}
- **恢复时间**: {{timeformat .LastEvalTime}}
- **告警持续时长**: {{humanizeDurationInterface $time_duration}}
{{- end}}
- **告警事件标签**:
{{- range $key, $val := .TagsMap}}
{{- if ne $key "rulename" }}
  - `{{$key}}`: `{{$val}}`
{{- end}}
{{- end}}

这里最为关键的逻辑是判断 .AnnotationsJSON.recovery_value 的逻辑:

{{- if .AnnotationsJSON.recovery_value}}
- **恢复时值**: {{formatDecimal .AnnotationsJSON.recovery_value 4}}
{{- end}}

如果 .AnnotationsJSON 中包含 recovery_value 就展示,展示的时候把 recovery_value 保留 4 位小数。这个 .AnnotationsJSON 是夜莺告警规则中的自定义字段部分,如果告警事件中有恢复时的值,就会在这个字段中体现。

最终效果如下:

夜莺告警恢复时拿到恢复时的值-最终效果

把阈值从 promql 中拿掉,promql 只用于查询原始数据

使用这个做法的产品,以 Flashduty 举例,Flashduty 不但支持类似夜莺这样的配置 recovery_promql 的方式,还支持 promql 中不带阈值的方式。我们重点讲解 promql 中不带阈值的方式。

以 Memcached 的某个告警规则举例,查询条件里不写阈值,判定规则里写阈值,如下图所示:

夜莺告警恢复时拿到恢复时的值-Flashduty规则

这种方式需要先查到当前值,再拿着当前值去做判定,所以不管是告警时还是恢复时,都可以拿到当前值。这种方式非常直观,大部分场景都适用。边界也很清楚:如果一个查询条件过滤到很多时序,这种方式会查到特别多的数据,对告警引擎也是压力,需要斟酌。

如果你也想让自己的监控系统支持在恢复时拿到恢复时的值,可以参考上述两种方式。

结论

Prometheus 告警恢复时 $value 不是恢复时的最新值,不是一个简单的模板问题,而是 Prometheus 告警判定和 Alertmanager 事件分发职责边界共同决定的结果。想解决这个问题,要么在规则层面额外提供恢复时查询用的 PromQL,要么在告警产品里把“查询原始值”和“阈值判定”拆开,让告警和恢复两个阶段都能拿到当前值。

FAQ

Q1:为什么恢复时 $value 还是最后一次告警值?

A:因为恢复事件由 Alertmanager 生成,它会沿用上次告警的标签和注解,不会重新查询 Prometheus 获取恢复时的最新值。

Q2:Prometheus 为什么正常时不返回恢复值?

A:如果告警 PromQL 写成 cpu_usage_idle < 5,恢复状态意味着查询结果为空。时序库不返回数据,上层自然拿不到正常状态下的当前值。

Q3:能不能改 Alertmanager,让它恢复时查 Prometheus?

A:很难。Alertmanager 是告警分发中心,可能接收多个告警源;让它反查 Prometheus 会造成职责耦合,还会遇到标签还原和 PromQL 阈值解析等问题。

Q4:哪种解决方案更通用?

A:把阈值从 PromQL 中拿掉、由上层告警引擎做判定的方式更直观,但在查询命中大量时序时会增加告警引擎压力;recovery_promql 更适合能明确用标签定位恢复值的规则。

联系我们交流

延伸路径

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

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

标签 Prometheus
快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云