@@ -5,17 +5,21 @@ use tokio::sync::RwLock;
55
66static INSTANCE : OnceLock < ScannerMetrics > = OnceLock :: new ( ) ;
77
8+ const ERROR_WINDOW_SECS : u64 = 3600 ;
9+ const STALE_ERROR_MSG_SECS : u64 = 600 ;
10+
811pub struct ScannerMetrics {
912 pub blocks_scanned : AtomicU64 ,
1013 pub last_block_height : AtomicU64 ,
1114 pub chain_tip_height : AtomicU64 ,
1215 pub payments_detected : AtomicU64 ,
1316 pub mempool_txs_checked : AtomicU64 ,
14- pub scan_errors : AtomicU64 ,
17+ total_errors : AtomicU64 ,
1518 pub last_block_scan_ms : AtomicU64 ,
1619 pub last_mempool_scan_ms : AtomicU64 ,
1720 started_at : RwLock < Option < Instant > > ,
1821 last_error : RwLock < Option < ( String , Instant ) > > ,
22+ error_timestamps : RwLock < Vec < Instant > > ,
1923}
2024
2125impl ScannerMetrics {
@@ -26,11 +30,12 @@ impl ScannerMetrics {
2630 chain_tip_height : AtomicU64 :: new ( 0 ) ,
2731 payments_detected : AtomicU64 :: new ( 0 ) ,
2832 mempool_txs_checked : AtomicU64 :: new ( 0 ) ,
29- scan_errors : AtomicU64 :: new ( 0 ) ,
33+ total_errors : AtomicU64 :: new ( 0 ) ,
3034 last_block_scan_ms : AtomicU64 :: new ( 0 ) ,
3135 last_mempool_scan_ms : AtomicU64 :: new ( 0 ) ,
3236 started_at : RwLock :: new ( None ) ,
3337 last_error : RwLock :: new ( None ) ,
38+ error_timestamps : RwLock :: new ( Vec :: new ( ) ) ,
3439 }
3540 }
3641
@@ -67,30 +72,42 @@ impl ScannerMetrics {
6772 }
6873
6974 pub fn record_scan_error ( & self , msg : & str ) {
70- self . scan_errors . fetch_add ( 1 , Ordering :: Relaxed ) ;
75+ self . total_errors . fetch_add ( 1 , Ordering :: Relaxed ) ;
7176 if let Ok ( mut guard) = self . last_error . try_write ( ) {
7277 * guard = Some ( ( msg. to_string ( ) , Instant :: now ( ) ) ) ;
7378 }
79+ if let Ok ( mut ts) = self . error_timestamps . try_write ( ) {
80+ ts. push ( Instant :: now ( ) ) ;
81+ }
7482 }
7583
76- /// Clear error state after sustained healthy operation (10 min without errors).
77- pub async fn clear_stale_errors ( & self ) {
78- const STALE_THRESHOLD_SECS : u64 = 600 ;
79- let should_clear = self
80- . last_error
81- . read ( )
82- . await
83- . as_ref ( )
84- . map_or ( false , |( _, when) | when. elapsed ( ) . as_secs ( ) > STALE_THRESHOLD_SECS ) ;
85- if should_clear {
86- self . scan_errors . store ( 0 , Ordering :: Relaxed ) ;
87- * self . last_error . write ( ) . await = None ;
88- }
84+ /// Errors in the last hour (rolling window).
85+ pub async fn recent_errors ( & self ) -> u64 {
86+ let cutoff = Instant :: now ( ) - std:: time:: Duration :: from_secs ( ERROR_WINDOW_SECS ) ;
87+ let ts = self . error_timestamps . read ( ) . await ;
88+ ts. iter ( ) . filter ( |t| * * t > cutoff) . count ( ) as u64
89+ }
90+
91+ pub fn total_errors ( & self ) -> u64 {
92+ self . total_errors . load ( Ordering :: Relaxed )
93+ }
94+
95+ /// Evict old timestamps from the rolling window to avoid unbounded growth.
96+ pub async fn evict_old_errors ( & self ) {
97+ let cutoff = Instant :: now ( ) - std:: time:: Duration :: from_secs ( ERROR_WINDOW_SECS ) ;
98+ let mut ts = self . error_timestamps . write ( ) . await ;
99+ ts. retain ( |t| * t > cutoff) ;
89100 }
90101
102+ /// Returns last error message and age — only if the error is recent (< 10 min).
91103 pub async fn last_error ( & self ) -> Option < ( String , u64 ) > {
92- self . last_error . read ( ) . await . as_ref ( ) . map ( |( msg, when) | {
93- ( msg. clone ( ) , when. elapsed ( ) . as_secs ( ) )
104+ self . last_error . read ( ) . await . as_ref ( ) . and_then ( |( msg, when) | {
105+ let ago = when. elapsed ( ) . as_secs ( ) ;
106+ if ago < STALE_ERROR_MSG_SECS {
107+ Some ( ( msg. clone ( ) , ago) )
108+ } else {
109+ None
110+ }
94111 } )
95112 }
96113
@@ -109,7 +126,7 @@ impl ScannerMetrics {
109126 chain_tip_height : self . chain_tip_height . load ( Ordering :: Relaxed ) ,
110127 payments_detected : self . payments_detected . load ( Ordering :: Relaxed ) ,
111128 mempool_txs_checked : self . mempool_txs_checked . load ( Ordering :: Relaxed ) ,
112- scan_errors : self . scan_errors . load ( Ordering :: Relaxed ) ,
129+ total_errors : self . total_errors . load ( Ordering :: Relaxed ) ,
113130 last_block_scan_ms : self . last_block_scan_ms . load ( Ordering :: Relaxed ) ,
114131 last_mempool_scan_ms : self . last_mempool_scan_ms . load ( Ordering :: Relaxed ) ,
115132 }
@@ -123,7 +140,7 @@ pub struct MetricsSnapshot {
123140 pub chain_tip_height : u64 ,
124141 pub payments_detected : u64 ,
125142 pub mempool_txs_checked : u64 ,
126- pub scan_errors : u64 ,
143+ pub total_errors : u64 ,
127144 pub last_block_scan_ms : u64 ,
128145 pub last_mempool_scan_ms : u64 ,
129146}
0 commit comments