#[cfg(feature = "metrics")]
use std::collections::HashMap;
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[cfg(feature = "metrics")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "metrics")]
use std::time::Duration;
#[cfg(feature = "metrics")]
use once_cell::sync::Lazy;
#[cfg(feature = "metrics")]
use parking_lot::RwLock;
#[cfg(feature = "metrics")]
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
#[cfg(feature = "metrics")]
pub static METRICS: Lazy<GlobalMetrics> = Lazy::new(GlobalMetrics::new);
pub trait MetricsCollector: Send + Sync {
fn record_request(&self, method: &str, endpoint: &str, duration: Duration, success: bool);
fn record_cache_hit(&self, key: &str);
fn record_cache_miss(&self, key: &str);
fn record_error(&self, error_type: &str);
fn get_snapshot(&self) -> MetricsSnapshot;
fn reset(&self);
}
#[cfg(feature = "metrics")]
#[derive(Debug, Default)]
pub struct GlobalMetrics {
request_count: AtomicU64,
error_count: AtomicU64,
total_duration_ms: AtomicU64,
cache_hits: AtomicU64,
cache_misses: AtomicU64,
endpoint_metrics: RwLock<HashMap<String, Arc<EndpointMetrics>>>,
}
#[cfg(feature = "metrics")]
impl GlobalMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> MetricsSnapshot {
let request_count = self.request_count.load(Ordering::Relaxed);
let error_count = self.error_count.load(Ordering::Relaxed);
let total_duration = self.total_duration_ms.load(Ordering::Relaxed);
let cache_hits = self.cache_hits.load(Ordering::Relaxed);
let cache_misses = self.cache_misses.load(Ordering::Relaxed);
let avg_duration_ms = if request_count > 0 {
total_duration / request_count
} else {
0
};
let success_rate = if request_count > 0 {
((request_count - error_count) as f64 / request_count as f64) * 100.0
} else {
0.0
};
let cache_hit_rate = if (cache_hits + cache_misses) > 0 {
(cache_hits as f64 / (cache_hits + cache_misses) as f64) * 100.0
} else {
0.0
};
let endpoint_metrics = self
.endpoint_metrics
.read()
.iter()
.map(|(k, v)| (k.clone(), v.snapshot()))
.collect();
MetricsSnapshot {
request_count,
error_count,
avg_duration_ms,
success_rate,
cache_hits,
cache_misses,
cache_hit_rate,
endpoints: endpoint_metrics,
}
}
}
#[cfg(feature = "metrics")]
impl MetricsCollector for GlobalMetrics {
fn record_request(&self, method: &str, endpoint: &str, duration: Duration, success: bool) {
self.request_count.fetch_add(1, Ordering::Relaxed);
self.total_duration_ms
.fetch_add(duration.as_millis() as u64, Ordering::Relaxed);
if !success {
self.error_count.fetch_add(1, Ordering::Relaxed);
}
let endpoint_key = format!("{} {}", method, endpoint);
let metrics = {
let read_guard = self.endpoint_metrics.read();
read_guard.get(&endpoint_key).cloned()
};
let endpoint_metrics = match metrics {
Some(metrics) => metrics,
None => {
let mut write_guard = self.endpoint_metrics.write();
write_guard
.entry(endpoint_key)
.or_insert_with(|| Arc::new(EndpointMetrics::new()))
.clone()
}
};
endpoint_metrics.record_request(duration, success);
debug!(
method = method,
endpoint = endpoint,
duration_ms = duration.as_millis(),
success = success,
"Request metrics recorded"
);
}
fn record_cache_hit(&self, key: &str) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
debug!(cache_key = key, "Cache hit recorded");
}
fn record_cache_miss(&self, key: &str) {
self.cache_misses.fetch_add(1, Ordering::Relaxed);
debug!(cache_key = key, "Cache miss recorded");
}
fn record_error(&self, error_type: &str) {
self.error_count.fetch_add(1, Ordering::Relaxed);
debug!(error_type = error_type, "Error recorded");
}
fn get_snapshot(&self) -> MetricsSnapshot {
self.snapshot()
}
fn reset(&self) {
self.request_count.store(0, Ordering::Relaxed);
self.error_count.store(0, Ordering::Relaxed);
self.total_duration_ms.store(0, Ordering::Relaxed);
self.cache_hits.store(0, Ordering::Relaxed);
self.cache_misses.store(0, Ordering::Relaxed);
self.endpoint_metrics.write().clear();
info!("Metrics reset");
}
}
#[cfg(feature = "metrics")]
#[derive(Debug)]
pub struct EndpointMetrics {
request_count: AtomicU64,
error_count: AtomicU64,
total_duration_ms: AtomicU64,
min_duration_ms: AtomicU64,
max_duration_ms: AtomicU64,
}
#[cfg(feature = "metrics")]
impl EndpointMetrics {
fn new() -> Self {
Self {
request_count: AtomicU64::new(0),
error_count: AtomicU64::new(0),
total_duration_ms: AtomicU64::new(0),
min_duration_ms: AtomicU64::new(u64::MAX),
max_duration_ms: AtomicU64::new(0),
}
}
fn record_request(&self, duration: Duration, success: bool) {
let duration_ms = duration.as_millis() as u64;
self.request_count.fetch_add(1, Ordering::Relaxed);
self.total_duration_ms
.fetch_add(duration_ms, Ordering::Relaxed);
if !success {
self.error_count.fetch_add(1, Ordering::Relaxed);
}
self.min_duration_ms
.fetch_min(duration_ms, Ordering::Relaxed);
self.max_duration_ms
.fetch_max(duration_ms, Ordering::Relaxed);
}
fn snapshot(&self) -> EndpointMetricsSnapshot {
let request_count = self.request_count.load(Ordering::Relaxed);
let error_count = self.error_count.load(Ordering::Relaxed);
let total_duration = self.total_duration_ms.load(Ordering::Relaxed);
let min_duration = self.min_duration_ms.load(Ordering::Relaxed);
let max_duration = self.max_duration_ms.load(Ordering::Relaxed);
let avg_duration_ms = if request_count > 0 {
total_duration / request_count
} else {
0
};
let success_rate = if request_count > 0 {
((request_count - error_count) as f64 / request_count as f64) * 100.0
} else {
0.0
};
EndpointMetricsSnapshot {
request_count,
error_count,
avg_duration_ms,
min_duration_ms: if min_duration == u64::MAX {
0
} else {
min_duration
},
max_duration_ms: max_duration,
success_rate,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsSnapshot {
pub request_count: u64,
pub error_count: u64,
pub avg_duration_ms: u64,
pub success_rate: f64,
pub cache_hits: u64,
pub cache_misses: u64,
pub cache_hit_rate: f64,
pub endpoints: HashMap<String, EndpointMetricsSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EndpointMetricsSnapshot {
pub request_count: u64,
pub error_count: u64,
pub avg_duration_ms: u64,
pub min_duration_ms: u64,
pub max_duration_ms: u64,
pub success_rate: f64,
}
impl MetricsSnapshot {
pub fn empty() -> Self {
Self {
request_count: 0,
error_count: 0,
avg_duration_ms: 0,
success_rate: 0.0,
cache_hits: 0,
cache_misses: 0,
cache_hit_rate: 0.0,
endpoints: HashMap::new(),
}
}
pub fn get_endpoint_metrics(&self, endpoint: &str) -> Option<&EndpointMetricsSnapshot> {
self.endpoints.get(endpoint)
}
pub fn top_endpoints_by_requests(&self, n: usize) -> Vec<(String, &EndpointMetricsSnapshot)> {
let mut endpoints: Vec<_> = self.endpoints.iter().map(|(k, v)| (k.clone(), v)).collect();
endpoints.sort_by(|a, b| b.1.request_count.cmp(&a.1.request_count));
endpoints.truncate(n);
endpoints
}
pub fn highest_error_endpoints(
&self,
min_requests: u64,
) -> Vec<(String, &EndpointMetricsSnapshot)> {
let mut endpoints: Vec<_> = self
.endpoints
.iter()
.filter(|(_, metrics)| metrics.request_count >= min_requests)
.map(|(k, v)| (k.clone(), v))
.collect();
endpoints.sort_by(|a, b| {
let error_rate_a = (a.1.error_count as f64 / a.1.request_count as f64) * 100.0;
let error_rate_b = (b.1.error_count as f64 / b.1.request_count as f64) * 100.0;
error_rate_b
.partial_cmp(&error_rate_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
endpoints
}
}
#[cfg(not(feature = "metrics"))]
pub struct GlobalMetrics;
#[cfg(not(feature = "metrics"))]
impl GlobalMetrics {
pub fn new() -> Self {
Self
}
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot::empty()
}
}
#[cfg(not(feature = "metrics"))]
impl MetricsCollector for GlobalMetrics {
fn record_request(&self, _method: &str, _endpoint: &str, _duration: Duration, _success: bool) {}
fn record_cache_hit(&self, _key: &str) {}
fn record_cache_miss(&self, _key: &str) {}
fn record_error(&self, _error_type: &str) {}
fn get_snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot::empty()
}
fn reset(&self) {}
}
#[cfg(not(feature = "metrics"))]
pub static METRICS: GlobalMetrics = GlobalMetrics;