use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;
use std::time::{Duration, Instant};
#[derive(Debug, Default)]
pub struct Metrics {
pub requests_total: AtomicU64,
pub requests_success: AtomicU64,
pub requests_failed: AtomicU64,
pub requests_rate_limited: AtomicU64,
pub bytes_downloaded: AtomicU64,
pub retries_total: AtomicU64,
pub request_time_total_ms: AtomicU64,
pub documents_extracted: AtomicU64,
}
impl Metrics {
pub fn new() -> Self {
Self::default()
}
pub fn inc_requests(&self) {
self.requests_total.fetch_add(1, Ordering::Relaxed);
}
pub fn inc_success(&self) {
self.requests_success.fetch_add(1, Ordering::Relaxed);
}
pub fn inc_failed(&self) {
self.requests_failed.fetch_add(1, Ordering::Relaxed);
}
pub fn inc_rate_limited(&self) {
self.requests_rate_limited.fetch_add(1, Ordering::Relaxed);
}
pub fn add_bytes(&self, bytes: u64) {
self.bytes_downloaded.fetch_add(bytes, Ordering::Relaxed);
}
pub fn inc_retries(&self) {
self.retries_total.fetch_add(1, Ordering::Relaxed);
}
pub fn add_request_time(&self, duration_ms: u64) {
self.request_time_total_ms.fetch_add(duration_ms, Ordering::Relaxed);
}
pub fn inc_documents(&self) {
self.documents_extracted.fetch_add(1, Ordering::Relaxed);
}
pub fn success_rate(&self) -> f64 {
let total = self.requests_total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
let success = self.requests_success.load(Ordering::Relaxed);
success as f64 / total as f64
}
pub fn avg_latency_ms(&self) -> f64 {
let total = self.requests_total.load(Ordering::Relaxed);
if total == 0 {
return 0.0;
}
let time = self.request_time_total_ms.load(Ordering::Relaxed);
time as f64 / total as f64
}
pub fn snapshot(&self) -> MetricsSnapshot {
MetricsSnapshot {
requests_total: self.requests_total.load(Ordering::Relaxed),
requests_success: self.requests_success.load(Ordering::Relaxed),
requests_failed: self.requests_failed.load(Ordering::Relaxed),
requests_rate_limited: self.requests_rate_limited.load(Ordering::Relaxed),
bytes_downloaded: self.bytes_downloaded.load(Ordering::Relaxed),
retries_total: self.retries_total.load(Ordering::Relaxed),
documents_extracted: self.documents_extracted.load(Ordering::Relaxed),
success_rate: self.success_rate(),
avg_latency_ms: self.avg_latency_ms(),
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsSnapshot {
pub requests_total: u64,
pub requests_success: u64,
pub requests_failed: u64,
pub requests_rate_limited: u64,
pub bytes_downloaded: u64,
pub retries_total: u64,
pub documents_extracted: u64,
pub success_rate: f64,
pub avg_latency_ms: f64,
}
pub struct MetricsCollector {
global: Metrics,
by_domain: RwLock<HashMap<String, DomainMetrics>>,
started_at: Instant,
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
global: Metrics::new(),
by_domain: RwLock::new(HashMap::new()),
started_at: Instant::now(),
}
}
pub fn global(&self) -> &Metrics {
&self.global
}
pub fn record_request(&self, domain: &str) {
self.global.inc_requests();
self.with_domain(domain, |m| m.requests += 1);
}
pub fn record_success(&self, domain: &str, bytes: u64, duration_ms: u64) {
self.global.inc_success();
self.global.add_bytes(bytes);
self.global.add_request_time(duration_ms);
self.with_domain(domain, |m| {
m.successes += 1;
m.bytes += bytes;
m.total_time_ms += duration_ms;
});
}
pub fn record_failure(&self, domain: &str, duration_ms: u64) {
self.global.inc_failed();
self.global.add_request_time(duration_ms);
self.with_domain(domain, |m| {
m.failures += 1;
m.total_time_ms += duration_ms;
});
}
pub fn record_rate_limit(&self, domain: &str) {
self.global.inc_rate_limited();
self.with_domain(domain, |m| m.rate_limits += 1);
}
pub fn record_retry(&self, domain: &str) {
self.global.inc_retries();
self.with_domain(domain, |m| m.retries += 1);
}
pub fn record_document(&self) {
self.global.inc_documents();
}
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
pub fn requests_per_second(&self) -> f64 {
let elapsed = self.elapsed().as_secs_f64();
if elapsed < 0.001 {
return 0.0;
}
self.global.requests_total.load(Ordering::Relaxed) as f64 / elapsed
}
pub fn domain_stats(&self) -> HashMap<String, DomainMetrics> {
self.by_domain.read().unwrap().clone()
}
fn with_domain<F>(&self, domain: &str, f: F)
where
F: FnOnce(&mut DomainMetrics),
{
let mut by_domain = self.by_domain.write().unwrap();
let metrics = by_domain
.entry(domain.to_string())
.or_insert_with(DomainMetrics::default);
f(metrics);
}
}
#[derive(Debug, Clone, Default)]
pub struct DomainMetrics {
pub requests: u64,
pub successes: u64,
pub failures: u64,
pub rate_limits: u64,
pub retries: u64,
pub bytes: u64,
pub total_time_ms: u64,
}
impl DomainMetrics {
pub fn success_rate(&self) -> f64 {
if self.requests == 0 {
return 0.0;
}
self.successes as f64 / self.requests as f64
}
pub fn avg_latency_ms(&self) -> f64 {
if self.requests == 0 {
return 0.0;
}
self.total_time_ms as f64 / self.requests as f64
}
}