use std::collections::{BTreeMap, VecDeque};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const LATENCY_BOUNDS_MS: [u64; 10] = [1, 5, 10, 25, 50, 100, 250, 500, 1_000, 5_000];
pub const MAX_METHODS: usize = 256;
pub const OTHER_METHOD: &str = "(other)";
const MINUTES_KEPT: usize = 60;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct MethodMetrics {
pub requests: u64,
pub errors: BTreeMap<String, u64>,
pub latency_buckets: [u64; LATENCY_BOUNDS_MS.len() + 1],
pub total_micros: u64,
pub max_micros: u64,
pub per_minute: Vec<(u64, u64)>,
}
impl MethodMetrics {
pub fn error_count(&self) -> u64 {
self.errors.values().sum()
}
pub fn requests_in_minute_of(&self, now: SystemTime) -> u64 {
let minute = minute_of(now);
self.per_minute
.iter()
.find(|(m, _)| *m == minute)
.map_or(0, |(_, n)| *n)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HostMetrics {
pub methods: BTreeMap<String, MethodMetrics>,
pub inflight: usize,
pub inflight_max: usize,
pub connections: usize,
pub connections_peak: usize,
pub connection_limit: usize,
pub connections_refused: u64,
}
impl HostMetrics {
pub fn total_requests(&self) -> u64 {
self.methods.values().map(|m| m.requests).sum()
}
}
#[derive(Default)]
struct PerMethod {
stats: MethodMetrics,
minutes: VecDeque<(u64, u64)>,
}
pub(crate) struct Metrics {
methods: Mutex<BTreeMap<String, PerMethod>>,
inflight_max: AtomicUsize,
connections_peak: AtomicUsize,
refused: std::sync::atomic::AtomicU64,
connection_limit: usize,
}
fn minute_of(now: SystemTime) -> u64 {
now.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() / 60)
.unwrap_or(0)
}
impl Metrics {
pub(crate) fn new(connection_limit: usize) -> Self {
Self {
methods: Mutex::new(BTreeMap::new()),
inflight_max: AtomicUsize::new(0),
connections_peak: AtomicUsize::new(0),
refused: std::sync::atomic::AtomicU64::new(0),
connection_limit,
}
}
pub(crate) fn note_inflight(&self, now_inflight: usize) {
self.inflight_max.fetch_max(now_inflight, Ordering::SeqCst);
}
pub(crate) fn note_connections(&self, live: usize) {
self.connections_peak.fetch_max(live, Ordering::SeqCst);
}
pub(crate) fn note_refused(&self) {
self.refused.fetch_add(1, Ordering::SeqCst);
}
pub(crate) fn record(&self, method: &str, took: Duration, error_code: Option<&str>) {
let mut methods = self.methods.lock().unwrap_or_else(|e| e.into_inner());
let key = if methods.contains_key(method) || methods.len() < MAX_METHODS {
method
} else {
OTHER_METHOD
};
let entry = methods.entry(key.to_string()).or_default();
entry.stats.requests += 1;
if let Some(code) = error_code {
*entry.stats.errors.entry(code.to_string()).or_insert(0) += 1;
}
let ms = took.as_millis() as u64;
let bucket = LATENCY_BOUNDS_MS
.iter()
.position(|bound| ms <= *bound)
.unwrap_or(LATENCY_BOUNDS_MS.len());
entry.stats.latency_buckets[bucket] += 1;
let micros = took.as_micros().min(u128::from(u64::MAX)) as u64;
entry.stats.total_micros = entry.stats.total_micros.saturating_add(micros);
entry.stats.max_micros = entry.stats.max_micros.max(micros);
let minute = minute_of(SystemTime::now());
match entry.minutes.back_mut() {
Some((m, n)) if *m == minute => *n += 1,
_ => {
entry.minutes.push_back((minute, 1));
while entry.minutes.len() > MINUTES_KEPT {
entry.minutes.pop_front();
}
}
}
}
pub(crate) fn snapshot(&self, inflight: usize, connections: usize) -> HostMetrics {
let methods = self
.methods
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter()
.map(|(name, m)| {
let mut stats = m.stats.clone();
stats.per_minute = m.minutes.iter().copied().collect();
(name.clone(), stats)
})
.collect();
HostMetrics {
methods,
inflight,
inflight_max: self.inflight_max.load(Ordering::SeqCst),
connections,
connections_peak: self.connections_peak.load(Ordering::SeqCst),
connection_limit: self.connection_limit,
connections_refused: self.refused.load(Ordering::SeqCst),
}
}
}