@@ -21,6 +21,11 @@ pub struct Metrics {
2121 pub failed_calls_by_type : DashMap < String , u64 > ,
2222 /// Number of failed RPC calls by method
2323 pub failed_calls_by_method : DashMap < String , u64 > ,
24+ /// Duration histogram buckets (in milliseconds)
25+ /// Buckets: <10ms, 10-50ms, 50-100ms, 100-500ms, 500-1000ms, >1000ms
26+ pub duration_buckets : [ AtomicU64 ; 6 ] ,
27+ /// Total duration sum for average calculation
28+ pub total_duration_ms : AtomicU64 ,
2429}
2530
2631impl Metrics {
@@ -29,20 +34,40 @@ impl Metrics {
2934 self . total_calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
3035 }
3136
32- /// Increment successful calls counter
33- pub fn increment_successful_calls ( & self ) {
37+ /// Increment successful calls counter and record duration
38+ pub fn increment_successful_calls ( & self , duration_ms : u64 ) {
3439 self . successful_calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
40+ self . record_duration ( duration_ms) ;
3541 }
3642
37- /// Increment failed calls counter by error type
38- pub fn increment_failed_calls ( & self , error_type : & str , method : Option < & str > ) {
43+ /// Record duration in appropriate histogram bucket
44+ fn record_duration ( & self , duration_ms : u64 ) {
45+ self . total_duration_ms . fetch_add ( duration_ms, Ordering :: Relaxed ) ;
46+
47+ let bucket_index = match duration_ms {
48+ 0 ..=9 => 0 , // <10ms
49+ 10 ..=49 => 1 , // 10-50ms
50+ 50 ..=99 => 2 , // 50-100ms
51+ 100 ..=499 => 3 , // 100-500ms
52+ 500 ..=999 => 4 , // 500-1000ms
53+ _ => 5 , // >1000ms
54+ } ;
55+
56+ self . duration_buckets [ bucket_index] . fetch_add ( 1 , Ordering :: Relaxed ) ;
57+ }
58+
59+ /// Increment failed calls counter by error type and record duration
60+ pub fn increment_failed_calls ( & self , error_type : & str , method : Option < & str > , duration_ms : u64 ) {
3961 // Increment by error type using dashmap for concurrent access
4062 * self . failed_calls_by_type . entry ( error_type. to_string ( ) ) . or_insert ( 0 ) += 1 ;
4163
4264 // Increment by method if available
4365 if let Some ( method) = method {
4466 * self . failed_calls_by_method . entry ( method. to_string ( ) ) . or_insert ( 0 ) += 1 ;
4567 }
68+
69+ // Record duration for failed requests too
70+ self . record_duration ( duration_ms) ;
4671 }
4772
4873 /// Get current metrics as JSON value
@@ -57,19 +82,42 @@ impl Metrics {
5782 . iter ( )
5883 . map ( |entry| ( entry. key ( ) . clone ( ) , * entry. value ( ) ) )
5984 . collect ( ) ;
85+
86+ let total_calls = self . total_calls . load ( Ordering :: Relaxed ) ;
87+ let total_duration = self . total_duration_ms . load ( Ordering :: Relaxed ) ;
88+ let avg_duration = if total_calls > 0 { total_duration / total_calls } else { 0 } ;
89+
90+ // Collect histogram data
91+ let histogram = [
92+ ( "0-9ms" , self . duration_buckets [ 0 ] . load ( Ordering :: Relaxed ) ) ,
93+ ( "10-49ms" , self . duration_buckets [ 1 ] . load ( Ordering :: Relaxed ) ) ,
94+ ( "50-99ms" , self . duration_buckets [ 2 ] . load ( Ordering :: Relaxed ) ) ,
95+ ( "100-499ms" , self . duration_buckets [ 3 ] . load ( Ordering :: Relaxed ) ) ,
96+ ( "500-999ms" , self . duration_buckets [ 4 ] . load ( Ordering :: Relaxed ) ) ,
97+ ( "1000ms+" , self . duration_buckets [ 5 ] . load ( Ordering :: Relaxed ) ) ,
98+ ] ;
6099
61100 serde_json:: json!( {
62- "total_calls" : self . total_calls. load ( Ordering :: Relaxed ) ,
101+ "total_calls" : total_calls,
63102 "successful_calls" : self . successful_calls. load( Ordering :: Relaxed ) ,
64103 "failed_calls_by_type" : failed_by_type,
65- "failed_calls_by_method" : failed_by_method
104+ "failed_calls_by_method" : failed_by_method,
105+ "performance" : {
106+ "avg_duration_ms" : avg_duration,
107+ "total_duration_ms" : total_duration,
108+ "duration_histogram" : histogram
109+ }
66110 } )
67111 }
68112
69113 /// Reset all metrics (useful for testing)
70114 pub fn reset ( & self ) {
71115 self . total_calls . store ( 0 , Ordering :: Relaxed ) ;
72116 self . successful_calls . store ( 0 , Ordering :: Relaxed ) ;
117+ self . total_duration_ms . store ( 0 , Ordering :: Relaxed ) ;
118+ for bucket in & self . duration_buckets {
119+ bucket. store ( 0 , Ordering :: Relaxed ) ;
120+ }
73121 self . failed_calls_by_type . clear ( ) ;
74122 self . failed_calls_by_method . clear ( ) ;
75123 }
@@ -160,7 +208,7 @@ pub fn log_rpc_request_success(
160208 duration_ms : u64 ,
161209 result_summary : Option < & str > ,
162210) {
163- METRICS . increment_successful_calls ( ) ;
211+ METRICS . increment_successful_calls ( duration_ms ) ;
164212
165213 let span = Span :: current ( ) ;
166214 span. record ( "duration_ms" , duration_ms) ;
@@ -187,7 +235,7 @@ pub fn log_rpc_request_failure(
187235 duration_ms : u64 ,
188236 error_details : Option < & Value > ,
189237) {
190- METRICS . increment_failed_calls ( error_type, Some ( method) ) ;
238+ METRICS . increment_failed_calls ( error_type, Some ( method) , duration_ms ) ;
191239
192240 let span = Span :: current ( ) ;
193241 span. record ( "duration_ms" , duration_ms) ;
@@ -316,12 +364,20 @@ pub fn get_metrics() -> &'static Metrics {
316364
317365/// Create a parameters summary for logging (sanitized)
318366pub fn create_params_summary ( params : & Value ) -> String {
367+ use crate :: validation:: sanitization:: * ;
368+
319369 match params {
320370 Value :: Object ( map) => {
321371 let keys: Vec < String > = map. keys ( )
372+ . take ( MAX_OBJECT_KEYS_IN_SUMMARY )
322373 . map ( |k| k. to_string ( ) )
323374 . collect ( ) ;
324- format ! ( "params: [{}]" , keys. join( ", " ) )
375+ let summary = format ! ( "params: [{}]" , keys. join( ", " ) ) ;
376+ if map. len ( ) > MAX_OBJECT_KEYS_IN_SUMMARY {
377+ format ! ( "{}...({} more)" , summary, map. len( ) - MAX_OBJECT_KEYS_IN_SUMMARY )
378+ } else {
379+ summary
380+ }
325381 } ,
326382 Value :: Array ( arr) => {
327383 format ! ( "params: array[{}]" , arr. len( ) )
@@ -332,14 +388,16 @@ pub fn create_params_summary(params: &Value) -> String {
332388
333389/// Create a result summary for logging (sanitized)
334390pub fn create_result_summary ( result : & Value ) -> String {
391+ use crate :: validation:: sanitization:: * ;
392+
335393 match result {
336394 Value :: Object ( map) => {
337395 let keys: Vec < String > = map. keys ( )
338- . take ( 5 ) // Limit to first 5 keys
396+ . take ( MAX_OBJECT_KEYS_IN_SUMMARY )
339397 . map ( |k| k. to_string ( ) )
340398 . collect ( ) ;
341- if map. len ( ) > 5 {
342- format ! ( "result: {{{},...}} " , keys. join( ", " ) )
399+ if map. len ( ) > MAX_OBJECT_KEYS_IN_SUMMARY {
400+ format ! ( "result: {{{},...({} more)}} " , keys. join( ", " ) , map . len ( ) - MAX_OBJECT_KEYS_IN_SUMMARY )
343401 } else {
344402 format ! ( "result: {{{}}}" , keys. join( ", " ) )
345403 }
@@ -377,8 +435,8 @@ mod tests {
377435
378436 // Test incrementing
379437 metrics. increment_total_calls ( ) ;
380- metrics. increment_successful_calls ( ) ;
381- metrics. increment_failed_calls ( "validation" , Some ( "getBalance" ) ) ;
438+ metrics. increment_successful_calls ( 150 ) ;
439+ metrics. increment_failed_calls ( "validation" , Some ( "getBalance" ) , 200 ) ;
382440
383441 assert_eq ! ( metrics. total_calls. load( Ordering :: Relaxed ) , 1 ) ;
384442 assert_eq ! ( metrics. successful_calls. load( Ordering :: Relaxed ) , 1 ) ;
@@ -446,13 +504,45 @@ mod tests {
446504 fn test_metrics_json_serialization ( ) {
447505 let metrics = Metrics :: default ( ) ;
448506 metrics. increment_total_calls ( ) ;
449- metrics. increment_successful_calls ( ) ;
450- metrics. increment_failed_calls ( "rpc" , Some ( "getBalance" ) ) ;
507+ metrics. increment_successful_calls ( 100 ) ;
508+ metrics. increment_failed_calls ( "rpc" , Some ( "getBalance" ) , 250 ) ;
451509
452510 let json = metrics. to_json ( ) ;
453511 assert ! ( json. get( "total_calls" ) . is_some( ) ) ;
454512 assert ! ( json. get( "successful_calls" ) . is_some( ) ) ;
455513 assert ! ( json. get( "failed_calls_by_type" ) . is_some( ) ) ;
456514 assert ! ( json. get( "failed_calls_by_method" ) . is_some( ) ) ;
515+ assert ! ( json. get( "performance" ) . is_some( ) ) ;
516+
517+ // Check performance metrics
518+ let performance = json. get ( "performance" ) . unwrap ( ) ;
519+ assert ! ( performance. get( "avg_duration_ms" ) . is_some( ) ) ;
520+ assert ! ( performance. get( "duration_histogram" ) . is_some( ) ) ;
521+ }
522+
523+ #[ test]
524+ fn test_performance_histogram ( ) {
525+ let metrics = Metrics :: default ( ) ;
526+
527+ // Test different duration buckets
528+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 5 ) ; // 0-9ms bucket
529+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 25 ) ; // 10-49ms bucket
530+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 75 ) ; // 50-99ms bucket
531+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 200 ) ; // 100-499ms bucket
532+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 750 ) ; // 500-999ms bucket
533+ metrics. increment_total_calls ( ) ; metrics. increment_successful_calls ( 1500 ) ; // 1000ms+ bucket
534+
535+ let json = metrics. to_json ( ) ;
536+ let performance = json. get ( "performance" ) . unwrap ( ) ;
537+ let histogram = performance. get ( "duration_histogram" ) . unwrap ( ) ;
538+
539+ // Verify each bucket has 1 entry
540+ let histogram_array = histogram. as_array ( ) . unwrap ( ) ;
541+ assert_eq ! ( histogram_array. len( ) , 6 ) ;
542+
543+ // Check that average is calculated correctly
544+ // (5+25+75+200+750+1500)/6 = 425
545+ let avg_duration = performance. get ( "avg_duration_ms" ) . unwrap ( ) . as_u64 ( ) . unwrap ( ) ;
546+ assert_eq ! ( avg_duration, 425 ) ;
457547 }
458548}
0 commit comments