1use std::sync::Arc;
2
3pub struct L1Stats {
5 pub l1_hits: u64,
7 pub l2_hits: u64,
9 pub misses: u64,
11 pub l1_enabled: bool,
13}
14
15pub type MetricsProvider = Arc<dyn Fn() -> Option<L1Stats> + Send + Sync>;
17
18pub fn metrics_headers(provider: Option<&MetricsProvider>) -> Vec<(&'static str, String)> {
20 let disabled = vec![("X-CacheKit-L1-Status", "disabled".to_string())];
21
22 let provider = match provider {
23 Some(p) => p,
24 None => return disabled,
25 };
26
27 let stats = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (provider)())) {
28 Ok(Some(s)) => s,
29 _ => return disabled,
30 };
31
32 if !stats.l1_enabled {
33 return disabled;
34 }
35
36 let total = stats.l1_hits + stats.l2_hits + stats.misses;
37 let hit_rate = if total > 0 {
38 stats.l1_hits as f64 / total as f64
39 } else {
40 0.0
41 };
42
43 vec![
44 ("X-CacheKit-L1-Status", "enabled".to_string()),
45 ("X-CacheKit-L1-Hits", stats.l1_hits.to_string()),
46 ("X-CacheKit-L2-Hits", stats.l2_hits.to_string()),
47 ("X-CacheKit-Misses", stats.misses.to_string()),
48 ("X-CacheKit-L1-Hit-Rate", format!("{hit_rate:.3}")),
49 ]
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn disabled_when_no_provider() {
58 let headers = metrics_headers(None);
59 assert_eq!(headers.len(), 1);
60 assert_eq!(headers[0], ("X-CacheKit-L1-Status", "disabled".to_string()));
61 }
62
63 #[test]
64 fn disabled_when_l1_not_enabled() {
65 let provider: MetricsProvider = Arc::new(|| {
66 Some(L1Stats {
67 l1_hits: 0,
68 l2_hits: 0,
69 misses: 0,
70 l1_enabled: false,
71 })
72 });
73 let headers = metrics_headers(Some(&provider));
74 assert_eq!(headers[0].1, "disabled");
75 }
76
77 #[test]
78 fn correct_hit_rate_calculation() {
79 let provider: MetricsProvider = Arc::new(|| {
80 Some(L1Stats {
81 l1_hits: 3,
82 l2_hits: 2,
83 misses: 5,
84 l1_enabled: true,
85 })
86 });
87 let headers = metrics_headers(Some(&provider));
88 let rate = headers
89 .iter()
90 .find(|h| h.0 == "X-CacheKit-L1-Hit-Rate")
91 .unwrap();
92 assert_eq!(rate.1, "0.300"); }
94
95 #[test]
96 fn zero_division_guard() {
97 let provider: MetricsProvider = Arc::new(|| {
98 Some(L1Stats {
99 l1_hits: 0,
100 l2_hits: 0,
101 misses: 0,
102 l1_enabled: true,
103 })
104 });
105 let headers = metrics_headers(Some(&provider));
106 let rate = headers
107 .iter()
108 .find(|h| h.0 == "X-CacheKit-L1-Hit-Rate")
109 .unwrap();
110 assert_eq!(rate.1, "0.000");
111 }
112
113 #[test]
114 fn disabled_when_provider_panics() {
115 #[allow(clippy::panic)]
116 let provider: MetricsProvider = Arc::new(|| panic!("boom"));
117 let headers = metrics_headers(Some(&provider));
118 assert_eq!(headers[0].1, "disabled");
119 }
120}