use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct CacheStats {
pub name: String,
pub hits: u64,
pub misses: u64,
pub entries: u64,
pub hit_rate: f64,
}
impl CacheStats {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
hits: 0,
misses: 0,
entries: 0,
hit_rate: 0.0,
}
}
pub fn total_requests(&self) -> u64 {
self.hits + self.misses
}
pub fn print(&self) {
println!("Cache Stats: {}", self.name);
println!(" Entries: {}", self.entries);
println!(" Hits: {}", self.hits);
println!(" Misses: {}", self.misses);
println!(" Hit Rate: {:.2}%", self.hit_rate * 100.0);
println!(" Total Reqs: {}", self.total_requests());
}
pub fn to_json(&self) -> String {
format!(
r#"{{
"name": "{}",
"hits": {},
"misses": {},
"entries": {},
"hit_rate": {:.4},
"total_requests": {}
}}"#,
self.name,
self.hits,
self.misses,
self.entries,
self.hit_rate,
self.total_requests()
)
}
}
pub struct GlobalStats {
stats: Arc<Mutex<HashMap<String, CacheStats>>>,
}
impl GlobalStats {
pub fn new() -> Self {
Self {
stats: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn register(&self, name: impl Into<String>, stats: CacheStats) {
let mut global_stats = self.stats.lock().unwrap();
global_stats.insert(name.into(), stats);
}
pub fn update(&self, name: &str, stats: CacheStats) {
let mut global_stats = self.stats.lock().unwrap();
global_stats.insert(name.to_string(), stats);
}
pub fn get(&self, name: &str) -> Option<CacheStats> {
let stats = self.stats.lock().unwrap();
stats.get(name).cloned()
}
pub fn all(&self) -> HashMap<String, CacheStats> {
let stats = self.stats.lock().unwrap();
stats.clone()
}
pub fn print_all(&self) {
let stats = self.stats.lock().unwrap();
if stats.is_empty() {
println!("No cache statistics available");
return;
}
println!("= Fondue Cache Statistics =");
for stat in stats.values() {
stat.print();
println!();
}
}
pub fn print_table(&self) {
let stats = self.stats.lock().unwrap();
if stats.is_empty() {
println!("No cache statistics available");
return;
}
println!("┌─────────────────────────┬─────────┬──────┬────────┬──────────┬───────────┐");
println!("│ Cache Name │ Entries │ Hits │ Misses │ Hit Rate │ Total Req │");
println!("├─────────────────────────┼─────────┼──────┼────────┼──────────┼───────────┤");
for stat in stats.values() {
println!(
"│ {:<23} │ {:>7} │ {:>4} │ {:>6} │ {:>7.2}% │ {:>9} │",
truncate_string(&stat.name, 23),
stat.entries,
stat.hits,
stat.misses,
stat.hit_rate * 100.0,
stat.total_requests()
);
}
println!("└─────────────────────────┴─────────┴──────┴────────┴──────────┴───────────┘");
}
pub fn to_json(&self) -> String {
let stats = self.stats.lock().unwrap();
let mut json_parts = Vec::new();
for stat in stats.values() {
json_parts.push(stat.to_json());
}
format!("[\n{}\n]", json_parts.join(",\n"))
}
pub fn aggregate(&self) -> CacheStats {
let stats = self.stats.lock().unwrap();
let mut total_hits = 0;
let mut total_misses = 0;
let mut total_entries = 0;
for stat in stats.values() {
total_hits += stat.hits;
total_misses += stat.misses;
total_entries += stat.entries;
}
let total_requests = total_hits + total_misses;
let hit_rate = if total_requests > 0 {
total_hits as f64 / total_requests as f64
} else {
0.0
};
CacheStats {
name: "AGGREGATE".to_string(),
hits: total_hits,
misses: total_misses,
entries: total_entries,
hit_rate,
}
}
pub fn clear(&self) {
let mut stats = self.stats.lock().unwrap();
stats.clear();
}
pub fn remove(&self, name: &str) -> Option<CacheStats> {
let mut stats = self.stats.lock().unwrap();
stats.remove(name)
}
}
impl Default for GlobalStats {
fn default() -> Self {
Self::new()
}
}
use std::sync::OnceLock;
static GLOBAL_STATS: OnceLock<GlobalStats> = OnceLock::new();
pub fn get_global_stats() -> &'static GlobalStats {
GLOBAL_STATS.get_or_init(GlobalStats::new)
}
pub fn print_stats() {
get_global_stats().print_all();
}
pub fn print_stats_table() {
get_global_stats().print_table();
}
pub fn get_stats(name: &str) -> Option<CacheStats> {
get_global_stats().get(name)
}
pub fn export_json() -> String {
get_global_stats().to_json()
}
pub fn aggregate_stats() -> CacheStats {
get_global_stats().aggregate()
}
pub fn clear_stats() {
get_global_stats().clear();
}
pub fn register_stats(name: impl Into<String>, stats: CacheStats) {
get_global_stats().register(name, stats);
}
pub fn update_stats(name: &str, stats: CacheStats) {
get_global_stats().update(name, stats);
}
fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_stats() {
let mut stats = CacheStats::new("test_cache");
stats.hits = 80;
stats.misses = 20;
stats.entries = 50;
stats.hit_rate = 0.8;
assert_eq!(stats.total_requests(), 100);
assert_eq!(stats.hit_rate, 0.8);
let json = stats.to_json();
assert!(json.contains("\"name\": \"test_cache\""));
assert!(json.contains("\"hits\": 80"));
}
#[test]
fn test_global_stats() {
let global = GlobalStats::new();
let stats1 = CacheStats {
name: "cache1".to_string(),
hits: 50,
misses: 10,
entries: 30,
hit_rate: 0.833,
};
let stats2 = CacheStats {
name: "cache2".to_string(),
hits: 30,
misses: 20,
entries: 25,
hit_rate: 0.6,
};
global.register("cache1", stats1);
global.register("cache2", stats2);
let aggregate = global.aggregate();
assert_eq!(aggregate.hits, 80);
assert_eq!(aggregate.misses, 30);
assert_eq!(aggregate.entries, 55);
let retrieved = global.get("cache1").unwrap();
assert_eq!(retrieved.name, "cache1");
assert_eq!(retrieved.hits, 50);
}
#[test]
fn test_truncate_string() {
assert_eq!(truncate_string("short", 10), "short");
assert_eq!(
truncate_string("this_is_a_very_long_string", 10),
"this_is..."
);
}
}