1use std::{
18 collections::{HashMap, VecDeque},
19 sync::Arc,
20 time::{Duration, Instant},
21};
22
23use hdrhistogram::Histogram;
24use parking_lot::Mutex;
25use prometheus_filtered::{
26 MetricLevel, Opts, Registry,
27 core::{Collector, Desc},
28 prometheus::{GaugeVec, proto::MetricFamily},
29};
30
31const QUANTILES: &[(f64, &str)] = &[
33 (0.05, "0.05"),
34 (0.33, "0.33"),
35 (0.5, "0.5"),
36 (0.66, "0.66"),
37 (0.95, "0.95"),
38];
39
40pub(crate) const WINDOW_SLOT: Duration = Duration::from_secs(10);
42pub(crate) const WINDOW_SLOTS: usize = 12;
45
46const MAX_TRACKED_VALUE: u64 = 600_000_000;
49
50fn new_histogram() -> Histogram<u64> {
51 Histogram::new_with_bounds(1, MAX_TRACKED_VALUE, 2).expect("valid histogram bounds")
56}
57
58struct Window {
61 slots: VecDeque<Histogram<u64>>,
62 newest_slot_start: Instant,
63}
64
65impl Window {
66 fn new(now: Instant) -> Self {
67 let mut slots = VecDeque::with_capacity(WINDOW_SLOTS);
68 slots.push_back(new_histogram());
69 Self {
70 slots,
71 newest_slot_start: now,
72 }
73 }
74
75 fn rotate(&mut self, now: Instant) {
76 if now.duration_since(self.newest_slot_start) >= WINDOW_SLOT * WINDOW_SLOTS as u32 {
80 self.slots.clear();
81 self.slots.push_back(new_histogram());
82 self.newest_slot_start = now;
83 return;
84 }
85 while now.duration_since(self.newest_slot_start) >= WINDOW_SLOT {
86 self.newest_slot_start += WINDOW_SLOT;
87 self.slots.push_back(new_histogram());
88 if self.slots.len() > WINDOW_SLOTS {
89 self.slots.pop_front();
90 }
91 }
92 }
93
94 fn record(&mut self, seconds: f64) {
95 self.rotate(Instant::now());
96 self.slots
97 .back_mut()
98 .expect("the window always holds at least one slot")
99 .saturating_record((seconds * 1e6).max(1.0) as u64);
100 }
101
102 fn quantiles_seconds(&mut self) -> Option<Vec<f64>> {
106 self.rotate(Instant::now());
107 let mut merged = new_histogram();
108 for slot in &self.slots {
109 merged
110 .add(slot)
111 .expect("all slot histograms share significant figures");
112 }
113 if merged.is_empty() {
114 return None;
115 }
116 Some(
117 QUANTILES
118 .iter()
119 .map(|(quantile, _)| merged.value_at_quantile(*quantile) as f64 / 1e6)
120 .collect(),
121 )
122 }
123}
124
125#[derive(Clone)]
132pub struct QuantileGauge {
133 gauge: GaugeVec,
134 window: Arc<Mutex<Window>>,
135}
136
137impl QuantileGauge {
138 pub fn register(
142 name: &str,
143 help: &str,
144 module: &str,
145 registry: &Registry,
146 level: MetricLevel,
147 ) -> Self {
148 let gauge =
149 GaugeVec::new(Opts::new(name, help), &["quantile"]).expect("valid gauge options");
150 let this = Self {
151 gauge,
152 window: Arc::new(Mutex::new(Window::new(Instant::now()))),
153 };
154 registry
155 .register_filtered(name, module, level, this)
156 .expect("quantile gauge registers without collision")
157 }
158
159 pub fn observe(&self, seconds: f64) {
160 self.window.lock().record(seconds);
161 }
162}
163
164impl Collector for QuantileGauge {
165 fn desc(&self) -> Vec<&Desc> {
166 self.gauge.desc()
167 }
168
169 fn collect(&self) -> Vec<MetricFamily> {
170 match self.window.lock().quantiles_seconds() {
171 Some(values) => {
172 for ((_, label), value) in QUANTILES.iter().zip(values) {
173 self.gauge.with_label_values(&[label]).set(value);
174 }
175 }
176 None => {
177 for (_, label) in QUANTILES {
178 let _ = self.gauge.remove_label_values(&[label]);
179 }
180 }
181 }
182 self.gauge.collect()
183 }
184}
185
186#[derive(Clone)]
190pub struct QuantileGaugeVec {
191 gauge: GaugeVec,
192 windows: Arc<Mutex<HashMap<String, Window>>>,
193}
194
195impl QuantileGaugeVec {
196 pub fn register(
200 name: &str,
201 help: &str,
202 label: &str,
203 module: &str,
204 registry: &Registry,
205 level: MetricLevel,
206 ) -> Self {
207 let gauge = GaugeVec::new(Opts::new(name, help), &[label, "quantile"])
208 .expect("valid gauge options");
209 let this = Self {
210 gauge,
211 windows: Arc::new(Mutex::new(HashMap::new())),
212 };
213 registry
214 .register_filtered(name, module, level, this)
215 .expect("quantile gauge registers without collision")
216 }
217
218 pub fn observe(&self, label_value: &str, seconds: f64) {
219 let mut windows = self.windows.lock();
220 if let Some(window) = windows.get_mut(label_value) {
221 window.record(seconds);
222 } else {
223 let mut window = Window::new(Instant::now());
224 window.record(seconds);
225 windows.insert(label_value.to_owned(), window);
226 }
227 }
228}
229
230impl Collector for QuantileGaugeVec {
231 fn desc(&self) -> Vec<&Desc> {
232 self.gauge.desc()
233 }
234
235 fn collect(&self) -> Vec<MetricFamily> {
236 let mut windows = self.windows.lock();
237 windows.retain(|label_value, window| match window.quantiles_seconds() {
238 Some(values) => {
239 for ((_, quantile), value) in QUANTILES.iter().zip(values) {
240 self.gauge
241 .with_label_values(&[label_value.as_str(), quantile])
242 .set(value);
243 }
244 true
245 }
246 None => {
247 for (_, quantile) in QUANTILES {
250 let _ = self
251 .gauge
252 .remove_label_values(&[label_value.as_str(), quantile]);
253 }
254 false
255 }
256 });
257 self.gauge.collect()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn rotate_keeps_slot_within_window_slot() {
267 let t0 = Instant::now();
268 let mut window = Window::new(t0);
269 window.rotate(t0 + WINDOW_SLOT / 2);
270 assert_eq!(window.slots.len(), 1);
271 assert_eq!(window.newest_slot_start, t0);
272 }
273
274 #[test]
275 fn rotate_adds_one_slot_at_exactly_window_slot() {
276 let t0 = Instant::now();
277 let mut window = Window::new(t0);
278 window.rotate(t0 + WINDOW_SLOT);
279 assert_eq!(window.slots.len(), 2);
280 assert_eq!(window.newest_slot_start, t0 + WINDOW_SLOT);
281 }
282
283 #[test]
284 fn rotate_adds_multiple_slots_and_trims_to_window() {
285 let t0 = Instant::now();
286 let mut window = Window::new(t0);
287
288 window.rotate(t0 + WINDOW_SLOT * 5);
289 assert_eq!(window.slots.len(), 6);
290 assert_eq!(window.newest_slot_start, t0 + WINDOW_SLOT * 5);
291
292 window.rotate(t0 + WINDOW_SLOT * 16);
294 assert_eq!(window.slots.len(), WINDOW_SLOTS);
295 assert_eq!(window.newest_slot_start, t0 + WINDOW_SLOT * 16);
296 }
297
298 #[test]
299 fn rotate_resets_after_gap_longer_than_window() {
300 let t0 = Instant::now();
301 let mut window = Window::new(t0);
302 let gap = WINDOW_SLOT * WINDOW_SLOTS as u32;
303 window.rotate(t0 + gap);
304 assert_eq!(window.slots.len(), 1);
305 assert_eq!(window.newest_slot_start, t0 + gap);
306 }
307
308 #[test]
309 fn old_observations_fall_out_of_window() {
310 let t0 = Instant::now();
311 let mut window = Window::new(t0);
312 window.slots.back_mut().unwrap().saturating_record(1_000);
313
314 window.rotate(t0 + WINDOW_SLOT * (WINDOW_SLOTS as u32 - 1));
316 assert_eq!(window.slots.len(), WINDOW_SLOTS);
317 assert!(!window.slots.front().unwrap().is_empty());
318
319 window.rotate(t0 + WINDOW_SLOT * WINDOW_SLOTS as u32);
321 assert_eq!(window.slots.len(), WINDOW_SLOTS);
322 assert!(window.slots.iter().all(|slot| slot.is_empty()));
323 }
324
325 #[test]
326 fn collect_drops_idle_label_series_and_window() {
327 let registry = Registry::new();
328 let gauge = QuantileGaugeVec::register(
329 "test_quantile_gauge",
330 "help",
331 "peer",
332 module_path!(),
333 ®istry,
334 MetricLevel::Warn,
335 );
336 gauge.observe("peer1", 0.01);
337
338 {
340 let mut windows = gauge.windows.lock();
341 let window = windows.get_mut("peer1").unwrap();
342 window.slots.clear();
343 window.slots.push_back(new_histogram());
344 }
345
346 gauge.collect();
347 assert!(gauge.windows.lock().is_empty());
348 }
349}