Skip to main content

ic_metrics/summary/
mod.rs

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