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
35 .as_map()
36 .get("execution_summary")
37 .filter(|summary| !summary.is_null())
38 {
39 let e = summary
40 .get("last_execution")
41 .filter(|v| v.is_object())
42 .ok_or_else(invalid)?;
43 let status = e
44 .get("status")
45 .and_then(Value::as_str)
46 .ok_or_else(invalid)?
47 .to_owned();
48 let date = e
49 .get("date")
50 .and_then(Value::as_str)
51 .ok_or_else(invalid)?
52 .to_owned();
53 let mut message: Option<String> = optional(e, "message")?;
54 let mut message_truncated = false;
55 if let Some(m) = &mut message
56 && m.len() > super::MESSAGE_BYTES
57 {
58 let mut end = super::MESSAGE_BYTES;
59 while !m.is_char_boundary(end) {
60 end -= 1;
61 }
62 m.truncate(end);
63 message_truncated = true;
64 }
65 let empty = serde_json::json!({});
66 let metrics = e.get("metrics").unwrap_or(&empty);
67 if !metrics.is_object() {
68 return Err(invalid());
69 }
70 Some(LastExecution {
71 status,
72 date,
73 status_order: optional(e, "status_order")?,
74 message,
75 message_truncated,
76 metrics: ExecutionMetrics {
77 total_search_duration_ms: optional(metrics, "total_search_duration_ms")?,
78 total_indexing_duration_ms: optional(metrics, "total_indexing_duration_ms")?,
79 total_enrichment_duration_ms: optional(metrics, "total_enrichment_duration_ms")?,
80 execution_gap_duration_s: optional(metrics, "execution_gap_duration_s")?,
81 },
82 })
83 } else {
84 None
85 };
86 Ok(RuleSnapshot {
87 rule: normalize::canonical(&rule),
88 execution,
89 })
90}
91pub async fn snapshot_by_id(t: &Transport, rule_id: &str) -> Result<RuleSnapshot> {
92 let rule = rules::get(t, rule_id).await?;
93 if rule.rule_id()? != rule_id {
94 return Err(Error::new(ErrorKind::NotFound, "rule identity mismatch"));
95 }
96 snapshot(rule)
97}
98pub async fn snapshot_by_selector(t: &Transport, selector: &str) -> Result<RuleSnapshot> {
99 let (_, rule) = selection::to_rule(t, selector).await?;
100 snapshot(rule)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 fn recorded() -> Rule {
107 let value: Value = serde_json::from_str(include_str!(concat!(
108 env!("CARGO_MANIFEST_DIR"),
109 "/../../tests/fixtures/traditional-9.5.1/rules_get.json"
110 )))
111 .unwrap();
112 Rule::from_value(value["response"].clone()).unwrap()
113 }
114 #[test]
115 fn execution_decoder_and_character_boundary() {
116 let mut rule = recorded();
117 rule.as_map_mut().remove("execution_summary");
118 assert!(snapshot(rule.clone()).unwrap().execution.is_none());
119 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}}}));
120 let execution = snapshot(rule.clone()).unwrap().execution.unwrap();
121 assert_eq!(execution.message.as_ref().unwrap().len(), 2046);
122 assert!(execution.message_truncated);
123 assert_eq!(execution.metrics.total_search_duration_ms, Some(3.0));
124 for field in ["status", "date"] {
125 let mut bad = rule.clone();
126 bad.as_map_mut()["execution_summary"]["last_execution"]
127 .as_object_mut()
128 .unwrap()
129 .remove(field);
130 assert_eq!(snapshot(bad).unwrap_err().kind, ErrorKind::Http);
131 }
132 for field in [
133 "total_search_duration_ms",
134 "total_indexing_duration_ms",
135 "total_enrichment_duration_ms",
136 "execution_gap_duration_s",
137 ] {
138 let mut bad = rule.clone();
139 bad.as_map_mut()["execution_summary"]["last_execution"]["metrics"][field] =
140 serde_json::json!("bad");
141 assert_eq!(snapshot(bad).unwrap_err().kind, ErrorKind::Http);
142 }
143 }
144}