use parking_lot::RwLock;
use std::fmt::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::debug;
#[derive(Debug)]
pub struct RedbMetrics {
cache_hits: AtomicU64,
cache_misses: AtomicU64,
cache_evictions: AtomicU64,
cache_expirations: AtomicU64,
cache_items: AtomicU64,
cache_bytes: AtomicU64,
last_export: RwLock<u64>,
export_count: RwLock<u64>,
}
impl RedbMetrics {
pub fn new() -> Self {
Self {
cache_hits: AtomicU64::new(0),
cache_misses: AtomicU64::new(0),
cache_evictions: AtomicU64::new(0),
cache_expirations: AtomicU64::new(0),
cache_items: AtomicU64::new(0),
cache_bytes: AtomicU64::new(0),
last_export: RwLock::new(0),
export_count: RwLock::new(0),
}
}
#[inline]
pub fn record_cache_hit(&self) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_cache_miss(&self) {
self.cache_misses.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_cache_eviction(&self) {
self.cache_evictions.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_cache_expiration(&self) {
self.cache_expirations.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn update_cache_size(&self, items: usize, bytes: usize) {
self.cache_items.store(items as u64, Ordering::Relaxed);
self.cache_bytes.store(bytes as u64, Ordering::Relaxed);
}
#[inline]
pub fn cache_hits(&self) -> u64 {
self.cache_hits.load(Ordering::Relaxed)
}
#[inline]
pub fn cache_misses(&self) -> u64 {
self.cache_misses.load(Ordering::Relaxed)
}
#[inline]
pub fn cache_evictions(&self) -> u64 {
self.cache_evictions.load(Ordering::Relaxed)
}
#[inline]
pub fn cache_expirations(&self) -> u64 {
self.cache_expirations.load(Ordering::Relaxed)
}
pub fn cache_hit_rate(&self) -> f64 {
let hits = self.cache_hits();
let misses = self.cache_misses();
let total = hits + misses;
if total == 0 {
0.0
} else {
hits as f64 / total as f64
}
}
#[inline]
pub fn cache_items(&self) -> u64 {
self.cache_items.load(Ordering::Relaxed)
}
#[inline]
pub fn cache_bytes(&self) -> u64 {
self.cache_bytes.load(Ordering::Relaxed)
}
pub fn export_metrics(&self) -> String {
let mut output = String::with_capacity(2048);
let hits = self.cache_hits();
let misses = self.cache_misses();
let evictions = self.cache_evictions();
let expirations = self.cache_expirations();
let items = self.cache_items();
let bytes = self.cache_bytes();
let hit_rate = self.cache_hit_rate();
writeln!(output, "# HELP redb_cache_hits_total Total cache hits").ok();
writeln!(output, "# TYPE redb_cache_hits_total counter").ok();
writeln!(output, "redb_cache_hits_total {}", hits).ok();
writeln!(
output,
"\n# HELP redb_cache_misses_total Total cache misses"
)
.ok();
writeln!(output, "# TYPE redb_cache_misses_total counter").ok();
writeln!(output, "redb_cache_misses_total {}", misses).ok();
writeln!(output, "\n# HELP redb_cache_hit_rate Cache hit rate (0-1)").ok();
writeln!(output, "# TYPE redb_cache_hit_rate gauge").ok();
writeln!(output, "redb_cache_hit_rate {:.4}", hit_rate).ok();
writeln!(
output,
"\n# HELP redb_cache_evictions_total Total cache evictions"
)
.ok();
writeln!(output, "# TYPE redb_cache_evictions_total counter").ok();
writeln!(output, "redb_cache_evictions_total {}", evictions).ok();
writeln!(
output,
"\n# HELP redb_cache_expirations_total Total cache expirations"
)
.ok();
writeln!(output, "# TYPE redb_cache_expirations_total counter").ok();
writeln!(output, "redb_cache_expirations_total {}", expirations).ok();
writeln!(
output,
"\n# HELP redb_cache_items Current number of items in cache"
)
.ok();
writeln!(output, "# TYPE redb_cache_items gauge").ok();
writeln!(output, "redb_cache_items {}", items).ok();
writeln!(
output,
"\n# HELP redb_cache_bytes Total bytes used by cache"
)
.ok();
writeln!(output, "# TYPE redb_cache_bytes gauge").ok();
writeln!(output, "redb_cache_bytes {}", bytes).ok();
{
let mut count = self.export_count.write();
*count += 1;
let mut last = self.last_export.write();
*last = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
}
debug!("Exported {} bytes of redb metrics", output.len());
output
}
pub fn reset(&self) {
self.cache_hits.store(0, Ordering::Relaxed);
self.cache_misses.store(0, Ordering::Relaxed);
self.cache_evictions.store(0, Ordering::Relaxed);
self.cache_expirations.store(0, Ordering::Relaxed);
self.cache_items.store(0, Ordering::Relaxed);
self.cache_bytes.store(0, Ordering::Relaxed);
*self.export_count.write() = 0;
*self.last_export.write() = 0;
}
}
impl Default for RedbMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_creation() {
let metrics = RedbMetrics::new();
assert_eq!(metrics.cache_hits(), 0);
assert_eq!(metrics.cache_misses(), 0);
assert_eq!(metrics.cache_hit_rate(), 0.0);
}
#[test]
fn test_cache_hit() {
let metrics = RedbMetrics::new();
metrics.record_cache_hit();
metrics.record_cache_hit();
metrics.record_cache_miss();
assert_eq!(metrics.cache_hits(), 2);
assert_eq!(metrics.cache_misses(), 1);
assert!((metrics.cache_hit_rate() - 0.666).abs() < 0.01);
}
#[test]
fn test_cache_eviction() {
let metrics = RedbMetrics::new();
metrics.record_cache_eviction();
metrics.record_cache_eviction();
assert_eq!(metrics.cache_evictions(), 2);
}
#[test]
fn test_cache_size_update() {
let metrics = RedbMetrics::new();
metrics.update_cache_size(100, 1024000);
assert_eq!(metrics.cache_items(), 100);
assert_eq!(metrics.cache_bytes(), 1024000);
}
#[test]
fn test_export_format() {
let metrics = RedbMetrics::new();
metrics.record_cache_hit();
metrics.record_cache_miss();
metrics.update_cache_size(50, 50000);
let output = metrics.export_metrics();
assert!(output.contains("redb_cache_hits_total"));
assert!(output.contains("redb_cache_misses_total"));
assert!(output.contains("redb_cache_hit_rate"));
assert!(output.contains("redb_cache_items"));
assert!(output.contains("redb_cache_bytes"));
}
#[test]
fn test_reset() {
let metrics = RedbMetrics::new();
metrics.record_cache_hit();
metrics.record_cache_miss();
metrics.reset();
assert_eq!(metrics.cache_hits(), 0);
assert_eq!(metrics.cache_misses(), 0);
}
}