Skip to main content

modkit_auth/
metrics.rs

1/// Metrics tracking for auth events
2///
3/// This module provides a trait-based approach to metrics that can be
4/// implemented with various backends (Prometheus, `StatsD`, etc.)
5/// Auth event types for metrics tracking
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AuthEvent {
8    /// JWT validation succeeded
9    JwtValid,
10
11    /// JWT validation failed
12    JwtInvalid,
13
14    /// JWKS refresh succeeded
15    JwksRefreshSuccess,
16
17    /// JWKS refresh failed
18    JwksRefreshFailure,
19
20    /// Opaque token validation succeeded
21    OpaqueTokenValid,
22
23    /// Opaque token validation failed
24    OpaqueTokenInvalid,
25}
26
27impl AuthEvent {
28    /// Get the metric name for this event
29    #[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/// Labels for auth metrics
43#[derive(Default, Debug, Clone)]
44#[must_use]
45pub struct AuthMetricLabels {
46    /// Provider name (e.g., "keycloak", "`oidc_default`")
47    pub provider: Option<String>,
48
49    /// Issuer URL
50    pub issuer: Option<String>,
51
52    /// Key ID (for JWKS)
53    pub kid: Option<String>,
54
55    /// Error type (for failures)
56    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
81/// Trait for metrics backends
82pub trait AuthMetrics: Send + Sync {
83    /// Record an auth event
84    fn record_event(&self, event: AuthEvent, labels: &AuthMetricLabels);
85
86    /// Record validation duration
87    fn record_duration(&self, duration_ms: u64, labels: &AuthMetricLabels);
88}
89
90/// No-op metrics implementation (default)
91#[derive(Debug, Clone, Copy)]
92pub struct NoOpMetrics;
93
94impl AuthMetrics for NoOpMetrics {
95    fn record_event(&self, _event: AuthEvent, _labels: &AuthMetricLabels) {
96        // No-op
97    }
98
99    fn record_duration(&self, _duration_ms: u64, _labels: &AuthMetricLabels) {
100        // No-op
101    }
102}
103
104/// Logging-based metrics implementation (for debugging)
105#[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        // Should not panic
169        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        // Should not panic
181        metrics.record_event(AuthEvent::JwtValid, &labels);
182        metrics.record_duration(50, &labels);
183    }
184}