use std::collections::HashMap;
use std::sync::RwLock;
use std::time::{Duration, Instant};
pub trait MetricsProvider: Send + Sync {
fn record_operation(&self, operation: &str, duration: Duration);
fn increment_counter(&self, counter: &str, value: u64);
fn record_gauge(&self, gauge: &str, value: f64);
fn timer<'a>(&'a self, operation: &'a str) -> OperationTimer<'a>;
fn summary(&self) -> String;
}
pub struct OperationTimer<'a> {
provider: &'a dyn MetricsProvider,
operation: &'a str,
start: Instant,
}
impl<'a> OperationTimer<'a> {
fn new(provider: &'a dyn MetricsProvider, operation: &'a str) -> Self {
OperationTimer {
provider,
operation,
start: Instant::now(),
}
}
}
impl<'a> Drop for OperationTimer<'a> {
fn drop(&mut self) {
let duration = self.start.elapsed();
self.provider.record_operation(self.operation, duration);
}
}
#[derive(Debug)]
pub struct SimpleMetrics {
counters: RwLock<HashMap<String, u64>>,
gauges: RwLock<HashMap<String, f64>>,
timers: RwLock<HashMap<String, Vec<Duration>>>,
}
impl SimpleMetrics {
pub fn new() -> Self {
SimpleMetrics {
counters: RwLock::new(HashMap::new()),
gauges: RwLock::new(HashMap::new()),
timers: RwLock::new(HashMap::new()),
}
}
pub fn get_counter(&self, counter: &str) -> Option<u64> {
self.counters.read().unwrap_or_else(|e| e.into_inner()).get(counter).cloned()
}
pub fn get_gauge(&self, gauge: &str) -> Option<f64> {
self.gauges.read().unwrap_or_else(|e| e.into_inner()).get(gauge).cloned()
}
pub fn get_average_duration(&self, operation: &str) -> Option<Duration> {
let timers = self.timers.read().unwrap_or_else(|e| e.into_inner());
let durations = timers.get(operation)?;
if durations.is_empty() {
return None;
}
let total_nanos: u128 = durations.iter().map(|d| d.as_nanos()).sum();
let avg_nanos = total_nanos / durations.len() as u128;
Some(Duration::from_nanos(avg_nanos as u64))
}
}
impl MetricsProvider for SimpleMetrics {
fn record_operation(&self, operation: &str, duration: Duration) {
let mut timers = self.timers.write().unwrap_or_else(|e| e.into_inner());
timers.entry(operation.to_string())
.or_insert_with(Vec::new)
.push(duration);
}
fn increment_counter(&self, counter: &str, value: u64) {
let mut counters = self.counters.write().unwrap_or_else(|e| e.into_inner());
*counters.entry(counter.to_string()).or_insert(0) += value;
}
fn record_gauge(&self, gauge: &str, value: f64) {
let mut gauges = self.gauges.write().unwrap_or_else(|e| e.into_inner());
gauges.insert(gauge.to_string(), value);
}
fn timer<'a>(&'a self, operation: &'a str) -> OperationTimer<'a> {
OperationTimer::new(self, operation)
}
fn summary(&self) -> String {
let mut result = String::new();
result.push_str("Counters:\n");
for (name, value) in self.counters.read().unwrap_or_else(|e| e.into_inner()).iter() {
result.push_str(&format!(" {}: {}\n", name, value));
}
result.push_str("\nGauges:\n");
for (name, value) in self.gauges.read().unwrap_or_else(|e| e.into_inner()).iter() {
result.push_str(&format!(" {}: {:.6}\n", name, value));
}
result.push_str("\nOperations:\n");
for (name, durations) in self.timers.read().unwrap_or_else(|e| e.into_inner()).iter() {
if durations.is_empty() {
continue;
}
let total_nanos: u128 = durations.iter().map(|d| d.as_nanos()).sum();
let avg_nanos = total_nanos / durations.len() as u128;
let avg_duration = Duration::from_nanos(avg_nanos as u64);
let min_duration = durations.iter().min().unwrap();
let max_duration = durations.iter().max().unwrap();
result.push_str(&format!(
" {}: count={}, avg={:?}, min={:?}, max={:?}\n",
name, durations.len(), avg_duration, min_duration, max_duration
));
}
result
}
}
impl Default for SimpleMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_counter_operations() {
let metrics = SimpleMetrics::new();
metrics.increment_counter("test_counter", 1);
metrics.increment_counter("test_counter", 2);
metrics.increment_counter("another_counter", 5);
assert_eq!(metrics.get_counter("test_counter"), Some(3));
assert_eq!(metrics.get_counter("another_counter"), Some(5));
assert_eq!(metrics.get_counter("nonexistent_counter"), None);
}
#[test]
fn test_gauge_operations() {
let metrics = SimpleMetrics::new();
metrics.record_gauge("test_gauge", 3.14);
metrics.record_gauge("another_gauge", 2.71);
assert!((metrics.get_gauge("test_gauge").unwrap() - 3.14).abs() < 0.0001);
assert!((metrics.get_gauge("another_gauge").unwrap() - 2.71).abs() < 0.0001);
assert_eq!(metrics.get_gauge("nonexistent_gauge"), None);
}
#[test]
fn test_timer_operations() {
let metrics = SimpleMetrics::new();
metrics.record_operation("op1", Duration::from_millis(100));
metrics.record_operation("op1", Duration::from_millis(200));
{
let _timer = metrics.timer("op2");
sleep(Duration::from_millis(10)); }
let avg_op1 = metrics.get_average_duration("op1").unwrap();
assert_eq!(avg_op1, Duration::from_millis(150));
let avg_op2 = metrics.get_average_duration("op2").unwrap();
assert!(avg_op2.as_millis() >= 10); }
#[test]
fn test_summary() {
let metrics = SimpleMetrics::new();
metrics.increment_counter("requests", 42);
metrics.record_gauge("memory_usage", 123.456);
metrics.record_operation("fetch", Duration::from_millis(50));
let summary = metrics.summary();
assert!(summary.contains("requests: 42"));
assert!(summary.contains("memory_usage:"));
assert!(summary.contains("fetch: count=1"));
}
}