1use std::collections::BTreeMap;
17
18use zenoh::sample::SampleKind;
19
20use crate::report::{AlertState, AlertTransition, RenderSource};
21
22pub fn alert_transition(
46 base: &str,
47 wire_key: &str,
48 kind: SampleKind,
49 decoded: Option<(RenderSource, &serde_json::Value)>,
50 stamped: Option<&str>,
51 at: &str,
52) -> Option<AlertTransition> {
53 use zenkey::grammar::{Class, ClassOrPlane};
54
55 let parsed = zenkey::grammar::parse_full(base, wire_key)?;
56 if parsed.class != ClassOrPlane::Class(Class::State) {
57 return None;
58 }
59 let prefix = zenkey::CommonFamily::Alert.prefix();
62 let alert_key = match parsed.subject.as_slice() {
63 [head, key] if [*head] == prefix[..] && !key.is_empty() => *key,
64 _ => return None,
65 };
66 let origin = parsed.origin.chunk().to_string();
67 let producer = parsed
70 .producer()
71 .map(|p| p.chunk())
72 .unwrap_or_else(|| origin.trim_start_matches('@').to_string());
73 let alert_ref = zenkey::alert::alert_ref(&origin, &producer, alert_key).ok()?;
74
75 let state = match kind {
76 SampleKind::Put => AlertState::Firing,
77 SampleKind::Delete => AlertState::Resolved,
78 };
79 let mut out = AlertTransition {
80 origin,
81 producer,
82 alert_key: alert_key.to_string(),
83 alert_ref,
84 state,
85 severity: None,
86 rule: None,
87 labels: BTreeMap::new(),
88 summary: None,
89 timestamp: stamped.map(str::to_string),
90 at: at.to_string(),
91 rendering: RenderSource::KeyOnly,
92 };
93 if state == AlertState::Resolved {
95 return Some(out);
96 }
97 if let Some((source, doc)) = decoded {
98 out.rendering = match source {
99 RenderSource::KeyOnly => RenderSource::Structural,
102 s => s,
103 };
104 let field = |name: &str| doc.get(name).and_then(|v| v.as_str()).map(str::to_string);
105 out.severity = field("severity");
106 out.rule = field("rule");
107 out.summary = field("summary").or_else(|| field("message"));
108 if let Some(labels) = doc.get("labels").and_then(|v| v.as_object()) {
109 for (k, v) in labels {
110 if k == "host" {
111 continue;
112 }
113 let v = match v {
116 serde_json::Value::String(s) => s.clone(),
117 other => other.to_string(),
118 };
119 out.labels.insert(k.clone(), v);
120 }
121 }
122 }
123 Some(out)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 const KEY: &str = "v1/h-3fa9c2d41b7e/state/netlink/alert/a659f813308ad1da";
131
132 #[test]
135 fn a_put_is_firing_with_the_documents_fields_lifted() {
136 let doc = serde_json::json!({
137 "severity": "warning",
138 "rule": "link_down",
139 "labels": {"port": "eth0", "host": "h-3fa9c2d41b7e", "vlan": 7},
140 "message": "eth0 is down",
141 });
142 let t = alert_transition(
143 "",
144 KEY,
145 SampleKind::Put,
146 Some((RenderSource::Schema, &doc)),
147 Some("7/…"),
148 "t0",
149 )
150 .expect("an alert key");
151 assert_eq!(t.state, AlertState::Firing);
152 assert_eq!(t.origin, "h-3fa9c2d41b7e");
153 assert_eq!(t.producer, "netlink");
154 assert_eq!(t.alert_key, "a659f813308ad1da");
155 assert_eq!(t.alert_ref, "h-3fa9c2d41b7e.netlink.a659f813308ad1da");
156 assert_eq!(t.severity.as_deref(), Some("warning"));
157 assert_eq!(t.rule.as_deref(), Some("link_down"));
158 assert_eq!(t.summary.as_deref(), Some("eth0 is down"));
159 assert_eq!(t.timestamp.as_deref(), Some("7/…"));
160 assert_eq!(t.rendering, RenderSource::Schema);
161 assert_eq!(
162 t.labels,
163 BTreeMap::from([
164 ("port".to_string(), "eth0".to_string()),
165 ("vlan".to_string(), "7".to_string()),
166 ]),
167 "host is dropped, a numeric label is rendered"
168 );
169 }
170
171 #[test]
174 fn a_delete_is_resolved_with_no_fields() {
175 let doc = serde_json::json!({"severity": "error"});
176 let t = alert_transition(
177 "",
178 KEY,
179 SampleKind::Delete,
180 Some((RenderSource::Structural, &doc)),
181 None,
182 "t1",
183 )
184 .expect("an alert key");
185 assert_eq!(t.state, AlertState::Resolved);
186 assert_eq!(t.severity, None);
187 assert!(t.labels.is_empty());
188 assert_eq!(t.rendering, RenderSource::KeyOnly);
189 assert_eq!(t.timestamp, None);
190 }
191
192 #[test]
196 fn a_non_alert_key_is_none() {
197 let based = format!("zensight/{KEY}");
198 assert!(alert_transition("zensight", &based, SampleKind::Put, None, None, "t").is_some());
199 assert!(alert_transition("other", &based, SampleKind::Put, None, None, "t").is_none());
200 for key in [
201 "v1/h-3fa9c2d41b7e/state/netlink/health",
202 "v1/h-3fa9c2d41b7e/telemetry/netlink/alert/a659f813308ad1da",
203 "v1/h-3fa9c2d41b7e/state/netlink/alert/a659f813308ad1da/extra",
204 "not/a/key",
205 ] {
206 assert!(
207 alert_transition("", key, SampleKind::Put, None, None, "t").is_none(),
208 "{key}"
209 );
210 }
211 let t = alert_transition("", KEY, SampleKind::Put, None, None, "t").unwrap();
213 assert_eq!(t.state, AlertState::Firing);
214 assert_eq!(t.rendering, RenderSource::KeyOnly);
215 }
216}