tower_http_cache/admin/
stats.rs

1//! Statistics collection for the admin API.
2
3use dashmap::DashMap;
4use serde::{Deserialize, Serialize};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// Global statistics collector.
10pub struct GlobalStats {
11    /// Total number of cache requests
12    pub total_requests: AtomicU64,
13
14    /// Total cache hits
15    pub hits: AtomicU64,
16
17    /// Total cache misses
18    pub misses: AtomicU64,
19
20    /// Total stale hits served
21    pub stale_hits: AtomicU64,
22
23    /// Total entries stored
24    pub stores: AtomicU64,
25
26    /// Total invalidations
27    pub invalidations: AtomicU64,
28
29    /// Per-key hit tracking for hot keys
30    key_hits: Arc<DashMap<String, KeyHitStats>>,
31
32    /// Uptime start
33    pub uptime_start: SystemTime,
34}
35
36impl GlobalStats {
37    /// Creates a new statistics collector.
38    pub fn new() -> Self {
39        Self {
40            total_requests: AtomicU64::new(0),
41            hits: AtomicU64::new(0),
42            misses: AtomicU64::new(0),
43            stale_hits: AtomicU64::new(0),
44            stores: AtomicU64::new(0),
45            invalidations: AtomicU64::new(0),
46            key_hits: Arc::new(DashMap::new()),
47            uptime_start: SystemTime::now(),
48        }
49    }
50
51    /// Records a cache hit.
52    pub fn record_hit(&self, key: &str) {
53        self.total_requests.fetch_add(1, Ordering::Relaxed);
54        self.hits.fetch_add(1, Ordering::Relaxed);
55        self.record_key_hit(key);
56    }
57
58    /// Records a cache miss.
59    pub fn record_miss(&self, _key: &str) {
60        self.total_requests.fetch_add(1, Ordering::Relaxed);
61        self.misses.fetch_add(1, Ordering::Relaxed);
62    }
63
64    /// Records a stale hit.
65    pub fn record_stale_hit(&self, key: &str) {
66        self.total_requests.fetch_add(1, Ordering::Relaxed);
67        self.stale_hits.fetch_add(1, Ordering::Relaxed);
68        self.record_key_hit(key);
69    }
70
71    /// Records a cache store operation.
72    pub fn record_store(&self, _key: &str) {
73        self.stores.fetch_add(1, Ordering::Relaxed);
74    }
75
76    /// Records an invalidation.
77    pub fn record_invalidation(&self) {
78        self.invalidations.fetch_add(1, Ordering::Relaxed);
79    }
80
81    /// Records a hit for a specific key (for hot key tracking).
82    fn record_key_hit(&self, key: &str) {
83        self.key_hits
84            .entry(key.to_string())
85            .or_insert_with(KeyHitStats::new)
86            .increment();
87    }
88
89    /// Returns the top N most frequently accessed keys.
90    pub fn hot_keys(&self, limit: usize) -> Vec<HotKeyInfo> {
91        let mut keys: Vec<_> = self
92            .key_hits
93            .iter()
94            .map(|entry| HotKeyInfo {
95                key: entry.key().clone(),
96                hits: entry.value().hits(),
97                last_accessed: entry.value().last_accessed,
98            })
99            .collect();
100
101        keys.sort_by(|a, b| b.hits.cmp(&a.hits));
102        keys.truncate(limit);
103        keys
104    }
105
106    /// Returns the current statistics as a serializable struct.
107    pub fn snapshot(&self) -> StatsSnapshot {
108        let total = self.total_requests.load(Ordering::Relaxed);
109        let hits = self.hits.load(Ordering::Relaxed);
110        let misses = self.misses.load(Ordering::Relaxed);
111
112        let hit_rate = if total > 0 {
113            hits as f64 / total as f64
114        } else {
115            0.0
116        };
117
118        let miss_rate = if total > 0 {
119            misses as f64 / total as f64
120        } else {
121            0.0
122        };
123
124        let uptime = SystemTime::now()
125            .duration_since(self.uptime_start)
126            .unwrap_or_default()
127            .as_secs();
128
129        StatsSnapshot {
130            total_requests: total,
131            hits,
132            misses,
133            stale_hits: self.stale_hits.load(Ordering::Relaxed),
134            stores: self.stores.load(Ordering::Relaxed),
135            invalidations: self.invalidations.load(Ordering::Relaxed),
136            hit_rate,
137            miss_rate,
138            uptime_seconds: uptime,
139        }
140    }
141
142    /// Resets all statistics.
143    pub fn reset(&self) {
144        self.total_requests.store(0, Ordering::Relaxed);
145        self.hits.store(0, Ordering::Relaxed);
146        self.misses.store(0, Ordering::Relaxed);
147        self.stale_hits.store(0, Ordering::Relaxed);
148        self.stores.store(0, Ordering::Relaxed);
149        self.invalidations.store(0, Ordering::Relaxed);
150        self.key_hits.clear();
151    }
152}
153
154impl Default for GlobalStats {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160/// Per-key hit statistics.
161struct KeyHitStats {
162    hits: AtomicU64,
163    last_accessed: SystemTime,
164}
165
166impl KeyHitStats {
167    fn new() -> Self {
168        Self {
169            hits: AtomicU64::new(0),
170            last_accessed: SystemTime::now(),
171        }
172    }
173
174    fn increment(&self) {
175        self.hits.fetch_add(1, Ordering::Relaxed);
176    }
177
178    fn hits(&self) -> u64 {
179        self.hits.load(Ordering::Relaxed)
180    }
181}
182
183/// Information about a hot key.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct HotKeyInfo {
186    pub key: String,
187    pub hits: u64,
188    #[serde(serialize_with = "serialize_system_time")]
189    pub last_accessed: SystemTime,
190}
191
192/// Snapshot of current statistics.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct StatsSnapshot {
195    pub total_requests: u64,
196    pub hits: u64,
197    pub misses: u64,
198    pub stale_hits: u64,
199    pub stores: u64,
200    pub invalidations: u64,
201    pub hit_rate: f64,
202    pub miss_rate: f64,
203    pub uptime_seconds: u64,
204}
205
206/// Helper to serialize SystemTime as ISO 8601.
207fn serialize_system_time<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
208where
209    S: serde::Serializer,
210{
211    let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
212    let secs = duration.as_secs();
213    let timestamp =
214        chrono::DateTime::from_timestamp(secs as i64, 0).unwrap_or(chrono::DateTime::UNIX_EPOCH);
215    serializer.serialize_str(&timestamp.to_rfc3339())
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn global_stats_initial_state() {
224        let stats = GlobalStats::new();
225        let snapshot = stats.snapshot();
226
227        assert_eq!(snapshot.total_requests, 0);
228        assert_eq!(snapshot.hits, 0);
229        assert_eq!(snapshot.misses, 0);
230        assert_eq!(snapshot.hit_rate, 0.0);
231    }
232
233    #[test]
234    fn global_stats_record_operations() {
235        let stats = GlobalStats::new();
236
237        stats.record_hit("key1");
238        stats.record_miss("key2");
239        stats.record_stale_hit("key3");
240        stats.record_store("key4");
241        stats.record_invalidation();
242
243        let snapshot = stats.snapshot();
244        assert_eq!(snapshot.total_requests, 3); // hit + miss + stale_hit
245        assert_eq!(snapshot.hits, 1);
246        assert_eq!(snapshot.misses, 1);
247        assert_eq!(snapshot.stale_hits, 1);
248        assert_eq!(snapshot.stores, 1);
249        assert_eq!(snapshot.invalidations, 1);
250    }
251
252    #[test]
253    fn global_stats_hit_rate_calculation() {
254        let stats = GlobalStats::new();
255
256        stats.record_hit("key1");
257        stats.record_hit("key2");
258        stats.record_miss("key3");
259
260        let snapshot = stats.snapshot();
261        assert_eq!(snapshot.total_requests, 3);
262        assert!((snapshot.hit_rate - 0.666).abs() < 0.01);
263        assert!((snapshot.miss_rate - 0.333).abs() < 0.01);
264    }
265
266    #[test]
267    fn global_stats_hot_keys() {
268        let stats = GlobalStats::new();
269
270        // Record hits with different frequencies
271        for _ in 0..10 {
272            stats.record_hit("hot_key");
273        }
274        for _ in 0..3 {
275            stats.record_hit("warm_key");
276        }
277        stats.record_hit("cold_key");
278
279        let hot_keys = stats.hot_keys(2);
280        assert_eq!(hot_keys.len(), 2);
281        assert_eq!(hot_keys[0].key, "hot_key");
282        assert_eq!(hot_keys[0].hits, 10);
283        assert_eq!(hot_keys[1].key, "warm_key");
284        assert_eq!(hot_keys[1].hits, 3);
285    }
286
287    #[test]
288    fn global_stats_reset() {
289        let stats = GlobalStats::new();
290
291        stats.record_hit("key1");
292        stats.record_miss("key2");
293
294        stats.reset();
295
296        let snapshot = stats.snapshot();
297        assert_eq!(snapshot.total_requests, 0);
298        assert_eq!(snapshot.hits, 0);
299        assert_eq!(snapshot.misses, 0);
300    }
301
302    #[test]
303    fn stats_snapshot_serialization() {
304        let snapshot = StatsSnapshot {
305            total_requests: 100,
306            hits: 80,
307            misses: 20,
308            stale_hits: 5,
309            stores: 90,
310            invalidations: 10,
311            hit_rate: 0.8,
312            miss_rate: 0.2,
313            uptime_seconds: 3600,
314        };
315
316        let json = serde_json::to_string(&snapshot).unwrap();
317        assert!(json.contains("\"total_requests\":100"));
318        assert!(json.contains("\"hit_rate\":0.8"));
319    }
320}