1use std::marker::PhantomData;
42
43use itertools::Itertools;
44use prometheus::Opts;
45use prometheus::core::{Collector, Desc};
46use prometheus::proto::MetricFamily;
47
48use crate::cast::CastLossy;
49use crate::metrics::UIntGauge;
50use crate::metrics::raw::UIntGaugeVec;
51
52pub const PERCENTILES: &[(&str, f64)] = &[
60 ("min", 0.0),
61 ("p25", 0.25),
62 ("p50", 0.5),
63 ("p75", 0.75),
64 ("p90", 0.9),
65 ("p99", 0.99),
66 ("p999", 0.999),
67 ("p9999", 0.9999),
68 ("max", 1.0),
69];
70
71pub trait AggregationKey {
73 const LABEL_NAMES: &'static [&'static str];
75
76 fn label_values(&self) -> Vec<String>;
79}
80
81#[derive(Debug, Clone)]
83pub struct AggregatedFamily {
84 pub name: String,
86 pub help: String,
88 pub sum: bool,
91 pub top_k: Option<usize>,
93}
94
95impl AggregatedFamily {
96 fn percentile_vec(&self) -> UIntGaugeVec {
97 let opts = Opts::new(
98 format!("{}_percentile", self.name),
99 format!("{} (percentiles over this process's objects)", self.help),
100 );
101 UIntGaugeVec::new(opts, &["percentile"]).expect("valid percentile family")
102 }
103
104 fn sum_gauge(&self) -> UIntGauge {
105 let opts = Opts::new(
106 format!("{}_sum", self.name),
107 format!(
108 "{} (sum over this process's objects, which other processes may also count)",
109 self.help
110 ),
111 );
112 UIntGauge::with_opts(opts).expect("valid sum family")
113 }
114
115 fn top_k_vec<K: AggregationKey>(&self, k: usize) -> UIntGaugeVec {
116 let opts = Opts::new(
117 format!("{}_topk", self.name),
118 format!("{} (the {k} largest objects)", self.help),
119 );
120 UIntGaugeVec::new(opts, K::LABEL_NAMES).expect("valid top-k family")
121 }
122}
123
124#[derive(Debug)]
126pub struct AggregatedFamilies<K> {
127 families: Vec<AggregatedFamily>,
128 descs: Vec<Desc>,
129 _key: PhantomData<fn(&K)>,
130}
131
132impl<K: AggregationKey> AggregatedFamilies<K> {
133 pub fn new(families: Vec<AggregatedFamily>) -> Self {
136 let mut descs = Vec::new();
137 for f in &families {
138 descs.extend(f.percentile_vec().desc().into_iter().cloned());
139 if f.sum {
140 descs.extend(f.sum_gauge().desc().into_iter().cloned());
141 }
142 if let Some(k) = f.top_k {
143 descs.extend(f.top_k_vec::<K>(k).desc().into_iter().cloned());
144 }
145 }
146 AggregatedFamilies {
147 families,
148 descs,
149 _key: PhantomData,
150 }
151 }
152
153 pub fn descs(&self) -> Vec<&Desc> {
155 self.descs.iter().collect()
156 }
157
158 pub fn snapshot(&self) -> AggregationSnapshot<'_, K> {
160 AggregationSnapshot {
161 families: self,
162 keys: Vec::new(),
163 columns: vec![Vec::new(); self.families.len()],
164 }
165 }
166}
167
168#[derive(Debug)]
173pub struct AggregationSnapshot<'a, K> {
174 families: &'a AggregatedFamilies<K>,
175 keys: Vec<K>,
176 columns: Vec<Vec<(u64, usize)>>,
178}
179
180impl<K: AggregationKey> AggregationSnapshot<'_, K> {
181 pub fn push(&mut self, key: K, values: &[u64]) {
183 for (column, value) in self.columns.iter_mut().zip_eq(values) {
184 column.push((*value, self.keys.len()));
185 }
186 self.keys.push(key);
187 }
188
189 pub fn finish(self) -> Vec<MetricFamily> {
191 let mut out = Vec::new();
192 for (f, mut column) in self.families.families.iter().zip_eq(self.columns) {
193 column.sort_unstable();
196
197 let percentiles = f.percentile_vec();
199 if !column.is_empty() {
200 let n = f64::cast_lossy(column.len());
201 for (label, q) in PERCENTILES {
202 let rank = usize::cast_lossy((q * n).ceil());
203 let (value, _) = column[rank.max(1) - 1];
204 percentiles.with_label_values(&[*label]).set(value);
205 }
206 }
207 out.extend(percentiles.collect());
208
209 if f.sum {
210 let sum = f.sum_gauge();
211 sum.set(column.iter().map(|(v, _)| v).sum());
212 out.extend(sum.collect());
213 }
214
215 if let Some(k) = f.top_k {
216 let top_k = f.top_k_vec::<K>(k);
217 for &(value, idx) in column.iter().rev().take_while(|(v, _)| *v > 0).take(k) {
219 top_k
220 .with_label_values(&self.keys[idx].label_values())
221 .set(value);
222 }
223 out.extend(top_k.collect());
224 }
225 }
226 out
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::collections::BTreeMap;
233
234 use super::*;
235
236 struct Obj(u64);
237
238 impl AggregationKey for Obj {
239 const LABEL_NAMES: &'static [&'static str] = &["obj"];
240 fn label_values(&self) -> Vec<String> {
241 vec![self.0.to_string()]
242 }
243 }
244
245 fn aggregate(values: impl IntoIterator<Item = u64>) -> Vec<MetricFamily> {
248 aggregate_with_sum(values, true)
249 }
250
251 fn aggregate_with_sum(values: impl IntoIterator<Item = u64>, sum: bool) -> Vec<MetricFamily> {
252 let families = AggregatedFamilies::<Obj>::new(vec![AggregatedFamily {
253 name: "m".into(),
254 help: "help".into(),
255 sum,
256 top_k: Some(10),
257 }]);
258 let mut snapshot = families.snapshot();
259 for v in values {
260 snapshot.push(Obj(v), &[v]);
261 }
262 snapshot.finish()
263 }
264
265 fn series(families: &[MetricFamily], name: &str) -> BTreeMap<String, u64> {
267 let family = families.iter().find(|f| f.name() == name).expect("family");
268 family
269 .get_metric()
270 .iter()
271 .map(|m| {
272 (
273 m.get_label()[0].value().to_string(),
274 u64::cast_lossy(m.get_gauge().value()),
275 )
276 })
277 .collect()
278 }
279
280 fn percentiles(values: [u64; 9]) -> BTreeMap<String, u64> {
281 PERCENTILES
282 .iter()
283 .map(|(l, _)| l.to_string())
284 .zip_eq(values)
285 .collect()
286 }
287
288 fn objs(values: impl IntoIterator<Item = u64>) -> BTreeMap<String, u64> {
289 values.into_iter().map(|v| (v.to_string(), v)).collect()
290 }
291
292 #[crate::test]
293 fn percentiles_are_exact() {
294 assert_eq!(
296 series(&aggregate((1..=10_000).rev()), "m_percentile"),
297 percentiles([1, 2500, 5000, 7500, 9000, 9900, 9990, 9999, 10_000])
298 );
299 assert_eq!(
301 series(&aggregate(1..=100), "m_percentile"),
302 percentiles([1, 25, 50, 75, 90, 99, 100, 100, 100])
303 );
304 assert_eq!(series(&aggregate([7]), "m_percentile"), percentiles([7; 9]));
305 assert!(series(&aggregate([]), "m_percentile").is_empty());
306 }
307
308 #[crate::test]
309 fn sum_is_exact() {
310 let sum = |values: Vec<u64>| {
311 let families = aggregate(values);
312 let family = families.iter().find(|f| f.name() == "m_sum").expect("sum");
313 u64::cast_lossy(family.get_metric()[0].get_gauge().value())
314 };
315 assert_eq!(sum((1..=100).collect()), 5050);
316 assert_eq!(sum(vec![]), 0);
317 }
318
319 #[crate::test]
320 fn sum_is_omitted_when_unset() {
321 let families = aggregate_with_sum(1..=10, false);
322 assert!(families.iter().all(|f| f.name() != "m_sum"));
323 assert!(families.iter().any(|f| f.name() == "m_percentile"));
324 }
325
326 #[crate::test]
327 fn topk_is_the_largest_non_zero_values() {
328 assert_eq!(series(&aggregate(1..=100), "m_topk"), objs(91..=100));
329 assert_eq!(series(&aggregate([0, 7, 0]), "m_topk"), objs([7]));
330 assert_eq!(series(&aggregate(1..=5), "m_topk"), objs(1..=5));
332 }
333
334 #[crate::test]
335 fn cardinality_is_flat_in_object_count() {
336 let series_count =
337 |n| -> usize { aggregate(1..=n).iter().map(|f| f.get_metric().len()).sum() };
338 assert_eq!(series_count(10), PERCENTILES.len() + 1 + 10);
339 assert_eq!(series_count(10), series_count(10_000));
340 }
341}