Skip to main content

mz_ore/
metrics.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Metrics for materialize systems.
17//!
18//! The idea here is that each subsystem keeps its metrics in a scoped-to-it struct, which gets
19//! registered (once) to the server's (or a test's) prometheus registry.
20//!
21//! Instead of using prometheus's (very verbose) metrics definitions, we rely on type inference to
22//! reduce the verbosity a little bit. A typical subsystem will look like the following:
23//!
24//! ```rust
25//! # use mz_ore::metrics::{MetricsRegistry, IntCounter};
26//! # use mz_ore::metric;
27//! #[derive(Debug, Clone)] // Note that prometheus metrics can safely be cloned
28//! struct Metrics {
29//!     pub bytes_sent: IntCounter,
30//! }
31//!
32//! impl Metrics {
33//!     pub fn register_into(registry: &MetricsRegistry) -> Metrics {
34//!         Metrics {
35//!             bytes_sent: registry.register(metric!(
36//!                 name: "mz_pg_sent_bytes",
37//!                 help: "total number of bytes sent here",
38//!             )),
39//!         }
40//!     }
41//! }
42//! ```
43
44use std::any::Any;
45use std::collections::BTreeMap;
46use std::fmt;
47use std::fmt::{Debug, Formatter};
48use std::future::Future;
49use std::pin::Pin;
50use std::sync::{Arc, Mutex};
51use std::task::{Context, Poll};
52use std::time::{Duration, Instant};
53
54use derivative::Derivative;
55use pin_project::pin_project;
56use prometheus::core::{
57    Atomic, AtomicF64, AtomicI64, AtomicU64, Collector, Desc, GenericCounter, GenericCounterVec,
58    GenericGauge, GenericGaugeVec,
59};
60use prometheus::proto::MetricFamily;
61use prometheus::{HistogramOpts, Registry};
62
63pub mod aggregation;
64mod delete_on_drop;
65mod deregister_on_drop;
66
67pub use delete_on_drop::*;
68pub use deregister_on_drop::*;
69pub use prometheus::Opts as PrometheusOpts;
70
71/// Define a metric for use in materialize.
72#[macro_export]
73macro_rules! metric {
74    (
75        name: $name:expr,
76        help: $help:expr
77        $(, subsystem: $subsystem_name:expr)?
78        $(, const_labels: { $($cl_key:expr => $cl_value:expr ),* })?
79        $(, var_labels: [ $($vl_name:expr),* ])?
80        $(, buckets: $bk_name:expr)?
81        $(, visibility: $visibility:expr)?
82        $(, tags: [ $($tag:expr),* $(,)? ])?
83        $(,)?
84    ) => {{
85        let const_labels = (&[
86            $($(
87                ($cl_key.to_string(), $cl_value.to_string()),
88            )*)?
89        ]).into_iter().cloned().collect();
90        let var_labels = vec![
91            $(
92                $($vl_name.into(),)*
93            )?];
94        #[allow(unused_mut)]
95        let mut mk_opts = $crate::metrics::MakeCollectorOpts {
96            opts: $crate::metrics::PrometheusOpts::new($name, $help)
97                $(.subsystem( $subsystem_name ))?
98                .const_labels(const_labels)
99                .variable_labels(var_labels),
100            buckets: None,
101        };
102        // Set buckets if passed
103        $(mk_opts.buckets = Some($bk_name);)*
104        // `visibility` is documentation metadata for the metrics catalog
105        // (`bin/gen-metrics-catalog`).
106        // It has no runtime effect; we only type-check it here so a bad value is
107        // a compile error rather than silently ignored.
108        $(let _: $crate::metrics::MetricVisibility = $visibility;)?
109        // `tags` is documentation metadata for the metrics catalog.
110        // It has no runtime effect.
111        $($(let _: $crate::metrics::MetricTag = $tag;)*)?
112        mk_opts
113    }}
114}
115
116/// Options for MakeCollector. This struct should be instantiated using the metric macro.
117#[derive(Debug, Clone)]
118pub struct MakeCollectorOpts {
119    /// Common Prometheus options
120    pub opts: PrometheusOpts,
121    /// Buckets to be used with Histogram and HistogramVec. Must be set to create Histogram types
122    /// and must not be set for other types.
123    pub buckets: Option<Vec<f64>>,
124}
125
126/// This is documentation metadata: it is set via the optional `visibility:`
127/// field of [`metric!`] and consumed by the metrics catalog
128/// (`bin/gen-metrics-catalog`), which reads it from the source tree to produce
129/// the user-facing metrics reference. It has no effect on the metric at
130/// runtime.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize)]
132#[serde(rename_all = "snake_case")]
133pub enum MetricVisibility {
134    /// A metric we use for internal development.
135    #[default]
136    Internal,
137    /// A metric we want customers to build dashboards and
138    /// alerts on. We do not guarantee stability for this group
139    /// of metrics.
140    Public,
141}
142
143/// A tag categorizing a metric in the user-facing metrics catalog.
144///
145/// It is set via the optional `tags:` field of [`metric!`] and
146/// consumed by the metrics catalog (`bin/gen-metrics-catalog`).
147/// It has no effect on the metric at runtime.
148///
149/// A metric may carry many tags, or none (the default). A tag names the
150/// grouping the metric is presented under in user-facing documentation.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
152#[serde(rename_all = "kebab-case")]
153pub enum MetricTag {
154    /// SQL Control plane metrics (client connections, availability, catalog).
155    Environment,
156    /// Metrics for compute objects (indexes, materialized views).
157    Compute,
158    /// Metrics for sources.
159    Source,
160    /// Metrics for sinks.
161    Sink,
162}
163
164/// The materialize metrics registry.
165#[derive(Clone, Derivative)]
166#[derivative(Debug)]
167pub struct MetricsRegistry {
168    inner: Registry,
169    #[derivative(Debug = "ignore")]
170    postprocessors: Arc<Mutex<Vec<Box<dyn FnMut(&mut Vec<MetricFamily>) + Send + Sync>>>>,
171}
172
173/// A wrapper for metrics to require delete on drop semantics
174///
175/// The wrapper behaves like regular metrics but only provides functions to create delete-on-drop
176/// variants. This way, no metrics of this type can be leaked.
177///
178/// In situations where the delete-on-drop behavior is not desired or in legacy code, use the raw
179/// variants of the metrics, as defined in [self::raw].
180#[derive(Clone)]
181pub struct DeleteOnDropWrapper<M> {
182    inner: M,
183}
184
185impl<M: MakeCollector + Debug> Debug for DeleteOnDropWrapper<M> {
186    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187        self.inner.fmt(f)
188    }
189}
190
191impl<M: Collector> Collector for DeleteOnDropWrapper<M> {
192    fn desc(&self) -> Vec<&Desc> {
193        self.inner.desc()
194    }
195
196    fn collect(&self) -> Vec<MetricFamily> {
197        self.inner.collect()
198    }
199}
200
201impl<M: MakeCollector> MakeCollector for DeleteOnDropWrapper<M> {
202    fn make_collector(opts: MakeCollectorOpts) -> Self {
203        DeleteOnDropWrapper {
204            inner: M::make_collector(opts),
205        }
206    }
207}
208
209impl<M: MetricVecExt> DeleteOnDropWrapper<M> {
210    /// Returns a metric that deletes its labels from this metrics vector when dropped.
211    pub fn get_delete_on_drop_metric<L: PromLabelsExt>(
212        &self,
213        labels: L,
214    ) -> DeleteOnDropMetric<M, L> {
215        self.inner.get_delete_on_drop_metric(labels)
216    }
217}
218
219/// The unsigned integer version of [`Gauge`]. Provides better performance if
220/// metric values are all unsigned integers.
221pub type UIntGauge = GenericGauge<AtomicU64>;
222
223/// Delete-on-drop shadow of Prometheus [prometheus::CounterVec].
224pub type CounterVec = DeleteOnDropWrapper<prometheus::CounterVec>;
225/// Delete-on-drop shadow of Prometheus [prometheus::Gauge].
226pub type Gauge = DeleteOnDropWrapper<prometheus::Gauge>;
227/// Delete-on-drop shadow of Prometheus [prometheus::GaugeVec].
228pub type GaugeVec = DeleteOnDropWrapper<prometheus::GaugeVec>;
229/// Delete-on-drop shadow of Prometheus [prometheus::HistogramVec].
230pub type HistogramVec = DeleteOnDropWrapper<prometheus::HistogramVec>;
231/// Delete-on-drop shadow of Prometheus [prometheus::IntCounterVec].
232pub type IntCounterVec = DeleteOnDropWrapper<prometheus::IntCounterVec>;
233/// Delete-on-drop shadow of Prometheus [prometheus::IntGaugeVec].
234pub type IntGaugeVec = DeleteOnDropWrapper<prometheus::IntGaugeVec>;
235/// Delete-on-drop shadow of Prometheus [raw::UIntGaugeVec].
236pub type UIntGaugeVec = DeleteOnDropWrapper<raw::UIntGaugeVec>;
237
238use crate::assert_none;
239
240pub use prometheus::{Counter, Histogram, IntCounter, IntGauge};
241
242/// Access to non-delete-on-drop vector types
243pub mod raw {
244    use prometheus::core::{AtomicU64, GenericGaugeVec};
245
246    /// The unsigned integer version of [`GaugeVec`].
247    /// Provides better performance if metric values are all unsigned integers.
248    pub type UIntGaugeVec = GenericGaugeVec<AtomicU64>;
249
250    pub use prometheus::{CounterVec, Gauge, GaugeVec, HistogramVec, IntCounterVec, IntGaugeVec};
251}
252
253impl MetricsRegistry {
254    /// Creates a new metrics registry.
255    pub fn new() -> Self {
256        MetricsRegistry {
257            inner: Registry::new(),
258            postprocessors: Arc::new(Mutex::new(vec![])),
259        }
260    }
261
262    /// Register a metric defined with the [`metric`] macro.
263    pub fn register<M>(&self, opts: MakeCollectorOpts) -> M
264    where
265        M: MakeCollector,
266    {
267        let collector = M::make_collector(opts);
268        self.inner.register(Box::new(collector.clone())).unwrap();
269        collector
270    }
271
272    /// Registers a gauge whose value is computed when observed.
273    pub fn register_computed_gauge<P>(
274        &self,
275        opts: MakeCollectorOpts,
276        f: impl Fn() -> P::T + Send + Sync + 'static,
277    ) -> ComputedGenericGauge<P>
278    where
279        P: Atomic + 'static,
280    {
281        let gauge = ComputedGenericGauge {
282            gauge: GenericGauge::make_collector(opts),
283            f: Arc::new(f),
284        };
285        self.inner.register(Box::new(gauge.clone())).unwrap();
286        gauge
287    }
288
289    /// Register a pre-defined prometheus collector.
290    pub fn register_collector<C: 'static + prometheus::core::Collector>(&self, collector: C) {
291        self.inner
292            .register(Box::new(collector))
293            .expect("registering pre-defined metrics collector");
294    }
295
296    /// Register a pre-defined collector and return a handle that unregisters it on drop.
297    ///
298    /// Use this for collectors with a bounded lifetime (e.g. a metric sink that is torn down with
299    /// its dataflow), so the collector's series stop being scraped once the owner drops the handle.
300    /// The returned value is opaque: the caller only needs to hold it for as long as the collector
301    /// should stay registered, then drop it.
302    ///
303    /// `prometheus::Registry::unregister` matches a collector by the id of its `Desc`s, not by
304    /// object identity, so the guard keeps a clone of the collector and hands it back to
305    /// `unregister` on drop.
306    ///
307    /// If a collector with the same descriptor id is already registered this soft-panics and
308    /// returns a handle that owns no registration. A duplicate registration is a logic error, but
309    /// panicking here would run on a worker thread and could crash the process, so it degrades to
310    /// missing series rather than taking down the whole scrape. The contract is that a caller
311    /// replacing a collector must drop the old handle before registering the new one: doing so
312    /// unregisters the old descriptor id first, so the re-registration succeeds cleanly.
313    ///
314    /// A caller that expects such a collision should use
315    /// [`MetricsRegistry::try_register_collector_with_dropper`] instead.
316    pub fn register_collector_with_dropper<C>(&self, collector: C) -> Box<dyn Any + Send + Sync>
317    where
318        C: 'static + prometheus::core::Collector + Clone + Send + Sync,
319    {
320        self.try_register_collector_with_dropper(collector)
321            .unwrap_or_else(|e| {
322                crate::soft_panic_or_log!("collector already registered: {e}");
323                // Nothing was registered, so the handle must not unregister anything on drop.
324                Box::new(())
325            })
326    }
327
328    /// Like [`MetricsRegistry::register_collector_with_dropper`], but returns `Err` on a duplicate
329    /// descriptor id instead of soft-panicking.
330    ///
331    /// For callers that expect a transient collision, where a collector is re-registered before
332    /// its predecessor's teardown has dropped the old handle, and want to retry until the id frees
333    /// up. Nothing is registered on `Err`, so the caller may retry with the same collector.
334    pub fn try_register_collector_with_dropper<C>(
335        &self,
336        collector: C,
337    ) -> Result<Box<dyn Any + Send + Sync>, prometheus::Error>
338    where
339        C: 'static + prometheus::core::Collector + Clone + Send + Sync,
340    {
341        self.inner.register(Box::new(collector.clone()))?;
342        // `prometheus::Registry` is `Arc`-backed, so this clone is cheap and shares the same
343        // underlying registry the collector was registered into.
344        let registry = self.inner.clone();
345        Ok(Box::new(scopeguard::guard(collector, move |c| {
346            let _ = registry.unregister(Box::new(c));
347        })))
348    }
349
350    /// Registers a metric postprocessor.
351    ///
352    /// Postprocessors are invoked on every call to [`MetricsRegistry::gather`]
353    /// in the order that they are registered.
354    pub fn register_postprocessor<F>(&self, f: F)
355    where
356        F: FnMut(&mut Vec<MetricFamily>) + Send + Sync + 'static,
357    {
358        let mut postprocessors = self.postprocessors.lock().expect("lock poisoned");
359        postprocessors.push(Box::new(f));
360    }
361
362    /// Gather all the metrics from the metrics registry for reporting.
363    ///
364    /// This function invokes the postprocessors on all gathered metrics (see
365    /// [`MetricsRegistry::register_postprocessor`]) in the order the
366    /// postprocessors were registered.
367    ///
368    /// See also [`prometheus::Registry::gather`].
369    pub fn gather(&self) -> Vec<MetricFamily> {
370        let mut metrics = self.inner.gather();
371        let mut postprocessors = self.postprocessors.lock().expect("lock poisoned");
372        for postprocessor in &mut *postprocessors {
373            postprocessor(&mut metrics);
374        }
375        metrics
376    }
377}
378
379/// A wrapper for creating prometheus metrics more conveniently.
380///
381/// Together with the [`metric`] macro, this trait is mainly used by [`MetricsRegistry`] and should
382/// not normally be used outside the metric registration flow.
383pub trait MakeCollector: Collector + Clone + 'static {
384    /// Creates a new collector.
385    fn make_collector(opts: MakeCollectorOpts) -> Self;
386}
387
388impl<T> MakeCollector for GenericCounter<T>
389where
390    T: Atomic + 'static,
391{
392    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
393        assert_none!(mk_opts.buckets);
394        Self::with_opts(mk_opts.opts).expect("defining a counter")
395    }
396}
397
398impl<T> MakeCollector for GenericCounterVec<T>
399where
400    T: Atomic + 'static,
401{
402    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
403        assert_none!(mk_opts.buckets);
404        let labels: Vec<String> = mk_opts.opts.variable_labels.clone();
405        let label_refs: Vec<&str> = labels.iter().map(String::as_str).collect();
406        Self::new(mk_opts.opts, label_refs.as_slice()).expect("defining a counter vec")
407    }
408}
409
410impl<T> MakeCollector for GenericGauge<T>
411where
412    T: Atomic + 'static,
413{
414    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
415        assert_none!(mk_opts.buckets);
416        Self::with_opts(mk_opts.opts).expect("defining a gauge")
417    }
418}
419
420impl<T> MakeCollector for GenericGaugeVec<T>
421where
422    T: Atomic + 'static,
423{
424    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
425        assert_none!(mk_opts.buckets);
426        let labels = mk_opts.opts.variable_labels.clone();
427        let labels = &labels.iter().map(|x| x.as_str()).collect::<Vec<_>>();
428        Self::new(mk_opts.opts, labels).expect("defining a gauge vec")
429    }
430}
431
432impl MakeCollector for Histogram {
433    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
434        assert!(mk_opts.buckets.is_some());
435        Self::with_opts(HistogramOpts {
436            common_opts: mk_opts.opts,
437            buckets: mk_opts.buckets.unwrap(),
438        })
439        .expect("defining a histogram")
440    }
441}
442
443impl MakeCollector for raw::HistogramVec {
444    fn make_collector(mk_opts: MakeCollectorOpts) -> Self {
445        assert!(mk_opts.buckets.is_some());
446        let labels = mk_opts.opts.variable_labels.clone();
447        let labels = &labels.iter().map(|x| x.as_str()).collect::<Vec<_>>();
448        Self::new(
449            HistogramOpts {
450                common_opts: mk_opts.opts,
451                buckets: mk_opts.buckets.unwrap(),
452            },
453            labels,
454        )
455        .expect("defining a histogram vec")
456    }
457}
458
459/// A [`Gauge`] whose value is computed whenever it is observed.
460pub struct ComputedGenericGauge<P>
461where
462    P: Atomic,
463{
464    gauge: GenericGauge<P>,
465    f: Arc<dyn Fn() -> P::T + Send + Sync>,
466}
467
468impl<P> fmt::Debug for ComputedGenericGauge<P>
469where
470    P: Atomic + fmt::Debug,
471{
472    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
473        f.debug_struct("ComputedGenericGauge")
474            .field("gauge", &self.gauge)
475            .finish_non_exhaustive()
476    }
477}
478
479impl<P> Clone for ComputedGenericGauge<P>
480where
481    P: Atomic,
482{
483    fn clone(&self) -> ComputedGenericGauge<P> {
484        ComputedGenericGauge {
485            gauge: self.gauge.clone(),
486            f: Arc::clone(&self.f),
487        }
488    }
489}
490
491impl<T> Collector for ComputedGenericGauge<T>
492where
493    T: Atomic,
494{
495    fn desc(&self) -> Vec<&prometheus::core::Desc> {
496        self.gauge.desc()
497    }
498
499    fn collect(&self) -> Vec<MetricFamily> {
500        self.gauge.set((self.f)());
501        self.gauge.collect()
502    }
503}
504
505impl<P> ComputedGenericGauge<P>
506where
507    P: Atomic,
508{
509    /// Computes the current value of the gauge.
510    pub fn get(&self) -> P::T {
511        (self.f)()
512    }
513}
514
515/// A [`ComputedGenericGauge`] for 64-bit floating point numbers.
516pub type ComputedGauge = ComputedGenericGauge<AtomicF64>;
517
518/// A [`ComputedGenericGauge`] for 64-bit signed integers.
519pub type ComputedIntGauge = ComputedGenericGauge<AtomicI64>;
520
521/// A [`ComputedGenericGauge`] for 64-bit unsigned integers.
522pub type ComputedUIntGauge = ComputedGenericGauge<AtomicU64>;
523
524/// Exposes combinators that report metrics related to the execution of a [`Future`] to prometheus.
525pub trait MetricsFutureExt<F> {
526    /// Records the number of seconds it takes a [`Future`] to complete according to "the clock on
527    /// the wall".
528    ///
529    /// More specifically, it records the instant at which the `Future` was first polled, and the
530    /// instant at which the `Future` completes. Then reports the duration between those two
531    /// instances to the provided metric.
532    ///
533    /// # Wall Time vs Execution Time
534    ///
535    /// There is also [`MetricsFutureExt::exec_time`], which measures how long a [`Future`] spent
536    /// executing, instead of how long it took to complete. For example, a network request may have
537    /// a wall time of 1 second, meanwhile it's execution time may have only been 50ms. The 950ms
538    /// delta would be how long the [`Future`] waited for a response from the network.
539    ///
540    /// # Uses
541    ///
542    /// Recording the wall time can be useful for monitoring latency, for example the latency of a
543    /// SQL request.
544    ///
545    /// Note: You must call either [`observe`] to record the execution time to a [`Histogram`] or
546    /// [`inc_by`] to record to a [`Counter`]. The following will not compile:
547    ///
548    /// ```compile_fail
549    /// use mz_ore::metrics::MetricsFutureExt;
550    ///
551    /// # let _ = async {
552    /// async { Ok(()) }
553    ///     .wall_time()
554    ///     .await;
555    /// # };
556    /// ```
557    ///
558    /// [`observe`]: WallTimeFuture::observe
559    /// [`inc_by`]: WallTimeFuture::inc_by
560    fn wall_time(self) -> WallTimeFuture<F, UnspecifiedMetric>;
561
562    /// Records the total number of seconds for which a [`Future`] was executing.
563    ///
564    /// More specifically, every time the `Future` is polled it records how long that individual
565    /// call took, and maintains a running sum until the `Future` completes. Then we report that
566    /// duration to the provided metric.
567    ///
568    /// # Wall Time vs Execution Time
569    ///
570    /// There is also [`MetricsFutureExt::wall_time`], which measures how long a [`Future`] took to
571    /// complete, instead of how long it spent executing. For example, a network request may have
572    /// a wall time of 1 second, meanwhile it's execution time may have only been 50ms. The 950ms
573    /// delta would be how long the [`Future`] waited for a response from the network.
574    ///
575    /// # Uses
576    ///
577    /// Recording execution time can be useful if you want to monitor [`Future`]s that could be
578    /// sensitive to CPU usage. For example, if you have a single logical control thread you'll
579    /// want to make sure that thread never spends too long running a single `Future`. Reporting
580    /// the execution time of `Future`s running on this thread can help ensure there is no
581    /// unexpected blocking.
582    ///
583    /// Note: You must call either [`observe`] to record the execution time to a [`Histogram`] or
584    /// [`inc_by`] to record to a [`Counter`]. The following will not compile:
585    ///
586    /// ```compile_fail
587    /// use mz_ore::metrics::MetricsFutureExt;
588    ///
589    /// # let _ = async {
590    /// async { Ok(()) }
591    ///     .exec_time()
592    ///     .await;
593    /// # };
594    /// ```
595    ///
596    /// [`observe`]: ExecTimeFuture::observe
597    /// [`inc_by`]: ExecTimeFuture::inc_by
598    fn exec_time(self) -> ExecTimeFuture<F, UnspecifiedMetric>;
599}
600
601impl<F: Future> MetricsFutureExt<F> for F {
602    fn wall_time(self) -> WallTimeFuture<F, UnspecifiedMetric> {
603        WallTimeFuture {
604            fut: self,
605            metric: UnspecifiedMetric(()),
606            start: None,
607            filter: None,
608        }
609    }
610
611    fn exec_time(self) -> ExecTimeFuture<F, UnspecifiedMetric> {
612        ExecTimeFuture {
613            fut: self,
614            metric: UnspecifiedMetric(()),
615            running_duration: Duration::from_millis(0),
616            filter: None,
617        }
618    }
619}
620
621/// Future returned by [`MetricsFutureExt::wall_time`].
622#[must_use = "futures do nothing unless you `.await` or poll them"]
623#[pin_project]
624pub struct WallTimeFuture<F, Metric> {
625    /// The inner [`Future`] that we're recording the wall time for.
626    #[pin]
627    fut: F,
628    /// Prometheus metric that we'll report to.
629    metric: Metric,
630    /// [`Instant`] at which the [`Future`] was first polled.
631    start: Option<Instant>,
632    /// Optional filter that determines if we observe the wall time of this [`Future`].
633    filter: Option<Box<dyn FnMut(Duration) -> bool + Send + Sync>>,
634}
635
636impl<F: Debug, M: Debug> fmt::Debug for WallTimeFuture<F, M> {
637    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
638        f.debug_struct("WallTimeFuture")
639            .field("fut", &self.fut)
640            .field("metric", &self.metric)
641            .field("start", &self.start)
642            .field("filter", &self.filter.is_some())
643            .finish()
644    }
645}
646
647impl<F> WallTimeFuture<F, UnspecifiedMetric> {
648    /// Sets the recored metric to be a [`prometheus::Histogram`].
649    ///
650    /// ```text
651    /// my_future
652    ///     .wall_time()
653    ///     .observe(metrics.slow_queries_hist.with_label_values(&["select"]))
654    /// ```
655    pub fn observe(
656        self,
657        histogram: prometheus::Histogram,
658    ) -> WallTimeFuture<F, prometheus::Histogram> {
659        WallTimeFuture {
660            fut: self.fut,
661            metric: histogram,
662            start: self.start,
663            filter: self.filter,
664        }
665    }
666
667    /// Sets the recored metric to be a [`prometheus::Counter`].
668    ///
669    /// ```text
670    /// my_future
671    ///     .wall_time()
672    ///     .inc_by(metrics.slow_queries.with_label_values(&["select"]))
673    /// ```
674    pub fn inc_by(self, counter: prometheus::Counter) -> WallTimeFuture<F, prometheus::Counter> {
675        WallTimeFuture {
676            fut: self.fut,
677            metric: counter,
678            start: self.start,
679            filter: self.filter,
680        }
681    }
682
683    /// Sets the recorded duration in a specific f64.
684    pub fn set_at(self, place: &mut f64) -> WallTimeFuture<F, &mut f64> {
685        WallTimeFuture {
686            fut: self.fut,
687            metric: place,
688            start: self.start,
689            filter: self.filter,
690        }
691    }
692}
693
694impl<F, M> WallTimeFuture<F, M> {
695    /// Specifies a filter which much return `true` for the wall time to be recorded.
696    ///
697    /// This can be particularly useful if you have a high volume `Future` and you only want to
698    /// record ones that take a long time to complete.
699    pub fn with_filter(
700        mut self,
701        filter: impl FnMut(Duration) -> bool + Send + Sync + 'static,
702    ) -> Self {
703        self.filter = Some(Box::new(filter));
704        self
705    }
706}
707
708impl<F: Future, M: DurationMetric> Future for WallTimeFuture<F, M> {
709    type Output = F::Output;
710
711    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
712        let this = self.project();
713
714        if this.start.is_none() {
715            *this.start = Some(Instant::now());
716        }
717
718        let result = match this.fut.poll(cx) {
719            Poll::Ready(r) => r,
720            Poll::Pending => return Poll::Pending,
721        };
722        let duration = Instant::now().duration_since(this.start.expect("timer to be started"));
723
724        let pass = this
725            .filter
726            .as_mut()
727            .map(|filter| filter(duration))
728            .unwrap_or(true);
729        if pass {
730            this.metric.record(duration.as_secs_f64())
731        }
732
733        Poll::Ready(result)
734    }
735}
736
737/// Future returned by [`MetricsFutureExt::exec_time`].
738#[must_use = "futures do nothing unless you `.await` or poll them"]
739#[pin_project]
740pub struct ExecTimeFuture<F, Metric> {
741    /// The inner [`Future`] that we're recording the wall time for.
742    #[pin]
743    fut: F,
744    /// Prometheus metric that we'll report to.
745    metric: Metric,
746    /// Total [`Duration`] for which this [`Future`] has been executing.
747    running_duration: Duration,
748    /// Optional filter that determines if we observe the execution time of this [`Future`].
749    filter: Option<Box<dyn FnMut(Duration) -> bool + Send + Sync>>,
750}
751
752impl<F: Debug, M: Debug> fmt::Debug for ExecTimeFuture<F, M> {
753    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
754        f.debug_struct("ExecTimeFuture")
755            .field("fut", &self.fut)
756            .field("metric", &self.metric)
757            .field("running_duration", &self.running_duration)
758            .field("filter", &self.filter.is_some())
759            .finish()
760    }
761}
762
763impl<F> ExecTimeFuture<F, UnspecifiedMetric> {
764    /// Sets the recored metric to be a [`prometheus::Histogram`].
765    ///
766    /// ```text
767    /// my_future
768    ///     .exec_time()
769    ///     .observe(metrics.slow_queries_hist.with_label_values(&["select"]))
770    /// ```
771    pub fn observe(
772        self,
773        histogram: prometheus::Histogram,
774    ) -> ExecTimeFuture<F, prometheus::Histogram> {
775        ExecTimeFuture {
776            fut: self.fut,
777            metric: histogram,
778            running_duration: self.running_duration,
779            filter: self.filter,
780        }
781    }
782
783    /// Sets the recored metric to be a [`prometheus::Counter`].
784    ///
785    /// ```text
786    /// my_future
787    ///     .exec_time()
788    ///     .inc_by(metrics.slow_queries.with_label_values(&["select"]))
789    /// ```
790    pub fn inc_by(self, counter: prometheus::Counter) -> ExecTimeFuture<F, prometheus::Counter> {
791        ExecTimeFuture {
792            fut: self.fut,
793            metric: counter,
794            running_duration: self.running_duration,
795            filter: self.filter,
796        }
797    }
798}
799
800impl<F, M> ExecTimeFuture<F, M> {
801    /// Specifies a filter which much return `true` for the execution time to be recorded.
802    pub fn with_filter(
803        mut self,
804        filter: impl FnMut(Duration) -> bool + Send + Sync + 'static,
805    ) -> Self {
806        self.filter = Some(Box::new(filter));
807        self
808    }
809}
810
811impl<F: Future, M: DurationMetric> Future for ExecTimeFuture<F, M> {
812    type Output = F::Output;
813
814    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
815        let this = self.project();
816
817        let start = Instant::now();
818        let result = this.fut.poll(cx);
819        let duration = Instant::now().duration_since(start);
820
821        *this.running_duration = this.running_duration.saturating_add(duration);
822
823        let result = match result {
824            Poll::Ready(result) => result,
825            Poll::Pending => return Poll::Pending,
826        };
827
828        let duration = *this.running_duration;
829        let pass = this
830            .filter
831            .as_mut()
832            .map(|filter| filter(duration))
833            .unwrap_or(true);
834        if pass {
835            this.metric.record(duration.as_secs_f64());
836        }
837
838        Poll::Ready(result)
839    }
840}
841
842/// A type level flag used to ensure callers specify the kind of metric to record for
843/// [`MetricsFutureExt`].
844///
845/// For example, `WallTimeFuture<F, M>` only implements [`Future`] for `M` that implements
846/// `DurationMetric` which [`UnspecifiedMetric`] does not. This forces users at build time to
847/// call [`WallTimeFuture::observe`] or [`WallTimeFuture::inc_by`].
848#[derive(Debug)]
849pub struct UnspecifiedMetric(());
850
851/// A trait makes recording a duration generic over different prometheus metrics. This allows us to
852/// de-dupe the implemenation of [`Future`] for our wrapper Futures like [`WallTimeFuture`] and
853/// [`ExecTimeFuture`] over different kinds of prometheus metrics.
854trait DurationMetric {
855    fn record(&mut self, seconds: f64);
856}
857
858impl DurationMetric for prometheus::Histogram {
859    fn record(&mut self, seconds: f64) {
860        self.observe(seconds)
861    }
862}
863
864impl DurationMetric for prometheus::Counter {
865    fn record(&mut self, seconds: f64) {
866        self.inc_by(seconds)
867    }
868}
869
870// An implementation of `DurationMetric` that lets the user take the recorded
871// value and use it elsewhere.
872impl DurationMetric for &'_ mut f64 {
873    fn record(&mut self, seconds: f64) {
874        **self = seconds;
875    }
876}
877
878/// Register the Tokio runtime's metrics in our metrics registry.
879#[cfg(feature = "async")]
880pub fn register_runtime_metrics(
881    name: &'static str,
882    runtime_metrics: tokio::runtime::RuntimeMetrics,
883    registry: &MetricsRegistry,
884) {
885    macro_rules! register {
886        ($method:ident, $doc:literal) => {
887            let metrics = runtime_metrics.clone();
888            registry.register_computed_gauge::<prometheus::core::AtomicU64>(
889                crate::metric!(
890                    name: concat!("mz_tokio_", stringify!($method)),
891                    help: $doc,
892                    const_labels: {"runtime" => name},
893                ),
894                move || <u64 as crate::cast::CastFrom<_>>::cast_from(metrics.$method()),
895            );
896        };
897    }
898
899    macro_rules! register_per_worker {
900        ($method:ident, $doc:literal) => {
901            let metrics = runtime_metrics.clone();
902            registry.register_computed_gauge::<prometheus::core::AtomicU64>(
903                crate::metric!(
904                    name: concat!("mz_tokio_", stringify!($method)),
905                    help: $doc,
906                    const_labels: {"runtime" => name},
907                ),
908                move || {
909                    (0..metrics.num_workers())
910                        .map(|i| <u64 as crate::cast::CastFrom<_>>::cast_from(metrics.$method(i)))
911                        .sum::<u64>()
912                },
913            );
914        };
915    }
916
917    macro_rules! register_per_worker_duration_secs {
918        ($method:ident, $doc:literal) => {
919            let metrics = runtime_metrics.clone();
920            registry.register_computed_gauge::<prometheus::core::AtomicF64>(
921                crate::metric!(
922                    name: concat!("mz_tokio_", stringify!($method)),
923                    help: $doc,
924                    const_labels: {"runtime" => name},
925                ),
926                move || {
927                    (0..metrics.num_workers())
928                        .map(|i| metrics.$method(i).as_secs_f64())
929                        .sum::<f64>()
930                },
931            );
932        };
933    }
934
935    register!(
936        num_workers,
937        "The number of worker threads used by the runtime."
938    );
939    register!(
940        num_alive_tasks,
941        "The current number of alive tasks in the runtime."
942    );
943    register!(
944        global_queue_depth,
945        "The number of tasks currently scheduled in the runtime's global queue."
946    );
947    register_per_worker_duration_secs!(
948        worker_total_busy_duration,
949        "The amount of time the worker threads have been busy, in seconds."
950    );
951    register_per_worker!(
952        worker_park_count,
953        "The total number of times the worker threads have parked."
954    );
955    register_per_worker!(
956        worker_park_unpark_count,
957        "The total number of times the worker threads have parked and unparked."
958    );
959
960    #[cfg(tokio_unstable)]
961    {
962        register!(
963            num_blocking_threads,
964            "The number of additional threads spawned by the runtime."
965        );
966        register!(
967            num_idle_blocking_threads,
968            "The number of idle threads which have spawned by the runtime for spawn_blocking calls."
969        );
970        register_per_worker!(
971            worker_local_queue_depth,
972            "The number of tasks currently scheduled in the workers' local queues."
973        );
974        register!(
975            blocking_queue_depth,
976            "The number of tasks currently scheduled in the blocking thread pool, spawned using spawn_blocking."
977        );
978        register!(
979            spawned_tasks_count,
980            "The number of tasks spawned in this runtime since it was created."
981        );
982        register!(
983            remote_schedule_count,
984            "The number of tasks scheduled from outside of the runtime."
985        );
986        register!(
987            budget_forced_yield_count,
988            "The number of times that tasks have been forced to yield back to the scheduler after exhausting their task budgets."
989        );
990        register_per_worker!(
991            worker_noop_count,
992            "The number of times the given worker thread unparked but performed no work before parking again."
993        );
994        register_per_worker!(
995            worker_steal_count,
996            "The number of tasks the given worker thread stole from another worker thread."
997        );
998        register_per_worker!(
999            worker_steal_operations,
1000            "The number of times the given worker thread stole tasks from another worker thread."
1001        );
1002        register_per_worker!(
1003            worker_poll_count,
1004            "The number of tasks the given worker thread has polled."
1005        );
1006        register_per_worker!(
1007            worker_local_schedule_count,
1008            "The number of tasks scheduled from within the runtime on the given worker's local queue."
1009        );
1010        register_per_worker!(
1011            worker_overflow_count,
1012            "The number of times the given worker thread saturated its local queue."
1013        );
1014        register_per_worker_duration_secs!(
1015            worker_mean_poll_time,
1016            "The mean duration of task polls in seconds."
1017        );
1018    }
1019}
1020
1021/// Returns the `(name, help, labels, source)` of every Tokio runtime metric
1022/// registered by [`register_runtime_metrics`].
1023#[cfg(feature = "async")]
1024pub fn describe_runtime_metrics() -> Vec<(String, String, Vec<String>, &'static str)> {
1025    // A current-thread runtime is enough to enumerate the metrics; we only read
1026    // their names, help text, and labels, never their values.
1027    let runtime = tokio::runtime::Builder::new_current_thread()
1028        .build()
1029        .expect("building a current-thread runtime");
1030    let registry = MetricsRegistry::new();
1031    register_runtime_metrics("describe", runtime.handle().metrics(), &registry);
1032    registry
1033        .gather()
1034        .into_iter()
1035        .map(|mf| {
1036            // Every series in a family shares the same label keys, so the first
1037            // metric's labels are representative.
1038            let mut labels: Vec<String> = mf
1039                .get_metric()
1040                .first()
1041                .map(|m| m.get_label().iter().map(|l| l.name().to_owned()).collect())
1042                .unwrap_or_default();
1043            labels.sort();
1044            labels.dedup();
1045            (mf.name().to_owned(), mf.help().to_owned(), labels, file!())
1046        })
1047        .collect()
1048}
1049
1050/// Removes every child of `vec` whose label `name` has the value `value`.
1051///
1052/// Prometheus removes children only by their full label tuple, so this learns
1053/// the tuples by collecting the vec, which clones every child once. Meant for
1054/// occasional cleanup such as dropping an object's series, not for hot paths.
1055pub fn remove_children_with_label<V: MetricVec_ + Collector>(vec: &V, name: &str, value: &str) {
1056    let descs = vec.desc();
1057    // A metric vec has exactly one `Desc`.
1058    let Some(desc) = descs.first() else {
1059        return;
1060    };
1061    for family in vec.collect() {
1062        for child in family.get_metric() {
1063            let labels: BTreeMap<&str, &str> = child
1064                .get_label()
1065                .iter()
1066                .map(|pair| (pair.name(), pair.value()))
1067                .collect();
1068            if labels.get(name) != Some(&value) {
1069                continue;
1070            }
1071            let values: Vec<&str> = desc
1072                .variable_labels
1073                .iter()
1074                .map(|label| labels.get(label.as_str()).copied().unwrap_or_default())
1075                .collect();
1076            // `remove_label_values` fails when the series is already gone, which happens
1077            // if another caller removed it between the `collect` snapshot above and now.
1078            // That is the state we wanted, so the error is ignored.
1079            let _ = vec.remove_label_values(&values);
1080        }
1081    }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use std::time::Duration;
1087
1088    use prometheus::core::Collector;
1089    use prometheus::{CounterVec, HistogramVec};
1090
1091    use crate::stats::histogram_seconds_buckets;
1092
1093    use super::{MetricsFutureExt, MetricsRegistry};
1094
1095    struct Metrics {
1096        pub wall_time_hist: HistogramVec,
1097        pub wall_time_cnt: CounterVec,
1098        pub exec_time_hist: HistogramVec,
1099        pub exec_time_cnt: CounterVec,
1100    }
1101
1102    impl Metrics {
1103        pub fn register_into(registry: &MetricsRegistry) -> Self {
1104            Self {
1105                wall_time_hist: registry.register(metric!(
1106                    name: "wall_time_hist",
1107                    help: "help",
1108                    var_labels: ["action"],
1109                    buckets: histogram_seconds_buckets(0.000_128, 8.0),
1110                )),
1111                wall_time_cnt: registry.register(metric!(
1112                    name: "wall_time_cnt",
1113                    help: "help",
1114                    var_labels: ["action"],
1115                )),
1116                exec_time_hist: registry.register(metric!(
1117                    name: "exec_time_hist",
1118                    help: "help",
1119                    var_labels: ["action"],
1120                    buckets: histogram_seconds_buckets(0.000_128, 8.0),
1121                )),
1122                exec_time_cnt: registry.register(metric!(
1123                    name: "exec_time_cnt",
1124                    help: "help",
1125                    var_labels: ["action"],
1126                )),
1127            }
1128        }
1129    }
1130
1131    #[crate::test]
1132    #[cfg_attr(miri, ignore)] // unsupported operation: integer-to-pointer casts and `ptr::from_exposed_addr` are not supported with `-Zmiri-strict-provenance`
1133    fn smoke_test_metrics_future_ext() {
1134        let runtime = tokio::runtime::Builder::new_current_thread()
1135            .enable_time()
1136            .build()
1137            .expect("failed to start runtime");
1138        let registry = MetricsRegistry::new();
1139        let metrics = Metrics::register_into(&registry);
1140
1141        // Record the walltime and execution time of an async sleep.
1142        let async_sleep_future = async {
1143            tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
1144        };
1145        runtime.block_on(
1146            async_sleep_future
1147                .wall_time()
1148                .observe(metrics.wall_time_hist.with_label_values(&["async_sleep_w"]))
1149                .exec_time()
1150                .observe(metrics.exec_time_hist.with_label_values(&["async_sleep_e"])),
1151        );
1152
1153        let reports = registry.gather();
1154
1155        let exec_family = reports
1156            .iter()
1157            .find(|m| m.name() == "exec_time_hist")
1158            .expect("metric not found");
1159        let exec_metric = exec_family.get_metric();
1160        assert_eq!(exec_metric.len(), 1);
1161        assert_eq!(exec_metric[0].get_label()[0].value(), "async_sleep_e");
1162
1163        let exec_histogram = exec_metric[0].get_histogram();
1164        assert_eq!(exec_histogram.get_sample_count(), 1);
1165        // This future will normally complete very quickly, but it's hard to guarantee any
1166        // particular timing in an arbitrary test environment, so we don't assert on it
1167        // here.
1168
1169        let wall_family = reports
1170            .iter()
1171            .find(|m| m.name() == "wall_time_hist")
1172            .expect("metric not found");
1173        let wall_metric = wall_family.get_metric();
1174        assert_eq!(wall_metric.len(), 1);
1175        assert_eq!(wall_metric[0].get_label()[0].value(), "async_sleep_w");
1176
1177        let wall_histogram = wall_metric[0].get_histogram();
1178        assert_eq!(wall_histogram.get_sample_count(), 1);
1179        // The 13th bucket is 512ms, which the wall time should be longer than, but is also much
1180        // faster than the actual execution time of the async sleep.
1181        assert_eq!(wall_histogram.get_bucket()[12].cumulative_count(), 0);
1182
1183        // Reset the registery to make collecting metrics easier.
1184        let registry = MetricsRegistry::new();
1185        let metrics = Metrics::register_into(&registry);
1186
1187        // Record the walltime and execution time of a thread sleep.
1188        let thread_sleep_future = async {
1189            std::thread::sleep(std::time::Duration::from_secs(1));
1190        };
1191        runtime.block_on(
1192            thread_sleep_future
1193                .wall_time()
1194                .with_filter(|duration| duration < Duration::from_millis(10))
1195                .inc_by(metrics.wall_time_cnt.with_label_values(&["thread_sleep_w"]))
1196                .exec_time()
1197                .inc_by(metrics.exec_time_cnt.with_label_values(&["thread_sleep_e"])),
1198        );
1199
1200        let reports = registry.gather();
1201
1202        let exec_family = reports
1203            .iter()
1204            .find(|m| m.name() == "exec_time_cnt")
1205            .expect("metric not found");
1206        let exec_metric = exec_family.get_metric();
1207        assert_eq!(exec_metric.len(), 1);
1208        assert_eq!(exec_metric[0].get_label()[0].value(), "thread_sleep_e");
1209
1210        let exec_counter = exec_metric[0].get_counter();
1211        // Since we're synchronously sleeping the execution time will be long.
1212        assert!(exec_counter.value() >= 1.0);
1213
1214        let wall_family = reports
1215            .iter()
1216            .find(|m| m.name() == "wall_time_cnt")
1217            .expect("metric not found");
1218        let wall_metric = wall_family.get_metric();
1219        assert_eq!(wall_metric.len(), 1);
1220
1221        let wall_counter = wall_metric[0].get_counter();
1222        // We filtered wall time to < 10ms, so our wall time metric should be filtered out.
1223        assert_eq!(wall_counter.value(), 0.0);
1224    }
1225
1226    #[crate::test]
1227    fn collector_drop_handle_unregisters() {
1228        use prometheus::IntGauge;
1229
1230        let registry = MetricsRegistry::new();
1231        let gauge = IntGauge::new("mz_test_guarded", "help").unwrap();
1232        gauge.set(7);
1233        let before = registry.gather().len();
1234
1235        let handle = registry.register_collector_with_dropper(gauge.clone());
1236        assert_eq!(registry.gather().len(), before + 1);
1237
1238        // Dropping the handle unregisters the collector, so its series stops being scraped.
1239        drop(handle);
1240        assert_eq!(registry.gather().len(), before);
1241    }
1242
1243    #[crate::test]
1244    fn register_drop_then_reregister() {
1245        use prometheus::IntGauge;
1246
1247        // Two collectors sharing a descriptor id: re-registering the second while the first is
1248        // still registered would collide. This locks in the contract that dropping the old handle
1249        // first clears the id so the re-registration succeeds cleanly, the ordering a caller
1250        // replacing a collector (e.g. a metric sink re-rendered on reconciliation) must uphold.
1251        let registry = MetricsRegistry::new();
1252        let old = IntGauge::new("mz_test_reregister", "help").unwrap();
1253        let new = IntGauge::new("mz_test_reregister", "help").unwrap();
1254        let before = registry.gather().len();
1255
1256        let handle = registry.register_collector_with_dropper(old);
1257        assert_eq!(registry.gather().len(), before + 1);
1258
1259        // Drop old before registering new, matching the required ordering.
1260        drop(handle);
1261        let handle = registry.register_collector_with_dropper(new);
1262        assert_eq!(registry.gather().len(), before + 1);
1263
1264        drop(handle);
1265        assert_eq!(registry.gather().len(), before);
1266    }
1267
1268    #[crate::test]
1269    fn try_register_errors_on_duplicate_then_succeeds_after_drop() {
1270        use prometheus::IntGauge;
1271
1272        // The fallible variant reports the collision instead of soft-panicking, and the collision
1273        // clears as soon as the incumbent's handle drops. This is the retry contract the metric
1274        // sink operator leans on when a new incarnation races the old one's teardown.
1275        let registry = MetricsRegistry::new();
1276        let old = IntGauge::new("mz_test_try_register", "help").unwrap();
1277        let new = IntGauge::new("mz_test_try_register", "help").unwrap();
1278        let before = registry.gather().len();
1279
1280        let handle = registry
1281            .try_register_collector_with_dropper(old)
1282            .expect("first registration succeeds");
1283        assert_eq!(registry.gather().len(), before + 1);
1284
1285        let err = registry
1286            .try_register_collector_with_dropper(new.clone())
1287            .err()
1288            .expect("duplicate descriptor id is rejected");
1289        assert!(matches!(err, prometheus::Error::AlreadyReg));
1290        // The failed attempt registered nothing, so the incumbent is still the only series.
1291        assert_eq!(registry.gather().len(), before + 1);
1292
1293        drop(handle);
1294        let handle = registry
1295            .try_register_collector_with_dropper(new)
1296            .expect("registration succeeds once the id is free");
1297        assert_eq!(registry.gather().len(), before + 1);
1298
1299        drop(handle);
1300        assert_eq!(registry.gather().len(), before);
1301    }
1302
1303    #[crate::test]
1304    fn remove_children_with_label_removes_only_matching_children() {
1305        let registry = MetricsRegistry::new();
1306        let vec: HistogramVec = registry.register(metric!(
1307            name: "labeled_hist",
1308            help: "help",
1309            var_labels: ["cluster", "kind"],
1310            buckets: histogram_seconds_buckets(0.000_128, 8.0),
1311        ));
1312        vec.with_label_values(&["u1", "a"]).observe(1.0);
1313        vec.with_label_values(&["u1", "b"]).observe(1.0);
1314        vec.with_label_values(&["u2", "a"]).observe(1.0);
1315
1316        super::remove_children_with_label(&vec, "cluster", "u1");
1317
1318        let remaining: Vec<Vec<String>> = vec
1319            .collect()
1320            .into_iter()
1321            .flat_map(|family| family.get_metric().to_vec())
1322            .map(|child| {
1323                child
1324                    .get_label()
1325                    .iter()
1326                    .map(|pair| pair.value().to_string())
1327                    .collect()
1328            })
1329            .collect();
1330        assert_eq!(remaining, vec![vec!["u2".to_string(), "a".to_string()]]);
1331    }
1332}