Prometheus 告警恢复时如何获取实际值
在 Prometheus 监控系统中,告警事件中的 $value 展示的是告警触发时的数值。然而,当告警恢复时,Resolved 事件中的 $value 仍然显示的是告警触发时的值,而非恢复时的实际数值。这是为什么呢?是否有解决方法?
原理解析
告警规则配置在 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 推送的事件,还可能接收其他告警源的事件,过度耦合不利于系统架构。
-
Prometheus 的告警规则可以附加标签,这些标签与监控指标的标签一起作为事件的标签集发送给 Alertmanager。Alertmanager 需要根据这些标签查询 Prometheus 获取原始数据,这在某些场景下不可行:
- 需要从标签中剔除附加标签,只保留数据标签,但 Alertmanager 无法完成此操作
- 某些 PromQL 查询结果没有标签,无法查询
- Alertmanager 需要解析 PromQL 并移除阈值部分,但某些 PromQL 并非基于数字阈值
试图通过修改 Alertmanager 来解决这个问题是不现实的。
解决方案
有两种常用解决方案:
- 在告警规则中配置恢复时的 PromQL
- 将阈值从 PromQL 中移除,仅使用 PromQL 查询原始数据,然后在高层进行阈值判定
方案一:在告警规则中配置恢复时的 PromQL
以 Nightingale 监控系统为例,说明具体实现方法。核心是在两个地方进行配置:告警规则中配置恢复时的 PromQL,以及在告警模板中配置恢复值的渲染。
例如,有一个用于检测 HTTP 地址探测失败的告警规则:
需要在告警规则的自定义字段部分添加 recovery_promql 配置:
recovery_promql: http_response_result_code{target="{{.target}}"}
理解这一工作逻辑,我们先查看 http_response_result_code 指标的数据结构:
该指标包含多个时间序列,其中 agent_hostname 和 method 字段相同,target 字段用于区分不同时间序列。告警规则 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,则显示该值,保留4位小数。.AnnotationsJSON 是夜莺告警规则中的自定义字段部分,如果告警事件包含恢复时的值,会在此字段中体现。
方案二:将阈值从 PromQL 中移除
以 FlashDuty 为例,它支持在 PromQL 中不包含阈值的方式。我们重点讲解这种方法。
以 Memcached 的告警规则为例,查询条件中不写阈值,在判定规则中写阈值:
这种方式需要先查询当前值,然后进行阈值判定,因此在告警触发和恢复时都能获取当前值。这种方式非常直观,适用于大多数场景。但对于查询条件过滤出大量时间序列的场景,这种方式会查询大量数据,对告警引擎造成压力,需要谨慎使用。
如果您希望自己的监控系统能够在告警恢复时获取实际值,可以参考上述两种方法实现。