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