1#![allow(dead_code)]
7
8#[derive(Debug, Clone)]
10pub struct SlaTarget {
11 pub max_processing_secs: u64,
13 pub max_queue_depth: u32,
15 pub availability_pct: f32,
17 pub notification_threshold_pct: f32,
20}
21
22impl SlaTarget {
23 #[must_use]
25 pub fn new(
26 max_processing_secs: u64,
27 max_queue_depth: u32,
28 availability_pct: f32,
29 notification_threshold_pct: f32,
30 ) -> Self {
31 Self {
32 max_processing_secs,
33 max_queue_depth,
34 availability_pct,
35 notification_threshold_pct,
36 }
37 }
38}
39
40impl Default for SlaTarget {
41 fn default() -> Self {
42 Self {
43 max_processing_secs: 3600,
44 max_queue_depth: 100,
45 availability_pct: 99.0,
46 notification_threshold_pct: 0.8,
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum ViolationType {
54 ProcessingTimeout,
56 QueueDepthExceeded,
58 AvailabilityBreach,
60}
61
62#[derive(Debug, Clone)]
64pub struct SlaViolation {
65 pub workflow_id: String,
67 pub violation_type: ViolationType,
69 pub expected: f64,
71 pub actual: f64,
73 pub timestamp_ms: u64,
75}
76
77impl SlaViolation {
78 #[must_use]
80 pub fn new(
81 workflow_id: impl Into<String>,
82 violation_type: ViolationType,
83 expected: f64,
84 actual: f64,
85 timestamp_ms: u64,
86 ) -> Self {
87 Self {
88 workflow_id: workflow_id.into(),
89 violation_type,
90 expected,
91 actual,
92 timestamp_ms,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99struct CompletionRecord {
100 workflow_id: String,
101 processing_secs: u64,
102 timestamp_ms: u64,
103 success: bool,
104}
105
106#[derive(Debug)]
108pub struct SlaTracker {
109 target: SlaTarget,
110 completions: Vec<CompletionRecord>,
111 violations: Vec<SlaViolation>,
112 total_uptime_ms: u64,
114 total_downtime_ms: u64,
116 tracking_start_ms: u64,
118}
119
120impl SlaTracker {
121 #[must_use]
123 pub fn new(target: SlaTarget) -> Self {
124 Self {
125 target,
126 completions: Vec::new(),
127 violations: Vec::new(),
128 total_uptime_ms: 0,
129 total_downtime_ms: 0,
130 tracking_start_ms: current_time_ms(),
131 }
132 }
133
134 #[must_use]
136 pub fn with_default_target() -> Self {
137 Self::new(SlaTarget::default())
138 }
139
140 pub fn record_completion(&mut self, workflow_id: &str, processing_secs: u64) {
142 let now = current_time_ms();
143
144 self.completions.push(CompletionRecord {
145 workflow_id: workflow_id.to_string(),
146 processing_secs,
147 timestamp_ms: now,
148 success: true,
149 });
150
151 if processing_secs > self.target.max_processing_secs {
153 self.violations.push(SlaViolation::new(
154 workflow_id,
155 ViolationType::ProcessingTimeout,
156 self.target.max_processing_secs as f64,
157 processing_secs as f64,
158 now,
159 ));
160 }
161 }
162
163 #[must_use]
165 pub fn check_queue_depth(&mut self, depth: u32) -> Option<SlaViolation> {
166 if depth > self.target.max_queue_depth {
167 let violation = SlaViolation::new(
168 "system",
169 ViolationType::QueueDepthExceeded,
170 f64::from(self.target.max_queue_depth),
171 f64::from(depth),
172 current_time_ms(),
173 );
174 self.violations.push(violation.clone());
175 Some(violation)
176 } else {
177 None
178 }
179 }
180
181 #[must_use]
183 pub fn violations_in_window(&self, window_ms: u64) -> Vec<&SlaViolation> {
184 let now = current_time_ms();
185 let cutoff = now.saturating_sub(window_ms);
186 self.violations
187 .iter()
188 .filter(|v| v.timestamp_ms >= cutoff)
189 .collect()
190 }
191
192 #[must_use]
194 pub fn all_violations(&self) -> &[SlaViolation] {
195 &self.violations
196 }
197
198 pub fn record_downtime(&mut self, duration_ms: u64) {
200 self.total_downtime_ms += duration_ms;
201
202 let now = current_time_ms();
203 let availability = self.current_availability();
204 if availability < self.target.availability_pct {
205 self.violations.push(SlaViolation::new(
206 "system",
207 ViolationType::AvailabilityBreach,
208 f64::from(self.target.availability_pct),
209 f64::from(availability),
210 now,
211 ));
212 }
213 }
214
215 pub fn record_uptime(&mut self, duration_ms: u64) {
217 self.total_uptime_ms += duration_ms;
218 }
219
220 #[must_use]
222 pub fn current_availability(&self) -> f32 {
223 let total = self.total_uptime_ms + self.total_downtime_ms;
224 if total == 0 {
225 return 100.0;
226 }
227 (self.total_uptime_ms as f32 / total as f32) * 100.0
228 }
229
230 #[must_use]
232 pub fn processing_times(&self) -> Vec<u64> {
233 self.completions.iter().map(|c| c.processing_secs).collect()
234 }
235
236 #[must_use]
238 pub fn percentile_processing_secs(&self, percentile: f64) -> f64 {
239 let mut times: Vec<u64> = self.processing_times();
240 if times.is_empty() {
241 return 0.0;
242 }
243 times.sort_unstable();
244 let idx = ((times.len() as f64 * percentile).ceil() as usize).min(times.len()) - 1;
245 times[idx] as f64
246 }
247
248 #[must_use]
250 pub fn target(&self) -> &SlaTarget {
251 &self.target
252 }
253
254 #[must_use]
256 pub fn total_completions(&self) -> u64 {
257 self.completions.len() as u64
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct SlaReport {
264 pub period_ms: u64,
266 pub total_workflows: u64,
268 pub violations: u64,
270 pub avg_processing_secs: f64,
272 pub p95_processing_secs: f64,
274 pub availability_pct: f32,
276}
277
278impl SlaReport {
279 #[must_use]
281 pub fn generate(tracker: &SlaTracker, window_ms: u64) -> Self {
282 let now = current_time_ms();
283 let cutoff = now.saturating_sub(window_ms);
284
285 let window_completions: Vec<&CompletionRecord> = tracker
286 .completions
287 .iter()
288 .filter(|c| c.timestamp_ms >= cutoff)
289 .collect();
290
291 let total_workflows = window_completions.len() as u64;
292
293 let avg_processing_secs = if total_workflows > 0 {
294 window_completions
295 .iter()
296 .map(|c| c.processing_secs as f64)
297 .sum::<f64>()
298 / total_workflows as f64
299 } else {
300 0.0
301 };
302
303 let p95_processing_secs = if window_completions.is_empty() {
305 0.0
306 } else {
307 let mut times: Vec<u64> = window_completions
308 .iter()
309 .map(|c| c.processing_secs)
310 .collect();
311 times.sort_unstable();
312 let idx = ((times.len() as f64 * 0.95).ceil() as usize).min(times.len()) - 1;
313 times[idx] as f64
314 };
315
316 let violations = tracker
317 .violations
318 .iter()
319 .filter(|v| v.timestamp_ms >= cutoff)
320 .count() as u64;
321
322 SlaReport {
323 period_ms: window_ms,
324 total_workflows,
325 violations,
326 avg_processing_secs,
327 p95_processing_secs,
328 availability_pct: tracker.current_availability(),
329 }
330 }
331
332 #[must_use]
334 pub fn is_compliant(&self) -> bool {
335 self.violations == 0
336 }
337}
338
339fn current_time_ms() -> u64 {
341 use std::time::{SystemTime, UNIX_EPOCH};
342 SystemTime::now()
343 .duration_since(UNIX_EPOCH)
344 .map(|d| d.as_millis() as u64)
345 .unwrap_or(0)
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 fn make_tracker() -> SlaTracker {
353 SlaTracker::new(SlaTarget::new(60, 10, 99.0, 0.8))
354 }
355
356 #[test]
357 fn test_sla_target_defaults() {
358 let target = SlaTarget::default();
359 assert_eq!(target.max_processing_secs, 3600);
360 assert_eq!(target.max_queue_depth, 100);
361 assert!((target.availability_pct - 99.0).abs() < f32::EPSILON);
362 }
363
364 #[test]
365 fn test_record_completion_no_violation() {
366 let mut tracker = make_tracker();
367 tracker.record_completion("wf-1", 30); assert_eq!(tracker.all_violations().len(), 0);
369 assert_eq!(tracker.total_completions(), 1);
370 }
371
372 #[test]
373 fn test_record_completion_with_violation() {
374 let mut tracker = make_tracker();
375 tracker.record_completion("wf-1", 120); let violations = tracker.all_violations();
377 assert_eq!(violations.len(), 1);
378 assert_eq!(
379 violations[0].violation_type,
380 ViolationType::ProcessingTimeout
381 );
382 assert_eq!(violations[0].expected, 60.0);
383 assert_eq!(violations[0].actual, 120.0);
384 }
385
386 #[test]
387 fn test_check_queue_depth_no_violation() {
388 let mut tracker = make_tracker();
389 let result = tracker.check_queue_depth(5); assert!(result.is_none());
391 assert_eq!(tracker.all_violations().len(), 0);
392 }
393
394 #[test]
395 fn test_check_queue_depth_violation() {
396 let mut tracker = make_tracker();
397 let result = tracker.check_queue_depth(15); assert!(result.is_some());
399 let violation = result.expect("should succeed in test");
400 assert_eq!(violation.violation_type, ViolationType::QueueDepthExceeded);
401 assert_eq!(violation.expected, 10.0);
402 assert_eq!(violation.actual, 15.0);
403 }
404
405 #[test]
406 fn test_violations_in_window() {
407 let mut tracker = make_tracker();
408 tracker.record_completion("wf-1", 120);
409 tracker.record_completion("wf-2", 200);
410
411 let violations = tracker.violations_in_window(60_000); assert_eq!(violations.len(), 2);
413 }
414
415 #[test]
416 fn test_violations_in_empty_window() {
417 let tracker = make_tracker();
418 let violations = tracker.violations_in_window(0); assert_eq!(violations.len(), 0);
420 }
421
422 #[test]
423 fn test_current_availability_no_data() {
424 let tracker = make_tracker();
425 assert!((tracker.current_availability() - 100.0).abs() < f32::EPSILON);
426 }
427
428 #[test]
429 fn test_current_availability_with_downtime() {
430 let mut tracker = make_tracker();
431 tracker.record_uptime(90_000);
432 tracker.record_downtime(10_000);
433 let availability = tracker.current_availability();
434 assert!((availability - 90.0).abs() < 0.01);
435 }
436
437 #[test]
438 fn test_percentile_processing() {
439 let mut tracker = make_tracker();
440 for i in 1..=100u64 {
441 tracker.record_completion(&format!("wf-{}", i), i);
442 }
443 let p95 = tracker.percentile_processing_secs(0.95);
444 assert!(p95 >= 90.0 && p95 <= 100.0);
445 }
446
447 #[test]
448 fn test_sla_report_generation() {
449 let mut tracker = make_tracker();
450 tracker.record_completion("wf-1", 30);
451 tracker.record_completion("wf-2", 90); tracker.record_uptime(3_600_000);
453
454 let report = SlaReport::generate(&tracker, 60_000);
455 assert_eq!(report.total_workflows, 2);
456 assert_eq!(report.violations, 1);
457 assert!(report.avg_processing_secs > 0.0);
458 assert_eq!(report.period_ms, 60_000);
459 }
460
461 #[test]
462 fn test_sla_report_compliant() {
463 let mut tracker = make_tracker();
464 tracker.record_completion("wf-1", 30);
465 tracker.record_completion("wf-2", 45);
466
467 let report = SlaReport::generate(&tracker, 60_000);
468 assert!(report.is_compliant());
469 }
470
471 #[test]
472 fn test_sla_report_empty() {
473 let tracker = make_tracker();
474 let report = SlaReport::generate(&tracker, 60_000);
475 assert_eq!(report.total_workflows, 0);
476 assert_eq!(report.violations, 0);
477 assert_eq!(report.avg_processing_secs, 0.0);
478 assert!(report.is_compliant());
479 }
480}