hyperstack_auth/
metrics.rs1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Instant;
3
4#[derive(Debug, Default)]
6pub struct AuthMetrics {
7 total_attempts: AtomicU64,
9 success_count: AtomicU64,
11 failure_counts: std::sync::Mutex<std::collections::HashMap<String, u64>>,
13 jwks_fetch_count: AtomicU64,
15 jwks_fetch_latency_us: AtomicU64,
17 jwks_fetch_failures: AtomicU64,
19 verification_latency_us: AtomicU64,
21}
22
23impl AuthMetrics {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn record_attempt(&self) {
31 self.total_attempts.fetch_add(1, Ordering::Relaxed);
32 }
33
34 pub fn record_success(&self) {
36 self.success_count.fetch_add(1, Ordering::Relaxed);
37 }
38
39 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 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 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 pub fn total_attempts(&self) -> u64 {
63 self.total_attempts.load(Ordering::Relaxed)
64 }
65
66 pub fn success_count(&self) -> u64 {
68 self.success_count.load(Ordering::Relaxed)
69 }
70
71 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 pub fn failure_counts(&self) -> std::collections::HashMap<String, u64> {
83 self.failure_counts.lock().unwrap().clone()
84 }
85
86 pub fn jwks_fetch_count(&self) -> u64 {
88 self.jwks_fetch_count.load(Ordering::Relaxed)
89 }
90
91 pub fn jwks_fetch_latency_us(&self) -> u64 {
93 self.jwks_fetch_latency_us.load(Ordering::Relaxed)
94 }
95
96 pub fn jwks_fetch_failures(&self) -> u64 {
98 self.jwks_fetch_failures.load(Ordering::Relaxed)
99 }
100
101 pub fn verification_latency_us(&self) -> u64 {
103 self.verification_latency_us.load(Ordering::Relaxed)
104 }
105
106 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#[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
134pub trait AuthMetricsCollector: Send + Sync {
136 fn metrics(&self) -> Option<&AuthMetrics> {
138 None
139 }
140
141 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 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 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}