hotmic/data/
snapshot.rs

1use super::{histogram::HistogramSnapshot, Percentile};
2use std::{collections::HashMap, fmt::Display};
3
4/// A typed metric measurement, used in snapshots.
5///
6/// This type provides a way to wrap the value of a metric, for use in a snapshot, while also
7/// providing the overall type of the metric, so that downstream consumers who how to properly
8/// format the data.
9#[derive(Debug, PartialEq, Eq)]
10pub enum TypedMeasurement {
11    Counter(String, i64),
12    Gauge(String, u64),
13    TimingHistogram(String, SummarizedHistogram),
14    ValueHistogram(String, SummarizedHistogram),
15}
16
17/// A point-in-time view of metric data.
18#[derive(Default, Debug)]
19pub struct Snapshot {
20    measurements: Vec<TypedMeasurement>,
21}
22
23impl Snapshot {
24    /// Stores a counter value for the given metric key.
25    pub(crate) fn set_count<T>(&mut self, key: T, value: i64)
26    where
27        T: Display,
28    {
29        self.measurements
30            .push(TypedMeasurement::Counter(key.to_string(), value));
31    }
32
33    /// Stores a gauge value for the given metric key.
34    pub(crate) fn set_gauge<T>(&mut self, key: T, value: u64)
35    where
36        T: Display,
37    {
38        self.measurements.push(TypedMeasurement::Gauge(key.to_string(), value));
39    }
40
41    /// Sets timing percentiles for the given metric key.
42    ///
43    /// From the given `HdrHistogram`, all the specific `percentiles` will be extracted and stored.
44    pub(crate) fn set_timing_histogram<T>(&mut self, key: T, h: HistogramSnapshot, percentiles: &[Percentile])
45    where
46        T: Display,
47    {
48        let summarized = SummarizedHistogram::from_histogram(h, percentiles);
49        self.measurements
50            .push(TypedMeasurement::TimingHistogram(key.to_string(), summarized));
51    }
52
53    /// Sets value percentiles for the given metric key.
54    ///
55    /// From the given `HdrHistogram`, all the specific `percentiles` will be extracted and stored.
56    pub(crate) fn set_value_histogram<T>(&mut self, key: T, h: HistogramSnapshot, percentiles: &[Percentile])
57    where
58        T: Display,
59    {
60        let summarized = SummarizedHistogram::from_histogram(h, percentiles);
61        self.measurements
62            .push(TypedMeasurement::ValueHistogram(key.to_string(), summarized));
63    }
64
65    /// Converts this [`Snapshot`] into [`SimpleSnapshot`].
66    ///
67    /// [`SimpleSnapshot`] provides a programmatic interface to more easily sift through the
68    /// metrics within, without needing to evaluate all of them.
69    pub fn into_simple(self) -> SimpleSnapshot { SimpleSnapshot::from_snapshot(self) }
70
71    /// Converts this [`Snapshot`] to the underlying vector of measurements.
72    pub fn into_vec(self) -> Vec<TypedMeasurement> { self.measurements }
73}
74
75/// A user-friendly metric snapshot that allows easy retrieval of values.
76///
77/// This is good for programmatic exploration of values, whereas [`Snapshot`] is designed around
78/// being consumed by output adapters that send metrics to external collection systems.
79#[derive(Default)]
80pub struct SimpleSnapshot {
81    pub(crate) counters: HashMap<String, i64>,
82    pub(crate) gauges: HashMap<String, u64>,
83    pub(crate) timings: HashMap<String, SummarizedHistogram>,
84    pub(crate) values: HashMap<String, SummarizedHistogram>,
85}
86
87impl SimpleSnapshot {
88    pub(crate) fn from_snapshot(s: Snapshot) -> Self {
89        let mut ss = SimpleSnapshot::default();
90        for metric in s.into_vec() {
91            match metric {
92                TypedMeasurement::Counter(key, value) => {
93                    ss.counters.insert(key, value);
94                },
95                TypedMeasurement::Gauge(key, value) => {
96                    ss.gauges.insert(key, value);
97                },
98                TypedMeasurement::TimingHistogram(key, value) => {
99                    ss.timings.insert(key, value);
100                },
101                TypedMeasurement::ValueHistogram(key, value) => {
102                    ss.values.insert(key, value);
103                },
104            }
105        }
106        ss
107    }
108
109    /// Gets the counter value for the given metric key.
110    ///
111    /// Returns `None` if the metric key has no counter value in this snapshot.
112    pub fn count(&self, key: &str) -> Option<i64> { self.counters.get(key).cloned() }
113
114    /// Gets the gauge value for the given metric key.
115    ///
116    /// Returns `None` if the metric key has no gauge value in this snapshot.
117    pub fn gauge(&self, key: &str) -> Option<u64> { self.gauges.get(key).cloned() }
118
119    /// Gets the given timing percentile for given metric key.
120    ///
121    /// Returns `None` if the metric key has no value at the given percentile in this snapshot.
122    pub fn timing_histogram(&self, key: &str, percentile: f64) -> Option<u64> {
123        let p = Percentile::from(percentile);
124        self.timings.get(key).and_then(|s| s.measurements().get(&p)).cloned()
125    }
126
127    /// Gets the given value percentile for the given metric key.
128    ///
129    /// Returns `None` if the metric key has no value at the given percentile in this snapshot.
130    pub fn value_histogram(&self, key: &str, percentile: f64) -> Option<u64> {
131        let p = Percentile::from(percentile);
132        self.values.get(key).and_then(|s| s.measurements().get(&p)).cloned()
133    }
134}
135
136/// A pre-summarized histogram.
137///
138/// Based on the configuration of the [`Receiver`], this histogram will represent only the
139/// configured percentiles to extract for a given underlying histogram, as well as the measurement
140/// count for the underlying histogram.
141#[derive(Debug, PartialEq, Eq)]
142pub struct SummarizedHistogram {
143    count: u64,
144    sum: u64,
145    measurements: HashMap<Percentile, u64>,
146}
147
148impl SummarizedHistogram {
149    pub(crate) fn from_histogram(histogram: HistogramSnapshot, percentiles: &[Percentile]) -> Self {
150        let mut measurements = HashMap::default();
151        let count = histogram.count();
152        let sum = histogram.sum();
153
154        for percentile in percentiles {
155            let value = histogram.histogram().value_at_percentile(percentile.value);
156            measurements.insert(percentile.clone(), value);
157        }
158
159        SummarizedHistogram {
160            count,
161            sum,
162            measurements,
163        }
164    }
165
166    /// Gets the total count of measurements present in the underlying histogram.
167    pub fn count(&self) -> u64 { self.count }
168
169    /// Gets the total sum of the measurements recorded in the underlying histogram.
170    pub fn sum(&self) -> u64 { self.sum }
171
172    /// Gets the map of percentile/value pairs extracted from the underlying histogram.
173    pub fn measurements(&self) -> &HashMap<Percentile, u64> { &self.measurements }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{HistogramSnapshot, Percentile, Snapshot, TypedMeasurement};
179    use hdrhistogram::Histogram;
180
181    #[test]
182    fn test_snapshot_simple_set_and_get() {
183        let key = "ok".to_owned();
184        let mut snapshot = Snapshot::default();
185        snapshot.set_count(key.clone(), 1);
186        snapshot.set_gauge(key.clone(), 42);
187
188        let values = snapshot.into_vec();
189
190        assert_eq!(values[0], TypedMeasurement::Counter("ok".to_owned(), 1));
191        assert_eq!(values[1], TypedMeasurement::Gauge("ok".to_owned(), 42));
192    }
193
194    #[test]
195    fn test_snapshot_percentiles() {
196        {
197            let mut snapshot = Snapshot::default();
198            let mut h1 = Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap();
199            let mut sum = 0;
200            h1.saturating_record(500_000);
201            sum += 500_000;
202            h1.saturating_record(750_000);
203            sum += 750_000;
204            h1.saturating_record(1_000_000);
205            sum += 1_000_000;
206            h1.saturating_record(1_250_000);
207            sum += 1_250_000;
208
209            let tkey = "ok".to_owned();
210            let mut tpercentiles = Vec::new();
211            tpercentiles.push(Percentile::from(0.0));
212            tpercentiles.push(Percentile::from(50.0));
213            tpercentiles.push(Percentile::from(99.0));
214            tpercentiles.push(Percentile::from(100.0));
215            let fake = Percentile::from(63.0);
216
217            snapshot.set_timing_histogram(tkey.clone(), HistogramSnapshot::new(h1, sum), &tpercentiles);
218
219            let values = snapshot.into_vec();
220            match values.get(0) {
221                Some(TypedMeasurement::TimingHistogram(key, summary)) => {
222                    assert_eq!(key, "ok");
223                    assert_eq!(summary.count(), 4);
224                    assert_eq!(summary.sum(), 3_500_000);
225
226                    let min_tpercentile = summary.measurements().get(&tpercentiles[0]);
227                    let p50_tpercentile = summary.measurements().get(&tpercentiles[1]);
228                    let p99_tpercentile = summary.measurements().get(&tpercentiles[2]);
229                    let max_tpercentile = summary.measurements().get(&tpercentiles[3]);
230                    let fake_tpercentile = summary.measurements().get(&fake);
231
232                    assert!(min_tpercentile.is_some());
233                    assert!(p50_tpercentile.is_some());
234                    assert!(p99_tpercentile.is_some());
235                    assert!(max_tpercentile.is_some());
236                    assert!(fake_tpercentile.is_none());
237                },
238                _ => panic!("expected timing histogram value! actual: {:?}", values[0]),
239            }
240        }
241
242        {
243            let mut snapshot = Snapshot::default();
244            let mut h1 = Histogram::<u64>::new_with_bounds(1, u64::max_value(), 3).unwrap();
245            let mut sum = 0;
246            h1.saturating_record(500_000);
247            sum += 500_000;
248            h1.saturating_record(750_000);
249            sum += 750_000;
250            h1.saturating_record(1_000_000);
251            sum += 1_000_000;
252            h1.saturating_record(1_250_000);
253            sum += 1_250_000;
254
255            let tkey = "ok".to_owned();
256            let mut tpercentiles = Vec::new();
257            tpercentiles.push(Percentile::from(0.0));
258            tpercentiles.push(Percentile::from(50.0));
259            tpercentiles.push(Percentile::from(99.0));
260            tpercentiles.push(Percentile::from(100.0));
261            let fake = Percentile::from(63.0);
262
263            snapshot.set_value_histogram(tkey.clone(), HistogramSnapshot::new(h1, sum), &tpercentiles);
264
265            let values = snapshot.into_vec();
266            match values.get(0) {
267                Some(TypedMeasurement::ValueHistogram(key, summary)) => {
268                    assert_eq!(key, "ok");
269                    assert_eq!(summary.count(), 4);
270                    assert_eq!(summary.sum(), 3_500_000);
271
272                    let min_tpercentile = summary.measurements().get(&tpercentiles[0]);
273                    let p50_tpercentile = summary.measurements().get(&tpercentiles[1]);
274                    let p99_tpercentile = summary.measurements().get(&tpercentiles[2]);
275                    let max_tpercentile = summary.measurements().get(&tpercentiles[3]);
276                    let fake_tpercentile = summary.measurements().get(&fake);
277
278                    assert!(min_tpercentile.is_some());
279                    assert!(p50_tpercentile.is_some());
280                    assert!(p99_tpercentile.is_some());
281                    assert!(max_tpercentile.is_some());
282                    assert!(fake_tpercentile.is_none());
283                },
284                _ => panic!("expected value histogram value! actual: {:?}", values[0]),
285            }
286        }
287    }
288
289    #[test]
290    fn test_percentiles() {
291        let min_p = Percentile::from(0.0);
292        assert_eq!(min_p.label(), "min");
293
294        let max_p = Percentile::from(100.0);
295        assert_eq!(max_p.label(), "max");
296
297        let clamped_min_p = Percentile::from(-20.0);
298        assert_eq!(clamped_min_p.label(), "min");
299        assert_eq!(clamped_min_p.percentile(), 0.0);
300
301        let clamped_max_p = Percentile::from(1442.0);
302        assert_eq!(clamped_max_p.label(), "max");
303        assert_eq!(clamped_max_p.percentile(), 100.0);
304
305        let p99_p = Percentile::from(99.0);
306        assert_eq!(p99_p.label(), "p99");
307
308        let p999_p = Percentile::from(99.9);
309        assert_eq!(p999_p.label(), "p999");
310
311        let p9999_p = Percentile::from(99.99);
312        assert_eq!(p9999_p.label(), "p9999");
313    }
314}