use crate::cmd::*;
use std::collections::{HashMap, HashSet};
#[derive(Default)]
pub struct MetricsState {
counter_state: HashMap<String, u64>,
gauge_state: HashMap<String, i64>,
histogram_state: HashMap<String, HistogramState>,
counter_updates: HashSet<String>,
gauge_updates: HashSet<String>,
histogram_updates: HashSet<String>,
}
impl MetricsState {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, cmd: MetricsCmd) {
match cmd {
MetricsCmd::Counter(counter_cmd) => match counter_cmd {
CounterCmd::Increment { name, value } => {
*self.counter_state.entry(name.clone()).or_insert(0) += value;
self.counter_updates.insert(name);
}
CounterCmd::Absolute { name, value } => {
self.counter_state.insert(name.clone(), value);
self.counter_updates.insert(name);
}
},
MetricsCmd::Gauge(gauge_cmd) => match gauge_cmd {
GaugeCmd::Increment { name, value } => {
*self.gauge_state.entry(name.clone()).or_insert(0) += value as i64;
self.gauge_updates.insert(name);
}
GaugeCmd::Decrement { name, value } => {
*self.gauge_state.entry(name.clone()).or_insert(0) -= value as i64;
self.gauge_updates.insert(name);
}
GaugeCmd::Set { name, value } => {
self.gauge_state.insert(name.clone(), value as i64);
self.gauge_updates.insert(name);
}
},
MetricsCmd::Histogram(histogram_cmd) => match histogram_cmd {
HistogramCmd::Record { name, value } => {
self.histogram_state
.entry(name.clone())
.and_modify(|x| x.update(value))
.or_default();
self.histogram_updates.insert(name);
}
},
}
}
pub fn output_logs(&mut self) -> Option<String> {
let mut logs = String::new();
for name in self.counter_updates.drain() {
if let Some(value) = self.counter_state.get(&name) {
logs.push_str(&format!("Counter: {} = {}\n", name, value));
}
}
for name in self.gauge_updates.drain() {
if let Some(value) = self.gauge_state.get(&name) {
logs.push_str(&format!("Gauge: {} = {}\n", name, value));
}
}
for name in self.histogram_updates.drain() {
if let Some(histogram) = self.histogram_state.get(&name) {
let avg = histogram.avg().unwrap_or(0.0);
let std_dev = histogram.std_dev().unwrap_or(0.0);
logs.push_str(&format!(
"Histogram: {} - avg: {:.2}, std_dev: {:.2}, min: {:.2}, max: {:.2}, samples: {}\n",
name, avg, std_dev, histogram.min, histogram.max, histogram.num_samples
));
}
}
if logs.is_empty() { None } else { Some(logs) }
}
}
#[derive(Default)]
struct HistogramState {
sum: f64,
sum_sq: f64,
num_samples: u64,
min: f64,
max: f64,
}
impl HistogramState {
fn update(&mut self, value: f64) {
self.sum += value;
self.sum_sq += value * value;
if self.num_samples == 0 {
self.min = value;
self.max = value;
} else {
self.min = self.min.min(value);
self.max = self.max.max(value);
}
self.num_samples += 1;
}
fn std_dev(&self) -> Option<f64> {
if self.num_samples == 0 {
None
} else {
let avg_sq = self.avg().unwrap() * self.avg().unwrap();
let avg_ss = self.sum_sq / self.num_samples as f64;
Some((avg_ss - avg_sq).sqrt())
}
}
fn avg(&self) -> Option<f64> {
if self.num_samples == 0 {
None
} else {
Some(self.sum / self.num_samples as f64)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_histogram_state_avg() {
let mut histogram = HistogramState::default();
histogram.update(10.0);
histogram.update(20.0);
histogram.update(30.0);
assert_eq!(histogram.avg(), Some(20.0));
}
#[test]
fn test_histogram_state_std_dev() {
let mut histogram = HistogramState::default();
histogram.update(10.0);
histogram.update(20.0);
histogram.update(30.0);
let expected_std_dev: f64 = 8.16;
let rounded_std_dev = (histogram.std_dev().unwrap() * 100.0).round() / 100.0;
assert_eq!(rounded_std_dev, expected_std_dev);
}
#[test]
fn test_histogram_state_empty_avg() {
let histogram = HistogramState::default();
assert_eq!(histogram.avg(), None);
}
#[test]
fn test_histogram_state_empty_std_dev() {
let histogram = HistogramState::default();
assert_eq!(histogram.std_dev(), None);
}
#[test]
fn test_histogram_state_min_max() {
let mut histogram = HistogramState::default();
histogram.update(15.0);
histogram.update(5.0);
histogram.update(25.0);
assert_eq!(histogram.min, 5.0);
assert_eq!(histogram.max, 25.0);
}
}