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