drive_abci/
metrics.rs

1//! # Metrics Module
2//!
3//! This module provides a singleton implementation for managing metrics.
4
5use std::time::Duration;
6use std::{sync::Once, time::Instant};
7
8use dapi_grpc::tonic::Code;
9use metrics::{counter, describe_counter, describe_gauge, describe_histogram, histogram, Label};
10use metrics_exporter_prometheus::PrometheusBuilder;
11
12/// Default Prometheus port (29090)
13pub const DEFAULT_PROMETHEUS_PORT: u16 = 29090;
14/// Last block time in seconds
15const COUNTER_LAST_BLOCK_TIME: &str = "abci_last_block_time_seconds";
16const COUNTER_LAST_HEIGHT: &str = "abci_last_finalized_height";
17const HISTOGRAM_FINALIZED_ROUND: &str = "abci_finalized_round";
18const HISTOGRAM_ABCI_REQUEST_DURATION: &str = "abci_request_duration_seconds";
19/// State transition processing duration metric
20const HISTOGRAM_STATE_TRANSITION_PROCESSING_DURATION: &str =
21    "state_transition_processing_duration_seconds";
22const LABEL_ENDPOINT: &str = "endpoint";
23/// Metrics label to specify ABCI response code
24pub const LABEL_ABCI_RESPONSE_CODE: &str = "response_code";
25const HISTOGRAM_QUERY_DURATION: &str = "abci_query_duration";
26/// Metrics label to specify state transition name
27pub const LABEL_STATE_TRANSITION_NAME: &str = "st_name";
28/// State transition execution code
29const LABEL_STATE_TRANSITION_EXECUTION_CODE: &str = "st_exec_code";
30/// Metrics label to specify check tx mode: 0 - first time check, 1 - recheck
31pub const LABEL_CHECK_TX_MODE: &str = "check_tx_mode";
32/// Withdrawal daily limit available credits
33pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_AVAILABLE: &str = "credit_withdrawal_limit_available";
34/// Total withdrawal daily limit in credits
35pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_TOTAL: &str = "credit_withdrawal_limit_total";
36
37/// Error returned by metrics subsystem
38#[derive(thiserror::Error, Debug)]
39pub enum Error {
40    /// Prometheus server failed
41    #[error("prometheus server: {0}")]
42    ServerFailed(#[from] metrics_exporter_prometheus::BuildError),
43    /// Listen address invalid
44    #[error("invalid listen address {0}: {1}")]
45    InvalidListenAddress(url::Url, String),
46}
47
48/// Measure execution time and record as a metric.
49///
50/// `HistogramTiming` contains a metric key and a start time, and is designed to be used
51/// with the Drop trait for automatic timing measurements.
52///
53/// When a `HistogramTiming` instance is dropped, [HistogramTiming::Drop()] method calculates and records the elapsed time
54/// since the start time.
55pub struct HistogramTiming {
56    key: metrics::Key,
57    start: Instant,
58    skip: bool,
59}
60
61impl HistogramTiming {
62    /// Creates a new `HistogramTiming` instance.
63    ///
64    /// # Arguments
65    ///
66    /// * `metric` - The metric key for the histogram.
67    ///
68    /// # Returns
69    ///
70    /// A new `HistogramTiming` instance with the given metric key and the current time as the start time.
71    #[inline]
72    fn new(metric: metrics::Key) -> Self {
73        Self {
74            key: metric,
75            start: Instant::now(),
76            skip: false,
77        }
78    }
79
80    /// Returns the elapsed time since the metric was started.
81    pub fn elapsed(&self) -> std::time::Duration {
82        self.start.elapsed()
83    }
84
85    /// Add label to the histrgram
86    pub fn add_label(&mut self, label: Label) {
87        self.key = self.key.with_extra_labels(vec![label]);
88    }
89
90    /// Cancel timing measurement and discard the metric.
91    pub fn cancel(mut self) {
92        self.skip = true;
93
94        drop(self);
95    }
96}
97
98impl Drop for HistogramTiming {
99    /// Implements the Drop trait for `HistogramTiming`.
100    ///
101    /// When a `HistogramTiming` instance is dropped, this method calculates and records the elapsed time
102    /// since the start time.
103    #[inline]
104    fn drop(&mut self) {
105        if self.skip {
106            return;
107        }
108
109        let stop = self.start.elapsed();
110        let key = self.key.name().to_string();
111
112        let labels: Vec<Label> = self.key.labels().cloned().collect();
113        histogram!(key, labels).record(stop.as_secs_f64());
114    }
115}
116
117/// `Prometheus` is a struct that represents a Prometheus exporter server.
118///
119//
120/// # Examples
121///
122/// ```
123/// use drive_abci::metrics::Prometheus;
124/// use url::Url;
125///
126/// let listen_address = Url::parse("http://127.0.0.1:57090").unwrap();
127/// let prometheus = Prometheus::new(listen_address).unwrap();
128/// ```
129pub struct Prometheus {}
130
131impl Prometheus {
132    /// Creates and starts a new Prometheus server.
133    ///
134    /// # Arguments
135    ///
136    /// * `listen_address` - A `[url::Url]` representing the address the server should listen on.
137    ///   The URL scheme must be "http". Any other scheme will result in an `Error::InvalidListenAddress`.
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use drive_abci::metrics::Prometheus;
143    /// use url::Url;
144    ///
145    /// let listen_address = Url::parse("http://127.0.0.1:43238").unwrap();
146    /// let prometheus = Prometheus::new(listen_address).unwrap();
147    /// ```
148    ///
149    /// # Errors
150    ///
151    /// Returns an `Error::InvalidListenAddress` if the provided `listen_address` has an unsupported scheme.
152    ///
153    /// # Default Port
154    ///
155    /// If the port number is not specified, it defaults to [DEFAULT_PROMETHEUS_PORT].
156    pub fn new(listen_address: url::Url) -> Result<Self, Error> {
157        if listen_address.scheme() != "http" {
158            return Err(Error::InvalidListenAddress(
159                listen_address.clone(),
160                format!("unsupported scheme {}", listen_address.scheme()),
161            ));
162        }
163
164        let saddr = listen_address
165            .socket_addrs(|| Some(DEFAULT_PROMETHEUS_PORT))
166            .map_err(|e| Error::InvalidListenAddress(listen_address.clone(), e.to_string()))?;
167        if saddr.len() > 1 {
168            tracing::warn!(
169                "too many listen addresses resolved from {}: {:?}",
170                listen_address,
171                saddr
172            )
173        }
174        let saddr = saddr.first().ok_or(Error::InvalidListenAddress(
175            listen_address,
176            "failed to resolve listen address".to_string(),
177        ))?;
178
179        let builder = PrometheusBuilder::new().with_http_listener(*saddr);
180        builder.install()?;
181
182        Self::register_metrics();
183
184        Ok(Self {})
185    }
186
187    fn register_metrics() {
188        static START: Once = Once::new();
189
190        START.call_once(|| {
191            describe_counter!(
192                COUNTER_LAST_HEIGHT,
193                "Last finalized height of platform chain (eg. Tenderdash)"
194            );
195
196            describe_counter!(
197                COUNTER_LAST_BLOCK_TIME,
198                metrics::Unit::Seconds,
199                "Time of last finalized block, seconds since epoch"
200            );
201
202            describe_histogram!(
203                HISTOGRAM_FINALIZED_ROUND,
204                "Rounds at which blocks are finalized"
205            );
206
207            describe_histogram!(
208                HISTOGRAM_ABCI_REQUEST_DURATION,
209                metrics::Unit::Seconds,
210                "Duration of ABCI request execution inside Drive per endpoint, in seconds"
211            );
212
213            describe_histogram!(
214                HISTOGRAM_QUERY_DURATION,
215                metrics::Unit::Seconds,
216                "Duration of query request execution inside Drive per endpoint, in seconds"
217            );
218
219            describe_gauge!(
220                GAUGE_CREDIT_WITHDRAWAL_LIMIT_AVAILABLE,
221                "Available withdrawal limit for last 24 hours in credits"
222            );
223
224            describe_gauge!(
225                GAUGE_CREDIT_WITHDRAWAL_LIMIT_TOTAL,
226                "Total withdrawal limit for last 24 hours in credits"
227            );
228        });
229    }
230}
231
232/// Sets the last finalized height metric to the provided height value.
233///
234/// # Examples
235///
236/// ```
237/// use drive_abci::metrics::abci_last_platform_height;
238///
239/// let height = 42;
240/// abci_last_platform_height(height);
241/// ```
242pub fn abci_last_platform_height(height: u64) {
243    counter!(COUNTER_LAST_HEIGHT).absolute(height);
244}
245
246/// Add round of last finalized round to [HISTOGRAM_FINALIZED_ROUND] metric.
247pub fn abci_last_finalized_round(round: u32) {
248    histogram!(HISTOGRAM_FINALIZED_ROUND).record(round as f64);
249}
250
251/// Set time of last block into [COUNTER_LAST_BLOCK_TIME].
252pub fn abci_last_block_time(time: u64) {
253    counter!(COUNTER_LAST_BLOCK_TIME).absolute(time);
254}
255
256/// Returns a `[HistogramTiming]` instance for measuring ABCI request duration.
257///
258/// Duration measurement starts when this function is called, and stops when returned value
259/// goes out of scope.
260///
261/// # Arguments
262///
263/// * `endpoint` - A string slice representing the ABCI endpoint name.
264///
265/// # Examples
266///
267/// ```
268/// use drive_abci::metrics::abci_request_duration;
269/// let endpoint = "check_tx";
270/// let timing = abci_request_duration(endpoint);
271/// // Your code here
272/// drop(timing); // stop measurement and report the metric
273/// ```
274pub fn abci_request_duration(endpoint: &str) -> HistogramTiming {
275    let labels = vec![Label::new(LABEL_ENDPOINT, endpoint.to_string())];
276    HistogramTiming::new(
277        metrics::Key::from_name(HISTOGRAM_ABCI_REQUEST_DURATION).with_extra_labels(labels),
278    )
279}
280
281/// Returns a `[HistogramTiming]` instance for measuring query duration.
282///
283/// Duration measurement starts when this function is called, and stops when returned value
284/// goes out of scope.
285///
286/// # Arguments
287///
288/// * `endpoint` - A string slice representing the query name.
289///
290/// # Examples
291///
292/// ```
293/// use drive_abci::metrics::query_duration_metric;
294/// let endpoint = "get_identity";
295/// let timing = query_duration_metric(endpoint);
296/// // Your code here
297/// drop(timing); // stop measurement and report the metric
298/// ```
299pub fn query_duration_metric(endpoint: &str) -> HistogramTiming {
300    let labels = vec![endpoint_metric_label(endpoint)];
301    HistogramTiming::new(
302        metrics::Key::from_name(HISTOGRAM_QUERY_DURATION).with_extra_labels(labels),
303    )
304}
305
306/// Create a label for the response code.
307pub fn abci_response_code_metric_label(code: Code) -> Label {
308    Label::new(
309        LABEL_ABCI_RESPONSE_CODE,
310        format!("{:?}", code).to_lowercase(),
311    )
312}
313
314/// Create a label for the endpoint.
315pub fn endpoint_metric_label(name: &str) -> Label {
316    Label::new(LABEL_ENDPOINT, name.to_string())
317}
318
319/// Store a histogram metric for state transition processing duration
320pub fn state_transition_execution_histogram(
321    elapsed_time: Duration,
322    state_transition_name: &str,
323    code: u32,
324) {
325    histogram!(
326        HISTOGRAM_STATE_TRANSITION_PROCESSING_DURATION,
327        vec![
328            Label::new(
329                LABEL_STATE_TRANSITION_NAME,
330                state_transition_name.to_string()
331            ),
332            Label::new(LABEL_STATE_TRANSITION_EXECUTION_CODE, code.to_string())
333        ],
334    )
335    .record(elapsed_time.as_secs_f64());
336}