Skip to content

Commit 89842bd

Browse files
Copilot0xrinegade
andcommitted
Add performance monitoring with histograms and enhance sanitization strategy
Co-authored-by: 0xrinegade <[email protected]>
1 parent e4bd1df commit 89842bd

File tree

2 files changed

+180
-22
lines changed

2 files changed

+180
-22
lines changed

src/logging.rs

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2631
impl 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)
318366
pub 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)
334390
pub 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
}

src/validation.rs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22
use anyhow::{Result, anyhow};
33
use url::Url;
44

5+
/// Sanitization constants for consistent data handling
6+
pub mod sanitization {
7+
/// Maximum length for truncated strings in logs
8+
pub const MAX_LOG_STRING_LENGTH: usize = 100;
9+
10+
/// Maximum length for parameter summaries
11+
pub const MAX_PARAM_SUMMARY_LENGTH: usize = 200;
12+
13+
/// Maximum number of object keys to include in summaries
14+
pub const MAX_OBJECT_KEYS_IN_SUMMARY: usize = 5;
15+
16+
/// Regex pattern for sensitive data detection (compiled once)
17+
pub const SENSITIVE_PATTERNS: &[&str] = &[
18+
r"(?i)(password|secret|key|token|auth)=([^&\s]+)",
19+
r"(?i)(api[_-]?key|access[_-]?token)[:=]\s*[^\s&]+",
20+
r"(?i)(bearer\s+|basic\s+)[a-zA-Z0-9+/=]+",
21+
];
22+
23+
/// Common sensitive parameter names to redact
24+
pub const SENSITIVE_PARAM_NAMES: &[&str] = &[
25+
"password", "secret", "key", "token", "auth", "authorization",
26+
"api_key", "apikey", "access_token", "refresh_token", "private_key",
27+
"seed", "mnemonic", "signature", "private", "credential"
28+
];
29+
}
30+
531
/// Validates that a URL is well-formed and uses HTTPS protocol
632
///
733
/// # Arguments
@@ -135,17 +161,41 @@ pub fn validate_commitment(commitment: &str) -> Result<()> {
135161
/// # Returns
136162
/// * `String` - Sanitized string safe for logging
137163
pub fn sanitize_for_logging(input: &str) -> String {
164+
use sanitization::*;
165+
166+
// Check for sensitive parameter names and redact completely
167+
let input_lower = input.to_lowercase();
168+
for sensitive_name in SENSITIVE_PARAM_NAMES {
169+
if input_lower.contains(sensitive_name) {
170+
return format!("[REDACTED-{}]", sensitive_name.to_uppercase());
171+
}
172+
}
173+
138174
// For URLs, only show scheme and host, hide path/params
139175
if let Ok(url) = Url::parse(input) {
140176
if let Some(host) = url.host_str() {
141-
return format!("{}://{}", url.scheme(), host);
177+
let mut sanitized = format!("{}://{}", url.scheme(), host);
178+
if let Some(port) = url.port() {
179+
sanitized.push_str(&format!(":{}", port));
180+
}
181+
// Indicate if there were paths/queries without revealing them
182+
if !url.path().is_empty() && url.path() != "/" {
183+
sanitized.push_str("/[PATH_REDACTED]");
184+
}
185+
if url.query().is_some() {
186+
sanitized.push_str("?[QUERY_REDACTED]");
187+
}
188+
return sanitized;
142189
}
143190
}
144191

145-
// For other strings, truncate if too long
146-
if input.len() > 100 {
147-
format!("{}...[truncated]", &input[..100])
192+
// For other strings, apply length truncation
193+
if input.len() > MAX_LOG_STRING_LENGTH {
194+
format!("{}...[truncated {} chars]",
195+
&input[..MAX_LOG_STRING_LENGTH],
196+
input.len() - MAX_LOG_STRING_LENGTH)
148197
} else {
198+
// Return as-is if not sensitive and within limits
149199
input.to_string()
150200
}
151201
}
@@ -199,13 +249,31 @@ mod tests {
199249

200250
#[test]
201251
fn test_sanitize_for_logging() {
252+
// Test URL sanitization with path and query
202253
let url = "https://api.opensvm.com/v1/accounts/abc123?encoding=json";
203254
let sanitized = sanitize_for_logging(url);
204-
assert_eq!(sanitized, "https://api.opensvm.com");
255+
assert_eq!(sanitized, "https://api.opensvm.com/[PATH_REDACTED]?[QUERY_REDACTED]");
205256

257+
// Test simple URL without path/query
258+
let simple_url = "https://api.opensvm.com";
259+
let sanitized_simple = sanitize_for_logging(simple_url);
260+
assert_eq!(sanitized_simple, "https://api.opensvm.com");
261+
262+
// Test sensitive data redaction
263+
let sensitive = "my_secret_key=abc123";
264+
let sanitized_sensitive = sanitize_for_logging(sensitive);
265+
assert_eq!(sanitized_sensitive, "[REDACTED-SECRET]");
266+
267+
// Test long string truncation
206268
let long_string = "x".repeat(150);
207269
let sanitized_long = sanitize_for_logging(&long_string);
208-
assert!(sanitized_long.len() <= 115); // 100 + "...[truncated]"
270+
assert!(sanitized_long.contains("truncated 50 chars"));
271+
assert!(sanitized_long.len() < long_string.len());
272+
273+
// Test normal string
274+
let normal = "normal_string";
275+
let sanitized_normal = sanitize_for_logging(normal);
276+
assert_eq!(sanitized_normal, "normal_string");
209277
}
210278

211279
#[test]

0 commit comments

Comments
 (0)