1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AuthEvent {
8 JwtValid,
10
11 JwtInvalid,
13
14 JwksRefreshSuccess,
16
17 JwksRefreshFailure,
19
20 OpaqueTokenValid,
22
23 OpaqueTokenInvalid,
25}
26
27impl AuthEvent {
28 #[must_use]
30 pub fn metric_name(&self) -> &'static str {
31 match self {
32 AuthEvent::JwtValid => "auth.jwt.valid",
33 AuthEvent::JwtInvalid => "auth.jwt.invalid",
34 AuthEvent::JwksRefreshSuccess => "auth.jwks.refresh.ok",
35 AuthEvent::JwksRefreshFailure => "auth.jwks.refresh.fail",
36 AuthEvent::OpaqueTokenValid => "auth.opaque.valid",
37 AuthEvent::OpaqueTokenInvalid => "auth.opaque.invalid",
38 }
39 }
40}
41
42#[derive(Default, Debug, Clone)]
44#[must_use]
45pub struct AuthMetricLabels {
46 pub provider: Option<String>,
48
49 pub issuer: Option<String>,
51
52 pub kid: Option<String>,
54
55 pub error_type: Option<String>,
57}
58
59impl AuthMetricLabels {
60 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
61 self.provider = Some(provider.into());
62 self
63 }
64
65 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
66 self.issuer = Some(issuer.into());
67 self
68 }
69
70 pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
71 self.kid = Some(kid.into());
72 self
73 }
74
75 pub fn with_error_type(mut self, error_type: impl Into<String>) -> Self {
76 self.error_type = Some(error_type.into());
77 self
78 }
79}
80
81pub trait AuthMetrics: Send + Sync {
83 fn record_event(&self, event: AuthEvent, labels: &AuthMetricLabels);
85
86 fn record_duration(&self, duration_ms: u64, labels: &AuthMetricLabels);
88}
89
90#[derive(Debug, Clone, Copy)]
92pub struct NoOpMetrics;
93
94impl AuthMetrics for NoOpMetrics {
95 fn record_event(&self, _event: AuthEvent, _labels: &AuthMetricLabels) {
96 }
98
99 fn record_duration(&self, _duration_ms: u64, _labels: &AuthMetricLabels) {
100 }
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct LoggingMetrics;
107
108impl AuthMetrics for LoggingMetrics {
109 fn record_event(&self, event: AuthEvent, labels: &AuthMetricLabels) {
110 tracing::debug!(
111 metric = event.metric_name(),
112 provider = ?labels.provider,
113 issuer = ?labels.issuer,
114 kid = ?labels.kid,
115 error_type = ?labels.error_type,
116 "Auth event recorded"
117 );
118 }
119
120 fn record_duration(&self, duration_ms: u64, labels: &AuthMetricLabels) {
121 tracing::debug!(
122 metric = "auth.validation.duration_ms",
123 duration_ms = duration_ms,
124 provider = ?labels.provider,
125 issuer = ?labels.issuer,
126 "Validation duration recorded"
127 );
128 }
129}
130
131#[cfg(test)]
132#[cfg_attr(coverage_nightly, coverage(off))]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_auth_event_metric_names() {
138 assert_eq!(AuthEvent::JwtValid.metric_name(), "auth.jwt.valid");
139 assert_eq!(AuthEvent::JwtInvalid.metric_name(), "auth.jwt.invalid");
140 assert_eq!(
141 AuthEvent::JwksRefreshSuccess.metric_name(),
142 "auth.jwks.refresh.ok"
143 );
144 assert_eq!(
145 AuthEvent::JwksRefreshFailure.metric_name(),
146 "auth.jwks.refresh.fail"
147 );
148 }
149
150 #[test]
151 fn test_metric_labels_builder() {
152 let labels = AuthMetricLabels::default()
153 .with_provider("keycloak")
154 .with_issuer("https://kc.example.com")
155 .with_kid("key-123");
156
157 assert_eq!(labels.provider, Some("keycloak".to_owned()));
158 assert_eq!(labels.issuer, Some("https://kc.example.com".to_owned()));
159 assert_eq!(labels.kid, Some("key-123".to_owned()));
160 assert_eq!(labels.error_type, None);
161 }
162
163 #[test]
164 fn test_noop_metrics() {
165 let metrics = NoOpMetrics;
166 let labels = AuthMetricLabels::default();
167
168 metrics.record_event(AuthEvent::JwtValid, &labels);
170 metrics.record_duration(100, &labels);
171 }
172
173 #[test]
174 fn test_logging_metrics() {
175 let metrics = LoggingMetrics;
176 let labels = AuthMetricLabels::default()
177 .with_provider("test")
178 .with_issuer("https://test.example.com");
179
180 metrics.record_event(AuthEvent::JwtValid, &labels);
182 metrics.record_duration(50, &labels);
183 }
184}