1use parking_lot::RwLock;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14#[derive(Clone)]
16pub struct Metrics {
17 inner: Arc<RwLock<MetricsInner>>,
18}
19
20struct MetricsInner {
21 rpc_calls: HashMap<String, u64>,
23 rpc_call_time: HashMap<String, Duration>,
25 transaction_attempts: u64,
27 transaction_successes: u64,
29 transaction_failures: u64,
31 storage_queries: u64,
33 cache_hits: u64,
35 cache_misses: u64,
37 start_time: Instant,
39}
40
41impl Metrics {
42 pub fn new() -> Self {
44 Self {
45 inner: Arc::new(RwLock::new(MetricsInner {
46 rpc_calls: HashMap::new(),
47 rpc_call_time: HashMap::new(),
48 transaction_attempts: 0,
49 transaction_successes: 0,
50 transaction_failures: 0,
51 storage_queries: 0,
52 cache_hits: 0,
53 cache_misses: 0,
54 start_time: Instant::now(),
55 })),
56 }
57 }
58
59 pub fn record_rpc_call(&self, method: &str) {
61 let mut inner = self.inner.write();
62 *inner.rpc_calls.entry(method.to_string()).or_insert(0) += 1;
63 }
64
65 pub fn record_rpc_call_time(&self, method: &str, duration: Duration) {
67 let mut inner = self.inner.write();
68 *inner
69 .rpc_call_time
70 .entry(method.to_string())
71 .or_insert(Duration::ZERO) += duration;
72 *inner.rpc_calls.entry(method.to_string()).or_insert(0) += 1;
73 }
74
75 pub fn record_transaction_attempt(&self) {
77 self.inner.write().transaction_attempts += 1;
78 }
79
80 pub fn record_transaction_success(&self) {
82 self.inner.write().transaction_successes += 1;
83 }
84
85 pub fn record_transaction_failure(&self) {
87 self.inner.write().transaction_failures += 1;
88 }
89
90 pub fn record_storage_query(&self) {
92 self.inner.write().storage_queries += 1;
93 }
94
95 pub fn record_cache_hit(&self) {
97 self.inner.write().cache_hits += 1;
98 }
99
100 pub fn record_cache_miss(&self) {
102 self.inner.write().cache_misses += 1;
103 }
104
105 pub fn snapshot(&self) -> MetricsSnapshot {
107 let inner = self.inner.read();
108
109 let total_rpc_calls: u64 = inner.rpc_calls.values().sum();
110 let total_rpc_time: Duration = inner.rpc_call_time.values().sum();
111
112 MetricsSnapshot {
113 total_rpc_calls,
114 rpc_calls_by_method: inner.rpc_calls.clone(),
115 avg_rpc_time: if total_rpc_calls > 0 {
116 total_rpc_time / total_rpc_calls as u32
117 } else {
118 Duration::ZERO
119 },
120 transaction_attempts: inner.transaction_attempts,
121 transaction_successes: inner.transaction_successes,
122 transaction_failures: inner.transaction_failures,
123 transaction_success_rate: if inner.transaction_attempts > 0 {
124 (inner.transaction_successes as f64 / inner.transaction_attempts as f64) * 100.0
125 } else {
126 0.0
127 },
128 storage_queries: inner.storage_queries,
129 cache_hits: inner.cache_hits,
130 cache_misses: inner.cache_misses,
131 cache_hit_rate: {
132 let total_cache_requests = inner.cache_hits + inner.cache_misses;
133 if total_cache_requests > 0 {
134 (inner.cache_hits as f64 / total_cache_requests as f64) * 100.0
135 } else {
136 0.0
137 }
138 },
139 uptime: inner.start_time.elapsed(),
140 }
141 }
142
143 pub fn reset(&self) {
145 let mut inner = self.inner.write();
146 inner.rpc_calls.clear();
147 inner.rpc_call_time.clear();
148 inner.transaction_attempts = 0;
149 inner.transaction_successes = 0;
150 inner.transaction_failures = 0;
151 inner.storage_queries = 0;
152 inner.cache_hits = 0;
153 inner.cache_misses = 0;
154 inner.start_time = Instant::now();
155 }
156
157 pub fn to_prometheus(&self) -> String {
159 let snapshot = self.snapshot();
160 let mut output = String::new();
161
162 output.push_str("# HELP substrate_rpc_calls_total Total number of RPC calls\n");
164 output.push_str("# TYPE substrate_rpc_calls_total counter\n");
165 output.push_str(&format!(
166 "substrate_rpc_calls_total {}\n",
167 snapshot.total_rpc_calls
168 ));
169
170 for (method, count) in &snapshot.rpc_calls_by_method {
171 output.push_str(&format!(
172 "substrate_rpc_calls_by_method{{method=\"{}\"}} {}\n",
173 method, count
174 ));
175 }
176
177 output
179 .push_str("\n# HELP substrate_transaction_attempts_total Total transaction attempts\n");
180 output.push_str("# TYPE substrate_transaction_attempts_total counter\n");
181 output.push_str(&format!(
182 "substrate_transaction_attempts_total {}\n",
183 snapshot.transaction_attempts
184 ));
185
186 output.push_str(
187 "\n# HELP substrate_transaction_successes_total Total successful transactions\n",
188 );
189 output.push_str("# TYPE substrate_transaction_successes_total counter\n");
190 output.push_str(&format!(
191 "substrate_transaction_successes_total {}\n",
192 snapshot.transaction_successes
193 ));
194
195 output
196 .push_str("\n# HELP substrate_transaction_failures_total Total failed transactions\n");
197 output.push_str("# TYPE substrate_transaction_failures_total counter\n");
198 output.push_str(&format!(
199 "substrate_transaction_failures_total {}\n",
200 snapshot.transaction_failures
201 ));
202
203 output.push_str(
204 "\n# HELP substrate_transaction_success_rate Transaction success rate percentage\n",
205 );
206 output.push_str("# TYPE substrate_transaction_success_rate gauge\n");
207 output.push_str(&format!(
208 "substrate_transaction_success_rate {:.2}\n",
209 snapshot.transaction_success_rate
210 ));
211
212 output.push_str("\n# HELP substrate_storage_queries_total Total storage queries\n");
214 output.push_str("# TYPE substrate_storage_queries_total counter\n");
215 output.push_str(&format!(
216 "substrate_storage_queries_total {}\n",
217 snapshot.storage_queries
218 ));
219
220 output.push_str("\n# HELP substrate_cache_hits_total Total cache hits\n");
222 output.push_str("# TYPE substrate_cache_hits_total counter\n");
223 output.push_str(&format!(
224 "substrate_cache_hits_total {}\n",
225 snapshot.cache_hits
226 ));
227
228 output.push_str("\n# HELP substrate_cache_misses_total Total cache misses\n");
229 output.push_str("# TYPE substrate_cache_misses_total counter\n");
230 output.push_str(&format!(
231 "substrate_cache_misses_total {}\n",
232 snapshot.cache_misses
233 ));
234
235 output.push_str("\n# HELP substrate_cache_hit_rate Cache hit rate percentage\n");
236 output.push_str("# TYPE substrate_cache_hit_rate gauge\n");
237 output.push_str(&format!(
238 "substrate_cache_hit_rate {:.2}\n",
239 snapshot.cache_hit_rate
240 ));
241
242 output.push_str("\n# HELP substrate_uptime_seconds Uptime in seconds\n");
244 output.push_str("# TYPE substrate_uptime_seconds counter\n");
245 output.push_str(&format!(
246 "substrate_uptime_seconds {}\n",
247 snapshot.uptime.as_secs()
248 ));
249
250 output
251 }
252}
253
254impl Default for Metrics {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct MetricsSnapshot {
263 pub total_rpc_calls: u64,
265 pub rpc_calls_by_method: HashMap<String, u64>,
267 pub avg_rpc_time: Duration,
269 pub transaction_attempts: u64,
271 pub transaction_successes: u64,
273 pub transaction_failures: u64,
275 pub transaction_success_rate: f64,
277 pub storage_queries: u64,
279 pub cache_hits: u64,
281 pub cache_misses: u64,
283 pub cache_hit_rate: f64,
285 pub uptime: Duration,
287}
288
289impl std::fmt::Display for MetricsSnapshot {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 write!(
292 f,
293 "Substrate Adapter Metrics:\n\
294 RPC Calls: {}\n\
295 Avg RPC Time: {:?}\n\
296 Transactions: {} attempts, {} successes, {} failures ({:.2}% success rate)\n\
297 Storage Queries: {}\n\
298 Cache: {} hits, {} misses ({:.2}% hit rate)\n\
299 Uptime: {:?}",
300 self.total_rpc_calls,
301 self.avg_rpc_time,
302 self.transaction_attempts,
303 self.transaction_successes,
304 self.transaction_failures,
305 self.transaction_success_rate,
306 self.storage_queries,
307 self.cache_hits,
308 self.cache_misses,
309 self.cache_hit_rate,
310 self.uptime
311 )
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_metrics_creation() {
321 let metrics = Metrics::new();
322 let snapshot = metrics.snapshot();
323
324 assert_eq!(snapshot.total_rpc_calls, 0);
325 assert_eq!(snapshot.transaction_attempts, 0);
326 assert_eq!(snapshot.storage_queries, 0);
327 }
328
329 #[test]
330 fn test_rpc_call_tracking() {
331 let metrics = Metrics::new();
332
333 metrics.record_rpc_call("get_balance");
334 metrics.record_rpc_call("get_balance");
335 metrics.record_rpc_call("get_nonce");
336
337 let snapshot = metrics.snapshot();
338 assert_eq!(snapshot.total_rpc_calls, 3);
339 assert_eq!(snapshot.rpc_calls_by_method.get("get_balance"), Some(&2));
340 assert_eq!(snapshot.rpc_calls_by_method.get("get_nonce"), Some(&1));
341 }
342
343 #[test]
344 fn test_transaction_tracking() {
345 let metrics = Metrics::new();
346
347 metrics.record_transaction_attempt();
348 metrics.record_transaction_success();
349 metrics.record_transaction_attempt();
350 metrics.record_transaction_failure();
351
352 let snapshot = metrics.snapshot();
353 assert_eq!(snapshot.transaction_attempts, 2);
354 assert_eq!(snapshot.transaction_successes, 1);
355 assert_eq!(snapshot.transaction_failures, 1);
356 assert_eq!(snapshot.transaction_success_rate, 50.0);
357 }
358
359 #[test]
360 fn test_cache_tracking() {
361 let metrics = Metrics::new();
362
363 metrics.record_cache_hit();
364 metrics.record_cache_hit();
365 metrics.record_cache_hit();
366 metrics.record_cache_miss();
367
368 let snapshot = metrics.snapshot();
369 assert_eq!(snapshot.cache_hits, 3);
370 assert_eq!(snapshot.cache_misses, 1);
371 assert_eq!(snapshot.cache_hit_rate, 75.0);
372 }
373
374 #[test]
375 fn test_metrics_reset() {
376 let metrics = Metrics::new();
377
378 metrics.record_rpc_call("test");
379 metrics.record_transaction_attempt();
380 metrics.record_storage_query();
381
382 let snapshot = metrics.snapshot();
383 assert_eq!(snapshot.total_rpc_calls, 1);
384
385 metrics.reset();
386
387 let snapshot = metrics.snapshot();
388 assert_eq!(snapshot.total_rpc_calls, 0);
389 assert_eq!(snapshot.transaction_attempts, 0);
390 assert_eq!(snapshot.storage_queries, 0);
391 }
392
393 #[test]
394 fn test_prometheus_export() {
395 let metrics = Metrics::new();
396
397 metrics.record_rpc_call("get_balance");
398 metrics.record_transaction_attempt();
399 metrics.record_transaction_success();
400
401 let prometheus = metrics.to_prometheus();
402
403 assert!(prometheus.contains("substrate_rpc_calls_total"));
404 assert!(prometheus.contains("substrate_transaction_attempts_total"));
405 assert!(prometheus.contains("substrate_transaction_success_rate"));
406 }
407}