elasticctl_api/compose/
rule_health.rs1use super::{
4 AlertActivity, ExceptionCoverage, Part, RECENT_ALERTS, RuleSnapshot, alert_activity, degrade,
5 exception_coverage, snapshot_by_selector, validate_since,
6};
7use elasticctl_core::{ErrorKind, Result, Transport};
8use serde_json::Value;
9
10#[derive(Debug)]
11pub struct RuleHealth {
12 pub rule: RuleSnapshot,
13 pub alerts: Part<AlertActivity>,
14 pub exceptions: Part<ExceptionCoverage>,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum HealthFinding {
20 RuleDisabled,
21 NeverExecuted,
22 LastExecutionFailed,
23 LastExecutionPartialFailure,
24 ExecutionGap,
25 NoAlertsInWindow,
26 ExceptionListMissing,
27}
28
29pub async fn rule_health(t: &Transport, selector: &str, since: &str) -> Result<RuleHealth> {
33 validate_since(since)?;
34 let rule = snapshot_by_selector(t, selector).await?;
35 let rule_id = rule.rule.rule_id()?.to_owned();
36 let alerts = degrade(alert_activity(t, &rule_id, since, RECENT_ALERTS).await)?;
37 let exceptions = degrade(exception_coverage(t, &rule.rule).await)?;
38 Ok(RuleHealth {
39 rule,
40 alerts,
41 exceptions,
42 })
43}
44
45impl RuleHealth {
46 pub fn findings(&self) -> Vec<HealthFinding> {
49 let mut out = Vec::new();
50 let enabled = self
51 .rule
52 .rule
53 .as_map()
54 .get("enabled")
55 .and_then(Value::as_bool);
56 let execution = self.rule.execution.as_ref();
57 if enabled == Some(false) {
58 out.push(HealthFinding::RuleDisabled);
59 }
60 if enabled == Some(true) && execution.is_none() {
61 out.push(HealthFinding::NeverExecuted);
62 }
63 if let Some(e) = execution {
64 match e.status.as_str() {
65 "failed" => out.push(HealthFinding::LastExecutionFailed),
66 "partial failure" => out.push(HealthFinding::LastExecutionPartialFailure),
67 _ => {}
68 }
69 if e.metrics
70 .execution_gap_duration_s
71 .is_some_and(|gap| gap > 0.0)
72 {
73 out.push(HealthFinding::ExecutionGap);
74 }
75 }
76 if let Part::Ok(alerts) = &self.alerts
77 && alerts.total == 0
78 {
79 out.push(HealthFinding::NoAlertsInWindow);
80 }
81 if let Part::Ok(coverage) = &self.exceptions
82 && coverage
83 .lists
84 .iter()
85 .any(|row| matches!(&row.facts, Err(e) if e.kind == ErrorKind::NotFound))
86 {
87 out.push(HealthFinding::ExceptionListMissing);
88 }
89 out
90 }
91
92 pub fn partial(&self) -> bool {
94 matches!(self.alerts, Part::Unavailable(_))
95 || match &self.exceptions {
96 Part::Ok(coverage) => coverage.lists.iter().any(|row| row.facts.is_err()),
97 Part::Unavailable(_) => true,
98 Part::Skipped(_) => false,
99 }
100 }
101}