Skip to main content

sui_indexer_alt_metrics/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::net::SocketAddr;
5use std::time::Instant;
6
7use anyhow::Context;
8use axum::Extension;
9use axum::Router;
10use axum::http::StatusCode;
11use axum::routing::get;
12use prometheus::Registry;
13use prometheus::TextEncoder;
14use prometheus::core::Collector;
15use prometheus_closure_metric::ClosureMetric;
16use prometheus_closure_metric::ValueType;
17use sui_futures::service::Service;
18use tokio::net::TcpListener;
19use tokio::sync::oneshot;
20use tracing::info;
21
22#[cfg(feature = "postgres")]
23pub mod db;
24
25#[derive(clap::Args, Debug, Clone)]
26pub struct MetricsArgs {
27    /// Address to serve Prometheus metrics from.
28    #[arg(long, default_value_t = Self::default().metrics_address)]
29    pub metrics_address: SocketAddr,
30}
31
32/// A service that exposes prometheus metrics over HTTP on a "/metrics" route on the provided
33/// listen address.
34pub struct MetricsService {
35    addr: SocketAddr,
36    registry: Registry,
37}
38
39impl MetricsService {
40    /// Create a new instance of the service, listening on the address provided in `args`, serving
41    /// metrics from the `registry`.
42    ///
43    /// The service will not be run until [Self::run] is called.
44    pub fn new(args: MetricsArgs, registry: Registry) -> Self {
45        Self {
46            addr: args.metrics_address,
47            registry,
48        }
49    }
50
51    /// Add metrics to this registry to serve them from this service.
52    pub fn registry(&self) -> &Registry {
53        &self.registry
54    }
55
56    /// Start the service. The service will run until the cancellation token is triggered.
57    pub async fn run(self) -> anyhow::Result<Service> {
58        let Self { addr, registry } = self;
59
60        let listener = TcpListener::bind(&self.addr)
61            .await
62            .with_context(|| format!("Failed to bind metrics at {addr}"))?;
63
64        let app = Router::new()
65            .route("/metrics", get(metrics))
66            .layer(Extension(registry));
67
68        let (stx, srx) = oneshot::channel::<()>();
69        Ok(Service::new()
70            .with_shutdown_signal(async move {
71                let _ = stx.send(());
72            })
73            .spawn(async move {
74                info!("Starting metrics service on {addr}");
75                Ok(axum::serve(listener, app)
76                    .with_graceful_shutdown(async move {
77                        let _ = srx.await;
78                        info!("Shutdown received, shutting down metrics service");
79                    })
80                    .await?)
81            }))
82    }
83}
84
85impl Default for MetricsArgs {
86    fn default() -> Self {
87        Self {
88            metrics_address: "0.0.0.0:9184".parse().unwrap(),
89        }
90    }
91}
92
93/// A metric that tracks the service uptime.
94pub fn uptime(version: &str) -> anyhow::Result<Box<dyn Collector>> {
95    let init = Instant::now();
96    let opts = prometheus::opts!("uptime", "how long the service has been running in seconds")
97        .variable_label("version");
98
99    let metric = move || init.elapsed().as_secs();
100    let uptime = ClosureMetric::new(opts, ValueType::Counter, metric, &[version])
101        .context("Failed to create uptime metric")?;
102
103    Ok(Box::new(uptime))
104}
105
106/// Route handler for metrics service
107async fn metrics(Extension(registry): Extension<Registry>) -> (StatusCode, String) {
108    match TextEncoder.encode_to_string(&registry.gather()) {
109        Ok(s) => (StatusCode::OK, s),
110        Err(e) => (
111            StatusCode::INTERNAL_SERVER_ERROR,
112            format!("unable to encode metrics: {e}"),
113        ),
114    }
115}