@@ -7,6 +7,10 @@ pub struct ExecutorMetrics {
7
7
pub transaction_queued_to_sent_duration : HistogramVec ,
8
8
pub transaction_queued_to_confirmed_duration : HistogramVec ,
9
9
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 ,
10
14
}
11
15
12
16
impl ExecutorMetrics {
@@ -39,10 +43,41 @@ impl ExecutorMetrics {
39
43
registry
40
44
) ?;
41
45
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
+
42
74
Ok ( ExecutorMetrics {
43
75
transaction_queued_to_sent_duration,
44
76
transaction_queued_to_confirmed_duration,
45
77
eoa_job_processing_duration,
78
+ eoa_degraded_send_duration,
79
+ eoa_degraded_confirmation_duration,
80
+ eoa_stuck_duration,
46
81
} )
47
82
}
48
83
}
@@ -116,6 +151,76 @@ pub fn record_eoa_job_processing_time(chain_id: u64, duration_seconds: f64) {
116
151
. observe ( duration_seconds) ;
117
152
}
118
153
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
+
119
224
/// Helper to calculate duration in seconds from unix timestamps (milliseconds)
120
225
pub fn calculate_duration_seconds ( start_timestamp_ms : u64 , end_timestamp_ms : u64 ) -> f64 {
121
226
( end_timestamp_ms. saturating_sub ( start_timestamp_ms) ) as f64 / 1000.0
@@ -165,11 +270,24 @@ mod tests {
165
270
record_transaction_queued_to_confirmed ( "test" , 1 , 10.0 ) ;
166
271
record_eoa_job_processing_time ( 1 , 2.0 ) ;
167
272
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
+
168
283
// Test that default metrics can be exported
169
284
let metrics_output = export_default_metrics ( ) . expect ( "Should be able to export default metrics" ) ;
170
285
assert ! ( metrics_output. contains( "tw_engine_executor_transaction_queued_to_sent_duration_seconds" ) ) ;
171
286
assert ! ( metrics_output. contains( "tw_engine_executor_transaction_queued_to_confirmed_duration_seconds" ) ) ;
172
287
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" ) ) ;
173
291
}
174
292
175
293
#[ test]
0 commit comments