Repository navigation
feat: add SumCounter for exact u64 counter sums #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Minor][F2] Pre-existing, outside the diff: this import path is right, but the one two blocks up is not.
|
||
|
|
||
| let total_requests: Option<u64> = 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,47 @@ pub trait Sum { | |
| fn sum(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option<f64>; | ||
| } | ||
|
|
||
| /// 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion][F3] Pin the Both |
||
| /// 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<Counter>::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<u64>; | ||
| } | ||
|
|
||
| impl SumCounter for MetricCollection { | ||
| fn sum_counter(&self, metric_name: &MetricName, label_set_criteria: &LabelSet) -> Option<u64> { | ||
| self.counters | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion][F4] One counter lookup instead of two. This body is |
||
| .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<f64> { | ||
| 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)); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Minor][F1] Pre-existing, outside the diff: the Type System block at
README.md:122contradicts this sentence.let counter_sum: Option<u64> = counter_collection.sum(&name, &labels);matches no method at the merged head: the only(&MetricName, &LabelSet)aggregate onMetricKindCollection<Counter>isSum, which returnsOption<f64>(src/metric_collection/aggregate/sum.rs:67-73), and the only one returningOption<u64>is the newsum_counter. With this line now stating thatSumreturns anf64for counters, the README says both. A follow-up can rewrite line 122 asmetric_collection.sum_counter(&name, &labels).