@@ -7,6 +7,10 @@ pub struct ExecutorMetrics {
77 pub transaction_queued_to_sent_duration : HistogramVec ,
88 pub transaction_queued_to_confirmed_duration : HistogramVec ,
99 pub eoa_job_processing_duration : HistogramVec ,
10+ // EOA degradation and stuck metrics (low cardinality - only problematic EOAs)
11+ pub eoa_degraded_send_duration : HistogramVec ,
12+ pub eoa_degraded_confirmation_duration : HistogramVec ,
13+ pub eoa_stuck_duration : HistogramVec ,
1014}
1115
1216impl ExecutorMetrics {
@@ -39,10 +43,41 @@ impl ExecutorMetrics {
3943 registry
4044 ) ?;
4145
46+ // EOA degradation and stuck metrics (low cardinality - only problematic EOAs)
47+ let eoa_degraded_send_duration = register_histogram_vec_with_registry ! (
48+ HistogramOpts :: new(
49+ "tw_engine_executor_eoa_degraded_send_duration_seconds" ,
50+ "Duration of EOA transactions that exceeded the send degradation threshold"
51+ ) . buckets( vec![ 5.0 , 10.0 , 20.0 , 30.0 , 60.0 , 120.0 , 300.0 , 600.0 ] ) ,
52+ & [ "eoa_address" , "chain_id" ] ,
53+ registry
54+ ) ?;
55+
56+ let eoa_degraded_confirmation_duration = register_histogram_vec_with_registry ! (
57+ HistogramOpts :: new(
58+ "tw_engine_executor_eoa_degraded_confirmation_duration_seconds" ,
59+ "Duration of EOA transactions that exceeded the confirmation degradation threshold"
60+ ) . buckets( vec![ 30.0 , 45.0 , 60.0 , 120.0 , 300.0 , 600.0 , 1200.0 , 1800.0 , 3600.0 ] ) ,
61+ & [ "eoa_address" , "chain_id" ] ,
62+ registry
63+ ) ?;
64+
65+ let eoa_stuck_duration = register_histogram_vec_with_registry ! (
66+ HistogramOpts :: new(
67+ "tw_engine_executor_eoa_stuck_duration_seconds" ,
68+ "Duration since last nonce movement for EOAs that are considered stuck"
69+ ) . buckets( vec![ 200.0 , 300.0 , 600.0 , 1200.0 , 1800.0 , 3600.0 , 7200.0 , 14400.0 ] ) ,
70+ & [ "eoa_address" , "chain_id" ] ,
71+ registry
72+ ) ?;
73+
4274 Ok ( ExecutorMetrics {
4375 transaction_queued_to_sent_duration,
4476 transaction_queued_to_confirmed_duration,
4577 eoa_job_processing_duration,
78+ eoa_degraded_send_duration,
79+ eoa_degraded_confirmation_duration,
80+ eoa_stuck_duration,
4681 } )
4782 }
4883}
@@ -116,6 +151,76 @@ pub fn record_eoa_job_processing_time(chain_id: u64, duration_seconds: f64) {
116151 . observe ( duration_seconds) ;
117152}
118153
154+ /// EOA Metrics abstraction that encapsulates configuration and provides clean interface
155+ #[ derive( Debug , Clone ) ]
156+ pub struct EoaMetrics {
157+ pub send_degradation_threshold_seconds : u64 ,
158+ pub confirmation_degradation_threshold_seconds : u64 ,
159+ pub stuck_threshold_seconds : u64 ,
160+ }
161+
162+ impl EoaMetrics {
163+ /// Create new EoaMetrics with configuration
164+ pub fn new (
165+ send_degradation_threshold_seconds : u64 ,
166+ confirmation_degradation_threshold_seconds : u64 ,
167+ stuck_threshold_seconds : u64 ,
168+ ) -> Self {
169+ Self {
170+ send_degradation_threshold_seconds,
171+ confirmation_degradation_threshold_seconds,
172+ stuck_threshold_seconds,
173+ }
174+ }
175+
176+ /// Record EOA transaction send metrics with automatic degradation detection
177+ pub fn record_transaction_sent ( & self , eoa_address : alloy:: primitives:: Address , chain_id : u64 , duration_seconds : f64 ) {
178+ // Always record the regular metric
179+ record_transaction_queued_to_sent ( "eoa" , chain_id, duration_seconds) ;
180+
181+ // Only record degraded metric if threshold exceeded (low cardinality)
182+ if duration_seconds > self . send_degradation_threshold_seconds as f64 {
183+ let metrics = get_metrics ( ) ;
184+ metrics. eoa_degraded_send_duration
185+ . with_label_values ( & [ & eoa_address. to_string ( ) , & chain_id. to_string ( ) ] )
186+ . observe ( duration_seconds) ;
187+ }
188+ }
189+
190+ /// Record EOA transaction confirmation metrics with automatic degradation detection
191+ pub fn record_transaction_confirmed ( & self , eoa_address : alloy:: primitives:: Address , chain_id : u64 , duration_seconds : f64 ) {
192+ // Always record the regular metric
193+ record_transaction_queued_to_confirmed ( "eoa" , chain_id, duration_seconds) ;
194+
195+ // Only record degraded metric if threshold exceeded (low cardinality)
196+ if duration_seconds > self . confirmation_degradation_threshold_seconds as f64 {
197+ let metrics = get_metrics ( ) ;
198+ metrics. eoa_degraded_confirmation_duration
199+ . with_label_values ( & [ & eoa_address. to_string ( ) , & chain_id. to_string ( ) ] )
200+ . observe ( duration_seconds) ;
201+ }
202+ }
203+
204+ /// Record stuck EOA metric when nonce hasn't moved for too long
205+ pub fn record_stuck_eoa ( & self , eoa_address : alloy:: primitives:: Address , chain_id : u64 , time_since_last_movement_seconds : f64 ) {
206+ // Only record if EOA is actually stuck (exceeds threshold)
207+ if time_since_last_movement_seconds > self . stuck_threshold_seconds as f64 {
208+ let metrics = get_metrics ( ) ;
209+ metrics. eoa_stuck_duration
210+ . with_label_values ( & [ & eoa_address. to_string ( ) , & chain_id. to_string ( ) ] )
211+ . observe ( time_since_last_movement_seconds) ;
212+ }
213+ }
214+
215+ /// Check if an EOA should be considered stuck based on time since last nonce movement
216+ pub fn is_stuck ( & self , time_since_last_movement_ms : u64 ) -> bool {
217+ let time_since_last_movement_seconds = time_since_last_movement_ms as f64 / 1000.0 ;
218+ time_since_last_movement_seconds > self . stuck_threshold_seconds as f64
219+ }
220+ }
221+
222+
223+
119224/// Helper to calculate duration in seconds from unix timestamps (milliseconds)
120225pub fn calculate_duration_seconds ( start_timestamp_ms : u64 , end_timestamp_ms : u64 ) -> f64 {
121226 ( end_timestamp_ms. saturating_sub ( start_timestamp_ms) ) as f64 / 1000.0
@@ -165,11 +270,24 @@ mod tests {
165270 record_transaction_queued_to_confirmed ( "test" , 1 , 10.0 ) ;
166271 record_eoa_job_processing_time ( 1 , 2.0 ) ;
167272
273+ // Test new EOA metrics abstraction
274+ let eoa_metrics = EoaMetrics :: new ( 10 , 120 , 600 ) ;
275+ let test_address = "0x1234567890123456789012345678901234567890" . parse ( ) . unwrap ( ) ;
276+
277+ eoa_metrics. record_transaction_sent ( test_address, 1 , 5.0 ) ; // Won't record degradation (below threshold)
278+ eoa_metrics. record_transaction_sent ( test_address, 1 , 15.0 ) ; // Will record degradation (above threshold)
279+ eoa_metrics. record_transaction_confirmed ( test_address, 1 , 60.0 ) ; // Won't record degradation (below threshold)
280+ eoa_metrics. record_transaction_confirmed ( test_address, 1 , 180.0 ) ; // Will record degradation (above threshold)
281+ eoa_metrics. record_stuck_eoa ( test_address, 1 , 900.0 ) ; // Will record stuck EOA
282+
168283 // Test that default metrics can be exported
169284 let metrics_output = export_default_metrics ( ) . expect ( "Should be able to export default metrics" ) ;
170285 assert ! ( metrics_output. contains( "tw_engine_executor_transaction_queued_to_sent_duration_seconds" ) ) ;
171286 assert ! ( metrics_output. contains( "tw_engine_executor_transaction_queued_to_confirmed_duration_seconds" ) ) ;
172287 assert ! ( metrics_output. contains( "tw_engine_eoa_executor_job_processing_duration_seconds" ) ) ;
288+ assert ! ( metrics_output. contains( "tw_engine_executor_eoa_degraded_send_duration_seconds" ) ) ;
289+ assert ! ( metrics_output. contains( "tw_engine_executor_eoa_degraded_confirmation_duration_seconds" ) ) ;
290+ assert ! ( metrics_output. contains( "tw_engine_executor_eoa_stuck_duration_seconds" ) ) ;
173291 }
174292
175293 #[ test]
0 commit comments