#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthEvent {
JwtValid,
JwtInvalid,
JwksRefreshSuccess,
JwksRefreshFailure,
OpaqueTokenValid,
OpaqueTokenInvalid,
}
impl AuthEvent {
#[must_use]
pub fn metric_name(&self) -> &'static str {
match self {
AuthEvent::JwtValid => "auth.jwt.valid",
AuthEvent::JwtInvalid => "auth.jwt.invalid",
AuthEvent::JwksRefreshSuccess => "auth.jwks.refresh.ok",
AuthEvent::JwksRefreshFailure => "auth.jwks.refresh.fail",
AuthEvent::OpaqueTokenValid => "auth.opaque.valid",
AuthEvent::OpaqueTokenInvalid => "auth.opaque.invalid",
}
}
}
#[derive(Default, Debug, Clone)]
#[must_use]
pub struct AuthMetricLabels {
pub provider: Option<String>,
pub issuer: Option<String>,
pub kid: Option<String>,
pub error_type: Option<String>,
}
impl AuthMetricLabels {
pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
pub fn with_error_type(mut self, error_type: impl Into<String>) -> Self {
self.error_type = Some(error_type.into());
self
}
}
pub trait AuthMetrics: Send + Sync {
fn record_event(&self, event: AuthEvent, labels: &AuthMetricLabels);
fn record_duration(&self, duration_ms: u64, labels: &AuthMetricLabels);
}
#[derive(Debug, Clone, Copy)]
pub struct NoOpMetrics;
impl AuthMetrics for NoOpMetrics {
fn record_event(&self, _event: AuthEvent, _labels: &AuthMetricLabels) {
}
fn record_duration(&self, _duration_ms: u64, _labels: &AuthMetricLabels) {
}
}
#[derive(Debug, Clone, Copy)]
pub struct LoggingMetrics;
impl AuthMetrics for LoggingMetrics {
fn record_event(&self, event: AuthEvent, labels: &AuthMetricLabels) {
tracing::debug!(
metric = event.metric_name(),
provider = ?labels.provider,
issuer = ?labels.issuer,
kid = ?labels.kid,
error_type = ?labels.error_type,
"Auth event recorded"
);
}
fn record_duration(&self, duration_ms: u64, labels: &AuthMetricLabels) {
tracing::debug!(
metric = "auth.validation.duration_ms",
duration_ms = duration_ms,
provider = ?labels.provider,
issuer = ?labels.issuer,
"Validation duration recorded"
);
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn test_auth_event_metric_names() {
assert_eq!(AuthEvent::JwtValid.metric_name(), "auth.jwt.valid");
assert_eq!(AuthEvent::JwtInvalid.metric_name(), "auth.jwt.invalid");
assert_eq!(
AuthEvent::JwksRefreshSuccess.metric_name(),
"auth.jwks.refresh.ok"
);
assert_eq!(
AuthEvent::JwksRefreshFailure.metric_name(),
"auth.jwks.refresh.fail"
);
}
#[test]
fn test_metric_labels_builder() {
let labels = AuthMetricLabels::default()
.with_provider("keycloak")
.with_issuer("https://kc.example.com")
.with_kid("key-123");
assert_eq!(labels.provider, Some("keycloak".to_owned()));
assert_eq!(labels.issuer, Some("https://kc.example.com".to_owned()));
assert_eq!(labels.kid, Some("key-123".to_owned()));
assert_eq!(labels.error_type, None);
}
#[test]
fn test_noop_metrics() {
let metrics = NoOpMetrics;
let labels = AuthMetricLabels::default();
metrics.record_event(AuthEvent::JwtValid, &labels);
metrics.record_duration(100, &labels);
}
#[test]
fn test_logging_metrics() {
let metrics = LoggingMetrics;
let labels = AuthMetricLabels::default()
.with_provider("test")
.with_issuer("https://test.example.com");
metrics.record_event(AuthEvent::JwtValid, &labels);
metrics.record_duration(50, &labels);
}
}