Skip to main content

provide_telemetry/
slo.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use std::collections::BTreeMap;
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::OnceLock;
9
10use crate::metrics::{counter, gauge, histogram, Counter, Gauge, Histogram};
11
12static SLO_INITIALIZED: AtomicBool = AtomicBool::new(false);
13static SLO_ERROR_COUNT: AtomicU64 = AtomicU64::new(0);
14static SLO_REQUEST_COUNT: AtomicU64 = AtomicU64::new(0);
15
16static RED_REQUESTS: OnceLock<Counter> = OnceLock::new();
17static RED_ERRORS: OnceLock<Counter> = OnceLock::new();
18static RED_DURATION: OnceLock<Histogram> = OnceLock::new();
19static USE_UTILIZATION: OnceLock<Gauge> = OnceLock::new();
20
21pub fn record_red_metrics(route: &str, method: &str, status_code: u16, duration_ms: f64) {
22    SLO_INITIALIZED.store(true, Ordering::SeqCst);
23    let attrs: BTreeMap<String, String> = [
24        ("route".to_string(), route.to_string()),
25        ("method".to_string(), method.to_string()),
26        ("status_code".to_string(), status_code.to_string()),
27    ]
28    .into_iter()
29    .collect();
30    SLO_REQUEST_COUNT.fetch_add(1, Ordering::SeqCst);
31    RED_REQUESTS
32        .get_or_init(|| counter("http.requests.total", Some("Total HTTP requests"), None))
33        .add(1.0, Some(attrs.clone()));
34    if method != "WS" && status_code >= 500 {
35        SLO_ERROR_COUNT.fetch_add(1, Ordering::SeqCst);
36        RED_ERRORS
37            .get_or_init(|| counter("http.errors.total", Some("Total HTTP errors"), None))
38            .add(1.0, Some(attrs.clone()));
39    }
40    RED_DURATION
41        .get_or_init(|| {
42            histogram(
43                "http.request.duration_ms",
44                Some("HTTP request latency"),
45                Some("ms"),
46            )
47        })
48        .record(duration_ms, Some(attrs));
49}
50
51pub fn record_use_metrics(resource: &str, utilization_percent: i32) {
52    SLO_INITIALIZED.store(true, Ordering::SeqCst);
53    let mut attrs = BTreeMap::new();
54    attrs.insert("resource".to_string(), resource.to_string());
55    USE_UTILIZATION
56        .get_or_init(|| {
57            gauge(
58                "resource.utilization.percent",
59                Some("Resource utilization"),
60                Some("%"),
61            )
62        })
63        .set(utilization_percent as f64, Some(attrs));
64}
65
66pub fn classify_error(status_code: u16) -> String {
67    SLO_INITIALIZED.store(true, Ordering::SeqCst);
68    match status_code {
69        0 => "timeout".to_string(),
70        400..=499 => "client_error".to_string(),
71        500..=599 => "server_error".to_string(),
72        _ => "ok".to_string(),
73    }
74}
75
76pub fn slo_initialized_for_tests() -> bool {
77    SLO_INITIALIZED.load(Ordering::SeqCst)
78}
79
80pub fn get_error_count_for_tests() -> u64 {
81    SLO_ERROR_COUNT.load(Ordering::SeqCst)
82}
83
84pub fn get_request_count_for_tests() -> u64 {
85    SLO_REQUEST_COUNT.load(Ordering::SeqCst)
86}
87
88pub fn reset_slo_for_tests() {
89    SLO_INITIALIZED.store(false, Ordering::SeqCst);
90    SLO_ERROR_COUNT.store(0, Ordering::SeqCst);
91    SLO_REQUEST_COUNT.store(0, Ordering::SeqCst);
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::testing::acquire_test_state_lock;
98
99    #[test]
100    fn slo_test_reset_helper_clears_initialized_flag() {
101        let _guard = acquire_test_state_lock();
102        reset_slo_for_tests();
103        assert!(!slo_initialized_for_tests());
104
105        assert_eq!(classify_error(503), "server_error");
106        assert!(slo_initialized_for_tests());
107
108        reset_slo_for_tests();
109        assert!(!slo_initialized_for_tests());
110    }
111}