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.
63 pub const fn from_parts(
64 aggregate: MeasurementAggregate,
65 latest: Option<u64>,
66 ) -> Result<Self, MeasurementStateError> {
67 if aggregate.samples() == 0 {
68 return if latest.is_none() {
69 Ok(Self::EMPTY)
70 } else {
71 Err(MeasurementStateError::InvalidLatest)
72 };
73 }
74 let (Some(latest), Some(maximum)) = (latest, aggregate.maximum()) else {
75 return Err(MeasurementStateError::InvalidLatest);
76 };
77 if latest > maximum
78 || (aggregate.samples() == 1 && latest != maximum)
79 || (latest < maximum && aggregate.total() < latest as u128 + maximum as u128)
80 {
81 return Err(MeasurementStateError::InvalidLatest);
82 }
83 Ok(Self { aggregate, latest })
84 }
85
86 /// Empty summary, with no latest or maximum observation.
87 pub const EMPTY: Self = Self {
88 aggregate: MeasurementAggregate::EMPTY,
89 latest: 0,
90 };
91
92 /// Record one completed observation, including zero, in the existing unit.
93 pub const fn record(&mut self, value: u64) {
94 self.aggregate.record(value);
95 self.latest = value;
96 }
97
98 /// Mergeable count/total/maximum snapshot without observation ordering.
99 #[must_use]
100 pub const fn aggregate(self) -> MeasurementAggregate {
101 self.aggregate
102 }
103
104 /// Sample count, clipped at `u64::MAX`; saturation is in [`Self::aggregate`].
105 #[must_use]
106 pub const fn samples(self) -> u64 {
107 self.aggregate.samples()
108 }
109
110 /// Observation total, clipped at `u128::MAX`; saturation is in [`Self::aggregate`].
111 #[must_use]
112 pub const fn total(self) -> u128 {
113 self.aggregate.total()
114 }
115
116 /// Floor-rounded mean, or `None` when empty. An exactly reached cap is valid.
117 ///
118 /// # Errors
119 /// Returns [`MeasurementMeanError`] on actual count or total overflow.
120 pub const fn mean(self) -> Result<Option<u128>, MeasurementMeanError> {
121 self.aggregate.mean()
122 }
123
124 /// Floor-rounded mean multiplied by `scale`, retaining the fractional part.
125 ///
126 /// Uses the aggregate's checked projection; a scale of ten reports tenths.
127 /// Empty remains `None`, and scale zero does not conceal actual overflow.
128 ///
129 /// # Errors
130 /// Returns [`MeasurementMeanError`] for inexact counts/totals or a failed
131 /// scaled projection, with the same precedence as the aggregate.
132 pub const fn scaled_mean(self, scale: u64) -> Result<Option<u128>, MeasurementMeanError> {
133 self.aggregate.scaled_mean(scale)
134 }
135
136 /// Latest observation, or `None` when empty. Summaries cannot be merged.
137 #[must_use]
138 pub const fn latest(self) -> Option<u64> {
139 if self.samples() == 0 {
140 None
141 } else {
142 Some(self.latest)
143 }
144 }
145
146 /// Largest observation, or `None` when empty, including after overflow.
147 #[must_use]
148 pub const fn maximum(self) -> Option<u64> {
149 self.aggregate.maximum()
150 }
151}