1use 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
12pub const DEFAULT_PROMETHEUS_PORT: u16 = 29090;
14const 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";
21const HISTOGRAM_STATE_TRANSITION_PROCESSING_DURATION: &str =
23 "state_transition_processing_duration_seconds";
24const LABEL_ENDPOINT: &str = "endpoint";
25pub const LABEL_ABCI_RESPONSE_CODE: &str = "response_code";
27const HISTOGRAM_QUERY_DURATION: &str = "abci_query_duration";
28pub const LABEL_STATE_TRANSITION_NAME: &str = "st_name";
30const LABEL_STATE_TRANSITION_EXECUTION_CODE: &str = "st_exec_code";
32pub const LABEL_CHECK_TX_MODE: &str = "check_tx_mode";
34pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_AVAILABLE: &str = "credit_withdrawal_limit_available";
36pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_TOTAL: &str = "credit_withdrawal_limit_total";
38
39#[derive(thiserror::Error, Debug)]
41pub enum Error {
42 #[error("prometheus server: {0}")]
44 ServerFailed(#[from] metrics_exporter_prometheus::BuildError),
45 #[error("invalid listen address {0}: {1}")]
47 InvalidListenAddress(url::Url, String),
48}
49
50pub struct HistogramTiming {
58 key: metrics::Key,
59 start: Instant,
60 skip: bool,
61}
62
63impl HistogramTiming {
64 #[inline]
74 fn new(metric: metrics::Key) -> Self {
75 Self {
76 key: metric,
77 start: Instant::now(),
78 skip: false,
79 }
80 }
81
82 pub fn elapsed(&self) -> std::time::Duration {
84 self.start.elapsed()
85 }
86
87 pub fn add_label(&mut self, label: Label) {
89 self.key = self.key.with_extra_labels(vec![label]);
90 }
91
92 pub fn cancel(mut self) {
94 self.skip = true;
95
96 drop(self);
97 }
98}
99
100impl Drop for HistogramTiming {
101 #[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
119pub struct Prometheus {}
132
133impl Prometheus {
134 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
244pub fn abci_last_platform_height(height: u64) {
255 counter!(COUNTER_LAST_HEIGHT).absolute(height);
256}
257
258pub fn abci_last_finalized_round(round: u32) {
260 histogram!(HISTOGRAM_FINALIZED_ROUND).record(round as f64);
261}
262
263pub fn abci_last_block_time(time: u64) {
265 counter!(COUNTER_LAST_BLOCK_TIME).absolute(time);
266}
267
268pub fn abci_last_checkpoint_height(height: u64) {
270 counter!(COUNTER_LAST_CHECKPOINT_HEIGHT).absolute(height);
271}
272
273pub fn abci_checkpoint_failed() {
275 counter!(COUNTER_CHECKPOINT_FAILURES).increment(1);
276}
277
278pub 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
303pub 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
328pub 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
336pub fn endpoint_metric_label(name: &str) -> Label {
338 Label::new(LABEL_ENDPOINT, name.to_string())
339}
340
341pub 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}