Skip to main content

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