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