partner_chains_data_source_metrics/
lib.rs

1//! Substrate Prometheus metrics client for Partner Chain data sources
2use log::warn;
3use substrate_prometheus_endpoint::{
4	CounterVec, HistogramOpts, HistogramVec, Opts, PrometheusError, Registry, U64, register,
5};
6
7pub type MetricsRegistry = Registry;
8
9/// Substrate Prometheus metrics client used by Partner Chain data sources
10#[derive(Clone)]
11pub struct McFollowerMetrics {
12	time_elapsed: HistogramVec,
13	call_count: CounterVec<U64>,
14}
15
16impl McFollowerMetrics {
17	pub fn time_elapsed(&self) -> &HistogramVec {
18		&self.time_elapsed
19	}
20	pub fn call_count(&self) -> &CounterVec<U64> {
21		&self.call_count
22	}
23	pub fn register(registry: &Registry) -> Result<Self, PrometheusError> {
24		Ok(Self {
25			time_elapsed: register(
26				HistogramVec::new(
27					HistogramOpts::new(
28						"partner_chains_data_source_method_time_elapsed",
29						"Time spent in a method call",
30					),
31					&["method_name"],
32				)?,
33				registry,
34			)?,
35			call_count: register(
36				CounterVec::new(
37					Opts::new(
38						"partner_chains_data_source_method_call_count",
39						"Total number of data source method calls",
40					),
41					&["method_name"],
42				)?,
43				registry,
44			)?,
45		})
46	}
47}
48
49/// Registers new metrics with Substrate Prometheus metrics service and returns a client instance
50pub fn register_metrics_warn_errors(
51	metrics_registry_opt: Option<&Registry>,
52) -> Option<McFollowerMetrics> {
53	metrics_registry_opt.and_then(|registry| match McFollowerMetrics::register(registry) {
54		Ok(metrics) => Some(metrics),
55		Err(err) => {
56			warn!("Failed registering data source metrics with err: {}", err);
57			None
58		},
59	})
60}
61
62pub use async_trait::async_trait;
63
64/// Logs each method invocation and each returned result.
65/// Has to be made at the level of trait, because otherwise #[async_trait] is expanded first.
66/// '&self' matching yields "__self" identifier not found error, so "&$self:tt" is required.
67/// Works only if return type is Result.
68#[macro_export]
69macro_rules! observed_async_trait {
70	(impl $(<$($type_param:tt),+>)? $trait_name:ident $(<$($type_arg:ident),+>)? for $target_type:ty
71		$(where $($where_type:ident : $where_bound:tt ,)+)?
72
73		{
74		$(type $type_name:ident = $type:ty;)*
75		$(async fn $method:ident(&$self:tt $(,$param_name:ident: $param_type:ty)* $(,)?) -> $res:ty $body:block)*
76	})=> {
77		#[$crate::async_trait]
78		impl $(<$($type_param),+>)? $trait_name $(<$($type_arg),+>)? for $target_type
79		$(where $($where_type : $where_bound ,)+)?
80
81		{
82		$(type $type_name = $type;)*
83		$(
84			async fn $method(&$self $(,$param_name: $param_type)*,) -> $res {
85				let method_name = stringify!($method);
86				let _timer = if let Some(metrics) = &$self.metrics_opt {
87					metrics.call_count().with_label_values(&[method_name]).inc();
88					Some(metrics.time_elapsed().with_label_values(&[method_name]).start_timer())
89				} else { None };
90				let params: Vec<String> = vec![$(format!("{:?}", $param_name.clone()),)*];
91				log::debug!("{} called with parameters: {:?}", method_name, params);
92				let result = $body;
93				match &result {
94					Ok(value) => {
95						log::debug!("{} returns {:?}", method_name, value);
96					},
97					Err(error) => {
98						log::error!("{} failed with {:?}", method_name, error);
99					},
100				};
101				result
102			}
103		)*
104		}
105	};
106}
107
108pub mod mock {
109	use crate::McFollowerMetrics;
110	use substrate_prometheus_endpoint::{CounterVec, HistogramOpts, HistogramVec, Opts};
111
112	pub fn test_metrics() -> McFollowerMetrics {
113		McFollowerMetrics {
114			time_elapsed: HistogramVec::new(HistogramOpts::new("test", "test"), &["method_name"])
115				.unwrap(),
116			call_count: CounterVec::new(Opts::new("test", "test"), &["method_name"]).unwrap(),
117		}
118	}
119}
120
121#[cfg(test)]
122mod tests {
123	use crate::{McFollowerMetrics, mock::test_metrics};
124	use async_trait::async_trait;
125	use std::convert::Infallible;
126	use substrate_prometheus_endpoint::prometheus::core::Metric;
127
128	struct MetricsMacroTestStruct {
129		metrics_opt: Option<McFollowerMetrics>,
130	}
131
132	#[async_trait]
133	trait MetricMacroTestTrait {
134		async fn test_method_one(&self) -> Result<(), Infallible>;
135		async fn test_method_two(&self) -> Result<(), Infallible>;
136	}
137
138	observed_async_trait!(
139	impl MetricMacroTestTrait for MetricsMacroTestStruct {
140		async fn test_method_one(&self) -> Result<(), Infallible> {
141			tokio::time::sleep(core::time::Duration::from_millis(10)).await;
142			Ok(())
143		}
144
145		async fn test_method_two(&self) -> Result<(), Infallible> {
146			Ok(())
147		}
148	});
149
150	#[tokio::test]
151	async fn calculate_metrics_correctly() {
152		let metrics = test_metrics();
153		let histogram_method_one = metrics.time_elapsed().with_label_values(&["test_method_one"]);
154		let histogram_method_two = metrics.time_elapsed().with_label_values(&["test_method_two"]);
155		let histogram_method_random = metrics.time_elapsed().with_label_values(&["random"]);
156		let counter_method_one = metrics.call_count().with_label_values(&["test_method_one"]);
157		let counter_method_two = metrics.call_count().with_label_values(&["test_method_two"]);
158		let counter_method_random = metrics.call_count().with_label_values(&["random"]);
159
160		let metrics_struct = MetricsMacroTestStruct { metrics_opt: Some(metrics.clone()) };
161		metrics_struct.test_method_one().await.unwrap();
162		metrics_struct.test_method_two().await.unwrap();
163
164		for bucket in histogram_method_one.metric().get_histogram().get_bucket().iter().take(2) {
165			assert_eq!(bucket.get_cumulative_count(), 0);
166		}
167
168		// Assert below has a tiny potential to be flaky - if it is, please increase sleep time in MetricMacroTestTrait implementation or
169		// remove the Assert completely
170		assert!(histogram_method_one.get_sample_sum() > histogram_method_two.get_sample_sum());
171		assert_eq!(histogram_method_one.get_sample_count(), 1);
172		assert_eq!(histogram_method_two.get_sample_count(), 1);
173		assert_eq!(histogram_method_random.get_sample_count(), 0);
174
175		assert_eq!(counter_method_one.get(), 1);
176		assert_eq!(counter_method_two.get(), 1);
177		assert_eq!(counter_method_random.get(), 0);
178	}
179}