1#![allow(dead_code)]
4#![allow(clippy::cast_precision_loss)]
5
6use std::collections::HashMap;
7use std::time::{Duration, Instant};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SlaLevel {
12 Bronze,
14 Silver,
16 Gold,
18 Platinum,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum SlaStatus {
25 Active,
27 AtRisk,
29 Breached,
31 Met,
33}
34
35#[derive(Debug, Clone)]
37pub struct EscalationRule {
38 pub id: String,
40 pub trigger_level: SlaLevel,
42 pub notify_contacts: Vec<String>,
44 pub escalation_delay: Duration,
46 pub fired: bool,
48}
49
50impl EscalationRule {
51 #[must_use]
53 pub fn new(id: &str, trigger_level: SlaLevel, contacts: Vec<String>, delay: Duration) -> Self {
54 Self {
55 id: id.to_string(),
56 trigger_level,
57 notify_contacts: contacts,
58 escalation_delay: delay,
59 fired: false,
60 }
61 }
62
63 pub fn fire(&mut self) {
65 self.fired = true;
66 }
67}
68
69#[derive(Debug, Clone)]
71struct SlaEntryInternal {
72 id: String,
73 name: String,
74 deadline: Instant,
75 warning_threshold: Duration,
76 status: SlaStatus,
77 level: SlaLevel,
78 escalation_rules: Vec<EscalationRule>,
79 created_at: Instant,
80 breach_detected_at: Option<Instant>,
81}
82
83impl SlaEntryInternal {
84 fn new(id: &str, name: &str, ttd: Duration, level: SlaLevel) -> Self {
85 let warning = if ttd.as_secs() == 0 {
86 Duration::ZERO
87 } else {
88 ttd / 5
89 };
90 Self {
91 id: id.to_string(),
92 name: name.to_string(),
93 deadline: Instant::now() + ttd,
94 warning_threshold: warning,
95 status: SlaStatus::Active,
96 level,
97 escalation_rules: Vec::new(),
98 created_at: Instant::now(),
99 breach_detected_at: None,
100 }
101 }
102
103 fn remaining(&self) -> Duration {
104 let now = Instant::now();
105 if now >= self.deadline {
106 Duration::ZERO
107 } else {
108 self.deadline - now
109 }
110 }
111
112 fn refresh_status(&mut self) {
113 if self.status == SlaStatus::Met {
114 return;
115 }
116 let now = Instant::now();
117 if now >= self.deadline {
118 if self.status != SlaStatus::Breached {
119 self.breach_detected_at = Some(now);
120 }
121 self.status = SlaStatus::Breached;
122 } else if self.remaining() <= self.warning_threshold {
123 self.status = SlaStatus::AtRisk;
124 }
125 }
126}
127
128#[derive(Debug, Default)]
130pub struct SlaTracker {
131 entries: HashMap<String, SlaEntryInternal>,
132}
133
134impl SlaTracker {
135 #[must_use]
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn register(&mut self, id: &str, name: &str, ttd: Duration, level: SlaLevel) {
143 let entry = SlaEntryInternal::new(id, name, ttd, level);
144 self.entries.insert(id.to_string(), entry);
145 }
146
147 pub fn add_escalation_rule(&mut self, entry_id: &str, rule: EscalationRule) -> bool {
149 if let Some(e) = self.entries.get_mut(entry_id) {
150 e.escalation_rules.push(rule);
151 true
152 } else {
153 false
154 }
155 }
156
157 pub fn mark_met(&mut self, id: &str) -> bool {
159 if let Some(e) = self.entries.get_mut(id) {
160 e.status = SlaStatus::Met;
161 true
162 } else {
163 false
164 }
165 }
166
167 pub fn tick(&mut self) -> Vec<String> {
169 let mut breached = Vec::new();
170 for entry in self.entries.values_mut() {
171 let was_breached = entry.status == SlaStatus::Breached;
172 entry.refresh_status();
173 if !was_breached && entry.status == SlaStatus::Breached {
174 breached.push(entry.id.clone());
175 }
176 }
177 breached
178 }
179
180 #[must_use]
182 pub fn at_risk_ids(&self) -> Vec<String> {
183 self.entries
184 .values()
185 .filter(|e| e.status == SlaStatus::AtRisk)
186 .map(|e| e.id.clone())
187 .collect()
188 }
189
190 #[must_use]
192 pub fn breached_ids(&self) -> Vec<String> {
193 self.entries
194 .values()
195 .filter(|e| e.status == SlaStatus::Breached)
196 .map(|e| e.id.clone())
197 .collect()
198 }
199
200 #[must_use]
202 pub fn status(&self, id: &str) -> Option<SlaStatus> {
203 self.entries.get(id).map(|e| e.status.clone())
204 }
205
206 #[must_use]
208 pub fn level(&self, id: &str) -> Option<&SlaLevel> {
209 self.entries.get(id).map(|e| &e.level)
210 }
211
212 #[must_use]
214 pub fn remaining(&self, id: &str) -> Option<Duration> {
215 self.entries.get(id).map(SlaEntryInternal::remaining)
216 }
217
218 #[must_use]
220 pub fn count(&self) -> usize {
221 self.entries.len()
222 }
223
224 #[must_use]
226 pub fn count_by_status(&self, status: &SlaStatus) -> usize {
227 self.entries
228 .values()
229 .filter(|e| &e.status == status)
230 .count()
231 }
232
233 #[must_use]
235 pub fn breach_rate(&self) -> f64 {
236 if self.entries.is_empty() {
237 return 0.0;
238 }
239 let breached = self.count_by_status(&SlaStatus::Breached) as f64;
240 let total = self.entries.len() as f64;
241 breached / total
242 }
243
244 #[must_use]
246 pub fn compliance_rate(&self) -> f64 {
247 if self.entries.is_empty() {
248 return 1.0;
249 }
250 let met = self.count_by_status(&SlaStatus::Met) as f64;
251 let total = self.entries.len() as f64;
252 met / total
253 }
254
255 pub fn fire_escalations(&mut self) -> Vec<String> {
257 let mut fired_ids = Vec::new();
258 for entry in self.entries.values_mut() {
259 if entry.status != SlaStatus::Breached {
260 continue;
261 }
262 if let Some(breach_time) = entry.breach_detected_at {
263 let elapsed = Instant::now().duration_since(breach_time);
264 for rule in &mut entry.escalation_rules {
265 if !rule.fired && elapsed >= rule.escalation_delay {
266 rule.fired = true;
267 fired_ids.push(rule.id.clone());
268 }
269 }
270 }
271 }
272 fired_ids
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct BreachReport {
279 pub entry_id: String,
281 pub entry_name: String,
283 pub level: SlaLevel,
285 pub breach_age: Duration,
287}
288
289#[must_use]
291pub fn build_breach_report(tracker: &SlaTracker) -> Vec<BreachReport> {
292 tracker
293 .entries
294 .values()
295 .filter(|e| e.status == SlaStatus::Breached)
296 .map(|e| BreachReport {
297 entry_id: e.id.clone(),
298 entry_name: e.name.clone(),
299 level: e.level.clone(),
300 breach_age: e
301 .breach_detected_at
302 .map_or(Duration::ZERO, |t| Instant::now().duration_since(t)),
303 })
304 .collect()
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_tracker_register_and_count() {
313 let mut tracker = SlaTracker::new();
314 tracker.register(
315 "w1",
316 "Workflow 1",
317 Duration::from_secs(60),
318 SlaLevel::Bronze,
319 );
320 tracker.register("w2", "Workflow 2", Duration::from_secs(120), SlaLevel::Gold);
321 assert_eq!(tracker.count(), 2);
322 }
323
324 #[test]
325 fn test_initial_status_active() {
326 let mut tracker = SlaTracker::new();
327 tracker.register(
328 "w1",
329 "Workflow 1",
330 Duration::from_secs(60),
331 SlaLevel::Silver,
332 );
333 assert_eq!(tracker.status("w1"), Some(SlaStatus::Active));
334 }
335
336 #[test]
337 fn test_mark_met() {
338 let mut tracker = SlaTracker::new();
339 tracker.register("w1", "Workflow 1", Duration::from_secs(60), SlaLevel::Gold);
340 assert!(tracker.mark_met("w1"));
341 assert_eq!(tracker.status("w1"), Some(SlaStatus::Met));
342 }
343
344 #[test]
345 fn test_mark_met_nonexistent() {
346 let mut tracker = SlaTracker::new();
347 assert!(!tracker.mark_met("nonexistent"));
348 }
349
350 #[test]
351 fn test_status_unknown_id() {
352 let tracker = SlaTracker::new();
353 assert_eq!(tracker.status("missing"), None);
354 }
355
356 #[test]
357 fn test_breach_on_zero_duration() {
358 let mut tracker = SlaTracker::new();
359 tracker.register("w1", "Workflow 1", Duration::ZERO, SlaLevel::Platinum);
360 tracker.tick();
361 assert_eq!(tracker.status("w1"), Some(SlaStatus::Breached));
362 }
363
364 #[test]
365 fn test_compliance_rate_all_met() {
366 let mut tracker = SlaTracker::new();
367 tracker.register("w1", "W1", Duration::from_secs(60), SlaLevel::Bronze);
368 tracker.register("w2", "W2", Duration::from_secs(60), SlaLevel::Bronze);
369 tracker.mark_met("w1");
370 tracker.mark_met("w2");
371 assert!((tracker.compliance_rate() - 1.0).abs() < f64::EPSILON);
372 }
373
374 #[test]
375 fn test_breach_rate_none_breached() {
376 let mut tracker = SlaTracker::new();
377 tracker.register("w1", "W1", Duration::from_secs(3600), SlaLevel::Silver);
378 assert!((tracker.breach_rate() - 0.0).abs() < f64::EPSILON);
379 }
380
381 #[test]
382 fn test_breach_rate_empty() {
383 let tracker = SlaTracker::new();
384 assert_eq!(tracker.breach_rate(), 0.0);
385 }
386
387 #[test]
388 fn test_compliance_rate_empty() {
389 let tracker = SlaTracker::new();
390 assert_eq!(tracker.compliance_rate(), 1.0);
391 }
392
393 #[test]
394 fn test_at_risk_ids_empty() {
395 let tracker = SlaTracker::new();
396 assert!(tracker.at_risk_ids().is_empty());
397 }
398
399 #[test]
400 fn test_breached_ids_empty() {
401 let tracker = SlaTracker::new();
402 assert!(tracker.breached_ids().is_empty());
403 }
404
405 #[test]
406 fn test_count_by_status() {
407 let mut tracker = SlaTracker::new();
408 tracker.register("w1", "W1", Duration::from_secs(60), SlaLevel::Bronze);
409 tracker.register("w2", "W2", Duration::from_secs(60), SlaLevel::Bronze);
410 tracker.mark_met("w1");
411 assert_eq!(tracker.count_by_status(&SlaStatus::Met), 1);
412 assert_eq!(tracker.count_by_status(&SlaStatus::Active), 1);
413 }
414
415 #[test]
416 fn test_escalation_rule_new() {
417 let rule = EscalationRule::new(
418 "rule1",
419 SlaLevel::Gold,
420 vec!["ops@example.com".to_string()],
421 Duration::from_secs(300),
422 );
423 assert_eq!(rule.id, "rule1");
424 assert!(!rule.fired);
425 assert_eq!(rule.notify_contacts.len(), 1);
426 }
427
428 #[test]
429 fn test_escalation_rule_fire() {
430 let mut rule = EscalationRule::new(
431 "rule2",
432 SlaLevel::Platinum,
433 vec!["cto@example.com".to_string()],
434 Duration::ZERO,
435 );
436 rule.fire();
437 assert!(rule.fired);
438 }
439
440 #[test]
441 fn test_add_escalation_rule() {
442 let mut tracker = SlaTracker::new();
443 tracker.register("w1", "W1", Duration::from_secs(60), SlaLevel::Gold);
444 let rule = EscalationRule::new("r1", SlaLevel::Gold, vec![], Duration::from_secs(30));
445 assert!(tracker.add_escalation_rule("w1", rule));
446 }
447
448 #[test]
449 fn test_add_escalation_rule_missing_entry() {
450 let mut tracker = SlaTracker::new();
451 let rule = EscalationRule::new("r1", SlaLevel::Gold, vec![], Duration::from_secs(30));
452 assert!(!tracker.add_escalation_rule("nonexistent", rule));
453 }
454
455 #[test]
456 fn test_breach_report_fields() {
457 let report = BreachReport {
458 entry_id: "w1".to_string(),
459 entry_name: "Workflow 1".to_string(),
460 level: SlaLevel::Gold,
461 breach_age: Duration::from_secs(10),
462 };
463 assert_eq!(report.entry_id, "w1");
464 assert_eq!(report.level, SlaLevel::Gold);
465 }
466
467 #[test]
468 fn test_build_breach_report_empty() {
469 let tracker = SlaTracker::new();
470 let report = build_breach_report(&tracker);
471 assert!(report.is_empty());
472 }
473
474 #[test]
475 fn test_level_accessor() {
476 let mut tracker = SlaTracker::new();
477 tracker.register("w1", "W1", Duration::from_secs(60), SlaLevel::Platinum);
478 assert_eq!(tracker.level("w1"), Some(&SlaLevel::Platinum));
479 }
480
481 #[test]
482 fn test_remaining_positive() {
483 let mut tracker = SlaTracker::new();
484 tracker.register("w1", "W1", Duration::from_secs(3600), SlaLevel::Bronze);
485 let rem = tracker.remaining("w1").expect("should succeed in test");
486 assert!(rem > Duration::from_secs(3590));
487 }
488}