1#![allow(dead_code)]
2
3use std::{
16 collections::HashMap,
17 sync::{Arc, RwLock},
18};
19
20use hotshot_types::traits::metrics;
21use itertools::Itertools;
22use prometheus::{
23 Encoder, HistogramVec, Opts, Registry, TextEncoder,
24 core::{AtomicU64, GenericCounter, GenericCounterVec, GenericGauge, GenericGaugeVec},
25};
26use snafu::Snafu;
27use tracing::warn;
28
29#[derive(Debug, Snafu)]
30pub enum MetricsError {
31 NoSuchSubgroup {
32 path: Vec<String>,
33 },
34 NoSuchMetric {
35 namespace: Vec<String>,
36 name: String,
37 },
38 Prometheus {
39 source: prometheus::Error,
40 },
41}
42
43impl From<prometheus::Error> for MetricsError {
44 fn from(source: prometheus::Error) -> Self {
45 Self::Prometheus { source }
46 }
47}
48
49#[derive(Clone, Debug, Default)]
69pub struct PrometheusMetrics {
70 metrics: Registry,
71 namespace: Vec<String>,
72 children: Arc<RwLock<HashMap<String, PrometheusMetrics>>>,
73 counters: Arc<RwLock<HashMap<String, Counter>>>,
74 gauges: Arc<RwLock<HashMap<String, Gauge>>>,
75 histograms: Arc<RwLock<HashMap<String, Histogram>>>,
76 counter_families: Arc<RwLock<HashMap<String, CounterFamily>>>,
77 gauge_families: Arc<RwLock<HashMap<String, GaugeFamily>>>,
78 histogram_families: Arc<RwLock<HashMap<String, HistogramFamily>>>,
79}
80
81impl PrometheusMetrics {
82 pub fn get_counter(&self, name: &str) -> Result<Counter, MetricsError> {
84 self.get_metric(&self.counters, name)
85 }
86
87 pub fn get_gauge(&self, name: &str) -> Result<Gauge, MetricsError> {
89 self.get_metric(&self.gauges, name)
90 }
91
92 pub fn get_histogram(&self, name: &str) -> Result<Histogram, MetricsError> {
94 self.get_metric(&self.histograms, name)
95 }
96
97 pub fn get_counter_family(&self, name: &str) -> Result<CounterFamily, MetricsError> {
99 self.get_metric(&self.counter_families, name)
100 }
101
102 pub fn gauge_family(&self, name: &str) -> Result<GaugeFamily, MetricsError> {
104 self.get_metric(&self.gauge_families, name)
105 }
106
107 pub fn get_histogram_family(&self, name: &str) -> Result<HistogramFamily, MetricsError> {
109 self.get_metric(&self.histogram_families, name)
110 }
111
112 pub fn get_subgroup<I>(&self, path: I) -> Result<PrometheusMetrics, MetricsError>
114 where
115 I: IntoIterator,
116 I::Item: AsRef<str>,
117 {
118 let mut curr = self.clone();
119 for seg in path.into_iter() {
120 let next = curr
121 .children
122 .read()
123 .unwrap()
124 .get(seg.as_ref())
125 .ok_or_else(|| MetricsError::NoSuchSubgroup {
126 path: {
127 let mut path = curr.namespace.clone();
128 path.push(seg.as_ref().to_string());
129 path
130 },
131 })?
132 .clone();
133 curr = next;
134 }
135 Ok(curr)
136 }
137
138 fn get_metric<M: Clone>(
139 &self,
140 metrics: &Arc<RwLock<HashMap<String, M>>>,
141 name: &str,
142 ) -> Result<M, MetricsError> {
143 metrics
144 .read()
145 .unwrap()
146 .get(name)
147 .cloned()
148 .ok_or_else(|| MetricsError::NoSuchMetric {
149 namespace: self.namespace.clone(),
150 name: name.to_string(),
151 })
152 }
153
154 fn metric_opts(&self, name: String, unit_label: Option<String>) -> Opts {
155 let help = unit_label.unwrap_or_else(|| name.clone());
156 let mut opts = Opts::new(name, help);
157 let mut group_names = self.namespace.iter();
158 if let Some(namespace) = group_names.next() {
159 opts = opts
160 .namespace(namespace.clone())
161 .subsystem(group_names.join("_"));
162 }
163 opts
164 }
165}
166
167impl tide_disco::metrics::Metrics for PrometheusMetrics {
168 type Error = MetricsError;
169
170 fn export(&self) -> Result<String, Self::Error> {
171 let encoder = TextEncoder::new();
172 let metric_families = self.metrics.gather();
173 let mut buffer = vec![];
174 encoder.encode(&metric_families, &mut buffer)?;
175 String::from_utf8(buffer).map_err(|err| MetricsError::Prometheus {
176 source: prometheus::Error::Msg(format!(
177 "could not convert Prometheus output to UTF-8: {err}"
178 )),
179 })
180 }
181}
182
183impl metrics::Metrics for PrometheusMetrics {
184 fn create_counter(
185 &self,
186 name: String,
187 unit_label: Option<String>,
188 ) -> Box<dyn metrics::Counter> {
189 let counter = Counter::new(&self.metrics, self.metric_opts(name.clone(), unit_label));
190 self.counters.write().unwrap().insert(name, counter.clone());
191 Box::new(counter)
192 }
193
194 fn create_gauge(&self, name: String, unit_label: Option<String>) -> Box<dyn metrics::Gauge> {
195 let gauge = Gauge::new(&self.metrics, self.metric_opts(name.clone(), unit_label));
196 self.gauges.write().unwrap().insert(name, gauge.clone());
197 Box::new(gauge)
198 }
199
200 fn create_histogram(
201 &self,
202 name: String,
203 unit_label: Option<String>,
204 ) -> Box<dyn metrics::Histogram> {
205 let histogram = Histogram::new(&self.metrics, self.metric_opts(name.clone(), unit_label));
206 self.histograms
207 .write()
208 .unwrap()
209 .insert(name, histogram.clone());
210 Box::new(histogram)
211 }
212
213 fn create_text(&self, name: String) {
214 self.create_gauge(name, None).set(1);
215 }
216
217 fn counter_family(&self, name: String, labels: Vec<String>) -> Box<dyn metrics::CounterFamily> {
218 let family =
219 CounterFamily::new(&self.metrics, self.metric_opts(name.clone(), None), &labels);
220 self.counter_families
221 .write()
222 .unwrap()
223 .insert(name, family.clone());
224 Box::new(family)
225 }
226
227 fn gauge_family(&self, name: String, labels: Vec<String>) -> Box<dyn metrics::GaugeFamily> {
228 let family = GaugeFamily::new(&self.metrics, self.metric_opts(name.clone(), None), &labels);
229 self.gauge_families
230 .write()
231 .unwrap()
232 .insert(name, family.clone());
233 Box::new(family)
234 }
235
236 fn histogram_family(
237 &self,
238 name: String,
239 labels: Vec<String>,
240 ) -> Box<dyn metrics::HistogramFamily> {
241 let family =
242 HistogramFamily::new(&self.metrics, self.metric_opts(name.clone(), None), &labels);
243 self.histogram_families
244 .write()
245 .unwrap()
246 .insert(name, family.clone());
247 Box::new(family)
248 }
249
250 fn text_family(&self, name: String, labels: Vec<String>) -> Box<dyn metrics::TextFamily> {
251 Box::new(TextFamily::new(
252 &self.metrics,
253 self.metric_opts(name.clone(), None),
254 &labels,
255 ))
256 }
257
258 fn subgroup(&self, subgroup_name: String) -> Box<dyn metrics::Metrics> {
259 Box::new(
260 self.children
261 .write()
262 .unwrap()
263 .entry(subgroup_name.clone())
264 .or_insert_with(|| Self {
265 metrics: self.metrics.clone(),
266 namespace: {
267 let mut namespace = self.namespace.clone();
268 namespace.push(subgroup_name);
269 namespace
270 },
271 ..Default::default()
272 })
273 .clone(),
274 )
275 }
276}
277
278#[derive(Clone, Debug)]
280pub struct Counter(GenericCounter<AtomicU64>);
281
282impl Counter {
283 fn new(registry: &Registry, opts: Opts) -> Self {
284 let counter = GenericCounter::with_opts(opts).unwrap();
285 registry.register(Box::new(counter.clone())).unwrap();
286 Self(counter)
287 }
288
289 pub fn get(&self) -> usize {
290 self.0.get() as usize
291 }
292}
293
294impl metrics::Counter for Counter {
295 fn add(&self, amount: usize) {
296 self.0.inc_by(amount as u64);
297 }
298}
299
300#[derive(Clone, Debug)]
302pub struct Gauge(GenericGauge<AtomicU64>);
303
304impl Gauge {
305 fn new(registry: &Registry, opts: Opts) -> Self {
306 let gauge = GenericGauge::with_opts(opts).unwrap();
307 registry.register(Box::new(gauge.clone())).unwrap();
308 Self(gauge)
309 }
310
311 pub fn get(&self) -> usize {
312 self.0.get() as usize
313 }
314}
315
316impl metrics::Gauge for Gauge {
317 fn set(&self, amount: usize) {
318 self.0.set(amount as u64);
319 }
320
321 fn update(&self, delta: i64) {
322 if delta >= 0 {
323 self.0.add(delta as u64);
324 } else {
325 self.0.sub(-delta as u64);
326 }
327 }
328}
329
330#[derive(Clone, Debug)]
332pub struct Histogram(prometheus::Histogram);
333
334impl Histogram {
335 fn new(registry: &Registry, opts: Opts) -> Self {
336 let histogram = prometheus::Histogram::with_opts(opts.into()).unwrap();
337 registry.register(Box::new(histogram.clone())).unwrap();
338 Self(histogram)
339 }
340
341 pub fn sample_count(&self) -> usize {
342 self.0.get_sample_count() as usize
343 }
344
345 pub fn sum(&self) -> f64 {
346 self.0.get_sample_sum()
347 }
348
349 pub fn mean(&self) -> f64 {
350 self.sum() / (self.sample_count() as f64)
351 }
352}
353
354impl metrics::Histogram for Histogram {
355 fn add_point(&self, point: f64) {
356 self.0.observe(point);
357 }
358}
359
360#[derive(Clone, Debug)]
362pub struct CounterFamily(GenericCounterVec<AtomicU64>);
363
364impl CounterFamily {
365 fn new(registry: &Registry, opts: Opts, labels: &[String]) -> Self {
366 let labels = labels.iter().map(String::as_str).collect::<Vec<_>>();
367 let family = GenericCounterVec::new(opts, &labels).unwrap();
368 registry.register(Box::new(family.clone())).unwrap();
369 Self(family)
370 }
371
372 pub fn get(&self, label_values: &[impl AsRef<str>]) -> Counter {
373 let labels = label_values.iter().map(AsRef::as_ref).collect::<Vec<_>>();
374 Counter(self.0.get_metric_with_label_values(&labels).unwrap())
375 }
376}
377
378impl metrics::MetricsFamily<Box<dyn metrics::Counter>> for CounterFamily {
379 fn create(&self, labels: Vec<String>) -> Box<dyn metrics::Counter> {
380 Box::new(self.get(&labels))
381 }
382
383 fn destroy(&self, labels: &[&str]) {
384 if let Err(err) = self.0.remove_label_values(labels) {
385 warn!(%err, "failed to remove prometheus counter")
386 }
387 }
388}
389
390#[derive(Clone, Debug)]
392pub struct GaugeFamily(GenericGaugeVec<AtomicU64>);
393
394impl GaugeFamily {
395 fn new(registry: &Registry, opts: Opts, labels: &[String]) -> Self {
396 let labels = labels.iter().map(String::as_str).collect::<Vec<_>>();
397 let family = GenericGaugeVec::new(opts, &labels).unwrap();
398 registry.register(Box::new(family.clone())).unwrap();
399 Self(family)
400 }
401
402 pub fn get(&self, label_values: &[impl AsRef<str>]) -> Gauge {
403 let labels = label_values.iter().map(AsRef::as_ref).collect::<Vec<_>>();
404 Gauge(self.0.get_metric_with_label_values(&labels).unwrap())
405 }
406}
407
408impl metrics::MetricsFamily<Box<dyn metrics::Gauge>> for GaugeFamily {
409 fn create(&self, labels: Vec<String>) -> Box<dyn metrics::Gauge> {
410 Box::new(self.get(&labels))
411 }
412
413 fn destroy(&self, labels: &[&str]) {
414 if let Err(err) = self.0.remove_label_values(labels) {
415 warn!(%err, "failed to remove prometheus gauge")
416 }
417 }
418}
419
420#[derive(Clone, Debug)]
422pub struct HistogramFamily(HistogramVec);
423
424impl HistogramFamily {
425 fn new(registry: &Registry, opts: Opts, labels: &[String]) -> Self {
426 let labels = labels.iter().map(String::as_str).collect::<Vec<_>>();
427 let family = HistogramVec::new(opts.into(), &labels).unwrap();
428 registry.register(Box::new(family.clone())).unwrap();
429 Self(family)
430 }
431
432 pub fn get(&self, label_values: &[impl AsRef<str>]) -> Histogram {
433 let labels = label_values.iter().map(AsRef::as_ref).collect::<Vec<_>>();
434 Histogram(self.0.get_metric_with_label_values(&labels).unwrap())
435 }
436}
437
438impl metrics::MetricsFamily<Box<dyn metrics::Histogram>> for HistogramFamily {
439 fn create(&self, labels: Vec<String>) -> Box<dyn metrics::Histogram> {
440 Box::new(self.get(&labels))
441 }
442
443 fn destroy(&self, labels: &[&str]) {
444 if let Err(err) = self.0.remove_label_values(labels) {
445 warn!(%err, "failed to remove prometheus histogram")
446 }
447 }
448}
449
450#[derive(Clone, Debug)]
452pub struct TextFamily(GaugeFamily);
453
454impl TextFamily {
455 fn new(registry: &Registry, opts: Opts, labels: &[String]) -> Self {
456 Self(GaugeFamily::new(registry, opts, labels))
457 }
458}
459
460impl metrics::MetricsFamily<()> for TextFamily {
461 fn create(&self, labels: Vec<String>) {
462 self.0.create(labels).set(1);
463 }
464
465 fn destroy(&self, labels: &[&str]) {
466 self.0.destroy(labels)
467 }
468}
469
470#[cfg(test)]
471mod test {
472 use metrics::Metrics;
473 use tide_disco::metrics::Metrics as _;
474
475 use super::*;
476
477 #[test_log::test]
478 fn test_prometheus_metrics() {
479 let metrics = PrometheusMetrics::default();
480
481 let counter = metrics.create_counter("counter".into(), None);
483 let gauge = metrics.create_gauge("gauge".into(), None);
484 let histogram = metrics.create_histogram("histogram".into(), None);
485 metrics.create_text("text".into());
486
487 counter.add(20);
489 gauge.set(42);
490 histogram.add_point(20f64);
491
492 assert_eq!(metrics.get_counter("counter").unwrap().get(), 20);
494 assert_eq!(metrics.get_gauge("gauge").unwrap().get(), 42);
495 assert_eq!(
496 metrics.get_histogram("histogram").unwrap().sample_count(),
497 1
498 );
499 assert_eq!(metrics.get_histogram("histogram").unwrap().sum(), 20f64);
500 assert_eq!(metrics.get_histogram("histogram").unwrap().mean(), 20f64);
501
502 counter.add(22);
504 gauge.set(100);
505 histogram.add_point(22f64);
506
507 assert_eq!(metrics.get_counter("counter").unwrap().get(), 42);
509 assert_eq!(metrics.get_gauge("gauge").unwrap().get(), 100);
510 assert_eq!(
511 metrics.get_histogram("histogram").unwrap().sample_count(),
512 2
513 );
514 assert_eq!(metrics.get_histogram("histogram").unwrap().sum(), 42f64);
515 assert_eq!(metrics.get_histogram("histogram").unwrap().mean(), 21f64);
516
517 let string = metrics.export().unwrap();
519 let lines = string.lines().collect::<Vec<_>>();
521 assert!(lines.contains(&"counter 42"));
522 assert!(lines.contains(&"gauge 100"));
523 assert!(lines.contains(&"histogram_sum 42"));
524 assert!(lines.contains(&"histogram_count 2"));
525 assert!(lines.contains(&"text 1"));
526 }
527
528 #[test_log::test]
529 fn test_namespace() {
530 let metrics = PrometheusMetrics::default();
531 let subgroup1 = metrics.subgroup("subgroup1".into());
532 let subgroup2 = subgroup1.subgroup("subgroup2".into());
533 let counter = subgroup2.create_counter("counter".into(), None);
534 subgroup2.create_text("text".into());
535 counter.add(42);
536
537 assert_eq!(
539 metrics.get_subgroup(["subgroup1"]).unwrap().namespace,
540 ["subgroup1"]
541 );
542 assert_eq!(
543 metrics
544 .get_subgroup(["subgroup1", "subgroup2"])
545 .unwrap()
546 .namespace,
547 ["subgroup1", "subgroup2"]
548 );
549 assert_eq!(
550 metrics
551 .get_subgroup(["subgroup1"])
552 .unwrap()
553 .get_subgroup(["subgroup2"])
554 .unwrap()
555 .namespace,
556 ["subgroup1", "subgroup2"]
557 );
558
559 assert_eq!(
561 metrics
562 .get_subgroup(["subgroup1", "subgroup2"])
563 .unwrap()
564 .get_counter("counter")
565 .unwrap()
566 .get(),
567 42
568 );
569 assert_eq!(
570 metrics
571 .get_subgroup(["subgroup1"])
572 .unwrap()
573 .get_subgroup(["subgroup2"])
574 .unwrap()
575 .get_counter("counter")
576 .unwrap()
577 .get(),
578 42
579 );
580
581 assert!(
583 metrics
584 .export()
585 .unwrap()
586 .lines()
587 .contains(&"subgroup1_subgroup2_counter 42")
588 );
589
590 assert!(
592 metrics
593 .export()
594 .unwrap()
595 .lines()
596 .contains(&"subgroup1_subgroup2_text 1")
597 );
598 }
599
600 #[test_log::test]
601 fn test_labels() {
602 let metrics = PrometheusMetrics::default();
603
604 let http_count = metrics.counter_family("http".into(), vec!["method".into()]);
605 let get_count = http_count.create(vec!["GET".into()]);
606 let post_count = http_count.create(vec!["POST".into()]);
607 get_count.add(1);
608 post_count.add(2);
609
610 metrics
611 .text_family("version".into(), vec!["semver".into(), "rev".into()])
612 .create(vec!["0.1.0".into(), "d1b650a7".into()]);
613
614 assert_eq!(
615 metrics
616 .get_counter_family("http")
617 .unwrap()
618 .get(&["GET"])
619 .get(),
620 1
621 );
622 assert_eq!(
623 metrics
624 .get_counter_family("http")
625 .unwrap()
626 .get(&["POST"])
627 .get(),
628 2
629 );
630
631 let string = metrics.export().unwrap();
633 let lines = string.lines().collect::<Vec<_>>();
635 assert!(lines.contains(&"http{method=\"GET\"} 1"), "{lines:?}");
636 assert!(lines.contains(&"http{method=\"POST\"} 2"), "{lines:?}");
637 assert!(
638 lines.contains(&"version{rev=\"d1b650a7\",semver=\"0.1.0\"} 1"),
639 "{lines:?}"
640 );
641 }
642
643 #[test_log::test]
644 fn test_destroy() {
645 let metrics = PrometheusMetrics::default();
646
647 let counters = metrics.counter_family("requests".into(), vec!["peer".into()]);
648 counters.create(vec!["alice".into()]).add(1);
649 counters.create(vec!["bob".into()]).add(2);
650
651 let gauges = Metrics::gauge_family(&metrics, "queue".into(), vec!["peer".into()]);
652 gauges.create(vec!["alice".into()]).set(7);
653 gauges.create(vec!["bob".into()]).set(9);
654
655 let histograms = metrics.histogram_family("latency".into(), vec!["peer".into()]);
656 histograms.create(vec!["alice".into()]).add_point(1.0);
657 histograms.create(vec!["bob".into()]).add_point(2.0);
658
659 let texts = metrics.text_family("version".into(), vec!["peer".into()]);
660 texts.create(vec!["alice".into()]);
661 texts.create(vec!["bob".into()]);
662
663 let before = metrics.export().unwrap();
665 assert!(before.contains("requests{peer=\"alice\"} 1"), "{before}");
666 assert!(before.contains("requests{peer=\"bob\"} 2"), "{before}");
667 assert!(before.contains("queue{peer=\"alice\"} 7"), "{before}");
668 assert!(before.contains("queue{peer=\"bob\"} 9"), "{before}");
669 assert!(before.contains("latency_count{peer=\"alice\"}"), "{before}");
670 assert!(before.contains("latency_count{peer=\"bob\"}"), "{before}");
671 assert!(before.contains("version{peer=\"alice\"} 1"), "{before}");
672 assert!(before.contains("version{peer=\"bob\"} 1"), "{before}");
673
674 counters.destroy(&["alice"]);
676 gauges.destroy(&["alice"]);
677 histograms.destroy(&["alice"]);
678 texts.destroy(&["alice"]);
679
680 let after = metrics.export().unwrap();
682 assert!(!after.contains("peer=\"alice\""), "{after}");
683 assert!(after.contains("requests{peer=\"bob\"} 2"), "{after}");
684 assert!(after.contains("queue{peer=\"bob\"} 9"), "{after}");
685 assert!(after.contains("latency_count{peer=\"bob\"}"), "{after}");
686 assert!(after.contains("version{peer=\"bob\"} 1"), "{after}");
687
688 counters.destroy(&["nobody"]);
690 gauges.destroy(&["nobody"]);
691 histograms.destroy(&["nobody"]);
692 texts.destroy(&["nobody"]);
693 }
694}