use std::fmt::{self, Display, Write};
use std::num::NonZero;
use std::{cmp, iter};
use foldhash::{HashMap, HashMapExt};
use new_zealand::nz;
use crate::{EventName, GLOBAL_REGISTRY, Magnitude, ObservationBagSnapshot, Observations};
#[derive(Debug)]
pub struct Report {
events: Box<[EventMetrics]>,
}
impl Report {
#[must_use]
pub fn collect() -> Self {
let mut event_name_to_merged_snapshot = HashMap::new();
GLOBAL_REGISTRY.inspect(|observation_bags| {
for (event_name, observation_bag) in observation_bags {
let snapshot = observation_bag.snapshot();
event_name_to_merged_snapshot
.entry(event_name.clone())
.and_modify(|existing_snapshot: &mut ObservationBagSnapshot| {
existing_snapshot.merge_from(&snapshot);
})
.or_insert(snapshot);
}
});
let mut events = event_name_to_merged_snapshot
.into_iter()
.map(|(event_name, snapshot)| EventMetrics::new(event_name, snapshot))
.collect::<Vec<_>>();
events.sort_by_key(|event_metrics| event_metrics.name().clone());
Self {
events: events.into_boxed_slice(),
}
}
pub fn events(&self) -> impl Iterator<Item = &EventMetrics> {
self.events.iter()
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn fake(events: Vec<EventMetrics>) -> Self {
Self {
events: events.into_boxed_slice(),
}
}
}
impl Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for event in &self.events {
writeln!(f, "{event}")?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct EventMetrics {
name: EventName,
count: u64,
sum: Magnitude,
mean: Magnitude,
histogram: Option<Histogram>,
}
impl EventMetrics {
pub(crate) fn new(name: EventName, snapshot: ObservationBagSnapshot) -> Self {
let count = snapshot.count;
let sum = snapshot.sum;
#[expect(
clippy::arithmetic_side_effects,
reason = "NonZero protects against division by zero"
)]
#[expect(
clippy::integer_division,
reason = "we accept that we lose the remainder - 100% precision not required"
)]
let mean = Magnitude::try_from(count)
.ok()
.and_then(NonZero::new)
.map_or(0, |count| sum / count.get());
let histogram = if snapshot.bucket_magnitudes.is_empty() {
None
} else {
let plus_infinity_bucket_count = snapshot
.count
.saturating_sub(snapshot.bucket_counts.iter().sum::<u64>());
Some(Histogram {
magnitudes: snapshot.bucket_magnitudes,
counts: snapshot.bucket_counts,
plus_infinity_bucket_count,
})
};
Self {
name,
count,
sum,
mean,
histogram,
}
}
#[must_use]
pub fn name(&self) -> &EventName {
&self.name
}
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[must_use]
pub fn sum(&self) -> Magnitude {
self.sum
}
#[must_use]
pub fn mean(&self) -> Magnitude {
self.mean
}
#[must_use]
pub fn histogram(&self) -> Option<&Histogram> {
self.histogram.as_ref()
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn fake(
name: impl Into<EventName>,
count: u64,
sum: Magnitude,
histogram: Option<Histogram>,
) -> Self {
#[expect(
clippy::arithmetic_side_effects,
reason = "NonZero protects against division by zero"
)]
#[expect(
clippy::integer_division,
reason = "we accept that we lose the remainder - 100% precision not required"
)]
let mean = Magnitude::try_from(count)
.ok()
.and_then(NonZero::new)
.map_or(0, |count| sum / count.get());
Self {
name: name.into(),
count,
sum,
mean,
histogram,
}
}
}
impl Display for EventMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: ", self.name)?;
if self.count == 0 {
writeln!(f, "0")?;
return Ok(());
}
#[expect(
clippy::cast_possible_wrap,
reason = "intentional wrap - crate policy is that values out of safe range may be mangled"
)]
let count_as_magnitude = self.count as Magnitude;
if count_as_magnitude == self.sum && self.histogram.is_none() {
writeln!(f, "{} (counter)", self.count)?;
} else {
writeln!(f, "{}; sum {}; mean {}", self.count, self.sum, self.mean)?;
}
if let Some(histogram) = &self.histogram {
writeln!(f, "{histogram}")?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct Histogram {
magnitudes: &'static [Magnitude],
counts: Box<[u64]>,
plus_infinity_bucket_count: u64,
}
impl Histogram {
pub fn magnitudes(&self) -> impl Iterator<Item = Magnitude> {
self.magnitudes
.iter()
.copied()
.chain(iter::once(Magnitude::MAX))
}
pub fn counts(&self) -> impl Iterator<Item = u64> {
self.counts
.iter()
.copied()
.chain(iter::once(self.plus_infinity_bucket_count))
}
pub fn buckets(&self) -> impl Iterator<Item = (Magnitude, u64)> {
self.magnitudes().zip(self.counts())
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn fake(
magnitudes: &'static [Magnitude],
counts: Vec<u64>,
plus_infinity_count: u64,
) -> Self {
Self {
magnitudes,
counts: counts.into_boxed_slice(),
plus_infinity_bucket_count: plus_infinity_count,
}
}
}
const HISTOGRAM_BAR_WIDTH_CHARS: u64 = 50;
const HISTOGRAM_BAR_CHARS: &str =
"∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎";
const HISTOGRAM_BAR_CHARS_LEN_BYTES: NonZero<usize> =
NonZero::new(HISTOGRAM_BAR_CHARS.len()).unwrap();
const BYTES_PER_HISTOGRAM_BAR_CHAR: NonZero<usize> = nz!(3);
impl Display for Histogram {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let buckets = self.buckets().collect::<Vec<_>>();
let mut count_str = String::new();
let widest_count = buckets.iter().fold(0, |current, bucket| {
count_str.clear();
write!(&mut count_str, "{}", bucket.1)
.expect("we expect writing integer to String to be infallible");
cmp::max(current, count_str.len())
});
let mut upper_bound_str = String::new();
let widest_upper_bound = buckets.iter().fold(0, |current, bucket| {
upper_bound_str.clear();
if bucket.0 == Magnitude::MAX {
write!(&mut upper_bound_str, "+inf")
.expect("we expect writing integer to String to be infallible");
} else {
write!(&mut upper_bound_str, "{}", bucket.0)
.expect("we expect writing integer to String to be infallible");
}
cmp::max(current, upper_bound_str.len())
});
let histogram_scale = HistogramScale::new(self);
for (magnitude, count) in buckets {
upper_bound_str.clear();
if magnitude == Magnitude::MAX {
write!(&mut upper_bound_str, "+inf")?;
} else {
write!(&mut upper_bound_str, "{magnitude}")?;
}
let padding_needed = widest_upper_bound.saturating_sub(upper_bound_str.len());
for _ in 0..padding_needed {
upper_bound_str.insert(0, ' ');
}
count_str.clear();
write!(&mut count_str, "{count}")?;
let padding_needed = widest_count.saturating_sub(count_str.len());
for _ in 0..padding_needed {
count_str.insert(0, ' ');
}
write!(f, "value <= {upper_bound_str} [ {count_str} ]: ")?;
histogram_scale.write_bar(count, f)?;
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug)]
struct HistogramScale {
count_per_char: NonZero<u64>,
}
impl HistogramScale {
fn new(snapshot: &Histogram) -> Self {
let max_count = snapshot
.counts()
.max()
.expect("a histogram always has at least one bucket by definition (+inf)");
#[expect(
clippy::integer_division,
reason = "we accept the loss of precision here - the bar might not always reach 100% of desired width or even overshoot it"
)]
let count_per_char = NonZero::new(cmp::max(max_count / HISTOGRAM_BAR_WIDTH_CHARS, 1))
.expect("guarded by max()");
Self { count_per_char }
}
fn write_bar(&self, count: u64, f: &mut impl Write) -> fmt::Result {
let histogram_bar_width = count
.checked_div(self.count_per_char.get())
.expect("division by zero impossible - divisor is NonZero");
let bar_width = usize::try_from(histogram_bar_width).expect("safe range");
let chars_in_constant = HISTOGRAM_BAR_CHARS_LEN_BYTES
.get()
.checked_div(BYTES_PER_HISTOGRAM_BAR_CHAR.get())
.expect("NonZero - cannot be zero");
let mut remaining = bar_width;
while remaining > 0 {
let chunk_size =
NonZero::new(remaining.min(chars_in_constant)).expect("guarded by loop condition");
let byte_end = chunk_size
.checked_mul(BYTES_PER_HISTOGRAM_BAR_CHAR)
.expect("we are seeking into a small constant value, overflow impossible");
#[expect(
clippy::string_slice,
reason = "safe slicing - ∎ characters have known UTF-8 encoding"
)]
f.write_str(&HISTOGRAM_BAR_CHARS[..byte_end.get()])?;
remaining = remaining
.checked_sub(chunk_size.get())
.expect("guarded by min() above");
}
Ok(())
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
#![allow(clippy::indexing_slicing, reason = "panic is fine in tests")]
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::assert_impl_all;
use super::*;
assert_impl_all!(Report: UnwindSafe, RefUnwindSafe);
assert_impl_all!(EventMetrics: UnwindSafe, RefUnwindSafe);
assert_impl_all!(Histogram: UnwindSafe, RefUnwindSafe);
#[test]
fn histogram_properties_reflect_reality() {
let magnitudes = &[-5, 1, 10, 100];
let counts = &[66, 5, 3, 2];
let histogram = Histogram {
magnitudes,
counts: Vec::from(counts).into_boxed_slice(),
plus_infinity_bucket_count: 1,
};
assert_eq!(
histogram.magnitudes().collect::<Vec<_>>(),
magnitudes
.iter()
.copied()
.chain(iter::once(Magnitude::MAX))
.collect::<Vec<_>>()
);
assert_eq!(
histogram.counts().collect::<Vec<_>>(),
counts
.iter()
.copied()
.chain(iter::once(1))
.collect::<Vec<_>>()
);
let buckets: Vec<_> = histogram.buckets().collect();
assert_eq!(buckets.len(), 5);
assert_eq!(buckets[0], (-5, 66));
assert_eq!(buckets[1], (1, 5));
assert_eq!(buckets[2], (10, 3));
assert_eq!(buckets[3], (100, 2));
assert_eq!(buckets[4], (Magnitude::MAX, 1));
}
#[test]
fn histogram_display_contains_expected_information() {
let magnitudes = &[-5, 1, 10, 100];
let counts = &[666666, 5, 3, 2];
let histogram = Histogram {
magnitudes,
counts: Vec::from(counts).into_boxed_slice(),
plus_infinity_bucket_count: 1,
};
let mut output = String::new();
write!(&mut output, "{histogram}").unwrap();
println!("{output}");
assert!(output.contains("value <= -5 [ 666666 ]: "));
assert!(output.contains("value <= 1 [ 5 ]: "));
assert!(output.contains("value <= 10 [ 3 ]: "));
assert!(output.contains("value <= 100 [ 2 ]: "));
assert!(output.contains("value <= +inf [ 1 ]: "));
#[expect(clippy::cast_possible_truncation, reason = "safe range, tiny values")]
let max_acceptable_line_length = (HISTOGRAM_BAR_WIDTH_CHARS * 5) as usize;
for line in output.lines() {
assert!(
line.len() < max_acceptable_line_length,
"line is too long: {line}"
);
}
}
#[test]
fn event_properties_reflect_reality() {
let event_name = "test_event".to_string();
let count = 50;
let sum = Magnitude::from(1000);
let mean = Magnitude::from(20);
let histogram = Histogram {
magnitudes: &[1, 10, 100],
counts: vec![5, 3, 2].into_boxed_slice(),
plus_infinity_bucket_count: 1,
};
let event_metrics = EventMetrics {
name: event_name.clone().into(),
count,
sum,
mean,
histogram: Some(histogram),
};
assert_eq!(event_metrics.name(), &event_name);
assert_eq!(event_metrics.count(), count);
assert_eq!(event_metrics.sum(), sum);
assert_eq!(event_metrics.mean(), mean);
assert!(event_metrics.histogram().is_some());
}
#[test]
fn event_display_contains_expected_information() {
let event_name = "test_event".to_string();
let count = 50;
let sum = Magnitude::from(1000);
let mean = Magnitude::from(20);
let histogram = Histogram {
magnitudes: &[1, 10, 100],
counts: vec![5, 3, 2].into_boxed_slice(),
plus_infinity_bucket_count: 1,
};
let event_metrics = EventMetrics {
name: event_name.clone().into(),
count,
sum,
mean,
histogram: Some(histogram),
};
let mut output = String::new();
write!(&mut output, "{event_metrics}").unwrap();
println!("{output}");
assert!(output.contains(&event_name));
assert!(output.contains(&count.to_string()));
assert!(output.contains(&sum.to_string()));
assert!(output.contains(&mean.to_string()));
assert!(output.contains("value <= +inf [ 1 ]: "));
}
#[test]
fn report_properties_reflect_reality() {
let event1 = EventMetrics {
name: "event1".to_string().into(),
count: 10,
sum: Magnitude::from(100),
mean: Magnitude::from(10),
histogram: None,
};
let event2 = EventMetrics {
name: "event2".to_string().into(),
count: 5,
sum: Magnitude::from(50),
mean: Magnitude::from(10),
histogram: None,
};
let report = Report {
events: vec![event1, event2].into_boxed_slice(),
};
let events = report.events().collect::<Vec<_>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].name(), "event1");
assert_eq!(events[1].name(), "event2");
}
#[test]
fn report_display_contains_expected_events() {
let event1 = EventMetrics {
name: "event1".to_string().into(),
count: 10,
sum: Magnitude::from(100),
mean: Magnitude::from(10),
histogram: None,
};
let event2 = EventMetrics {
name: "event2".to_string().into(),
count: 5,
sum: Magnitude::from(50),
mean: Magnitude::from(10),
histogram: None,
};
let report = Report {
events: vec![event1, event2].into_boxed_slice(),
};
let mut output = String::new();
write!(&mut output, "{report}").unwrap();
println!("{output}");
assert!(output.contains("event1"));
assert!(output.contains("event2"));
}
#[test]
fn event_displayed_as_counter_if_unit_values_and_no_histogram() {
let counter = EventMetrics {
name: "test_event".to_string().into(),
count: 100,
sum: Magnitude::from(100),
mean: Magnitude::from(1),
histogram: None,
};
let not_counter = EventMetrics {
name: "test_event".to_string().into(),
count: 100,
sum: Magnitude::from(200),
mean: Magnitude::from(2),
histogram: None,
};
let also_not_counter = EventMetrics {
name: "test_event".to_string().into(),
count: 100,
sum: Magnitude::from(100),
mean: Magnitude::from(1),
histogram: Some(Histogram {
magnitudes: &[],
counts: Box::new([]),
plus_infinity_bucket_count: 100,
}),
};
let still_not_counter = EventMetrics {
name: "test_event".to_string().into(),
count: 100,
sum: Magnitude::from(200),
mean: Magnitude::from(2),
histogram: Some(Histogram {
magnitudes: &[],
counts: Box::new([]),
plus_infinity_bucket_count: 200,
}),
};
let mut output = String::new();
write!(&mut output, "{counter}").unwrap();
assert!(output.contains("100 (counter)"));
output.clear();
write!(&mut output, "{not_counter}").unwrap();
assert!(output.contains("100; sum 200; mean 2"));
output.clear();
write!(&mut output, "{also_not_counter}").unwrap();
assert!(output.contains("100; sum 100; mean 1"));
output.clear();
write!(&mut output, "{still_not_counter}").unwrap();
assert!(output.contains("100; sum 200; mean 2"));
}
#[test]
fn histogram_scale_zero() {
let histogram = Histogram {
magnitudes: &[1, 2, 3],
counts: Box::new([0, 0, 0]),
plus_infinity_bucket_count: 0,
};
let histogram_scale = HistogramScale::new(&histogram);
let mut output = String::new();
histogram_scale.write_bar(0, &mut output).unwrap();
assert_eq!(output, "");
}
#[test]
fn histogram_scale_small() {
let histogram = Histogram {
magnitudes: &[1, 2, 3],
counts: Box::new([1, 2, 3]),
plus_infinity_bucket_count: 0,
};
let histogram_scale = HistogramScale::new(&histogram);
let mut output = String::new();
histogram_scale.write_bar(0, &mut output).unwrap();
assert_eq!(output, "");
output.clear();
histogram_scale.write_bar(1, &mut output).unwrap();
assert_eq!(output, "∎");
output.clear();
histogram_scale.write_bar(2, &mut output).unwrap();
assert_eq!(output, "∎∎");
output.clear();
histogram_scale.write_bar(3, &mut output).unwrap();
assert_eq!(output, "∎∎∎");
}
#[test]
fn histogram_scale_just_over() {
let histogram = Histogram {
magnitudes: &[1, 2, 3],
counts: Box::new([
HISTOGRAM_BAR_WIDTH_CHARS + 1,
HISTOGRAM_BAR_WIDTH_CHARS + 1,
HISTOGRAM_BAR_WIDTH_CHARS + 1,
]),
plus_infinity_bucket_count: 0,
};
let histogram_scale = HistogramScale::new(&histogram);
let mut output = String::new();
histogram_scale
.write_bar(HISTOGRAM_BAR_WIDTH_CHARS + 1, &mut output)
.unwrap();
assert_eq!(
output,
"∎".repeat(
usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS + 1).expect("safe range, tiny value")
)
);
}
#[test]
fn histogram_scale_large_exact() {
let histogram = Histogram {
magnitudes: &[1, 2, 3],
counts: Box::new([
79,
HISTOGRAM_BAR_WIDTH_CHARS * 100,
HISTOGRAM_BAR_WIDTH_CHARS * 1000,
]),
plus_infinity_bucket_count: 0,
};
let histogram_scale = HistogramScale::new(&histogram);
let mut output = String::new();
histogram_scale.write_bar(0, &mut output).unwrap();
assert_eq!(output, "");
output.clear();
histogram_scale
.write_bar(histogram_scale.count_per_char.get(), &mut output)
.unwrap();
assert_eq!(output, "∎");
output.clear();
histogram_scale
.write_bar(HISTOGRAM_BAR_WIDTH_CHARS * 1000, &mut output)
.unwrap();
assert_eq!(
output,
"∎".repeat(usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS).expect("safe range, tiny value"))
);
}
#[test]
fn histogram_scale_large_inexact() {
let histogram = Histogram {
magnitudes: &[1, 2, 3],
counts: Box::new([
79,
HISTOGRAM_BAR_WIDTH_CHARS * 100,
HISTOGRAM_BAR_WIDTH_CHARS * 1000,
]),
plus_infinity_bucket_count: 0,
};
let histogram_scale = HistogramScale::new(&histogram);
let mut output = String::new();
histogram_scale.write_bar(3, &mut output).unwrap();
let mut output = String::new();
histogram_scale.write_bar(0, &mut output).unwrap();
assert_eq!(output, "");
output.clear();
histogram_scale
.write_bar(histogram_scale.count_per_char.get() - 1, &mut output)
.unwrap();
assert_eq!(output, "");
output.clear();
histogram_scale
.write_bar(histogram_scale.count_per_char.get(), &mut output)
.unwrap();
assert_eq!(output, "∎");
output.clear();
histogram_scale
.write_bar(HISTOGRAM_BAR_WIDTH_CHARS * 1000 - 1, &mut output)
.unwrap();
assert_eq!(
output,
"∎".repeat(
usize::try_from(HISTOGRAM_BAR_WIDTH_CHARS).expect("safe range, tiny value") - 1
)
);
}
#[test]
fn histogram_char_byte_count_is_correct() {
assert_eq!("∎".len(), BYTES_PER_HISTOGRAM_BAR_CHAR.get());
let expected_chars = HISTOGRAM_BAR_CHARS.chars().count();
let expected_bytes = expected_chars * BYTES_PER_HISTOGRAM_BAR_CHAR.get();
assert_eq!(HISTOGRAM_BAR_CHARS.len(), expected_bytes);
}
#[test]
fn event_metrics_display_zero_count_reports_flat_zero() {
let snapshot = ObservationBagSnapshot {
count: 0,
sum: 0,
bucket_magnitudes: &[],
bucket_counts: Box::new([]),
};
let metrics = EventMetrics::new("zero_event".into(), snapshot);
let output = format!("{metrics}");
assert!(output.contains("zero_event: 0"));
assert_eq!(output.trim(), "zero_event: 0");
}
#[test]
fn event_metrics_new_zero_count_empty_buckets() {
let snapshot = ObservationBagSnapshot {
count: 0,
sum: 0,
bucket_magnitudes: &[],
bucket_counts: Box::new([]),
};
let metrics = EventMetrics::new("empty_event".into(), snapshot);
assert_eq!(metrics.name(), "empty_event");
assert_eq!(metrics.count(), 0);
assert_eq!(metrics.sum(), 0);
assert_eq!(metrics.mean(), 0);
assert!(metrics.histogram().is_none());
}
#[test]
fn event_metrics_new_non_zero_count_empty_buckets() {
let snapshot = ObservationBagSnapshot {
count: 10,
sum: 100,
bucket_magnitudes: &[],
bucket_counts: Box::new([]),
};
let metrics = EventMetrics::new("sum_event".into(), snapshot);
assert_eq!(metrics.name(), "sum_event");
assert_eq!(metrics.count(), 10);
assert_eq!(metrics.sum(), 100);
assert_eq!(metrics.mean(), 10); assert!(metrics.histogram().is_none());
}
#[test]
fn event_metrics_new_non_zero_count_with_buckets() {
let snapshot = ObservationBagSnapshot {
count: 20,
sum: 500,
bucket_magnitudes: &[-10, 0, 10, 100],
bucket_counts: vec![2, 3, 4, 5].into_boxed_slice(),
};
let metrics = EventMetrics::new("histogram_event".into(), snapshot);
assert_eq!(metrics.name(), "histogram_event");
assert_eq!(metrics.count(), 20);
assert_eq!(metrics.sum(), 500);
assert_eq!(metrics.mean(), 25);
let histogram = metrics.histogram().expect("histogram should be present");
let magnitudes: Vec<_> = histogram.magnitudes().collect();
assert_eq!(magnitudes, vec![-10, 0, 10, 100, Magnitude::MAX]);
let counts: Vec<_> = histogram.counts().collect();
assert_eq!(counts, vec![2, 3, 4, 5, 6]);
let buckets: Vec<_> = histogram.buckets().collect();
assert_eq!(buckets.len(), 5);
assert_eq!(buckets[0], (-10, 2));
assert_eq!(buckets[1], (0, 3));
assert_eq!(buckets[2], (10, 4));
assert_eq!(buckets[3], (100, 5));
assert_eq!(buckets[4], (Magnitude::MAX, 6));
}
#[test]
fn event_metrics_fake_calculates_mean_correctly() {
let metrics = EventMetrics::fake("test_event", 10, 100, None);
assert_eq!(metrics.name(), "test_event");
assert_eq!(metrics.count(), 10);
assert_eq!(metrics.sum(), 100);
assert_eq!(metrics.mean(), 10); assert!(metrics.histogram().is_none());
}
#[test]
fn event_metrics_fake_calculates_mean_with_different_values() {
let metrics = EventMetrics::fake("test_event", 25, 500, None);
assert_eq!(metrics.mean(), 20); }
#[test]
fn event_metrics_fake_mean_zero_when_count_zero() {
let metrics = EventMetrics::fake("test_event", 0, 100, None);
assert_eq!(metrics.count(), 0);
assert_eq!(metrics.sum(), 100);
assert_eq!(metrics.mean(), 0);
}
#[test]
fn event_metrics_fake_mean_handles_integer_division() {
let metrics = EventMetrics::fake("test_event", 3, 10, None);
assert_eq!(metrics.mean(), 3); }
}