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 HISTOGRAM_FINALIZED_ROUND: &str = "abci_finalized_round";
18const HISTOGRAM_ABCI_REQUEST_DURATION: &str = "abci_request_duration_seconds";
19const HISTOGRAM_STATE_TRANSITION_PROCESSING_DURATION: &str =
21 "state_transition_processing_duration_seconds";
22const LABEL_ENDPOINT: &str = "endpoint";
23pub const LABEL_ABCI_RESPONSE_CODE: &str = "response_code";
25const HISTOGRAM_QUERY_DURATION: &str = "abci_query_duration";
26pub const LABEL_STATE_TRANSITION_NAME: &str = "st_name";
28const LABEL_STATE_TRANSITION_EXECUTION_CODE: &str = "st_exec_code";
30pub const LABEL_CHECK_TX_MODE: &str = "check_tx_mode";
32pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_AVAILABLE: &str = "credit_withdrawal_limit_available";
34pub const GAUGE_CREDIT_WITHDRAWAL_LIMIT_TOTAL: &str = "credit_withdrawal_limit_total";
36
37#[derive(thiserror::Error, Debug)]
39pub enum Error {
40 #[error("prometheus server: {0}")]
42 ServerFailed(#[from] metrics_exporter_prometheus::BuildError),
43 #[error("invalid listen address {0}: {1}")]
45 InvalidListenAddress(url::Url, String),
46}
47
48pub struct HistogramTiming {
56 key: metrics::Key,
57 start: Instant,
58 skip: bool,
59}
60
61impl HistogramTiming {
62 #[inline]
72 fn new(metric: metrics::Key) -> Self {
73 Self {
74 key: metric,
75 start: Instant::now(),
76 skip: false,
77 }
78 }
79
80 pub fn elapsed(&self) -> std::time::Duration {
82 self.start.elapsed()
83 }
84
85 pub fn add_label(&mut self, label: Label) {
87 self.key = self.key.with_extra_labels(vec![label]);
88 }
89
90 pub fn cancel(mut self) {
92 self.skip = true;
93
94 drop(self);
95 }
96}
97
98impl Drop for HistogramTiming {
99 #[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
117pub struct Prometheus {}
130
131impl Prometheus {
132 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
232pub fn abci_last_platform_height(height: u64) {
243 counter!(COUNTER_LAST_HEIGHT).absolute(height);
244}
245
246pub fn abci_last_finalized_round(round: u32) {
248 histogram!(HISTOGRAM_FINALIZED_ROUND).record(round as f64);
249}
250
251pub fn abci_last_block_time(time: u64) {
253 counter!(COUNTER_LAST_BLOCK_TIME).absolute(time);
254}
255
256pub 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
281pub 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
306pub 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
314pub fn endpoint_metric_label(name: &str) -> Label {
316 Label::new(LABEL_ENDPOINT, name.to_string())
317}
318
319pub 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}