elasticctl_api/compose/
rule_snapshot.rs1use crate::{Rule, normalize, rules, selection};
2use elasticctl_core::{Error, ErrorKind, Result, Transport};
3use serde_json::Value;
4#[derive(Debug)]
5pub struct ExecutionMetrics {
6 pub total_search_duration_ms: Option<f64>,
7 pub total_indexing_duration_ms: Option<f64>,
8 pub total_enrichment_duration_ms: Option<f64>,
9 pub execution_gap_duration_s: Option<f64>,
10}
11#[derive(Debug)]
12pub struct LastExecution {
13 pub status: String,
14 pub date: String,
15 pub status_order: Option<i64>,
16 pub message: Option<String>,
17 pub message_truncated: bool,
18 pub metrics: ExecutionMetrics,
19}
20#[derive(Debug)]
21pub struct RuleSnapshot {
22 pub rule: Rule,
23 pub execution: Option<LastExecution>,
24}
25fn invalid() -> Error {
26 Error::new(ErrorKind::Http, "invalid rule execution response")
27}
28fn optional<T: serde::de::DeserializeOwned>(v: &Value, k: &str) -> Result<Option<T>> {
29 v.get(k)
30 .map(|v| serde_json::from_value(v.clone()).map_err(|_| invalid()))
31 .transpose()
32}
33fn snapshot(rule: Rule) -> Result<RuleSnapshot> {
34 let execution = if let Some(summary) = rule
36 .as_map()
37 .get("execution_summary")
38 .filter(|summary| !summary.is_null())
39 {
40 let e = summary
41 .get("last_execution")
42 .filter(|v| v.is_object())
43 .ok_or_else(invalid)?;
44 let status = e
45 .get("status")
46 .and_then(Value::as_str)
47 .ok_or_else(invalid)?
48 .to_owned();
49 let date = e
50 .get("date")
51 .and_then(Value::as_str)
52 .ok_or_else(invalid)?
53 .to_owned();
54 let mut message: Option<String> = optional(e, "message")?;
55 let mut message_truncated = false;
56 if let Some(m) = &mut message
57 && m.len() > super::MESSAGE_BYTES
58 {
59 let mut end = super::MESSAGE_BYTES;
60 while !m.is_char_boundary(end) {
61 end -= 1;
62 }
63 m.truncate(end);
64 message_truncated = true;
65 }
66 let empty = serde_json::json!({});
67 let metrics = e.get("metrics").unwrap_or(&empty);
68 if !metrics.is_object() {
69 return Err(invalid());
70 }
71 Some(LastExecution {
72 status,
73 date,
74 status_order: optional(e, "status_order")?,
75 message,
76 message_truncated,
77 metrics: ExecutionMetrics {
78 total_search_duration_ms: optional(metrics, "total_search_duration_ms")?,
79 total_indexing_duration_ms: optional(metrics, "total_indexing_duration_ms")?,
80 total_enrichment_duration_ms: optional(metrics, "total_enrichment_duration_ms")?,
81 execution_gap_duration_s: optional(metrics, "execution_gap_duration_s")?,
82 },
83 })
84 } else {
85 None
86 };
87 Ok(RuleSnapshot {
88 rule: normalize::canonical(&rule),
89 execution,
90 })
91}
92pub async fn snapshot_by_id(t: &Transport, rule_id: &str) -> Result<RuleSnapshot> {
93 let rule = rules::get(t, rule_id).await?;
94 if rule.rule_id()? != rule_id {
95 return Err(Error::new(ErrorKind::NotFound, "rule identity mismatch"));
96 }
97 snapshot(rule)
98}
99pub async fn snapshot_by_selector(t: &Transport, selector: &str) -> Result<RuleSnapshot> {
100 let (_, rule) = selection::to_rule(t, selector).await?;
101 snapshot(rule)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 fn recorded() -> Rule {
108 let value: Value = serde_json::from_str(include_str!(concat!(
109 env!("CARGO_MANIFEST_DIR"),
110 "/../../tests/fixtures/traditional-9.5.1/rules_get.json"
111 )))
112 .unwrap();
113 Rule::from_value(value["response"].clone()).unwrap()
114 }
115 #[test]
116 fn execution_decoder_and_character_boundary() {
117 let mut rule = recorded();
118 rule.as_map_mut().remove("execution_summary");
119 assert!(snapshot(rule.clone()).unwrap().execution.is_none());
120 rule.as_map_mut().insert("execution_summary".into(),serde_json::json!({"last_execution":{"status":"failed","date":"today","message":"界".repeat(1000),"metrics":{"unknown":"ignored","total_search_duration_ms":3}}}));
121 let execution = snapshot(rule.clone()).unwrap().execution.unwrap();
122 assert_eq!(execution.message.as_ref().unwrap().len(), 2046);
123 assert!(execution.message_truncated);
124 assert_eq!(execution.metrics.total_search_duration_ms, Some(3.0));
125 for field in ["status", "date"] {
126 let mut bad = rule.clone();
127 bad.as_map_mut()["execution_summary"]["last_execution"]
128 .as_object_mut()
129 .unwrap()
130 .remove(field);
131 assert_eq!(snapshot(bad).unwrap_err().kind, ErrorKind::Http);
132 }
133 for field in [
134 "total_search_duration_ms",
135 "total_indexing_duration_ms",
136 "total_enrichment_duration_ms",
137 "execution_gap_duration_s",
138 ] {
139 let mut bad = rule.clone();
140 bad.as_map_mut()["execution_summary"]["last_execution"]["metrics"][field] =
141 serde_json::json!("bad");
142 assert_eq!(snapshot(bad).unwrap_err().kind, ErrorKind::Http);
143 }
144 }
145}