Skip to main content

iota_metrics/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    future::Future,
7    net::SocketAddr,
8    path::Path,
9    pin::Pin,
10    sync::Arc,
11    task::{Context, Poll},
12    time::Instant,
13};
14
15use axum::{
16    Router,
17    extract::{Extension, Request},
18    http::StatusCode,
19    middleware::Next,
20    response::Response,
21    routing::get,
22};
23use dashmap::DashMap;
24use once_cell::sync::OnceCell;
25use parking_lot::Mutex;
26use prometheus_filtered::{
27    Filter, Histogram, IntCounterVec, IntGaugeVec, Registry, TextEncoder,
28    core::{AtomicI64, GenericGauge},
29    register_histogram_with_registry, register_int_counter_vec_with_registry,
30    register_int_gauge_vec_with_registry,
31};
32pub use scopeguard;
33use simple_server_timing_header::Timer;
34use tap::TapFallible;
35use tracing::{Span, warn};
36use uuid::Uuid;
37
38mod guards;
39pub mod hardware_metrics;
40pub mod histogram;
41pub mod metered_channel;
42pub mod metric_groups;
43pub mod metrics_network;
44pub mod monitored_mpsc;
45pub mod thread_stall_monitor;
46pub use guards::*;
47pub use metric_groups::{MetricGroups, MetricLevel};
48
49pub const TX_TYPE_SINGLE_WRITER_TX: &str = "single_writer";
50pub const TX_TYPE_SHARED_OBJ_TX: &str = "shared_object";
51
52pub const SUBSECOND_LATENCY_SEC_BUCKETS: &[f64] = &[
53    0.005, 0.01, 0.02, 0.03, 0.05, 0.075, 0.1, 0.2, 0.3, 0.5, 0.7, 1.,
54];
55
56pub const COARSE_LATENCY_SEC_BUCKETS: &[f64] = &[
57    0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.2, 0.3, 0.5, 0.7, 1., 2., 3., 5., 10., 20., 30., 60.,
58];
59
60pub const LATENCY_SEC_BUCKETS: &[f64] = &[
61    0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.6,
62    0.7, 0.8, 0.9, 1., 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2., 2.5, 3., 3.5, 4., 4.5, 5.,
63    6., 7., 8., 9., 10., 15., 20., 25., 30., 60., 90.,
64];
65
66pub const COUNT_BUCKETS: &[f64] = &[
67    2., 5., 10., 20., 50., 100., 200., 500., 1000., 2000., 5000., 10000.,
68];
69
70pub const BYTES_BUCKETS: &[f64] = &[
71    1024., 4096., 16384., 65536., 262144., 524288., 1048576., 2097152., 4194304., 8388608.,
72    16777216., 33554432., 67108864.,
73];
74
75#[derive(Debug)]
76pub struct Metrics {
77    pub tasks: IntGaugeVec,
78    pub futures: IntGaugeVec,
79    pub channel_inflight: IntGaugeVec,
80    pub channel_sent: IntGaugeVec,
81    pub channel_received: IntGaugeVec,
82    pub future_active_duration_ns: IntGaugeVec,
83    pub scope_iterations: IntGaugeVec,
84    pub scope_duration_ns: IntGaugeVec,
85    pub scope_entrance: IntGaugeVec,
86    pub thread_stall_duration_sec: Histogram,
87    pub system_invariant_violations: IntCounterVec,
88}
89
90impl Metrics {
91    /// Creates a new instance of the monitoring metrics, registering various
92    /// gauges and histograms with the provided metrics `Registry`. The
93    /// gauges track metrics such as the number of running tasks, pending
94    /// futures, channel items, and scope activities, while the histogram
95    /// measures the duration of thread stalls. Each metric is registered
96    /// with descriptive labels to facilitate performance monitoring and
97    /// analysis.
98    fn new(registry: &Registry) -> Self {
99        Self {
100            tasks: register_int_gauge_vec_with_registry!(
101                "monitored_tasks",
102                "Number of running tasks per callsite.",
103                &["callsite"],
104                registry;
105                MetricLevel::Warn,
106            )
107            .unwrap(),
108            futures: register_int_gauge_vec_with_registry!(
109                "monitored_futures",
110                "Number of pending futures per callsite.",
111                &["callsite"],
112                registry;
113                MetricLevel::Warn,
114            )
115            .unwrap(),
116            channel_inflight: register_int_gauge_vec_with_registry!(
117                "monitored_channel_inflight",
118                "Inflight items in channels.",
119                &["name"],
120                registry;
121                MetricLevel::Warn,
122            )
123            .unwrap(),
124            channel_sent: register_int_gauge_vec_with_registry!(
125                "monitored_channel_sent",
126                "Sent items in channels.",
127                &["name"],
128                registry,
129            )
130            .unwrap(),
131            channel_received: register_int_gauge_vec_with_registry!(
132                "monitored_channel_received",
133                "Received items in channels.",
134                &["name"],
135                registry,
136            )
137            .unwrap(),
138            future_active_duration_ns: register_int_gauge_vec_with_registry!(
139                "monitored_future_active_duration_ns",
140                "Total duration in nanosecs where the monitored future is active (consuming CPU time)",
141                &["name"],
142                registry,
143            )
144            .unwrap(),
145            scope_entrance: register_int_gauge_vec_with_registry!(
146                "monitored_scope_entrance",
147                "Number of entrance in the scope.",
148                &["name"],
149                registry,
150            )
151            .unwrap(),
152            scope_iterations: register_int_gauge_vec_with_registry!(
153                "monitored_scope_iterations",
154                "Total number of times where the monitored scope runs",
155                &["name"],
156                registry,
157            )
158            .unwrap(),
159            scope_duration_ns: register_int_gauge_vec_with_registry!(
160                "monitored_scope_duration_ns",
161                "Total duration in nanosecs where the monitored scope is running",
162                &["name"],
163                registry,
164            )
165            .unwrap(),
166            thread_stall_duration_sec: register_histogram_with_registry!(
167                "thread_stall_duration_sec",
168                "Duration of thread stalls in seconds.",
169                registry,
170            )
171            .unwrap(),
172            system_invariant_violations: register_int_counter_vec_with_registry!(
173                "system_invariant_violations",
174                "Number of system invariant violations",
175                &["name"],
176                registry,
177            ).unwrap(),
178        }
179    }
180}
181
182static METRICS: OnceCell<Metrics> = OnceCell::new();
183
184/// Initializes the global `METRICS` instance by setting it to a new `Metrics`
185/// object registered with the provided `Registry`. If `METRICS` is already set,
186/// a warning is logged indicating that the metrics registry was overwritten.
187/// This function is intended to be called once during initialization to set up
188/// metrics collection.
189pub fn init_metrics(registry: &Registry) {
190    let _ = METRICS
191        .set(Metrics::new(registry))
192        // this happens many times during tests
193        .tap_err(|_| warn!("init_metrics registry overwritten"));
194}
195
196/// Retrieves the global `METRICS` instance if it has been initialized.
197pub fn get_metrics() -> Option<&'static Metrics> {
198    METRICS.get()
199}
200
201tokio::task_local! {
202    static SERVER_TIMING: Arc<Mutex<Timer>>;
203}
204
205/// Create a new task-local ServerTiming context and run the provided future
206/// within it. Should be used at the top-most level of a request handler. Can be
207/// added to an axum router as a layer by using
208/// iota_service::server_timing_middleware.
209pub async fn with_new_server_timing<T>(fut: impl Future<Output = T> + Send + 'static) -> T {
210    let timer = Arc::new(Mutex::new(Timer::new()));
211
212    let mut ret = None;
213    SERVER_TIMING
214        .scope(timer, async {
215            ret = Some(fut.await);
216        })
217        .await;
218
219    ret.unwrap()
220}
221
222/// The `Server-Timing` HTTP header key.
223pub fn server_timing_header_key() -> &'static str {
224    Timer::header_key()
225}
226
227/// Add a final `finish_request` entry and write the collected timings to the
228/// `Server-Timing` response header. No-op outside a server-timing context.
229pub fn finish_and_set_server_timing_header(headers: &mut http::HeaderMap) {
230    let Some(timer) = get_server_timing() else {
231        return;
232    };
233    let header_value = {
234        let mut timer = timer.lock();
235        timer.add("finish_request");
236        timer.header_value()
237    };
238    if let Ok(value) = http::HeaderValue::try_from(header_value) {
239        headers.insert(server_timing_header_key(), value);
240    }
241}
242
243pub async fn server_timing_middleware(request: Request, next: Next) -> Response {
244    with_new_server_timing(async move {
245        let mut response = next.run(request).await;
246        finish_and_set_server_timing_header(response.headers_mut());
247        response
248    })
249    .await
250}
251
252/// Create a new task-local ServerTiming context and run the provided future
253/// within it. Only intended for use by macros within this module.
254pub async fn with_server_timing<T>(
255    timer: Arc<Mutex<Timer>>,
256    fut: impl Future<Output = T> + Send + 'static,
257) -> T {
258    let mut ret = None;
259    SERVER_TIMING
260        .scope(timer, async {
261            ret = Some(fut.await);
262        })
263        .await;
264
265    ret.unwrap()
266}
267
268/// Get the currently active ServerTiming context. Only intended for use by
269/// macros within this module.
270pub fn get_server_timing() -> Option<Arc<Mutex<Timer>>> {
271    SERVER_TIMING.try_with(|timer| timer.clone()).ok()
272}
273
274/// Add a new entry to the ServerTiming header.
275/// If the caller is not currently in a ServerTiming context (created with
276/// `with_new_server_timing`), an error is logged.
277pub fn add_server_timing(name: &str) {
278    let res = SERVER_TIMING.try_with(|timer| {
279        timer.lock().add(name);
280    });
281
282    if res.is_err() {
283        tracing::error!("Server timing context not found");
284    }
285}
286
287#[macro_export]
288macro_rules! monitored_future {
289    ($fut: expr) => {{ monitored_future!(futures, $fut, "", INFO, false) }};
290
291    ($metric: ident, $fut: expr, $name: expr, $logging_level: ident, $logging_enabled: expr) => {{
292        let location: &str = if $name.is_empty() {
293            concat!(file!(), ':', line!())
294        } else {
295            concat!(file!(), ':', $name)
296        };
297
298        async move {
299            let metrics = $crate::get_metrics();
300
301            let _metrics_guard = if let Some(m) = metrics {
302                m.$metric.with_label_values(&[location]).inc();
303                Some($crate::scopeguard::guard(m, |_| {
304                    m.$metric.with_label_values(&[location]).dec();
305                }))
306            } else {
307                None
308            };
309            let _logging_guard = if $logging_enabled {
310                Some($crate::scopeguard::guard((), |_| {
311                    tracing::event!(
312                        tracing::Level::$logging_level,
313                        "Future {} completed",
314                        location
315                    );
316                }))
317            } else {
318                None
319            };
320
321            if $logging_enabled {
322                tracing::event!(
323                    tracing::Level::$logging_level,
324                    "Spawning future {}",
325                    location
326                );
327            }
328
329            $fut.await
330        }
331    }};
332}
333
334#[macro_export]
335macro_rules! forward_server_timing_and_spawn {
336    ($fut: expr) => {
337        if let Some(timing) = $crate::get_server_timing() {
338            tokio::task::spawn(async move { $crate::with_server_timing(timing, $fut).await })
339        } else {
340            tokio::task::spawn($fut)
341        }
342    };
343}
344
345#[macro_export]
346macro_rules! spawn_monitored_task {
347    ($fut: expr) => {
348        $crate::forward_server_timing_and_spawn!($crate::monitored_future!(
349            tasks, $fut, "", INFO, false
350        ))
351    };
352}
353
354#[macro_export]
355macro_rules! spawn_logged_monitored_task {
356    ($fut: expr) => {
357        $crate::forward_server_timing_and_spawn!($crate::monitored_future!(
358            tasks, $fut, "", INFO, true
359        ))
360    };
361
362    ($fut: expr, $name: expr) => {
363        $crate::forward_server_timing_and_spawn!($crate::monitored_future!(
364            tasks, $fut, $name, INFO, true
365        ))
366    };
367
368    ($fut: expr, $name: expr, $logging_level: ident) => {
369        $crate::forward_server_timing_and_spawn!($crate::monitored_future!(
370            tasks,
371            $fut,
372            $name,
373            $logging_level,
374            true
375        ))
376    };
377}
378
379pub struct MonitoredScopeGuard {
380    metrics: &'static Metrics,
381    name: &'static str,
382    timer: Instant,
383}
384
385impl Drop for MonitoredScopeGuard {
386    fn drop(&mut self) {
387        self.metrics
388            .scope_duration_ns
389            .with_label_values(&[self.name])
390            .add(self.timer.elapsed().as_nanos() as i64);
391        self.metrics
392            .scope_entrance
393            .with_label_values(&[self.name])
394            .dec();
395    }
396}
397
398/// This function creates a named scoped object, that keeps track of
399/// - the total iterations where the scope is called in the
400///   `monitored_scope_iterations` metric.
401/// - and the total duration of the scope in the `monitored_scope_duration_ns`
402///   metric.
403///
404/// The monitored scope should be single threaded, e.g. the scoped object
405/// encompass the lifetime of a select loop or guarded by mutex.
406/// Then the rate of `monitored_scope_duration_ns`, converted to the unit of sec
407/// / sec, would be how full the single threaded scope is running.
408pub fn monitored_scope(name: &'static str) -> Option<MonitoredScopeGuard> {
409    let metrics = get_metrics();
410    if let Some(m) = metrics {
411        m.scope_iterations.with_label_values(&[name]).inc();
412        m.scope_entrance.with_label_values(&[name]).inc();
413        Some(MonitoredScopeGuard {
414            metrics: m,
415            name,
416            timer: Instant::now(),
417        })
418    } else {
419        None
420    }
421}
422
423/// A trait extension for `Future` to allow monitoring the execution of the
424/// future within a specific scope. Provides the `in_monitored_scope` method to
425/// wrap the future in a `MonitoredScopeFuture`, which tracks the future's
426/// execution using a `MonitoredScopeGuard` for monitoring purposes.
427pub trait MonitoredFutureExt: Future + Sized {
428    /// Wraps the current future in a `MonitoredScopeFuture` that is associated
429    /// with a specific monitored scope name. The scope helps track the
430    /// execution of the future for performance analysis and metrics collection.
431    fn in_monitored_scope(self, name: &'static str) -> MonitoredScopeFuture<Self>;
432}
433
434impl<F: Future> MonitoredFutureExt for F {
435    fn in_monitored_scope(self, name: &'static str) -> MonitoredScopeFuture<Self> {
436        MonitoredScopeFuture {
437            f: Box::pin(self),
438            active_duration_metric: get_metrics()
439                .map(|m| m.future_active_duration_ns.with_label_values(&[name])),
440            _scope: monitored_scope(name),
441        }
442    }
443}
444
445/// A future that runs within a monitored scope. This struct wraps a pinned
446/// future and holds an optional `MonitoredScopeGuard` to measure and monitor
447/// the execution of the future. It forwards polling operations
448/// to the underlying future while maintaining the monitoring scope.
449pub struct MonitoredScopeFuture<F: Sized> {
450    f: Pin<Box<F>>,
451    active_duration_metric: Option<GenericGauge<AtomicI64>>,
452    _scope: Option<MonitoredScopeGuard>,
453}
454
455impl<F: Future> Future for MonitoredScopeFuture<F> {
456    type Output = F::Output;
457
458    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
459        let active_timer = Instant::now();
460        let ret = self.f.as_mut().poll(cx);
461        if let Some(m) = &self.active_duration_metric {
462            m.add(active_timer.elapsed().as_nanos() as i64);
463        }
464        ret
465    }
466}
467
468/// A future that runs within a monitored scope. This struct wraps a pinned
469/// future and holds an optional `MonitoredScopeGuard` to measure and monitor
470/// the execution of the future. It forwards polling operations
471/// to the underlying future while maintaining the monitoring scope.
472pub struct CancelMonitor<F: Sized> {
473    finished: bool,
474    inner: Pin<Box<F>>,
475}
476
477impl<F> CancelMonitor<F>
478where
479    F: Future,
480{
481    /// Creates a new `CancelMonitor` that wraps the given future (`inner`). The
482    /// monitor tracks whether the future has completed.
483    pub fn new(inner: F) -> Self {
484        Self {
485            finished: false,
486            inner: Box::pin(inner),
487        }
488    }
489
490    /// Returns `true` if the future has completed; otherwise, `false`.
491    pub fn is_finished(&self) -> bool {
492        self.finished
493    }
494}
495
496impl<F> Future for CancelMonitor<F>
497where
498    F: Future,
499{
500    type Output = F::Output;
501
502    /// Polls the inner future to determine if it is ready or still pending. For
503    /// `CancelMonitor`, if the future completes (`Poll::Ready`), `finished`
504    /// is set to `true`. If it is still pending, the status remains
505    /// unchanged. This allows monitoring of the future's completion status.
506    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
507        match self.inner.as_mut().poll(cx) {
508            Poll::Ready(output) => {
509                self.finished = true;
510                Poll::Ready(output)
511            }
512            Poll::Pending => Poll::Pending,
513        }
514    }
515}
516
517impl<F: Sized> Drop for CancelMonitor<F> {
518    /// When the `CancelMonitor` is dropped, it checks whether the future has
519    /// finished executing. If the future was not completed (`finished` is
520    /// `false`), it records that the future was cancelled by logging the
521    /// cancellation status using the current span.
522    fn drop(&mut self) {
523        if !self.finished {
524            Span::current().record("cancelled", true);
525        }
526    }
527}
528
529/// MonitorCancellation records a cancelled = true span attribute if the future
530/// it is decorating is dropped before completion. The cancelled attribute must
531/// be added at span creation, as you cannot add new attributes after the span
532/// is created.
533pub trait MonitorCancellation {
534    fn monitor_cancellation(self) -> CancelMonitor<Self>
535    where
536        Self: Sized + Future;
537}
538
539impl<T> MonitorCancellation for T
540where
541    T: Future,
542{
543    fn monitor_cancellation(self) -> CancelMonitor<Self> {
544        CancelMonitor::new(self)
545    }
546}
547
548pub type RegistryID = Uuid;
549
550/// A service to manage the prometheus registries. This service allow us to
551/// create a new Registry on demand and keep it accessible for
552/// processing/polling. The service can be freely cloned/shared across threads.
553#[derive(Clone)]
554pub struct RegistryService {
555    // Holds a Registry that is supposed to be used
556    default_registry: Registry,
557    registries_by_id: Arc<DashMap<Uuid, Registry>>,
558    filter: Arc<Filter>,
559}
560
561impl RegistryService {
562    // Creates a new registry service and also adds the main/default registry that
563    // is supposed to be preserved and never get removed
564    pub fn new(default_registry: Registry) -> Self {
565        Self {
566            filter: default_registry.filter(),
567            default_registry,
568            registries_by_id: Arc::new(DashMap::new()),
569        }
570    }
571
572    // Returns the default registry for the service that someone can use
573    // if they don't want to create a new one.
574    pub fn default_registry(&self) -> Registry {
575        self.default_registry.clone()
576    }
577
578    // Creates a new registry that shares the service's metric filter. Prefer
579    // this over `Registry::new()`/`Registry::new_custom()` for registries added
580    // via `add`, so that the configured filter applies to their metrics too.
581    pub fn new_registry_custom(
582        &self,
583        prefix: Option<String>,
584        labels: Option<std::collections::HashMap<String, String>>,
585    ) -> prometheus_filtered::Result<Registry> {
586        Registry::new_custom(prefix, labels, Some(self.filter.clone()))
587    }
588
589    // Adds a new registry to the service. The corresponding RegistryID is returned
590    // so can later be used for removing the Registry. Method panics if we try
591    // to insert a registry with the same id. As this can be quite serious for
592    // the operation of the node we don't want to accidentally swap an existing
593    // registry - we expected a removal to happen explicitly.
594    pub fn add(&self, registry: Registry) -> RegistryID {
595        let registry_id = Uuid::new_v4();
596        if self
597            .registries_by_id
598            .insert(registry_id, registry)
599            .is_some()
600        {
601            panic!("Other Registry already detected for the same id {registry_id}");
602        }
603
604        registry_id
605    }
606
607    // Removes the registry from the service. If Registry existed then this method
608    // returns true, otherwise false is returned instead.
609    pub fn remove(&self, registry_id: RegistryID) -> bool {
610        self.registries_by_id.remove(&registry_id).is_some()
611    }
612
613    // Returns all the registries of the service
614    pub fn get_all(&self) -> Vec<Registry> {
615        let mut registries: Vec<Registry> = self
616            .registries_by_id
617            .iter()
618            .map(|r| r.value().clone())
619            .collect();
620        registries.push(self.default_registry.clone());
621
622        registries
623    }
624
625    // Returns all the metric families from the registries that a service holds.
626    pub fn gather_all(&self) -> Vec<prometheus_filtered::proto::MetricFamily> {
627        self.get_all().iter().flat_map(|r| r.gather()).collect()
628    }
629}
630
631/// Create a metric that measures the uptime from when this metric was
632/// constructed. The metric is labeled with:
633/// - 'process': the process type, differentiating between validator and
634///   fullnode
635/// - 'version': binary version, generally be of the format:
636///   'semver-gitrevision'
637/// - 'chain_identifier': the identifier of the network which this process is
638///   part of
639pub fn uptime_metric(
640    process: &str,
641    version: &'static str,
642    chain_identifier: &str,
643) -> Box<dyn prometheus_filtered::core::Collector> {
644    let opts = prometheus_filtered::opts!("uptime", "uptime of the node service in seconds")
645        .variable_label("process")
646        .variable_label("version")
647        .variable_label("chain_identifier")
648        .variable_label("os_version")
649        .variable_label("is_docker");
650
651    let start_time = std::time::Instant::now();
652    let uptime = move || start_time.elapsed().as_secs();
653    let metric = prometheus_closure_metric::ClosureMetric::new(
654        opts,
655        prometheus_closure_metric::ValueType::Counter,
656        uptime,
657        &[
658            process,
659            version,
660            chain_identifier,
661            &sysinfo::System::long_os_version()
662                .unwrap_or_else(|| "os_version_unavailable".to_string()),
663            &is_running_in_docker().to_string(),
664        ],
665    )
666    .unwrap();
667
668    Box::new(metric)
669}
670
671pub fn is_running_in_docker() -> bool {
672    // Check for .dockerenv file instead. This file exists in the debian:__-slim
673    // image we use at runtime.
674    Path::new("/.dockerenv").exists()
675}
676
677pub const METRICS_ROUTE: &str = "/metrics";
678
679// Creates a new http server that has as a sole purpose to expose
680// and endpoint that prometheus agent can use to poll for the metrics.
681// A RegistryService is returned that can be used to get access in prometheus
682// Registries.
683pub fn start_prometheus_server(addr: SocketAddr) -> RegistryService {
684    start_prometheus_server_with_filter(addr, Filter::resolve(None))
685}
686
687pub fn start_prometheus_server_with_filter(addr: SocketAddr, filter: Filter) -> RegistryService {
688    // The default registry has no prefix or labels, so its construction is
689    // infallible.
690    let registry = Registry::new_custom(None, None, Some(Arc::new(filter)))
691        .expect("unprefixed registry is infallible");
692
693    let registry_service = RegistryService::new(registry);
694
695    if cfg!(msim) {
696        // prometheus uses difficult-to-support features such as
697        // TcpSocket::from_raw_fd(), so we can't yet run it in the simulator.
698        warn!("not starting prometheus server in simulator");
699        return registry_service;
700    }
701
702    let app = Router::new()
703        .route(METRICS_ROUTE, get(metrics))
704        .layer(Extension(registry_service.clone()));
705
706    tokio::spawn(async move {
707        let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
708        axum::serve(listener, app.into_make_service())
709            .await
710            .unwrap();
711    });
712
713    registry_service
714}
715
716/// Handles a request to retrieve metrics, using the provided `RegistryService`
717/// to gather all registered metric families. The metrics are then encoded to a
718/// text format for easy consumption by monitoring systems. If successful, it
719/// returns the metrics string with an `OK` status. If an error occurs during
720/// encoding, it returns an `INTERNAL_SERVER_ERROR` status along with an error
721/// message. Returns a tuple containing the status code and either the metrics
722/// data or an error description.
723pub async fn metrics(
724    Extension(registry_service): Extension<RegistryService>,
725) -> (StatusCode, String) {
726    let metrics_families = registry_service.gather_all();
727    match TextEncoder.encode_to_string(&metrics_families) {
728        Ok(metrics) => (StatusCode::OK, metrics),
729        Err(error) => (
730            StatusCode::INTERNAL_SERVER_ERROR,
731            format!("unable to encode metrics: {error}"),
732        ),
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use prometheus_filtered::{IntCounter, Registry};
739
740    use crate::RegistryService;
741
742    #[test]
743    fn registry_service() {
744        // GIVEN
745        let default_registry =
746            Registry::new_custom(Some("default".to_string()), None, None).unwrap();
747
748        let registry_service = RegistryService::new(default_registry.clone());
749        let default_counter = IntCounter::new("counter", "counter_desc").unwrap();
750        default_counter.inc();
751        default_registry
752            .register(Box::new(default_counter))
753            .unwrap();
754
755        // AND add a metric to the default registry
756
757        // AND a registry with one metric
758        let registry_1 = Registry::new_custom(Some("iota".to_string()), None, None).unwrap();
759        registry_1
760            .register(Box::new(
761                IntCounter::new("counter_1", "counter_1_desc").unwrap(),
762            ))
763            .unwrap();
764
765        // WHEN
766        let registry_1_id = registry_service.add(registry_1);
767
768        // THEN
769        let mut metrics = registry_service.gather_all();
770        metrics.sort_by(|m1, m2| Ord::cmp(m1.name(), m2.name()));
771
772        assert_eq!(metrics.len(), 2);
773
774        let metric_default = metrics.remove(0);
775        assert_eq!(metric_default.name(), "default_counter");
776        assert_eq!(metric_default.help(), "counter_desc");
777
778        let metric_1: prometheus_filtered::proto::MetricFamily = metrics.remove(0);
779        assert_eq!(metric_1.name(), "iota_counter_1");
780        assert_eq!(metric_1.help(), "counter_1_desc");
781
782        // AND add a second registry with a metric
783        let registry_2 = Registry::new_custom(Some("iota".to_string()), None, None).unwrap();
784        registry_2
785            .register(Box::new(
786                IntCounter::new("counter_2", "counter_2_desc").unwrap(),
787            ))
788            .unwrap();
789        let _registry_2_id = registry_service.add(registry_2);
790
791        // THEN all the metrics should be returned
792        let mut metrics = registry_service.gather_all();
793        metrics.sort_by(|m1, m2| Ord::cmp(m1.name(), m2.name()));
794
795        assert_eq!(metrics.len(), 3);
796
797        let metric_default = metrics.remove(0);
798        assert_eq!(metric_default.name(), "default_counter");
799        assert_eq!(metric_default.help(), "counter_desc");
800
801        let metric_1 = metrics.remove(0);
802        assert_eq!(metric_1.name(), "iota_counter_1");
803        assert_eq!(metric_1.help(), "counter_1_desc");
804
805        let metric_2 = metrics.remove(0);
806        assert_eq!(metric_2.name(), "iota_counter_2");
807        assert_eq!(metric_2.help(), "counter_2_desc");
808
809        // AND remove first registry
810        assert!(registry_service.remove(registry_1_id));
811
812        // THEN metrics should now not contain metric of registry_1
813        let mut metrics = registry_service.gather_all();
814        metrics.sort_by(|m1, m2| Ord::cmp(m1.name(), m2.name()));
815
816        assert_eq!(metrics.len(), 2);
817
818        let metric_default = metrics.remove(0);
819        assert_eq!(metric_default.name(), "default_counter");
820        assert_eq!(metric_default.help(), "counter_desc");
821
822        let metric_1 = metrics.remove(0);
823        assert_eq!(metric_1.name(), "iota_counter_2");
824        assert_eq!(metric_1.help(), "counter_2_desc");
825    }
826}