use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use parking_lot::RwLock;
#[derive(Debug, Default)]
pub struct Counter {
value: AtomicU64,
}
impl Counter {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&self) {
self.add(1);
}
pub fn add(&self, value: u64) {
self.value.fetch_add(value, Ordering::Relaxed);
}
pub fn get(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
}
#[derive(Debug, Default)]
pub struct Gauge {
value: AtomicU64,
}
impl Gauge {
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, value: u64) {
self.value.store(value, Ordering::Relaxed);
}
pub fn inc(&self) {
self.add(1);
}
pub fn dec(&self) {
self.sub(1);
}
pub fn add(&self, value: u64) {
self.value.fetch_add(value, Ordering::Relaxed);
}
pub fn sub(&self, value: u64) {
self.value.fetch_sub(value, Ordering::Relaxed);
}
pub fn get(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
}
#[derive(Debug, Clone)]
pub struct HistogramBuckets {
pub bounds: Vec<u64>,
}
impl Default for HistogramBuckets {
fn default() -> Self {
Self {
bounds: vec![
10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000,
],
}
}
}
impl HistogramBuckets {
pub fn latency() -> Self {
Self::default()
}
pub fn size() -> Self {
Self {
bounds: vec![100, 1000, 10000, 100000, 1000000, 10000000, 100000000],
}
}
pub fn custom(bounds: Vec<u64>) -> Self {
let mut bounds = bounds;
bounds.sort_unstable();
Self { bounds }
}
fn bucket_index(&self, value: u64) -> usize {
self.bounds
.iter()
.position(|&bound| value <= bound)
.unwrap_or(self.bounds.len())
}
}
#[derive(Debug)]
pub struct Histogram {
buckets: HistogramBuckets,
counts: Vec<AtomicU64>,
sum: AtomicU64,
count: AtomicU64,
}
impl Histogram {
pub fn new() -> Self {
Self::with_buckets(HistogramBuckets::default())
}
pub fn with_buckets(buckets: HistogramBuckets) -> Self {
let counts = (0..=buckets.bounds.len())
.map(|_| AtomicU64::new(0))
.collect();
Self {
buckets,
counts,
sum: AtomicU64::new(0),
count: AtomicU64::new(0),
}
}
pub fn observe(&self, value: u64) {
let idx = self.buckets.bucket_index(value);
self.counts[idx].fetch_add(1, Ordering::Relaxed);
self.sum.fetch_add(value, Ordering::Relaxed);
self.count.fetch_add(1, Ordering::Relaxed);
}
pub fn observe_duration(&self, duration: Duration) {
self.observe(duration.as_millis() as u64);
}
pub fn get_count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
pub fn get_sum(&self) -> u64 {
self.sum.load(Ordering::Relaxed)
}
pub fn get_mean(&self) -> f64 {
let count = self.get_count();
if count == 0 {
return 0.0;
}
self.get_sum() as f64 / count as f64
}
pub fn get_buckets(&self) -> Vec<(u64, u64)> {
self.buckets
.bounds
.iter()
.zip(self.counts.iter())
.map(|(&bound, count)| (bound, count.load(Ordering::Relaxed)))
.collect()
}
}
impl Default for Histogram {
fn default() -> Self {
Self::new()
}
}
pub struct Timer {
start: Instant,
histogram: Arc<Histogram>,
}
impl Timer {
pub fn new(histogram: Arc<Histogram>) -> Self {
Self {
start: Instant::now(),
histogram,
}
}
pub fn stop(self) {
self.histogram.observe_duration(self.start.elapsed());
}
pub fn elapsed(&self) -> Duration {
self.start.elapsed()
}
}
#[derive(Debug)]
pub struct LabeledMetric<T> {
metrics: RwLock<HashMap<String, Arc<T>>>,
factory: fn() -> T,
}
impl<T> LabeledMetric<T> {
pub fn new(factory: fn() -> T) -> Self {
Self {
metrics: RwLock::new(HashMap::new()),
factory,
}
}
pub fn with_label(&self, label: &str) -> Arc<T> {
{
let metrics = self.metrics.read();
if let Some(metric) = metrics.get(label) {
return Arc::clone(metric);
}
}
let mut metrics = self.metrics.write();
metrics
.entry(label.to_string())
.or_insert_with(|| Arc::new((self.factory)()))
.clone()
}
pub fn get_all(&self) -> HashMap<String, Arc<T>> {
self.metrics.read().clone()
}
}
#[derive(Debug)]
pub struct HttpMetrics {
pub requests_total: Counter,
pub requests_by_status: LabeledMetric<Counter>,
pub requests_by_host: LabeledMetric<Counter>,
pub errors_total: Counter,
pub errors_by_type: LabeledMetric<Counter>,
pub request_duration: Histogram,
pub request_duration_by_host: LabeledMetric<Histogram>,
pub response_size: Histogram,
pub active_connections: Gauge,
pub bytes_sent: Counter,
pub bytes_received: Counter,
pub redirects_total: Counter,
pub retries_total: Counter,
pub cache_hits: Counter,
pub cache_misses: Counter,
pub dns_duration: Histogram,
pub tls_duration: Histogram,
pub ttfb: Histogram,
}
impl Default for HttpMetrics {
fn default() -> Self {
Self::new()
}
}
impl HttpMetrics {
pub fn new() -> Self {
Self {
requests_total: Counter::new(),
requests_by_status: LabeledMetric::new(Counter::new),
requests_by_host: LabeledMetric::new(Counter::new),
errors_total: Counter::new(),
errors_by_type: LabeledMetric::new(Counter::new),
request_duration: Histogram::new(),
request_duration_by_host: LabeledMetric::new(Histogram::new),
response_size: Histogram::with_buckets(HistogramBuckets::size()),
active_connections: Gauge::new(),
bytes_sent: Counter::new(),
bytes_received: Counter::new(),
redirects_total: Counter::new(),
retries_total: Counter::new(),
cache_hits: Counter::new(),
cache_misses: Counter::new(),
dns_duration: Histogram::new(),
tls_duration: Histogram::new(),
ttfb: Histogram::new(),
}
}
pub fn record_request(&self, host: &str, status: u16, duration: Duration, body_size: u64) {
self.requests_total.inc();
let status_category = match status {
200..=299 => "2xx",
300..=399 => "3xx",
400..=499 => "4xx",
500..=599 => "5xx",
_ => "other",
};
self.requests_by_status.with_label(status_category).inc();
self.requests_by_host.with_label(host).inc();
self.request_duration.observe_duration(duration);
self.request_duration_by_host
.with_label(host)
.observe_duration(duration);
self.response_size.observe(body_size);
self.bytes_received.add(body_size);
}
pub fn record_error(&self, error_type: &str) {
self.errors_total.inc();
self.errors_by_type.with_label(error_type).inc();
}
pub fn record_redirect(&self) {
self.redirects_total.inc();
}
pub fn start_timer(&self) -> Timer {
Timer::new(Arc::new(Histogram::new()))
}
pub fn report(&self) -> MetricsReport {
MetricsReport {
requests_total: self.requests_total.get(),
errors_total: self.errors_total.get(),
active_connections: self.active_connections.get(),
avg_duration_ms: self.request_duration.get_mean(),
total_bytes_sent: self.bytes_sent.get(),
total_bytes_received: self.bytes_received.get(),
redirects_total: self.redirects_total.get(),
retries_total: self.retries_total.get(),
}
}
pub fn reset(&self) {
}
}
#[derive(Debug, Clone)]
pub struct MetricsReport {
pub requests_total: u64,
pub errors_total: u64,
pub active_connections: u64,
pub avg_duration_ms: f64,
pub total_bytes_sent: u64,
pub total_bytes_received: u64,
pub redirects_total: u64,
pub retries_total: u64,
}
impl std::fmt::Display for MetricsReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== HTTP Client Metrics ===")?;
writeln!(f, "Requests: {}", self.requests_total)?;
writeln!(f, "Errors: {}", self.errors_total)?;
writeln!(f, "Active Connections: {}", self.active_connections)?;
writeln!(f, "Avg Duration: {:.2}ms", self.avg_duration_ms)?;
writeln!(f, "Bytes Sent: {}", self.total_bytes_sent)?;
writeln!(f, "Bytes Received: {}", self.total_bytes_received)?;
writeln!(f, "Redirects: {}", self.redirects_total)?;
writeln!(f, "Retries: {}", self.retries_total)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_counter() {
let counter = Counter::new();
assert_eq!(counter.get(), 0);
counter.inc();
assert_eq!(counter.get(), 1);
counter.add(5);
assert_eq!(counter.get(), 6);
}
#[test]
fn test_gauge() {
let gauge = Gauge::new();
assert_eq!(gauge.get(), 0);
gauge.set(10);
assert_eq!(gauge.get(), 10);
gauge.inc();
assert_eq!(gauge.get(), 11);
gauge.dec();
assert_eq!(gauge.get(), 10);
}
#[test]
fn test_histogram() {
let hist = Histogram::new();
hist.observe(50);
hist.observe(100);
hist.observe(150);
assert_eq!(hist.get_count(), 3);
assert_eq!(hist.get_sum(), 300);
assert_eq!(hist.get_mean(), 100.0);
}
#[test]
fn test_labeled_metric() {
let labeled = LabeledMetric::new(Counter::new);
labeled.with_label("foo").inc();
labeled.with_label("foo").inc();
labeled.with_label("bar").inc();
assert_eq!(labeled.with_label("foo").get(), 2);
assert_eq!(labeled.with_label("bar").get(), 1);
}
#[test]
fn test_http_metrics() {
let metrics = HttpMetrics::new();
metrics.record_request("example.com", 200, Duration::from_millis(100), 1024);
metrics.record_request("example.com", 404, Duration::from_millis(50), 256);
metrics.record_error("timeout");
let report = metrics.report();
assert_eq!(report.requests_total, 2);
assert_eq!(report.errors_total, 1);
}
#[test]
fn test_metrics_report_display() {
let report = MetricsReport {
requests_total: 100,
errors_total: 5,
active_connections: 3,
avg_duration_ms: 150.5,
total_bytes_sent: 1024,
total_bytes_received: 2048,
redirects_total: 10,
retries_total: 2,
};
let display = format!("{}", report);
assert!(display.contains("Requests: 100"));
assert!(display.contains("Errors: 5"));
}
}