Skip to main content

ic_metrics/summary/
mod.rs

1//! Saturating arithmetic without sampling or attribution policy.
2
3use crate::{MeasurementAggregate, 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/// Project counts, totals, maximum and means through [`Self::aggregate`], just
16/// as for a histogram; this type adds ordered recording and [`Self::latest`].
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub struct MeasurementSummary {
19    aggregate: MeasurementAggregate,
20    latest: u64,
21}
22
23impl MeasurementSummary {
24    /// Restore an admitted aggregate and its latest ordered observation.
25    ///
26    /// The caller establishes identity and ordering; this validates numeric fields.
27    ///
28    /// # Errors
29    /// Rejects absent/nonempty latest values, values above the maximum and
30    /// observations inconsistent with the aggregate's exact total. With an exact
31    /// count, the other observations cannot contribute more than their maximum.
32    pub const fn from_parts(
33        aggregate: MeasurementAggregate,
34        latest: Option<u64>,
35    ) -> Result<Self, MeasurementStateError> {
36        if aggregate.samples() == 0 {
37            return if latest.is_none() {
38                Ok(Self::EMPTY)
39            } else {
40                Err(MeasurementStateError::InvalidLatest)
41            };
42        }
43        let (Some(latest), Some(maximum)) = (latest, aggregate.maximum()) else {
44            return Err(MeasurementStateError::InvalidLatest);
45        };
46        if latest > maximum
47            || (latest < maximum && aggregate.total() < latest as u128 + maximum as u128)
48            || (!aggregate.samples_overflowed()
49                && aggregate.total()
50                    > (aggregate.samples() - 1) as u128 * maximum as u128 + latest as u128)
51        {
52            return Err(MeasurementStateError::InvalidLatest);
53        }
54        Ok(Self { aggregate, latest })
55    }
56
57    /// Empty summary, with no latest or maximum observation.
58    pub const EMPTY: Self = Self {
59        aggregate: MeasurementAggregate::EMPTY,
60        latest: 0,
61    };
62
63    /// Record one completed observation, including zero, in the existing unit.
64    pub const fn record(&mut self, value: u64) {
65        self.aggregate.record(value);
66        self.latest = value;
67    }
68
69    /// Mergeable count/total/maximum snapshot without observation ordering.
70    #[must_use]
71    pub const fn aggregate(self) -> MeasurementAggregate {
72        self.aggregate
73    }
74
75    /// Latest observation, or `None` when empty. Summaries cannot be merged.
76    #[must_use]
77    pub const fn latest(self) -> Option<u64> {
78        if self.aggregate.samples() == 0 {
79            None
80        } else {
81            Some(self.latest)
82        }
83    }
84}