apex_sdk_evm/
metrics.rs

1//! Metrics and monitoring for EVM operations
2//!
3//! This module provides:
4//! - Performance metrics tracking
5//! - RPC call statistics
6//! - Transaction success/failure rates
7//! - Gas price tracking
8//! - Prometheus-compatible metrics export
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::time::Instant;
13use tokio::sync::RwLock;
14
15/// Metrics for RPC calls
16#[derive(Debug, Default)]
17pub struct RpcMetrics {
18    /// Total number of RPC calls
19    pub total_calls: AtomicU64,
20    /// Number of successful calls
21    pub successful_calls: AtomicU64,
22    /// Number of failed calls
23    pub failed_calls: AtomicU64,
24    /// Total latency in milliseconds
25    pub total_latency_ms: AtomicU64,
26    /// Number of retries
27    pub retries: AtomicU64,
28}
29
30impl RpcMetrics {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Record a successful RPC call
36    pub fn record_success(&self, latency_ms: u64) {
37        self.total_calls.fetch_add(1, Ordering::Relaxed);
38        self.successful_calls.fetch_add(1, Ordering::Relaxed);
39        self.total_latency_ms
40            .fetch_add(latency_ms, Ordering::Relaxed);
41    }
42
43    /// Record a failed RPC call
44    pub fn record_failure(&self, latency_ms: u64) {
45        self.total_calls.fetch_add(1, Ordering::Relaxed);
46        self.failed_calls.fetch_add(1, Ordering::Relaxed);
47        self.total_latency_ms
48            .fetch_add(latency_ms, Ordering::Relaxed);
49    }
50
51    /// Record a retry
52    pub fn record_retry(&self) {
53        self.retries.fetch_add(1, Ordering::Relaxed);
54    }
55
56    /// Get success rate as percentage
57    pub fn success_rate(&self) -> f64 {
58        let total = self.total_calls.load(Ordering::Relaxed);
59        if total == 0 {
60            return 100.0;
61        }
62        let successful = self.successful_calls.load(Ordering::Relaxed);
63        (successful as f64 / total as f64) * 100.0
64    }
65
66    /// Get average latency in milliseconds
67    pub fn avg_latency_ms(&self) -> f64 {
68        let total = self.total_calls.load(Ordering::Relaxed);
69        if total == 0 {
70            return 0.0;
71        }
72        let latency = self.total_latency_ms.load(Ordering::Relaxed);
73        latency as f64 / total as f64
74    }
75}
76
77/// Metrics for transactions
78#[derive(Debug, Default)]
79pub struct TransactionMetrics {
80    /// Total transactions submitted
81    pub submitted: AtomicU64,
82    /// Successful transactions
83    pub successful: AtomicU64,
84    /// Failed transactions
85    pub failed: AtomicU64,
86    /// Pending transactions
87    pub pending: AtomicU64,
88    /// Total gas used
89    pub total_gas_used: AtomicU64,
90    /// Total transaction cost in wei
91    pub total_cost_wei: AtomicU64,
92}
93
94impl TransactionMetrics {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Record a transaction submission
100    pub fn record_submission(&self) {
101        self.submitted.fetch_add(1, Ordering::Relaxed);
102        self.pending.fetch_add(1, Ordering::Relaxed);
103    }
104
105    /// Record a successful transaction
106    pub fn record_success(&self, gas_used: u64, cost_wei: u128) {
107        self.successful.fetch_add(1, Ordering::Relaxed);
108        self.pending.fetch_sub(1, Ordering::Relaxed);
109        self.total_gas_used.fetch_add(gas_used, Ordering::Relaxed);
110        // Truncate to u64 for atomic operations
111        self.total_cost_wei
112            .fetch_add((cost_wei & u64::MAX as u128) as u64, Ordering::Relaxed);
113    }
114
115    /// Record a failed transaction
116    pub fn record_failure(&self) {
117        self.failed.fetch_add(1, Ordering::Relaxed);
118        self.pending.fetch_sub(1, Ordering::Relaxed);
119    }
120
121    /// Get success rate as percentage
122    pub fn success_rate(&self) -> f64 {
123        let completed =
124            self.successful.load(Ordering::Relaxed) + self.failed.load(Ordering::Relaxed);
125        if completed == 0 {
126            return 100.0;
127        }
128        let successful = self.successful.load(Ordering::Relaxed);
129        (successful as f64 / completed as f64) * 100.0
130    }
131
132    /// Get average gas used
133    pub fn avg_gas_used(&self) -> f64 {
134        let successful = self.successful.load(Ordering::Relaxed);
135        if successful == 0 {
136            return 0.0;
137        }
138        let gas = self.total_gas_used.load(Ordering::Relaxed);
139        gas as f64 / successful as f64
140    }
141}
142
143/// Gas price tracking
144#[derive(Debug, Clone)]
145pub struct GasPriceSnapshot {
146    /// Timestamp of snapshot
147    pub timestamp: Instant,
148    /// Base fee per gas (EIP-1559)
149    pub base_fee_gwei: f64,
150    /// Priority fee per gas (EIP-1559)
151    pub priority_fee_gwei: f64,
152    /// Legacy gas price
153    pub gas_price_gwei: f64,
154}
155
156/// Metrics for gas prices
157pub struct GasMetrics {
158    recent_snapshots: Arc<RwLock<Vec<GasPriceSnapshot>>>,
159    max_snapshots: usize,
160}
161
162impl GasMetrics {
163    pub fn new(max_snapshots: usize) -> Self {
164        Self {
165            recent_snapshots: Arc::new(RwLock::new(Vec::new())),
166            max_snapshots,
167        }
168    }
169
170    /// Record a gas price snapshot
171    pub async fn record_snapshot(&self, snapshot: GasPriceSnapshot) {
172        let mut snapshots = self.recent_snapshots.write().await;
173
174        // Keep only recent snapshots
175        if snapshots.len() >= self.max_snapshots {
176            snapshots.remove(0);
177        }
178
179        snapshots.push(snapshot);
180    }
181
182    /// Get average base fee over recent snapshots
183    pub async fn avg_base_fee_gwei(&self) -> f64 {
184        let snapshots = self.recent_snapshots.read().await;
185        if snapshots.is_empty() {
186            return 0.0;
187        }
188
189        let sum: f64 = snapshots.iter().map(|s| s.base_fee_gwei).sum();
190        sum / snapshots.len() as f64
191    }
192
193    /// Get average priority fee over recent snapshots
194    pub async fn avg_priority_fee_gwei(&self) -> f64 {
195        let snapshots = self.recent_snapshots.read().await;
196        if snapshots.is_empty() {
197            return 0.0;
198        }
199
200        let sum: f64 = snapshots.iter().map(|s| s.priority_fee_gwei).sum();
201        sum / snapshots.len() as f64
202    }
203
204    /// Get gas price trend (increasing, stable, decreasing)
205    pub async fn gas_price_trend(&self) -> String {
206        let snapshots = self.recent_snapshots.read().await;
207        if snapshots.len() < 2 {
208            return "unknown".to_string();
209        }
210
211        let recent = &snapshots[snapshots.len() - 1];
212        let older = &snapshots[snapshots.len() / 2];
213
214        let diff_percent =
215            ((recent.base_fee_gwei - older.base_fee_gwei) / older.base_fee_gwei) * 100.0;
216
217        if diff_percent > 10.0 {
218            "increasing".to_string()
219        } else if diff_percent < -10.0 {
220            "decreasing".to_string()
221        } else {
222            "stable".to_string()
223        }
224    }
225}
226
227/// Comprehensive metrics collector
228pub struct MetricsCollector {
229    /// RPC call metrics
230    pub rpc: Arc<RpcMetrics>,
231    /// Transaction metrics
232    pub transactions: Arc<TransactionMetrics>,
233    /// Gas price metrics
234    pub gas: Arc<GasMetrics>,
235    /// Start time of the collector
236    start_time: Instant,
237}
238
239impl MetricsCollector {
240    /// Create a new metrics collector
241    pub fn new() -> Self {
242        Self {
243            rpc: Arc::new(RpcMetrics::new()),
244            transactions: Arc::new(TransactionMetrics::new()),
245            gas: Arc::new(GasMetrics::new(100)),
246            start_time: Instant::now(),
247        }
248    }
249
250    /// Get uptime in seconds
251    pub fn uptime_secs(&self) -> u64 {
252        self.start_time.elapsed().as_secs()
253    }
254
255    /// Export metrics in Prometheus format
256    pub async fn export_prometheus(&self) -> String {
257        let mut output = String::new();
258
259        // RPC metrics
260        output.push_str("# HELP apex_evm_rpc_calls_total Total number of RPC calls\n");
261        output.push_str("# TYPE apex_evm_rpc_calls_total counter\n");
262        output.push_str(&format!(
263            "apex_evm_rpc_calls_total {}\n",
264            self.rpc.total_calls.load(Ordering::Relaxed)
265        ));
266
267        output.push_str("# HELP apex_evm_rpc_calls_successful Successful RPC calls\n");
268        output.push_str("# TYPE apex_evm_rpc_calls_successful counter\n");
269        output.push_str(&format!(
270            "apex_evm_rpc_calls_successful {}\n",
271            self.rpc.successful_calls.load(Ordering::Relaxed)
272        ));
273
274        output.push_str("# HELP apex_evm_rpc_calls_failed Failed RPC calls\n");
275        output.push_str("# TYPE apex_evm_rpc_calls_failed counter\n");
276        output.push_str(&format!(
277            "apex_evm_rpc_calls_failed {}\n",
278            self.rpc.failed_calls.load(Ordering::Relaxed)
279        ));
280
281        output.push_str("# HELP apex_evm_rpc_latency_avg Average RPC latency in milliseconds\n");
282        output.push_str("# TYPE apex_evm_rpc_latency_avg gauge\n");
283        output.push_str(&format!(
284            "apex_evm_rpc_latency_avg {}\n",
285            self.rpc.avg_latency_ms()
286        ));
287
288        output.push_str("# HELP apex_evm_rpc_success_rate RPC success rate percentage\n");
289        output.push_str("# TYPE apex_evm_rpc_success_rate gauge\n");
290        output.push_str(&format!(
291            "apex_evm_rpc_success_rate {}\n",
292            self.rpc.success_rate()
293        ));
294
295        // Transaction metrics
296        output.push_str("# HELP apex_evm_transactions_submitted Total transactions submitted\n");
297        output.push_str("# TYPE apex_evm_transactions_submitted counter\n");
298        output.push_str(&format!(
299            "apex_evm_transactions_submitted {}\n",
300            self.transactions.submitted.load(Ordering::Relaxed)
301        ));
302
303        output.push_str("# HELP apex_evm_transactions_successful Successful transactions\n");
304        output.push_str("# TYPE apex_evm_transactions_successful counter\n");
305        output.push_str(&format!(
306            "apex_evm_transactions_successful {}\n",
307            self.transactions.successful.load(Ordering::Relaxed)
308        ));
309
310        output.push_str("# HELP apex_evm_transactions_failed Failed transactions\n");
311        output.push_str("# TYPE apex_evm_transactions_failed counter\n");
312        output.push_str(&format!(
313            "apex_evm_transactions_failed {}\n",
314            self.transactions.failed.load(Ordering::Relaxed)
315        ));
316
317        output.push_str("# HELP apex_evm_transactions_pending Pending transactions\n");
318        output.push_str("# TYPE apex_evm_transactions_pending gauge\n");
319        output.push_str(&format!(
320            "apex_evm_transactions_pending {}\n",
321            self.transactions.pending.load(Ordering::Relaxed)
322        ));
323
324        output.push_str("# HELP apex_evm_transactions_success_rate Transaction success rate\n");
325        output.push_str("# TYPE apex_evm_transactions_success_rate gauge\n");
326        output.push_str(&format!(
327            "apex_evm_transactions_success_rate {}\n",
328            self.transactions.success_rate()
329        ));
330
331        output.push_str("# HELP apex_evm_gas_avg Average gas used per transaction\n");
332        output.push_str("# TYPE apex_evm_gas_avg gauge\n");
333        output.push_str(&format!(
334            "apex_evm_gas_avg {}\n",
335            self.transactions.avg_gas_used()
336        ));
337
338        // Gas price metrics
339        output.push_str("# HELP apex_evm_gas_base_fee_avg Average base fee in gwei\n");
340        output.push_str("# TYPE apex_evm_gas_base_fee_avg gauge\n");
341        output.push_str(&format!(
342            "apex_evm_gas_base_fee_avg {}\n",
343            self.gas.avg_base_fee_gwei().await
344        ));
345
346        output.push_str("# HELP apex_evm_gas_priority_fee_avg Average priority fee in gwei\n");
347        output.push_str("# TYPE apex_evm_gas_priority_fee_avg gauge\n");
348        output.push_str(&format!(
349            "apex_evm_gas_priority_fee_avg {}\n",
350            self.gas.avg_priority_fee_gwei().await
351        ));
352
353        // Uptime
354        output.push_str("# HELP apex_evm_uptime_seconds Uptime in seconds\n");
355        output.push_str("# TYPE apex_evm_uptime_seconds counter\n");
356        output.push_str(&format!("apex_evm_uptime_seconds {}\n", self.uptime_secs()));
357
358        output
359    }
360
361    /// Print human-readable metrics summary
362    pub async fn print_summary(&self) {
363        println!("=== Apex EVM Metrics Summary ===");
364        println!("\nRPC Calls:");
365        println!("  Total: {}", self.rpc.total_calls.load(Ordering::Relaxed));
366        println!(
367            "  Successful: {}",
368            self.rpc.successful_calls.load(Ordering::Relaxed)
369        );
370        println!(
371            "  Failed: {}",
372            self.rpc.failed_calls.load(Ordering::Relaxed)
373        );
374        println!("  Success Rate: {:.2}%", self.rpc.success_rate());
375        println!("  Avg Latency: {:.2}ms", self.rpc.avg_latency_ms());
376        println!("  Retries: {}", self.rpc.retries.load(Ordering::Relaxed));
377
378        println!("\nTransactions:");
379        println!(
380            "  Submitted: {}",
381            self.transactions.submitted.load(Ordering::Relaxed)
382        );
383        println!(
384            "  Successful: {}",
385            self.transactions.successful.load(Ordering::Relaxed)
386        );
387        println!(
388            "  Failed: {}",
389            self.transactions.failed.load(Ordering::Relaxed)
390        );
391        println!(
392            "  Pending: {}",
393            self.transactions.pending.load(Ordering::Relaxed)
394        );
395        println!("  Success Rate: {:.2}%", self.transactions.success_rate());
396        println!("  Avg Gas Used: {:.0}", self.transactions.avg_gas_used());
397
398        println!("\nGas Prices:");
399        println!(
400            "  Avg Base Fee: {:.2} gwei",
401            self.gas.avg_base_fee_gwei().await
402        );
403        println!(
404            "  Avg Priority Fee: {:.2} gwei",
405            self.gas.avg_priority_fee_gwei().await
406        );
407        println!("  Trend: {}", self.gas.gas_price_trend().await);
408
409        println!("\nSystem:");
410        println!("  Uptime: {}s", self.uptime_secs());
411        println!("================================");
412    }
413}
414
415impl Default for MetricsCollector {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_rpc_metrics() {
427        let metrics = RpcMetrics::new();
428
429        metrics.record_success(100);
430        metrics.record_success(200);
431        metrics.record_failure(150);
432
433        assert_eq!(metrics.total_calls.load(Ordering::Relaxed), 3);
434        assert_eq!(metrics.successful_calls.load(Ordering::Relaxed), 2);
435        assert_eq!(metrics.failed_calls.load(Ordering::Relaxed), 1);
436        assert!((metrics.success_rate() - 66.67).abs() < 0.1);
437        assert!((metrics.avg_latency_ms() - 150.0).abs() < 0.1);
438    }
439
440    #[test]
441    fn test_transaction_metrics() {
442        let metrics = TransactionMetrics::new();
443
444        metrics.record_submission();
445        metrics.record_submission();
446        metrics.record_success(21000, 21000000000000);
447        metrics.record_failure();
448
449        assert_eq!(metrics.submitted.load(Ordering::Relaxed), 2);
450        assert_eq!(metrics.successful.load(Ordering::Relaxed), 1);
451        assert_eq!(metrics.failed.load(Ordering::Relaxed), 1);
452        assert_eq!(metrics.success_rate(), 50.0);
453    }
454
455    #[tokio::test]
456    async fn test_metrics_collector_prometheus() {
457        let collector = MetricsCollector::new();
458
459        collector.rpc.record_success(100);
460        collector.transactions.record_submission();
461
462        let output = collector.export_prometheus().await;
463
464        assert!(output.contains("apex_evm_rpc_calls_total"));
465        assert!(output.contains("apex_evm_transactions_submitted"));
466        assert!(output.contains("apex_evm_uptime_seconds"));
467    }
468}