#![allow(dead_code)]
use arc_swap::ArcSwap;
use serde::Serialize;
use std::sync::{Arc, OnceLock};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Serialize)]
pub struct EvaluationStats {
pub duration: Duration,
pub allowed: bool,
pub principal_id: String,
pub action_id: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct EvaluationPhases {
pub apply_labels_ms: f64,
pub construct_entities_ms: f64,
pub resolve_groups_ms: f64,
pub authorize_ms: f64,
pub total_ms: f64,
}
impl EvaluationPhases {
pub fn overhead_ms(&self) -> f64 {
(self.total_ms
- (self.apply_labels_ms
+ self.construct_entities_ms
+ self.resolve_groups_ms
+ self.authorize_ms))
.max(0.0)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ReloadStats {
pub reload_time: SystemTime,
}
pub trait MetricsSink: Send + Sync {
fn on_evaluation(&self, stats: &EvaluationStats);
fn on_reload(&self, stats: &ReloadStats);
fn on_evaluation_phases(&self, _stats: &EvaluationStats, _phases: &EvaluationPhases) {
}
}
struct NoOpSink;
impl MetricsSink for NoOpSink {
fn on_evaluation(&self, _stats: &EvaluationStats) {}
fn on_reload(&self, _stats: &ReloadStats) {}
}
static SINK: OnceLock<ArcSwap<Arc<dyn MetricsSink>>> = OnceLock::new();
fn sink() -> Arc<dyn MetricsSink> {
SINK.get_or_init(|| {
let default: Arc<dyn MetricsSink> = Arc::new(NoOpSink);
ArcSwap::from(Arc::new(default))
})
.load()
.as_ref()
.clone()
}
pub fn set_sink(sink: Arc<dyn MetricsSink>) {
SINK.get_or_init(|| {
let default: Arc<dyn MetricsSink> = Arc::new(NoOpSink);
ArcSwap::from(Arc::new(default))
})
.store(Arc::new(sink));
}
pub(crate) fn get_sink() -> Arc<dyn MetricsSink> {
sink()
}
pub(crate) fn record_evaluation(stats: &EvaluationStats) {
let sink = get_sink();
sink.on_evaluation(stats);
}
pub(crate) fn record_evaluation_phases(stats: &EvaluationStats, phases: &EvaluationPhases) {
let sink = get_sink();
sink.on_evaluation_phases(stats, phases);
}
pub(crate) fn record_reload() {
let sink = get_sink();
sink.on_reload(&ReloadStats {
reload_time: SystemTime::now(),
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
#[allow(dead_code)]
struct TestSink {
eval_count: AtomicU64,
allow_count: AtomicU64,
deny_count: AtomicU64,
reload_count: AtomicU64,
last_eval_duration: Mutex<Option<Duration>>,
was_called: AtomicBool,
}
#[allow(dead_code)]
impl TestSink {
fn new() -> Self {
Self {
eval_count: AtomicU64::new(0),
allow_count: AtomicU64::new(0),
deny_count: AtomicU64::new(0),
reload_count: AtomicU64::new(0),
last_eval_duration: Mutex::new(None),
was_called: AtomicBool::new(false),
}
}
}
impl MetricsSink for TestSink {
fn on_evaluation(&self, stats: &EvaluationStats) {
self.was_called.store(true, Ordering::SeqCst);
self.eval_count.fetch_add(1, Ordering::SeqCst);
if stats.allowed {
self.allow_count.fetch_add(1, Ordering::SeqCst);
} else {
self.deny_count.fetch_add(1, Ordering::SeqCst);
}
if let Ok(mut d) = self.last_eval_duration.lock() {
*d = Some(stats.duration);
}
}
fn on_reload(&self, _stats: &ReloadStats) {
self.reload_count.fetch_add(1, Ordering::SeqCst);
}
}
#[test]
fn test_evaluation_stats_serialization() {
let stats = EvaluationStats {
duration: Duration::from_millis(42),
allowed: true,
principal_id: "User::test".to_string(),
action_id: "Action::test".to_string(),
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("42") || json.contains("0.042")); assert!(json.contains("true"));
}
#[test]
fn test_reload_stats_serialization() {
let now = SystemTime::now();
let stats = ReloadStats { reload_time: now };
let json = serde_json::to_string(&stats).unwrap();
assert!(!json.is_empty());
}
#[test]
fn test_record_evaluation_with_no_op_sink() {
let stats1 = EvaluationStats {
duration: Duration::from_millis(100),
allowed: true,
principal_id: "User::test".to_string(),
action_id: "Action::test".to_string(),
};
record_evaluation(&stats1);
let stats2 = EvaluationStats {
duration: Duration::from_millis(50),
allowed: false,
principal_id: "User::alice".to_string(),
action_id: "Action::view".to_string(),
};
record_evaluation(&stats2);
}
#[test]
fn test_record_reload_with_no_op_sink() {
record_reload();
}
#[test]
fn test_noop_sink_impl() {
let sink = NoOpSink;
let stats = EvaluationStats {
duration: Duration::from_micros(1),
allowed: true,
principal_id: "User::test".to_string(),
action_id: "Action::test".to_string(),
};
sink.on_evaluation(&stats);
let reload_stats = ReloadStats {
reload_time: SystemTime::now(),
};
sink.on_reload(&reload_stats);
}
#[test]
fn test_evaluation_stats_clone() {
let stats1 = EvaluationStats {
duration: Duration::from_secs(1),
allowed: false,
principal_id: "User::test".to_string(),
action_id: "Action::test".to_string(),
};
let stats2 = stats1.clone();
assert_eq!(stats1.duration, stats2.duration);
assert_eq!(stats1.allowed, stats2.allowed);
assert_eq!(stats1.principal_id, stats2.principal_id);
assert_eq!(stats1.action_id, stats2.action_id);
}
#[test]
fn test_reload_stats_clone() {
let now = SystemTime::now();
let stats1 = ReloadStats { reload_time: now };
let stats2 = stats1.clone();
assert_eq!(stats1.reload_time, stats2.reload_time);
}
#[test]
fn test_evaluation_stats_debug() {
let stats = EvaluationStats {
duration: Duration::from_micros(250),
allowed: true,
principal_id: "User::test".to_string(),
action_id: "Action::test".to_string(),
};
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("EvaluationStats"));
assert!(debug_str.contains("true"));
}
#[test]
fn test_reload_stats_debug() {
let stats = ReloadStats {
reload_time: SystemTime::now(),
};
let debug_str = format!("{:?}", stats);
assert!(debug_str.contains("ReloadStats"));
}
#[test]
fn test_dynamic_sink_swapping() {
let sink1 = Arc::new(TestSink::new());
let sink2 = Arc::new(TestSink::new());
set_sink(sink1.clone());
let stats1 = EvaluationStats {
duration: Duration::from_millis(10),
allowed: true,
principal_id: "User::alice".to_string(),
action_id: "Action::read".to_string(),
};
record_evaluation(&stats1);
set_sink(sink2.clone());
let stats2 = EvaluationStats {
duration: Duration::from_millis(20),
allowed: false,
principal_id: "User::bob".to_string(),
action_id: "Action::write".to_string(),
};
record_evaluation(&stats2);
}
}