tacho/
lib.rs

1//! A thread-safe, `Future`-aware metrics library.
2//!
3//! Many programs need to information about runtime performance: the number of requests
4//! served, a distribution of request latency, the number of failures, the number of loop
5//! iterations, etc. `tacho::new` creates a shareable, scopable metrics registry and a
6//! `Reporter`. The `Scope` supports the creation of `Counter`, `Gauge`, and `Stat`
7//! handles that may be used to report values. Each of these receivers maintains a weak
8//! reference back to the central stats registry.
9//!
10//! ## Performance
11//!
12//! Labels are stored in a `BTreeMap` because they are used as hash keys and, therefore,
13//! need to implement `Hash`.
14
15
16#![cfg_attr(test, feature(test))]
17
18extern crate futures;
19extern crate hdrsample;
20#[macro_use]
21extern crate log;
22extern crate ordermap;
23#[cfg(test)]
24extern crate test;
25
26use futures::{Future, Poll};
27use hdrsample::Histogram;
28use ordermap::OrderMap;
29use std::boxed::Box;
30use std::collections::BTreeMap;
31use std::fmt;
32use std::sync::{Arc, Mutex, Weak};
33use std::sync::atomic::{AtomicUsize, Ordering};
34use std::time::Instant;
35
36pub mod prometheus;
37mod report;
38mod timing;
39
40pub use report::{Reporter, Report};
41pub use timing::Timing;
42
43type Labels = BTreeMap<&'static str, String>;
44type CounterMap = OrderMap<Key, Arc<AtomicUsize>>;
45type GaugeMap = OrderMap<Key, Arc<AtomicUsize>>;
46type StatMap = OrderMap<Key, Arc<Mutex<HistogramWithSum>>>;
47
48#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
49pub enum Prefix {
50    Root,
51    Node {
52        prefix: Arc<Prefix>,
53        value: &'static str,
54    },
55}
56
57
58/// Creates a metrics registry.
59///
60/// The returned `Scope` may be you used to instantiate metrics. Labels may be attached to
61/// the scope so that all metrics created by this `Scope` are annotated.
62///
63/// The returned `Reporter` supports consumption of metrics values.
64pub fn new() -> (Scope, Reporter) {
65    let registry = Arc::new(Mutex::new(Registry::default()));
66
67    let scope = Scope {
68        labels: Labels::default(),
69        prefix: Arc::new(Prefix::Root),
70        registry: registry.clone(),
71    };
72
73    (scope, report::new(registry))
74}
75
76/// Describes a metric.
77#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
78pub struct Key {
79    name: &'static str,
80    prefix: Arc<Prefix>,
81    labels: Labels,
82}
83impl Key {
84    fn new(name: &'static str, prefix: Arc<Prefix>, labels: Labels) -> Key {
85        Key {
86            name,
87            prefix,
88            labels,
89        }
90    }
91
92    pub fn name(&self) -> &'static str {
93        self.name
94    }
95    pub fn prefix(&self) -> &Arc<Prefix> {
96        &self.prefix
97    }
98    pub fn labels(&self) -> &Labels {
99        &self.labels
100    }
101}
102
103#[derive(Default)]
104pub struct Registry {
105    counters: CounterMap,
106    gauges: GaugeMap,
107    stats: StatMap,
108}
109
110/// Supports creation of scoped metrics.
111///
112/// `Scope`s may be cloned without copying the underlying metrics registry.
113///
114/// Labels may be attached to the scope so that all metrics created by the `Scope` are
115/// labeled.
116#[derive(Clone)]
117pub struct Scope {
118    labels: Labels,
119    prefix: Arc<Prefix>,
120    registry: Arc<Mutex<Registry>>,
121}
122
123impl Scope {
124    /// Accesses scoping labels.
125    pub fn labels(&self) -> &Labels {
126        &self.labels
127    }
128
129    /// Adds a label into scope (potentially overwriting).
130    pub fn labeled<D: fmt::Display>(mut self, k: &'static str, v: D) -> Self {
131        self.labels.insert(k, format!("{}", v));
132        self
133    }
134
135    /// Appends a prefix to the current scope.
136    pub fn prefixed(mut self, value: &'static str) -> Self {
137        let p = Prefix::Node {
138            prefix: self.prefix,
139            value,
140        };
141        self.prefix = Arc::new(p);
142        self
143    }
144
145    /// Creates a Counter with the given name.
146    pub fn counter(&self, name: &'static str) -> Counter {
147        let key = Key::new(name, self.prefix.clone(), self.labels.clone());
148        let mut reg = self.registry.lock().expect(
149            "failed to obtain lock on registry",
150        );
151
152        if let Some(c) = reg.counters.get(&key) {
153            return Counter(Arc::downgrade(c));
154        }
155
156        let c = Arc::new(AtomicUsize::new(0));
157        let counter = Counter(Arc::downgrade(&c));
158        reg.counters.insert(key, c);
159        counter
160    }
161
162    /// Creates a Gauge with the given name.
163    pub fn gauge(&self, name: &'static str) -> Gauge {
164        let key = Key::new(name, self.prefix.clone(), self.labels.clone());
165        let mut reg = self.registry.lock().expect(
166            "failed to obtain lock on registry",
167        );
168
169        if let Some(g) = reg.gauges.get(&key) {
170            return Gauge(Arc::downgrade(g));
171        }
172
173        let g = Arc::new(AtomicUsize::new(0));
174        let gauge = Gauge(Arc::downgrade(&g));
175        reg.gauges.insert(key, g);
176        gauge
177    }
178
179    /// Creates a Stat with the given name.
180    ///
181    /// The underlying histogram is automatically resized as values are added.
182    pub fn stat(&self, name: &'static str) -> Stat {
183        let key = Key::new(name, self.prefix.clone(), self.labels.clone());
184        self.mk_stat(key, None)
185    }
186
187    pub fn timer_us(&self, name: &'static str) -> Timer {
188        Timer {
189            stat: self.stat(name),
190            unit: TimeUnit::Micros,
191        }
192    }
193
194    pub fn timer_ms(&self, name: &'static str) -> Timer {
195        Timer {
196            stat: self.stat(name),
197            unit: TimeUnit::Millis,
198        }
199    }
200
201    /// Creates a Stat with the given name and histogram paramters.
202    pub fn stat_with_bounds(&self, name: &'static str, low: u64, high: u64) -> Stat {
203        let key = Key::new(name, self.prefix.clone(), self.labels.clone());
204        self.mk_stat(key, Some((low, high)))
205    }
206
207    fn mk_stat(&self, key: Key, bounds: Option<(u64, u64)>) -> Stat {
208        let mut reg = self.registry.lock().expect(
209            "failed to obtain lock on registry",
210        );
211
212        if let Some(h) = reg.stats.get(&key) {
213            let histo = Arc::downgrade(h);
214            return Stat { histo, bounds };
215        }
216
217        let h = Arc::new(Mutex::new(HistogramWithSum::new(bounds)));
218        let histo = Arc::downgrade(&h);
219        reg.stats.insert(key, h);
220        Stat { histo, bounds }
221    }
222}
223
224/// Counts monotically.
225#[derive(Clone)]
226pub struct Counter(Weak<AtomicUsize>);
227impl Counter {
228    pub fn incr(&self, v: usize) {
229        if let Some(c) = self.0.upgrade() {
230            c.fetch_add(v, Ordering::AcqRel);
231        }
232    }
233}
234
235/// Captures an instantaneous value.
236#[derive(Clone)]
237pub struct Gauge(Weak<AtomicUsize>);
238impl Gauge {
239    pub fn incr(&self, v: usize) {
240        if let Some(g) = self.0.upgrade() {
241            g.fetch_add(v, Ordering::AcqRel);
242        } else {
243            debug!("gauge dropped");
244        }
245    }
246    pub fn decr(&self, v: usize) {
247        if let Some(g) = self.0.upgrade() {
248            g.fetch_sub(v, Ordering::AcqRel);
249        } else {
250            debug!("gauge dropped");
251        }
252    }
253    pub fn set(&self, v: usize) {
254        if let Some(g) = self.0.upgrade() {
255            g.store(v, Ordering::Release);
256        } else {
257            debug!("gauge dropped");
258        }
259    }
260}
261
262/// Histograms hold up to 4 significant figures.
263const HISTOGRAM_PRECISION: u32 = 4;
264
265/// Tracks a distribution of values with their sum.
266///
267/// `hdrsample::Histogram` does not track a sum by default; but prometheus expects a `sum`
268/// for histograms.
269#[derive(Clone)]
270pub struct HistogramWithSum {
271    histogram: Histogram<usize>,
272    sum: u64,
273}
274
275impl HistogramWithSum {
276    /// Constructs a new `HistogramWithSum`, possibly with bounds.
277    fn new(bounds: Option<(u64, u64)>) -> Self {
278        let h = match bounds {
279            None => Histogram::<usize>::new(HISTOGRAM_PRECISION),
280            Some((l, h)) => Histogram::<usize>::new_with_bounds(l, h, HISTOGRAM_PRECISION),
281        };
282        let histogram = h.expect("failed to create histogram");
283        HistogramWithSum { histogram, sum: 0 }
284    }
285
286    /// Record a value to
287    fn record(&mut self, v: u64) {
288        if let Err(e) = self.histogram.record(v) {
289            error!("failed to add value to histogram: {:?}", e);
290        }
291        if v >= ::std::u64::MAX - self.sum {
292            self.sum = ::std::u64::MAX
293        } else {
294            self.sum += v;
295        }
296    }
297
298    pub fn histogram(&self) -> &Histogram<usize> {
299        &self.histogram
300    }
301    pub fn count(&self) -> u64 {
302        self.histogram.count()
303    }
304    pub fn max(&self) -> u64 {
305        self.histogram.max()
306    }
307    pub fn min(&self) -> u64 {
308        self.histogram.min()
309    }
310    pub fn sum(&self) -> u64 {
311        self.sum
312    }
313
314    pub fn clear(&mut self) {
315        self.histogram.reset();
316        self.sum = 0;
317    }
318}
319
320/// Caputres a distribution of values.
321#[derive(Clone)]
322pub struct Stat {
323    histo: Weak<Mutex<HistogramWithSum>>,
324    bounds: Option<(u64, u64)>,
325}
326
327impl Stat {
328    pub fn add(&self, v: u64) {
329        if let Some(h) = self.histo.upgrade() {
330            let mut histo = h.lock().expect("failed to obtain lock for stat");
331            histo.record(v);
332        }
333    }
334
335    pub fn add_values(&mut self, vs: &[u64]) {
336        if let Some(h) = self.histo.upgrade() {
337            let mut histo = h.lock().expect("failed to obtain lock for stat");
338            for v in vs {
339                histo.record(*v)
340            }
341        }
342    }
343}
344
345#[derive(Clone)]
346pub struct Timer {
347    stat: Stat,
348    unit: TimeUnit,
349}
350#[derive(Copy, Clone)]
351pub enum TimeUnit {
352    Millis,
353    Micros,
354}
355impl Timer {
356    pub fn record_since(&self, t0: Instant) {
357        self.stat.add(to_u64(t0, self.unit));
358    }
359
360    pub fn time<F>(&self, fut: F) -> Timed<F>
361    where
362        F: Future + 'static,
363    {
364        let stat = self.stat.clone();
365        let unit = self.unit;
366        let f = futures::lazy(move || {
367            // Start timing once the future is actually being invoked (and not
368            // when the object is created).
369            let t0 = Timing::start();
370            fut.then(move |v| {
371                stat.add(to_u64(t0, unit));
372                v
373            })
374        });
375        Timed(Box::new(f))
376    }
377}
378
379fn to_u64(t0: Instant, unit: TimeUnit) -> u64 {
380    match unit {
381        TimeUnit::Millis => t0.elapsed_ms(),
382        TimeUnit::Micros => t0.elapsed_us(),
383    }
384}
385
386pub struct Timed<F: Future>(Box<Future<Item = F::Item, Error = F::Error>>);
387impl<F: Future> Future for Timed<F> {
388    type Item = F::Item;
389    type Error = F::Error;
390    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
391        self.0.poll()
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use test::Bencher;
399
400    static DEFAULT_METRIC_NAME: &'static str = "a_sufficiently_long_name";
401
402    #[bench]
403    fn bench_scope_clone(b: &mut Bencher) {
404        let (metrics, _) = super::new();
405        b.iter(move || { let _ = metrics.clone(); });
406    }
407
408    #[bench]
409    fn bench_scope_label(b: &mut Bencher) {
410        let (metrics, _) = super::new();
411        b.iter(move || { let _ = metrics.clone().labeled("foo", "bar"); });
412    }
413
414    #[bench]
415    fn bench_scope_clone_x1000(b: &mut Bencher) {
416        let scopes = mk_scopes(1000, "bench_scope_clone_x1000");
417        b.iter(move || for scope in &scopes {
418            let _ = scope.clone();
419        });
420    }
421
422    #[bench]
423    fn bench_scope_label_x1000(b: &mut Bencher) {
424        let scopes = mk_scopes(1000, "bench_scope_label_x1000");
425        b.iter(move || for scope in &scopes {
426            let _ = scope.clone().labeled("foo", "bar");
427        });
428    }
429
430    #[bench]
431    fn bench_counter_create(b: &mut Bencher) {
432        let (metrics, _) = super::new();
433        b.iter(move || { let _ = metrics.counter(DEFAULT_METRIC_NAME); });
434    }
435
436    #[bench]
437    fn bench_gauge_create(b: &mut Bencher) {
438        let (metrics, _) = super::new();
439        b.iter(move || { let _ = metrics.gauge(DEFAULT_METRIC_NAME); });
440    }
441
442    #[bench]
443    fn bench_stat_create(b: &mut Bencher) {
444        let (metrics, _) = super::new();
445        b.iter(move || { let _ = metrics.stat(DEFAULT_METRIC_NAME); });
446    }
447
448    #[bench]
449    fn bench_counter_create_x1000(b: &mut Bencher) {
450        let scopes = mk_scopes(1000, "bench_counter_create_x1000");
451        b.iter(move || for scope in &scopes {
452            scope.counter(DEFAULT_METRIC_NAME);
453        });
454    }
455
456    #[bench]
457    fn bench_gauge_create_x1000(b: &mut Bencher) {
458        let scopes = mk_scopes(1000, "bench_gauge_create_x1000");
459        b.iter(move || for scope in &scopes {
460            scope.gauge(DEFAULT_METRIC_NAME);
461        });
462    }
463
464    #[bench]
465    fn bench_stat_create_x1000(b: &mut Bencher) {
466        let scopes = mk_scopes(1000, "bench_stat_create_x1000");
467        b.iter(move || for scope in &scopes {
468            scope.stat(DEFAULT_METRIC_NAME);
469        });
470    }
471
472    #[bench]
473    fn bench_counter_update(b: &mut Bencher) {
474        let (metrics, _) = super::new();
475        let c = metrics.counter(DEFAULT_METRIC_NAME);
476        b.iter(move || c.incr(1));
477    }
478
479    #[bench]
480    fn bench_gauge_update(b: &mut Bencher) {
481        let (metrics, _) = super::new();
482        let g = metrics.gauge(DEFAULT_METRIC_NAME);
483        b.iter(move || g.set(1));
484    }
485
486    #[bench]
487    fn bench_stat_update(b: &mut Bencher) {
488        let (metrics, _) = super::new();
489        let s = metrics.stat(DEFAULT_METRIC_NAME);
490        b.iter(move || s.add(1));
491    }
492
493    #[bench]
494    fn bench_counter_update_x1000(b: &mut Bencher) {
495        let counters: Vec<Counter> = mk_scopes(1000, "bench_counter_update_x1000")
496            .iter()
497            .map(|s| s.counter(DEFAULT_METRIC_NAME))
498            .collect();
499        b.iter(move || for c in &counters {
500            c.incr(1)
501        });
502    }
503
504    #[bench]
505    fn bench_gauge_update_x1000(b: &mut Bencher) {
506        let gauges: Vec<Gauge> = mk_scopes(1000, "bench_gauge_update_x1000")
507            .iter()
508            .map(|s| s.gauge(DEFAULT_METRIC_NAME))
509            .collect();
510        b.iter(move || for g in &gauges {
511            g.set(1)
512        });
513    }
514
515    #[bench]
516    fn bench_stat_update_x1000(b: &mut Bencher) {
517        let stats: Vec<Stat> = mk_scopes(1000, "bench_stat_update_x1000")
518            .iter()
519            .map(|s| s.stat(DEFAULT_METRIC_NAME))
520            .collect();
521        b.iter(move || for s in &stats {
522            s.add(1)
523        });
524    }
525
526    #[bench]
527    fn bench_stat_add_x1000(b: &mut Bencher) {
528        let s = {
529            let (metrics, _) = super::new();
530            metrics.stat(DEFAULT_METRIC_NAME)
531        };
532        b.iter(move || for i in 0..1000 {
533            s.add(i)
534        });
535    }
536
537    fn mk_scopes(n: usize, name: &str) -> Vec<Scope> {
538        let (metrics, _) = super::new();
539        let metrics = metrics.prefixed("t").labeled("test_name", name).labeled(
540            "total_iterations",
541            n,
542        );
543        (0..n)
544            .map(|i| metrics.clone().labeled("iteration", format!("{}", i)))
545            .collect()
546    }
547
548    #[test]
549    fn test_report_peek() {
550        let (metrics, reporter) = super::new();
551        let metrics = metrics.labeled("joy", "painting");
552
553        let happy_accidents = metrics.counter("happy_accidents");
554        let paint_level = metrics.gauge("paint_level");
555        let mut stroke_len = metrics.stat("stroke_len");
556
557        happy_accidents.incr(1);
558        paint_level.set(2);
559        stroke_len.add_values(&[1, 2, 3]);
560
561        {
562            let report = reporter.peek();
563            {
564                let k = report
565                    .counters()
566                    .keys()
567                    .find(|k| k.name() == "happy_accidents")
568                    .expect("expected counter: happy_accidents");
569                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
570                assert_eq!(report.counters().get(&k), Some(&1));
571            }
572            {
573                let k = report
574                    .gauges()
575                    .keys()
576                    .find(|k| k.name() == "paint_level")
577                    .expect("expected gauge: paint_level");
578                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
579                assert_eq!(report.gauges().get(&k), Some(&2));
580            }
581            assert_eq!(
582                report.gauges().keys().find(|k| k.name() == "brush_width"),
583                None
584            );
585            {
586                let k = report
587                    .stats()
588                    .keys()
589                    .find(|k| k.name() == "stroke_len")
590                    .expect("expected stat: stroke_len");
591                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
592                assert!(report.stats().contains_key(&k));
593            }
594            assert_eq!(report.stats().keys().find(|k| k.name() == "tree_len"), None);
595        }
596
597        drop(paint_level);
598        let brush_width = metrics.gauge("brush_width");
599        let mut tree_len = metrics.stat("tree_len");
600
601        happy_accidents.incr(2);
602        brush_width.set(5);
603        stroke_len.add_values(&[1, 2, 3]);
604        tree_len.add_values(&[3, 4, 5]);
605
606        {
607            let report = reporter.peek();
608            {
609                let k = report
610                    .counters()
611                    .keys()
612                    .find(|k| k.name() == "happy_accidents")
613                    .expect("expected counter: happy_accidents");
614                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
615                assert_eq!(report.counters().get(&k), Some(&3));
616            }
617            {
618                let k = report
619                    .gauges()
620                    .keys()
621                    .find(|k| k.name() == "paint_level")
622                    .expect("expected gauge: paint_level");
623                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
624                assert_eq!(report.gauges().get(&k), Some(&2));
625            }
626            {
627                let k = report
628                    .gauges()
629                    .keys()
630                    .find(|k| k.name() == "brush_width")
631                    .expect("expected gauge: brush_width");
632                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
633                assert_eq!(report.gauges().get(&k), Some(&5));
634            }
635            {
636                let k = report
637                    .stats()
638                    .keys()
639                    .find(|k| k.name() == "stroke_len")
640                    .expect("expected stat: stroke_len");
641                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
642                assert!(report.stats().contains_key(&k));
643            }
644            {
645                let k = report
646                    .stats()
647                    .keys()
648                    .find(|k| k.name() == "tree_len")
649                    .expect("expected stat: tree_len");
650                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
651                assert!(report.stats().contains_key(&k));
652            }
653        }
654    }
655
656    #[test]
657    fn test_report_take() {
658        let (metrics, mut reporter) = super::new();
659        let metrics = metrics.labeled("joy", "painting");
660
661        let happy_accidents = metrics.counter("happy_accidents");
662        let paint_level = metrics.gauge("paint_level");
663        let mut stroke_len = metrics.stat("stroke_len");
664        happy_accidents.incr(1);
665        paint_level.set(2);
666        stroke_len.add_values(&[1, 2, 3]);
667        {
668            let report = reporter.take();
669            {
670                let k = report
671                    .counters()
672                    .keys()
673                    .find(|k| k.name() == "happy_accidents")
674                    .expect("expected counter: happy_accidents");
675                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
676                assert_eq!(report.counters().get(&k), Some(&1));
677            }
678            {
679                let k = report
680                    .gauges()
681                    .keys()
682                    .find(|k| k.name() == "paint_level")
683                    .expect("expected gauge");
684                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
685                assert_eq!(report.gauges().get(&k), Some(&2));
686            }
687            assert_eq!(
688                report.gauges().keys().find(|k| k.name() == "brush_width"),
689                None
690            );
691            {
692                let k = report
693                    .stats()
694                    .keys()
695                    .find(|k| k.name() == "stroke_len")
696                    .expect("expected stat");
697                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
698                assert!(report.stats().contains_key(&k));
699            }
700            assert_eq!(report.stats().keys().find(|k| k.name() == "tree_len"), None);
701            {
702                let k = report
703                    .stats()
704                    .keys()
705                    .find(|k| k.name() == "stroke_len")
706                    .expect("expected stat");
707                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
708                assert!(report.stats().contains_key(&k));
709            }
710        }
711
712        drop(paint_level);
713        drop(stroke_len);
714        {
715            let report = reporter.take();
716            {
717                let counters = report.counters();
718                let k = counters
719                    .keys()
720                    .find(|k| k.name() == "happy_accidents")
721                    .expect("expected counter: happy_accidents");
722                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
723                assert_eq!(counters.get(&k), Some(&1));
724            }
725            {
726                let k = report
727                    .gauges()
728                    .keys()
729                    .find(|k| k.name() == "paint_level")
730                    .expect("expected gauge");
731                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
732                assert_eq!(report.gauges().get(&k), Some(&2));
733            }
734            {
735                let k = report
736                    .stats()
737                    .keys()
738                    .find(|k| k.name() == "stroke_len")
739                    .expect("expected stat");
740                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
741                assert!(report.stats().contains_key(&k));
742            }
743        }
744
745        let brush_width = metrics.gauge("brush_width");
746        let mut tree_len = metrics.stat("tree_len");
747        happy_accidents.incr(2);
748        brush_width.set(5);
749        tree_len.add_values(&[3, 4, 5]);
750        {
751            let report = reporter.take();
752            {
753                let k = report
754                    .counters()
755                    .keys()
756                    .find(|k| k.name() == "happy_accidents")
757                    .expect("expected counter: happy_accidents");
758                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
759                assert_eq!(report.counters().get(&k), Some(&3));
760            }
761            assert_eq!(
762                report.gauges().keys().find(|k| k.name() == "paint_level"),
763                None
764            );
765            {
766                let k = report
767                    .gauges()
768                    .keys()
769                    .find(|k| k.name() == "brush_width")
770                    .expect("expected gauge");
771                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
772                assert_eq!(report.gauges().get(&k), Some(&5));
773            }
774            assert_eq!(
775                report.stats().keys().find(|k| k.name() == "stroke_len"),
776                None
777            );
778            {
779                let k = report
780                    .stats()
781                    .keys()
782                    .find(|k| k.name() == "tree_len")
783                    .expect("expeced stat");
784                assert_eq!(k.labels.get("joy"), Some(&"painting".to_string()));
785                assert!(report.stats().contains_key(&k));
786            }
787        }
788    }
789}