Skip to main content

hotshot_query_service/
metrics.rs

1#![allow(dead_code)]
2
3// Copyright (c) 2022 Espresso Systems (espressosys.com)
4// This file is part of the HotShot Query Service library.
5//
6// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
7// General Public License as published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
10// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11// General Public License for more details.
12// You should have received a copy of the GNU General Public License along with this program. If not,
13// see <https://www.gnu.org/licenses/>.
14
15use 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/// A Prometheus-based implementation of a [Metrics](metrics::Metrics) registry.
50///
51/// [PrometheusMetrics] provides a collection of metrics including [Counter], [Gauge], and
52/// [Histogram]. These metrics can be created and associated with a [PrometheusMetrics] collection
53/// and then used as handles for updating and populating. The [PrometheusMetrics] registry can then
54/// be used to collect all of the associated metrics and export them in the Prometheus text format.
55///
56/// This implementation provides a few features beyond the basic [prometheus] features. It supports
57/// hierarchical namespaces; any [PrometheusMetrics] can be used to derive a subgroup with a certain
58/// name. The subgroup is then related to the parent, and any [PrometheusMetrics] in the tree of
59/// related groups can be used to collect _all_ registered metrics. The namespacing will be
60/// reflected in the fully qualified name of each metric in the Prometheus output. The subgroup
61/// relationship is pure and deterministic -- calling
62/// [get_subgroup](PrometheusMetrics::get_subgroup) with the same subgroup name will always return a
63/// handle to the same underlying [PrometheusMetrics] object.
64///
65/// [PrometheusMetrics] also supports querying for individual metrics by name, unlike
66/// [prometheus::Registry]. This provides a programming interface for inspecting the values of
67/// specific metrics at run-time, if that is preferable to exporting all metrics wholesale.
68#[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    /// Get a counter in this sub-group by name.
83    pub fn get_counter(&self, name: &str) -> Result<Counter, MetricsError> {
84        self.get_metric(&self.counters, name)
85    }
86
87    /// Get a gauge in this sub-group by name.
88    pub fn get_gauge(&self, name: &str) -> Result<Gauge, MetricsError> {
89        self.get_metric(&self.gauges, name)
90    }
91
92    /// Get a histogram in this sub-group by name.
93    pub fn get_histogram(&self, name: &str) -> Result<Histogram, MetricsError> {
94        self.get_metric(&self.histograms, name)
95    }
96
97    /// Get a counter family in this sub-group by name.
98    pub fn get_counter_family(&self, name: &str) -> Result<CounterFamily, MetricsError> {
99        self.get_metric(&self.counter_families, name)
100    }
101
102    /// Get a gauge family in this sub-group by name.
103    pub fn gauge_family(&self, name: &str) -> Result<GaugeFamily, MetricsError> {
104        self.get_metric(&self.gauge_families, name)
105    }
106
107    /// Get a histogram family in this sub-group by name.
108    pub fn get_histogram_family(&self, name: &str) -> Result<HistogramFamily, MetricsError> {
109        self.get_metric(&self.histogram_families, name)
110    }
111
112    /// Get a (possibly nested) subgroup of this group by its path.
113    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/// A [Counter](metrics::Counter) metric.
279#[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/// A [Gauge](metrics::Gauge) metric.
301#[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/// A [Histogram](metrics::Histogram) metric.
331#[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/// A [CounterFamily](metrics::CounterFamily) metric.
361#[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/// A [GaugeFamily](metrics::GaugeFamily) metric.
391#[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/// A [HistogramFamily](metrics::HistogramFamily) metric.
421#[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/// A [TextFamily](metrics::TextFamily) metric.
451#[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        // Register one metric of each type.
482        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        // Set the metric values.
488        counter.add(20);
489        gauge.set(42);
490        histogram.add_point(20f64);
491
492        // Check the values.
493        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        // Set the metric values again, to be sure they update properly.
503        counter.add(22);
504        gauge.set(100);
505        histogram.add_point(22f64);
506
507        // Check the updated values.
508        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        // Export to a Prometheus string.
518        let string = metrics.export().unwrap();
519        // Make sure the output makes sense.
520        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        // Check namespacing.
538        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        // Check different ways of accessing the counter.
560        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        // Check fully-qualified counter name in export.
582        assert!(
583            metrics
584                .export()
585                .unwrap()
586                .lines()
587                .contains(&"subgroup1_subgroup2_counter 42")
588        );
589
590        // Check fully-qualified text name in export.
591        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        // Export to a Prometheus string.
632        let string = metrics.export().unwrap();
633        // Make sure the output makes sense.
634        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        // Before destroy: both peers are present in the export.
664        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        // Destroy alice from every family.
675        counters.destroy(&["alice"]);
676        gauges.destroy(&["alice"]);
677        histograms.destroy(&["alice"]);
678        texts.destroy(&["alice"]);
679
680        // After destroy: alice is gone, bob is untouched.
681        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        // Destroying a non-existent variant is a no-op (just logs a warning).
689        counters.destroy(&["nobody"]);
690        gauges.destroy(&["nobody"]);
691        histograms.destroy(&["nobody"]);
692        texts.destroy(&["nobody"]);
693    }
694}