role_system/
metrics.rs

1//! Metrics collection for the role system.
2
3use dashmap::DashMap;
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9/// Metrics collector for role system operations.
10#[derive(Debug, Clone)]
11pub struct RoleSystemMetrics {
12    /// Number of permission checks performed.
13    pub permission_checks: Arc<AtomicU64>,
14    /// Number of cache hits.
15    pub cache_hits: Arc<AtomicU64>,
16    /// Number of cache misses.
17    pub cache_misses: Arc<AtomicU64>,
18    /// Number of role assignments.
19    pub role_assignments: Arc<AtomicU64>,
20    /// Number of role removals.
21    pub role_removals: Arc<AtomicU64>,
22    /// Number of role elevations.
23    pub role_elevations: Arc<AtomicU64>,
24    /// Permission check durations (simplified histogram).
25    pub permission_check_durations: Arc<DashMap<String, Duration>>,
26    /// Error counts by type.
27    pub error_counts: Arc<DashMap<String, AtomicU64>>,
28    /// Subject activity counts.
29    pub subject_activity: Arc<DashMap<String, AtomicU64>>,
30}
31
32impl Default for RoleSystemMetrics {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl RoleSystemMetrics {
39    /// Create a new metrics collector.
40    pub fn new() -> Self {
41        Self {
42            permission_checks: Arc::new(AtomicU64::new(0)),
43            cache_hits: Arc::new(AtomicU64::new(0)),
44            cache_misses: Arc::new(AtomicU64::new(0)),
45            role_assignments: Arc::new(AtomicU64::new(0)),
46            role_removals: Arc::new(AtomicU64::new(0)),
47            role_elevations: Arc::new(AtomicU64::new(0)),
48            permission_check_durations: Arc::new(DashMap::new()),
49            error_counts: Arc::new(DashMap::new()),
50            subject_activity: Arc::new(DashMap::new()),
51        }
52    }
53
54    /// Record a permission check.
55    pub fn record_permission_check(&self, duration: Duration) {
56        self.permission_checks.fetch_add(1, Ordering::Relaxed);
57
58        // Simple bucketed histogram
59        let bucket = self.duration_to_bucket(duration);
60        self.permission_check_durations
61            .entry(bucket)
62            .and_modify(|existing| {
63                if duration > *existing {
64                    *existing = duration;
65                }
66            })
67            .or_insert(duration);
68    }
69
70    /// Record a cache hit.
71    pub fn record_cache_hit(&self) {
72        self.cache_hits.fetch_add(1, Ordering::Relaxed);
73    }
74
75    /// Record a cache miss.
76    pub fn record_cache_miss(&self) {
77        self.cache_misses.fetch_add(1, Ordering::Relaxed);
78    }
79
80    /// Record a role assignment.
81    pub fn record_role_assignment(&self, subject_id: &str) {
82        self.role_assignments.fetch_add(1, Ordering::Relaxed);
83        self.record_subject_activity(subject_id);
84    }
85
86    /// Record a role removal.
87    pub fn record_role_removal(&self, subject_id: &str) {
88        self.role_removals.fetch_add(1, Ordering::Relaxed);
89        self.record_subject_activity(subject_id);
90    }
91
92    /// Record a role elevation.
93    pub fn record_role_elevation(&self, subject_id: &str) {
94        self.role_elevations.fetch_add(1, Ordering::Relaxed);
95        self.record_subject_activity(subject_id);
96    }
97
98    /// Record an error.
99    pub fn record_error(&self, error_type: &str) {
100        self.error_counts
101            .entry(error_type.to_string())
102            .and_modify(|count| {
103                count.fetch_add(1, Ordering::Relaxed);
104            })
105            .or_insert_with(|| AtomicU64::new(1));
106    }
107
108    /// Record subject activity.
109    pub fn record_subject_activity(&self, subject_id: &str) {
110        self.subject_activity
111            .entry(subject_id.to_string())
112            .and_modify(|count| {
113                count.fetch_add(1, Ordering::Relaxed);
114            })
115            .or_insert_with(|| AtomicU64::new(1));
116    }
117
118    /// Get cache hit ratio.
119    pub fn cache_hit_ratio(&self) -> f64 {
120        let hits = self.cache_hits.load(Ordering::Relaxed);
121        let misses = self.cache_misses.load(Ordering::Relaxed);
122        let total = hits + misses;
123
124        if total == 0 {
125            0.0
126        } else {
127            hits as f64 / total as f64
128        }
129    }
130
131    /// Get metrics summary.
132    pub fn summary(&self) -> MetricsSummary {
133        MetricsSummary {
134            permission_checks: self.permission_checks.load(Ordering::Relaxed),
135            cache_hits: self.cache_hits.load(Ordering::Relaxed),
136            cache_misses: self.cache_misses.load(Ordering::Relaxed),
137            cache_hit_ratio: self.cache_hit_ratio(),
138            role_assignments: self.role_assignments.load(Ordering::Relaxed),
139            role_removals: self.role_removals.load(Ordering::Relaxed),
140            role_elevations: self.role_elevations.load(Ordering::Relaxed),
141            error_counts: self
142                .error_counts
143                .iter()
144                .map(|entry| (entry.key().clone(), entry.value().load(Ordering::Relaxed)))
145                .collect(),
146            active_subjects: self.subject_activity.len() as u64,
147        }
148    }
149
150    /// Reset all metrics.
151    pub fn reset(&self) {
152        self.permission_checks.store(0, Ordering::Relaxed);
153        self.cache_hits.store(0, Ordering::Relaxed);
154        self.cache_misses.store(0, Ordering::Relaxed);
155        self.role_assignments.store(0, Ordering::Relaxed);
156        self.role_removals.store(0, Ordering::Relaxed);
157        self.role_elevations.store(0, Ordering::Relaxed);
158        self.permission_check_durations.clear();
159        self.error_counts.clear();
160        self.subject_activity.clear();
161    }
162
163    fn duration_to_bucket(&self, duration: Duration) -> String {
164        let micros = duration.as_micros();
165        match micros {
166            0..=99 => "0-99μs".to_string(),
167            100..=999 => "100-999μs".to_string(),
168            1000..=9999 => "1-9ms".to_string(),
169            10000..=99999 => "10-99ms".to_string(),
170            100000..=999999 => "100-999ms".to_string(),
171            _ => "1s+".to_string(),
172        }
173    }
174}
175
176/// Summary of metrics.
177#[derive(Debug, Clone)]
178pub struct MetricsSummary {
179    pub permission_checks: u64,
180    pub cache_hits: u64,
181    pub cache_misses: u64,
182    pub cache_hit_ratio: f64,
183    pub role_assignments: u64,
184    pub role_removals: u64,
185    pub role_elevations: u64,
186    pub error_counts: HashMap<String, u64>,
187    pub active_subjects: u64,
188}
189
190/// Timer for measuring operation durations.
191pub struct MetricsTimer {
192    start: Instant,
193    metrics: Arc<RoleSystemMetrics>,
194    operation: String,
195}
196
197impl MetricsTimer {
198    /// Create a new timer.
199    pub fn new(metrics: Arc<RoleSystemMetrics>, operation: impl Into<String>) -> Self {
200        Self {
201            start: Instant::now(),
202            metrics,
203            operation: operation.into(),
204        }
205    }
206}
207
208impl Drop for MetricsTimer {
209    fn drop(&mut self) {
210        let duration = self.start.elapsed();
211        if self.operation == "permission_check" {
212            self.metrics.record_permission_check(duration);
213        }
214    }
215}
216
217/// Trait for components that can provide metrics.
218pub trait MetricsProvider {
219    /// Get the metrics instance.
220    fn metrics(&self) -> &RoleSystemMetrics;
221
222    /// Start a timer for an operation.
223    fn start_timer(&self, operation: &str) -> MetricsTimer {
224        MetricsTimer::new(Arc::new(self.metrics().clone()), operation.to_string())
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use std::thread;
232    use std::time::Duration as StdDuration;
233
234    #[test]
235    fn test_metrics_basic_operations() {
236        let metrics = RoleSystemMetrics::new();
237
238        // Test permission check recording
239        metrics.record_permission_check(Duration::from_micros(500));
240        assert_eq!(metrics.permission_checks.load(Ordering::Relaxed), 1);
241
242        // Test cache hit/miss recording
243        metrics.record_cache_hit();
244        metrics.record_cache_miss();
245        assert_eq!(metrics.cache_hits.load(Ordering::Relaxed), 1);
246        assert_eq!(metrics.cache_misses.load(Ordering::Relaxed), 1);
247        assert_eq!(metrics.cache_hit_ratio(), 0.5);
248
249        // Test role operations
250        metrics.record_role_assignment("user1");
251        metrics.record_role_removal("user2");
252        metrics.record_role_elevation("user3");
253        assert_eq!(metrics.role_assignments.load(Ordering::Relaxed), 1);
254        assert_eq!(metrics.role_removals.load(Ordering::Relaxed), 1);
255        assert_eq!(metrics.role_elevations.load(Ordering::Relaxed), 1);
256
257        // Test error recording
258        metrics.record_error("ValidationError");
259        metrics.record_error("ValidationError");
260        assert_eq!(
261            metrics
262                .error_counts
263                .get("ValidationError")
264                .unwrap()
265                .load(Ordering::Relaxed),
266            2
267        );
268    }
269
270    #[test]
271    fn test_metrics_summary() {
272        let metrics = RoleSystemMetrics::new();
273
274        metrics.record_permission_check(Duration::from_millis(1));
275        metrics.record_cache_hit();
276        metrics.record_role_assignment("user1");
277        metrics.record_error("TestError");
278
279        let summary = metrics.summary();
280        assert_eq!(summary.permission_checks, 1);
281        assert_eq!(summary.cache_hits, 1);
282        assert_eq!(summary.role_assignments, 1);
283        assert_eq!(summary.error_counts.get("TestError"), Some(&1));
284        assert_eq!(summary.active_subjects, 1);
285    }
286
287    #[test]
288    fn test_metrics_reset() {
289        let metrics = RoleSystemMetrics::new();
290
291        metrics.record_permission_check(Duration::from_millis(1));
292        metrics.record_cache_hit();
293        metrics.record_role_assignment("user1");
294
295        metrics.reset();
296
297        let summary = metrics.summary();
298        assert_eq!(summary.permission_checks, 0);
299        assert_eq!(summary.cache_hits, 0);
300        assert_eq!(summary.role_assignments, 0);
301        assert_eq!(summary.active_subjects, 0);
302    }
303
304    #[test]
305    fn test_metrics_timer() {
306        let metrics = Arc::new(RoleSystemMetrics::new());
307
308        {
309            let _timer = MetricsTimer::new(metrics.clone(), "permission_check");
310            thread::sleep(StdDuration::from_millis(1));
311        } // Timer drops here and records the duration
312
313        assert_eq!(metrics.permission_checks.load(Ordering::Relaxed), 1);
314    }
315
316    #[test]
317    fn test_duration_bucketing() {
318        let metrics = RoleSystemMetrics::new();
319
320        assert_eq!(
321            metrics.duration_to_bucket(Duration::from_micros(50)),
322            "0-99μs"
323        );
324        assert_eq!(
325            metrics.duration_to_bucket(Duration::from_micros(500)),
326            "100-999μs"
327        );
328        assert_eq!(
329            metrics.duration_to_bucket(Duration::from_millis(5)),
330            "1-9ms"
331        );
332        assert_eq!(
333            metrics.duration_to_bucket(Duration::from_millis(50)),
334            "10-99ms"
335        );
336        assert_eq!(
337            metrics.duration_to_bucket(Duration::from_millis(500)),
338            "100-999ms"
339        );
340        assert_eq!(metrics.duration_to_bucket(Duration::from_secs(2)), "1s+");
341    }
342}