apex_sdk_substrate/
metrics.rs

1//! Metrics collection for Substrate adapter
2//!
3//! This module provides comprehensive metrics tracking including:
4//! - RPC call statistics
5//! - Transaction metrics
6//! - Storage query tracking
7//! - Performance monitoring
8
9use parking_lot::RwLock;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14/// Metrics collector for tracking adapter performance
15#[derive(Clone)]
16pub struct Metrics {
17    inner: Arc<RwLock<MetricsInner>>,
18}
19
20struct MetricsInner {
21    /// Total RPC calls made
22    rpc_calls: HashMap<String, u64>,
23    /// Total RPC call time
24    rpc_call_time: HashMap<String, Duration>,
25    /// Transaction attempts
26    transaction_attempts: u64,
27    /// Successful transactions
28    transaction_successes: u64,
29    /// Failed transactions
30    transaction_failures: u64,
31    /// Storage queries
32    storage_queries: u64,
33    /// Cache hits
34    cache_hits: u64,
35    /// Cache misses
36    cache_misses: u64,
37    /// Start time
38    start_time: Instant,
39}
40
41impl Metrics {
42    /// Create a new metrics collector
43    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    /// Record an RPC call
60    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    /// Record RPC call time
66    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    /// Record a transaction attempt
76    pub fn record_transaction_attempt(&self) {
77        self.inner.write().transaction_attempts += 1;
78    }
79
80    /// Record a successful transaction
81    pub fn record_transaction_success(&self) {
82        self.inner.write().transaction_successes += 1;
83    }
84
85    /// Record a failed transaction
86    pub fn record_transaction_failure(&self) {
87        self.inner.write().transaction_failures += 1;
88    }
89
90    /// Record a storage query
91    pub fn record_storage_query(&self) {
92        self.inner.write().storage_queries += 1;
93    }
94
95    /// Record a cache hit
96    pub fn record_cache_hit(&self) {
97        self.inner.write().cache_hits += 1;
98    }
99
100    /// Record a cache miss
101    pub fn record_cache_miss(&self) {
102        self.inner.write().cache_misses += 1;
103    }
104
105    /// Get a snapshot of current metrics
106    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    /// Reset all metrics
144    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    /// Export metrics in Prometheus format
158    pub fn to_prometheus(&self) -> String {
159        let snapshot = self.snapshot();
160        let mut output = String::new();
161
162        // RPC metrics
163        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        // Transaction metrics
178        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        // Storage metrics
213        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        // Cache metrics
221        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        // Uptime
243        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/// Snapshot of metrics at a point in time
261#[derive(Debug, Clone)]
262pub struct MetricsSnapshot {
263    /// Total RPC calls
264    pub total_rpc_calls: u64,
265    /// RPC calls by method
266    pub rpc_calls_by_method: HashMap<String, u64>,
267    /// Average RPC call time
268    pub avg_rpc_time: Duration,
269    /// Transaction attempts
270    pub transaction_attempts: u64,
271    /// Successful transactions
272    pub transaction_successes: u64,
273    /// Failed transactions
274    pub transaction_failures: u64,
275    /// Transaction success rate (percentage)
276    pub transaction_success_rate: f64,
277    /// Storage queries
278    pub storage_queries: u64,
279    /// Cache hits
280    pub cache_hits: u64,
281    /// Cache misses
282    pub cache_misses: u64,
283    /// Cache hit rate (percentage)
284    pub cache_hit_rate: f64,
285    /// Uptime
286    pub uptime: Duration,
287}
288
289impl MetricsSnapshot {
290    /// Format metrics as a human-readable string
291    #[allow(clippy::inherent_to_string_shadow_display)]
292    pub fn to_string(&self) -> String {
293        format!(
294            "Substrate Adapter Metrics:\n\
295             RPC Calls: {}\n\
296             Avg RPC Time: {:?}\n\
297             Transactions: {} attempts, {} successes, {} failures ({:.2}% success rate)\n\
298             Storage Queries: {}\n\
299             Cache: {} hits, {} misses ({:.2}% hit rate)\n\
300             Uptime: {:?}",
301            self.total_rpc_calls,
302            self.avg_rpc_time,
303            self.transaction_attempts,
304            self.transaction_successes,
305            self.transaction_failures,
306            self.transaction_success_rate,
307            self.storage_queries,
308            self.cache_hits,
309            self.cache_misses,
310            self.cache_hit_rate,
311            self.uptime
312        )
313    }
314}
315
316impl std::fmt::Display for MetricsSnapshot {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        write!(f, "{}", self.to_string())
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_metrics_creation() {
328        let metrics = Metrics::new();
329        let snapshot = metrics.snapshot();
330
331        assert_eq!(snapshot.total_rpc_calls, 0);
332        assert_eq!(snapshot.transaction_attempts, 0);
333        assert_eq!(snapshot.storage_queries, 0);
334    }
335
336    #[test]
337    fn test_rpc_call_tracking() {
338        let metrics = Metrics::new();
339
340        metrics.record_rpc_call("get_balance");
341        metrics.record_rpc_call("get_balance");
342        metrics.record_rpc_call("get_nonce");
343
344        let snapshot = metrics.snapshot();
345        assert_eq!(snapshot.total_rpc_calls, 3);
346        assert_eq!(snapshot.rpc_calls_by_method.get("get_balance"), Some(&2));
347        assert_eq!(snapshot.rpc_calls_by_method.get("get_nonce"), Some(&1));
348    }
349
350    #[test]
351    fn test_transaction_tracking() {
352        let metrics = Metrics::new();
353
354        metrics.record_transaction_attempt();
355        metrics.record_transaction_success();
356        metrics.record_transaction_attempt();
357        metrics.record_transaction_failure();
358
359        let snapshot = metrics.snapshot();
360        assert_eq!(snapshot.transaction_attempts, 2);
361        assert_eq!(snapshot.transaction_successes, 1);
362        assert_eq!(snapshot.transaction_failures, 1);
363        assert_eq!(snapshot.transaction_success_rate, 50.0);
364    }
365
366    #[test]
367    fn test_cache_tracking() {
368        let metrics = Metrics::new();
369
370        metrics.record_cache_hit();
371        metrics.record_cache_hit();
372        metrics.record_cache_hit();
373        metrics.record_cache_miss();
374
375        let snapshot = metrics.snapshot();
376        assert_eq!(snapshot.cache_hits, 3);
377        assert_eq!(snapshot.cache_misses, 1);
378        assert_eq!(snapshot.cache_hit_rate, 75.0);
379    }
380
381    #[test]
382    fn test_metrics_reset() {
383        let metrics = Metrics::new();
384
385        metrics.record_rpc_call("test");
386        metrics.record_transaction_attempt();
387        metrics.record_storage_query();
388
389        let snapshot = metrics.snapshot();
390        assert_eq!(snapshot.total_rpc_calls, 1);
391
392        metrics.reset();
393
394        let snapshot = metrics.snapshot();
395        assert_eq!(snapshot.total_rpc_calls, 0);
396        assert_eq!(snapshot.transaction_attempts, 0);
397        assert_eq!(snapshot.storage_queries, 0);
398    }
399
400    #[test]
401    fn test_prometheus_export() {
402        let metrics = Metrics::new();
403
404        metrics.record_rpc_call("get_balance");
405        metrics.record_transaction_attempt();
406        metrics.record_transaction_success();
407
408        let prometheus = metrics.to_prometheus();
409
410        assert!(prometheus.contains("substrate_rpc_calls_total"));
411        assert!(prometheus.contains("substrate_transaction_attempts_total"));
412        assert!(prometheus.contains("substrate_transaction_success_rate"));
413    }
414}