Skip to main content

hyperstack_auth/
metrics.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Instant;
3
4/// Authentication metrics for observability
5#[derive(Debug, Default)]
6pub struct AuthMetrics {
7    /// Total authentication attempts
8    total_attempts: AtomicU64,
9    /// Successful authentications
10    success_count: AtomicU64,
11    /// Failed authentications by error code
12    failure_counts: std::sync::Mutex<std::collections::HashMap<String, u64>>,
13    /// JWKS fetch count
14    jwks_fetch_count: AtomicU64,
15    /// JWKS fetch latency in microseconds (last value)
16    jwks_fetch_latency_us: AtomicU64,
17    /// JWKS fetch failures
18    jwks_fetch_failures: AtomicU64,
19    /// Token verification latency in microseconds (last value)
20    verification_latency_us: AtomicU64,
21}
22
23impl AuthMetrics {
24    /// Create new auth metrics
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Record an authentication attempt
30    pub fn record_attempt(&self) {
31        self.total_attempts.fetch_add(1, Ordering::Relaxed);
32    }
33
34    /// Record a successful authentication
35    pub fn record_success(&self) {
36        self.success_count.fetch_add(1, Ordering::Relaxed);
37    }
38
39    /// Record a failed authentication
40    pub fn record_failure(&self, error_code: &crate::AuthErrorCode) {
41        let mut counts = self.failure_counts.lock().unwrap();
42        *counts.entry(error_code.to_string()).or_insert(0) += 1;
43    }
44
45    /// Record JWKS fetch with latency
46    pub fn record_jwks_fetch(&self, latency: std::time::Duration, success: bool) {
47        self.jwks_fetch_count.fetch_add(1, Ordering::Relaxed);
48        self.jwks_fetch_latency_us
49            .store(latency.as_micros() as u64, Ordering::Relaxed);
50        if !success {
51            self.jwks_fetch_failures.fetch_add(1, Ordering::Relaxed);
52        }
53    }
54
55    /// Record token verification latency
56    pub fn record_verification_latency(&self, latency: std::time::Duration) {
57        self.verification_latency_us
58            .store(latency.as_micros() as u64, Ordering::Relaxed);
59    }
60
61    /// Get total attempts
62    pub fn total_attempts(&self) -> u64 {
63        self.total_attempts.load(Ordering::Relaxed)
64    }
65
66    /// Get success count
67    pub fn success_count(&self) -> u64 {
68        self.success_count.load(Ordering::Relaxed)
69    }
70
71    /// Get success rate (0.0 - 1.0)
72    pub fn success_rate(&self) -> f64 {
73        let total = self.total_attempts();
74        if total == 0 {
75            0.0
76        } else {
77            self.success_count() as f64 / total as f64
78        }
79    }
80
81    /// Get failure counts by error code
82    pub fn failure_counts(&self) -> std::collections::HashMap<String, u64> {
83        self.failure_counts.lock().unwrap().clone()
84    }
85
86    /// Get JWKS fetch count
87    pub fn jwks_fetch_count(&self) -> u64 {
88        self.jwks_fetch_count.load(Ordering::Relaxed)
89    }
90
91    /// Get JWKS fetch latency in microseconds
92    pub fn jwks_fetch_latency_us(&self) -> u64 {
93        self.jwks_fetch_latency_us.load(Ordering::Relaxed)
94    }
95
96    /// Get number of JWKS fetch failures
97    pub fn jwks_fetch_failures(&self) -> u64 {
98        self.jwks_fetch_failures.load(Ordering::Relaxed)
99    }
100
101    /// Get verification latency in microseconds
102    pub fn verification_latency_us(&self) -> u64 {
103        self.verification_latency_us.load(Ordering::Relaxed)
104    }
105
106    /// Get metrics as a serializable snapshot
107    pub fn snapshot(&self) -> AuthMetricsSnapshot {
108        AuthMetricsSnapshot {
109            total_attempts: self.total_attempts(),
110            success_count: self.success_count(),
111            success_rate: self.success_rate(),
112            failure_counts: self.failure_counts(),
113            jwks_fetch_count: self.jwks_fetch_count(),
114            jwks_fetch_latency_us: self.jwks_fetch_latency_us(),
115            jwks_fetch_failures: self.jwks_fetch_failures(),
116            verification_latency_us: self.verification_latency_us(),
117        }
118    }
119}
120
121/// Serializable snapshot of auth metrics
122#[derive(Debug, Clone, serde::Serialize)]
123pub struct AuthMetricsSnapshot {
124    pub total_attempts: u64,
125    pub success_count: u64,
126    pub success_rate: f64,
127    pub failure_counts: std::collections::HashMap<String, u64>,
128    pub jwks_fetch_count: u64,
129    pub jwks_fetch_latency_us: u64,
130    pub jwks_fetch_failures: u64,
131    pub verification_latency_us: u64,
132}
133
134/// Trait for collecting auth metrics
135pub trait AuthMetricsCollector: Send + Sync {
136    /// Get auth metrics
137    fn metrics(&self) -> Option<&AuthMetrics> {
138        None
139    }
140
141    /// Record an authentication attempt with metrics
142    fn record_auth_attempt(&self, success: bool, error_code: Option<&crate::AuthErrorCode>) {
143        if let Some(metrics) = self.metrics() {
144            metrics.record_attempt();
145            if success {
146                metrics.record_success();
147            } else if let Some(code) = error_code {
148                metrics.record_failure(code);
149            }
150        }
151    }
152
153    /// Time a JWKS fetch operation
154    fn time_jwks_fetch<F, R>(&self, f: F) -> R
155    where
156        F: FnOnce() -> R,
157    {
158        let start = Instant::now();
159        let result = f();
160        if let Some(metrics) = self.metrics() {
161            metrics.record_jwks_fetch(start.elapsed(), true);
162        }
163        result
164    }
165
166    /// Time a token verification operation
167    fn time_verification<F, R>(&self, f: F) -> R
168    where
169        F: FnOnce() -> R,
170    {
171        let start = Instant::now();
172        let result = f();
173        if let Some(metrics) = self.metrics() {
174            metrics.record_verification_latency(start.elapsed());
175        }
176        result
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_auth_metrics() {
186        let metrics = AuthMetrics::new();
187
188        metrics.record_attempt();
189        metrics.record_success();
190
191        metrics.record_attempt();
192        metrics.record_failure(&crate::AuthErrorCode::TokenExpired);
193
194        assert_eq!(metrics.total_attempts(), 2);
195        assert_eq!(metrics.success_count(), 1);
196        assert_eq!(metrics.success_rate(), 0.5);
197
198        let failures = metrics.failure_counts();
199        assert_eq!(failures.get("token-expired"), Some(&1));
200    }
201
202    #[test]
203    fn test_metrics_snapshot() {
204        let metrics = AuthMetrics::new();
205        metrics.record_attempt();
206        metrics.record_success();
207
208        let snapshot = metrics.snapshot();
209        assert_eq!(snapshot.total_attempts, 1);
210        assert_eq!(snapshot.success_count, 1);
211        assert_eq!(snapshot.success_rate, 1.0);
212    }
213}