diff --git a/README.md b/README.md index 5a426a4..6c45d36 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,20 @@ let avg_response_time = metrics.avg( println!("Average response time: {:?}", avg_response_time); ``` +`Sum` returns an `f64` for both counters and gauges. To get the exact `u64` total of a +counter (no rounding above `2^53`), use `SumCounter`: + +```rust +use torrust_metrics::metric_collection::aggregate::sum::SumCounter; + +let total_requests: Option = metrics.sum_counter( + &metric_name!("requests_total"), + &[("server", "tracker-01")].into(), +); +``` + +It returns `None` if the metric does not exist or is not a counter. + ## Architecture ### Core Components diff --git a/src/label/set.rs b/src/label/set.rs index f0799a0..2ab1961 100644 --- a/src/label/set.rs +++ b/src/label/set.rs @@ -576,8 +576,8 @@ mod tests { for (name, value) in label_set.iter() { count += 1; // Verify we can access name and value - assert!(!name.to_string().is_empty()); - assert!(!value.to_string().is_empty()); + assert_ne!(name.to_string(), ""); + assert_ne!(value.to_string(), ""); } assert_eq!(count, 2); diff --git a/src/metric_collection/aggregate/sum.rs b/src/metric_collection/aggregate/sum.rs index b677e9e..68333fa 100644 --- a/src/metric_collection/aggregate/sum.rs +++ b/src/metric_collection/aggregate/sum.rs @@ -9,6 +9,47 @@ pub trait Sum { fn sum(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option; } +/// Exact integer sum of a counter's samples. +/// +/// Unlike [`Sum`], which returns an `f64` for both counters and gauges, this +/// trait returns the exact `u64` total, so values above `2^53` are not rounded. +/// +/// It returns `None` if there is no counter with the given name. A metric with +/// that name that is a gauge is not a counter, so it also returns `None`. +/// +/// The addition is not checked: it panics on overflow in debug builds and +/// wraps in release builds, like [`Metric::sum`](MetricSumTrait::sum). +/// +/// # Example +/// +/// ``` +/// use torrust_clock::DurationSinceUnixEpoch; +/// use torrust_metrics::label::LabelSet; +/// use torrust_metrics::metric_collection::MetricCollection; +/// use torrust_metrics::metric_collection::aggregate::sum::SumCounter; +/// use torrust_metrics::metric_name; +/// +/// let mut collection = MetricCollection::default(); +/// let name = metric_name!("requests_total"); +/// let time = DurationSinceUnixEpoch::from_secs(1); +/// +/// collection.set_counter(&name, &LabelSet::empty(), (1 << 53) + 1, time).unwrap(); +/// +/// assert_eq!(collection.sum_counter(&name, &LabelSet::empty()), Some((1 << 53) + 1)); +/// ``` +pub trait SumCounter { + fn sum_counter(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option; +} + +impl SumCounter for MetricCollection { + fn sum_counter(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option { + self.counters + .metrics + .get(metric_name) + .map(|metric| metric.sum(label_set_criteria)) + } +} + impl Sum for MetricCollection { fn sum(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option { if let Some(value) = self.counters.sum(metric_name, label_set_criteria) { @@ -146,4 +187,81 @@ mod tests { assert_eq!(collection.sum(&metric_name!("missing_gauge"), &LabelSet::empty()), None); } } + + mod it_should_allow_summing_counter_samples_exactly { + + use torrust_clock::DurationSinceUnixEpoch; + + use crate::label::{LabelSet, LabelValue}; + use crate::metric_collection::MetricCollection; + use crate::metric_collection::aggregate::sum::{Sum, SumCounter}; + use crate::{label_name, metric_name}; + + fn time() -> DurationSinceUnixEpoch { + DurationSinceUnixEpoch::from_secs(1) + } + + #[test] + fn with_several_samples_and_a_label_filter() { + let name = metric_name!("test_counter"); + let label_1: LabelSet = (label_name!("label_1"), LabelValue::new("value_1")).into(); + let label_2: LabelSet = (label_name!("label_2"), LabelValue::new("value_2")).into(); + + let mut collection = MetricCollection::default(); + collection.set_counter(&name, &label_1, 3, time()).unwrap(); + collection.set_counter(&name, &label_2, 4, time()).unwrap(); + + assert_eq!(collection.sum_counter(&name, &label_1), Some(3)); + assert_eq!(collection.sum_counter(&name, &label_2), Some(4)); + } + + #[test] + fn summing_every_sample_when_the_label_set_is_empty() { + let name = metric_name!("test_counter"); + + let mut collection = MetricCollection::default(); + collection + .set_counter(&name, &(label_name!("label_1"), LabelValue::new("value_1")).into(), 3, time()) + .unwrap(); + collection + .set_counter(&name, &(label_name!("label_2"), LabelValue::new("value_2")).into(), 4, time()) + .unwrap(); + + assert_eq!(collection.sum_counter(&name, &LabelSet::empty()), Some(7)); + } + + #[test] + fn returning_none_when_the_metric_does_not_exist() { + let collection = MetricCollection::default(); + + assert_eq!( + collection.sum_counter(&metric_name!("does_not_exist"), &LabelSet::empty()), + None + ); + } + + #[test] + fn returning_none_when_the_metric_is_a_gauge() { + let name = metric_name!("test_gauge"); + + let mut collection = MetricCollection::default(); + collection.set_gauge(&name, &LabelSet::empty(), 1.0, time()).unwrap(); + + assert_eq!(collection.sum_counter(&name, &LabelSet::empty()), None); + } + + #[test] + fn returning_values_above_two_to_the_power_of_fifty_three_exactly() { + let name = metric_name!("test_counter"); + let value = (1_u64 << 53) + 1; + + let mut collection = MetricCollection::default(); + collection.set_counter(&name, &LabelSet::empty(), value, time()).unwrap(); + + assert_eq!(collection.sum_counter(&name, &LabelSet::empty()), Some(value)); + + // The `f64` based `Sum` rounds the same value down to `2^53`. + assert_eq!(collection.sum(&name, &LabelSet::empty()), Some(9_007_199_254_740_992.0)); + } + } } diff --git a/src/sample_collection.rs b/src/sample_collection.rs index 9f68413..db31ee7 100644 --- a/src/sample_collection.rs +++ b/src/sample_collection.rs @@ -308,7 +308,7 @@ mod tests { let prometheus_output = collection.to_prometheus(); - assert!(!prometheus_output.is_empty()); + assert_ne!(prometheus_output, ""); } #[test] diff --git a/src/unit.rs b/src/unit.rs index 3e9d348..8d33712 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -80,7 +80,7 @@ mod tests { let c = u; assert_eq!(u, c); let s = format!("{u:?}"); - assert!(!s.is_empty()); + assert_ne!(s, ""); let mut set = std::collections::HashSet::new(); set.insert(u); assert!(set.contains(&Unit::Count));