use dashmap::DashMap;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct RoleSystemMetrics {
pub permission_checks: Arc<AtomicU64>,
pub cache_hits: Arc<AtomicU64>,
pub cache_misses: Arc<AtomicU64>,
pub role_assignments: Arc<AtomicU64>,
pub role_removals: Arc<AtomicU64>,
pub role_elevations: Arc<AtomicU64>,
pub permission_check_durations: Arc<DashMap<String, Duration>>,
pub error_counts: Arc<DashMap<String, AtomicU64>>,
pub subject_activity: Arc<DashMap<String, AtomicU64>>,
}
impl Default for RoleSystemMetrics {
fn default() -> Self {
Self::new()
}
}
impl RoleSystemMetrics {
pub fn new() -> Self {
Self {
permission_checks: Arc::new(AtomicU64::new(0)),
cache_hits: Arc::new(AtomicU64::new(0)),
cache_misses: Arc::new(AtomicU64::new(0)),
role_assignments: Arc::new(AtomicU64::new(0)),
role_removals: Arc::new(AtomicU64::new(0)),
role_elevations: Arc::new(AtomicU64::new(0)),
permission_check_durations: Arc::new(DashMap::new()),
error_counts: Arc::new(DashMap::new()),
subject_activity: Arc::new(DashMap::new()),
}
}
pub fn record_permission_check(&self, duration: Duration) {
self.permission_checks.fetch_add(1, Ordering::Relaxed);
let bucket = self.duration_to_bucket(duration);
self.permission_check_durations
.entry(bucket)
.and_modify(|existing| {
if duration > *existing {
*existing = duration;
}
})
.or_insert(duration);
}
pub fn record_cache_hit(&self) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
}
pub fn record_cache_miss(&self) {
self.cache_misses.fetch_add(1, Ordering::Relaxed);
}
pub fn record_role_assignment(&self, subject_id: &str) {
self.role_assignments.fetch_add(1, Ordering::Relaxed);
self.record_subject_activity(subject_id);
}
pub fn record_role_removal(&self, subject_id: &str) {
self.role_removals.fetch_add(1, Ordering::Relaxed);
self.record_subject_activity(subject_id);
}
pub fn record_role_elevation(&self, subject_id: &str) {
self.role_elevations.fetch_add(1, Ordering::Relaxed);
self.record_subject_activity(subject_id);
}
pub fn record_error(&self, error_type: &str) {
self.error_counts
.entry(error_type.to_string())
.and_modify(|count| {
count.fetch_add(1, Ordering::Relaxed);
})
.or_insert_with(|| AtomicU64::new(1));
}
pub fn record_subject_activity(&self, subject_id: &str) {
self.subject_activity
.entry(subject_id.to_string())
.and_modify(|count| {
count.fetch_add(1, Ordering::Relaxed);
})
.or_insert_with(|| AtomicU64::new(1));
}
pub fn cache_hit_ratio(&self) -> f64 {
let hits = self.cache_hits.load(Ordering::Relaxed);
let misses = self.cache_misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
pub fn summary(&self) -> MetricsSummary {
MetricsSummary {
permission_checks: self.permission_checks.load(Ordering::Relaxed),
cache_hits: self.cache_hits.load(Ordering::Relaxed),
cache_misses: self.cache_misses.load(Ordering::Relaxed),
cache_hit_ratio: self.cache_hit_ratio(),
role_assignments: self.role_assignments.load(Ordering::Relaxed),
role_removals: self.role_removals.load(Ordering::Relaxed),
role_elevations: self.role_elevations.load(Ordering::Relaxed),
error_counts: self
.error_counts
.iter()
.map(|entry| (entry.key().clone(), entry.value().load(Ordering::Relaxed)))
.collect(),
active_subjects: self.subject_activity.len() as u64,
}
}
pub fn reset(&self) {
self.permission_checks.store(0, Ordering::Relaxed);
self.cache_hits.store(0, Ordering::Relaxed);
self.cache_misses.store(0, Ordering::Relaxed);
self.role_assignments.store(0, Ordering::Relaxed);
self.role_removals.store(0, Ordering::Relaxed);
self.role_elevations.store(0, Ordering::Relaxed);
self.permission_check_durations.clear();
self.error_counts.clear();
self.subject_activity.clear();
}
fn duration_to_bucket(&self, duration: Duration) -> String {
let micros = duration.as_micros();
match micros {
0..=99 => "0-99μs".to_string(),
100..=999 => "100-999μs".to_string(),
1000..=9999 => "1-9ms".to_string(),
10000..=99999 => "10-99ms".to_string(),
100000..=999999 => "100-999ms".to_string(),
_ => "1s+".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsSummary {
pub permission_checks: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub cache_hit_ratio: f64,
pub role_assignments: u64,
pub role_removals: u64,
pub role_elevations: u64,
pub error_counts: HashMap<String, u64>,
pub active_subjects: u64,
}
pub struct MetricsTimer {
start: Instant,
metrics: Arc<RoleSystemMetrics>,
operation: String,
}
impl MetricsTimer {
pub fn new(metrics: Arc<RoleSystemMetrics>, operation: impl Into<String>) -> Self {
Self {
start: Instant::now(),
metrics,
operation: operation.into(),
}
}
}
impl Drop for MetricsTimer {
fn drop(&mut self) {
let duration = self.start.elapsed();
if self.operation == "permission_check" {
self.metrics.record_permission_check(duration);
}
}
}
pub trait MetricsProvider {
fn metrics(&self) -> &RoleSystemMetrics;
fn start_timer(&self, operation: &str) -> MetricsTimer {
MetricsTimer::new(Arc::new(self.metrics().clone()), operation.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration as StdDuration;
#[test]
fn test_metrics_basic_operations() {
let metrics = RoleSystemMetrics::new();
metrics.record_permission_check(Duration::from_micros(500));
assert_eq!(metrics.permission_checks.load(Ordering::Relaxed), 1);
metrics.record_cache_hit();
metrics.record_cache_miss();
assert_eq!(metrics.cache_hits.load(Ordering::Relaxed), 1);
assert_eq!(metrics.cache_misses.load(Ordering::Relaxed), 1);
assert_eq!(metrics.cache_hit_ratio(), 0.5);
metrics.record_role_assignment("user1");
metrics.record_role_removal("user2");
metrics.record_role_elevation("user3");
assert_eq!(metrics.role_assignments.load(Ordering::Relaxed), 1);
assert_eq!(metrics.role_removals.load(Ordering::Relaxed), 1);
assert_eq!(metrics.role_elevations.load(Ordering::Relaxed), 1);
metrics.record_error("ValidationError");
metrics.record_error("ValidationError");
assert_eq!(
metrics
.error_counts
.get("ValidationError")
.unwrap()
.load(Ordering::Relaxed),
2
);
}
#[test]
fn test_metrics_summary() {
let metrics = RoleSystemMetrics::new();
metrics.record_permission_check(Duration::from_millis(1));
metrics.record_cache_hit();
metrics.record_role_assignment("user1");
metrics.record_error("TestError");
let summary = metrics.summary();
assert_eq!(summary.permission_checks, 1);
assert_eq!(summary.cache_hits, 1);
assert_eq!(summary.role_assignments, 1);
assert_eq!(summary.error_counts.get("TestError"), Some(&1));
assert_eq!(summary.active_subjects, 1);
}
#[test]
fn test_metrics_reset() {
let metrics = RoleSystemMetrics::new();
metrics.record_permission_check(Duration::from_millis(1));
metrics.record_cache_hit();
metrics.record_role_assignment("user1");
metrics.reset();
let summary = metrics.summary();
assert_eq!(summary.permission_checks, 0);
assert_eq!(summary.cache_hits, 0);
assert_eq!(summary.role_assignments, 0);
assert_eq!(summary.active_subjects, 0);
}
#[test]
fn test_metrics_timer() {
let metrics = Arc::new(RoleSystemMetrics::new());
{
let _timer = MetricsTimer::new(metrics.clone(), "permission_check");
thread::sleep(StdDuration::from_millis(1));
}
assert_eq!(metrics.permission_checks.load(Ordering::Relaxed), 1);
}
#[test]
fn test_duration_bucketing() {
let metrics = RoleSystemMetrics::new();
assert_eq!(
metrics.duration_to_bucket(Duration::from_micros(50)),
"0-99μs"
);
assert_eq!(
metrics.duration_to_bucket(Duration::from_micros(500)),
"100-999μs"
);
assert_eq!(
metrics.duration_to_bucket(Duration::from_millis(5)),
"1-9ms"
);
assert_eq!(
metrics.duration_to_bucket(Duration::from_millis(50)),
"10-99ms"
);
assert_eq!(
metrics.duration_to_bucket(Duration::from_millis(500)),
"100-999ms"
);
assert_eq!(metrics.duration_to_bucket(Duration::from_secs(2)), "1s+");
}
}