Skip to main content

ic_metrics/summary/
mod.rs

1//! Saturating arithmetic without sampling or attribution policy.
2
3use crate::{MeasurementAggregate, MeasurementMeanError, MeasurementStateError};
4
5#[cfg(test)]
6mod tests;
7
8/// Ordered observations with a mergeable aggregate and a separate latest value.
9///
10/// Count and total saturate independently, tracking actual overflow. An exactly
11/// reached numeric cap remains exact; inspect the aggregate's overflow flags
12/// before using raw counts/totals for exact arithmetic. Latest and maximum remain
13/// individual observations after overflow. Empty and measured zero are distinct.
14/// Consumers own units, identity, ordering and reset boundaries.
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
16pub struct MeasurementSummary {
17    aggregate: MeasurementAggregate,
18    latest: u64,
19}
20
21impl MeasurementSummary {
22    /// Restore an admitted aggregate and its latest ordered observation.
23    ///
24    /// The caller establishes identity and ordering; this validates numeric fields.
25    ///
26    /// # Errors
27    /// Rejects absent/nonempty latest values, values above the maximum and
28    /// observations inconsistent with the aggregate's exact total. With an exact
29    /// count, the other observations cannot contribute more than their maximum.
30    pub const fn from_parts(
31        aggregate: MeasurementAggregate,
32        latest: Option<u64>,
33    ) -> Result<Self, MeasurementStateError> {
34        if aggregate.samples() == 0 {
35            return if latest.is_none() {
36                Ok(Self::EMPTY)
37            } else {
38                Err(MeasurementStateError::InvalidLatest)
39            };
40        }
41        let (Some(latest), Some(maximum)) = (latest, aggregate.maximum()) else {
42            return Err(MeasurementStateError::InvalidLatest);
43        };
44        if latest > maximum
45            || (latest < maximum && aggregate.total() < latest as u128 + maximum as u128)
46            || (!aggregate.samples_overflowed()
47                && aggregate.total()
48                    > (aggregate.samples() - 1) as u128 * maximum as u128 + latest as u128)
49        {
50            return Err(MeasurementStateError::InvalidLatest);
51        }
52        Ok(Self { aggregate, latest })
53    }
54
55    /// Empty summary, with no latest or maximum observation.
56    pub const EMPTY: Self = Self {
57        aggregate: MeasurementAggregate::EMPTY,
58        latest: 0,
59    };
60
61    /// Record one completed observation, including zero, in the existing unit.
62    pub const fn record(&mut self, value: u64) {
63        self.aggregate.record(value);
64        self.latest = value;
65    }
66
67    /// Mergeable count/total/maximum snapshot without observation ordering.
68    #[must_use]
69    pub const fn aggregate(self) -> MeasurementAggregate {
70        self.aggregate
71    }
72
73    /// Sample count, clipped at `u64::MAX`; overflow is in [`Self::aggregate`].
74    #[must_use]
75    pub const fn samples(self) -> u64 {
76        self.aggregate.samples()
77    }
78
79    /// Observation total, clipped at `u128::MAX`; overflow is in [`Self::aggregate`].
80    #[must_use]
81    pub const fn total(self) -> u128 {
82        self.aggregate.total()
83    }
84
85    /// Floor-rounded mean, or `None` when empty. An exactly reached cap is valid.
86    ///
87    /// # Errors
88    /// Returns [`MeasurementMeanError::SampleCountOverflow`] on actual count overflow.
89    pub const fn mean(self) -> Result<Option<u128>, MeasurementMeanError> {
90        self.aggregate.mean()
91    }
92
93    /// Compute `floor(total * scale / samples)`, retaining fractional information.
94    ///
95    /// Uses the aggregate's checked projection; a scale of ten reports tenths.
96    /// Empty remains `None`, and scale zero does not conceal actual overflow.
97    ///
98    /// # Errors
99    /// Returns [`MeasurementMeanError::SampleCountOverflow`] on actual count overflow.
100    pub const fn scaled_mean(self, scale: u64) -> Result<Option<u128>, MeasurementMeanError> {
101        self.aggregate.scaled_mean(scale)
102    }
103
104    /// Latest observation, or `None` when empty. Summaries cannot be merged.
105    #[must_use]
106    pub const fn latest(self) -> Option<u64> {
107        if self.samples() == 0 {
108            None
109        } else {
110            Some(self.latest)
111        }
112    }
113
114    /// Largest observation, or `None` when empty, including after overflow.
115    #[must_use]
116    pub const fn maximum(self) -> Option<u64> {
117        self.aggregate.maximum()
118    }
119}