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 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}