1use 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(error_name: &str, status_code: Option<u16>) -> BTreeMap<String, String> {
72 SLO_INITIALIZED.store(true, Ordering::SeqCst);
73 let code = status_code.unwrap_or(0);
74 let is_timeout = error_name.to_ascii_lowercase().contains("timeout")
75 || code == 0
76 || code == 408
77 || code == 504;
78
79 let (category, severity, error_type) = if is_timeout {
80 ("timeout", "info", "internal")
81 } else if (500..=599).contains(&code) {
82 ("server_error", "critical", "server")
83 } else if (400..=499).contains(&code) {
84 (
85 "client_error",
86 if code == 429 { "critical" } else { "warning" },
87 "client",
88 )
89 } else {
90 ("unclassified", "info", "internal")
91 };
92
93 [
94 ("error_type", error_type),
95 ("error_code", &code.to_string()),
96 ("error_name", error_name),
97 ("error.type", error_name),
98 ("error.category", category),
99 ("error.severity", severity),
100 ("http.status_code", &code.to_string()),
101 ]
102 .into_iter()
103 .map(|(k, v)| (k.to_string(), v.to_string()))
104 .collect()
105}
106
107pub fn slo_initialized_for_tests() -> bool {
108 SLO_INITIALIZED.load(Ordering::SeqCst)
109}
110
111pub fn get_error_count_for_tests() -> u64 {
112 SLO_ERROR_COUNT.load(Ordering::SeqCst)
113}
114
115pub fn get_request_count_for_tests() -> u64 {
116 SLO_REQUEST_COUNT.load(Ordering::SeqCst)
117}
118
119pub fn reset_slo_for_tests() {
120 SLO_INITIALIZED.store(false, Ordering::SeqCst);
121 SLO_ERROR_COUNT.store(0, Ordering::SeqCst);
122 SLO_REQUEST_COUNT.store(0, Ordering::SeqCst);
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::testing::acquire_test_state_lock;
129
130 #[test]
131 fn slo_test_reset_helper_clears_initialized_flag() {
132 let _guard = acquire_test_state_lock();
133 reset_slo_for_tests();
134 assert!(!slo_initialized_for_tests());
135
136 assert_eq!(
137 classify_error("SomeError", Some(503))["error.category"],
138 "server_error"
139 );
140 assert!(slo_initialized_for_tests());
141
142 reset_slo_for_tests();
143 assert!(!slo_initialized_for_tests());
144 }
145}