use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
#[derive(Clone, Debug, serde::Serialize)]
pub struct MetricSample {
pub ts: i64,
pub active_conns: u32,
pub requests: u64,
pub failed_requests: u64,
pub latency_p95_ms: f64,
}
pub struct ServiceCounters {
pub active_conns: AtomicU32,
pub total_requests: AtomicU64,
pub failed_requests: AtomicU64,
latencies: Mutex<Vec<f64>>,
}
impl ServiceCounters {
pub fn new() -> Self {
Self {
active_conns: AtomicU32::new(0),
total_requests: AtomicU64::new(0),
failed_requests: AtomicU64::new(0),
latencies: Mutex::new(Vec::new()),
}
}
pub fn inc_conns(&self) {
self.active_conns.fetch_add(1, Ordering::Relaxed);
}
pub fn dec_conns(&self) {
self.active_conns.fetch_sub(1, Ordering::Relaxed);
}
pub async fn record_request(&self, success: bool, latency_ms: f64) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
if !success {
self.failed_requests.fetch_add(1, Ordering::Relaxed);
}
self.latencies.lock().await.push(latency_ms);
}
pub async fn snapshot_and_reset(&self) -> MetricSample {
let active_conns = self.active_conns.load(Ordering::Relaxed);
let requests = self.total_requests.swap(0, Ordering::Relaxed);
let failed_requests = self.failed_requests.swap(0, Ordering::Relaxed);
let mut lats = {
let mut guard = self.latencies.lock().await;
std::mem::take(&mut *guard)
};
let latency_p95_ms = compute_p95(&mut lats);
MetricSample {
ts: chrono::Utc::now().timestamp(),
active_conns,
requests,
failed_requests,
latency_p95_ms,
}
}
}
impl Default for ServiceCounters {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ServiceCounters {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceCounters")
.field("active_conns", &self.active_conns.load(Ordering::Relaxed))
.field(
"total_requests",
&self.total_requests.load(Ordering::Relaxed),
)
.field(
"failed_requests",
&self.failed_requests.load(Ordering::Relaxed),
)
.finish()
}
}
fn compute_p95(values: &mut [f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((values.len() as f64) * 0.95).ceil() as usize;
let idx = idx.min(values.len()) - 1;
values[idx]
}
struct Ring {
buf: VecDeque<MetricSample>,
cap: usize,
}
impl Ring {
fn new(cap: usize) -> Self {
Self {
buf: VecDeque::with_capacity(cap),
cap,
}
}
fn push(&mut self, sample: MetricSample) {
if self.buf.len() == self.cap {
self.buf.pop_front();
}
self.buf.push_back(sample);
}
fn to_vec(&self) -> Vec<MetricSample> {
self.buf.iter().cloned().collect()
}
}
struct ServiceRings {
tier0: Ring, tier1: Ring, tier2: Ring, tier1_acc: Vec<MetricSample>,
tier2_acc: Vec<MetricSample>,
}
impl ServiceRings {
fn new() -> Self {
Self {
tier0: Ring::new(15),
tier1: Ring::new(16),
tier2: Ring::new(18),
tier1_acc: Vec::with_capacity(15),
tier2_acc: Vec::with_capacity(16),
}
}
}
fn aggregate(samples: &[MetricSample]) -> MetricSample {
debug_assert!(!samples.is_empty());
let ts = samples.last().map(|s| s.ts).unwrap_or(0);
let conns_sum: u64 = samples.iter().map(|s| s.active_conns as u64).sum();
let active_conns = (conns_sum as f64 / samples.len() as f64).round() as u32;
let requests: u64 = samples.iter().map(|s| s.requests).sum();
let failed_requests: u64 = samples.iter().map(|s| s.failed_requests).sum();
let latency_p95_ms = samples
.iter()
.map(|s| s.latency_p95_ms)
.fold(0.0_f64, f64::max);
MetricSample {
ts,
active_conns,
requests,
failed_requests,
latency_p95_ms,
}
}
#[derive(Clone)]
pub struct MetricsStore {
inner: Arc<RwLock<HashMap<i32, ServiceRings>>>,
}
impl MetricsStore {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn push_sample(&self, service_type: i32, sample: MetricSample) {
let mut map = self.inner.write().await;
let rings = map.entry(service_type).or_insert_with(ServiceRings::new);
rings.tier0.push(sample.clone());
rings.tier1_acc.push(sample);
if rings.tier1_acc.len() == 15 {
let rolled = aggregate(&rings.tier1_acc);
rings.tier1_acc.clear();
rings.tier1.push(rolled.clone());
rings.tier2_acc.push(rolled);
if rings.tier2_acc.len() == 16 {
let rolled2 = aggregate(&rings.tier2_acc);
rings.tier2_acc.clear();
rings.tier2.push(rolled2);
}
}
}
pub async fn query(&self, service_type: i32, tier: u8) -> Vec<MetricSample> {
let map = self.inner.read().await;
let Some(rings) = map.get(&service_type) else {
return Vec::new();
};
match tier {
0 => rings.tier0.to_vec(),
1 => rings.tier1.to_vec(),
2 => rings.tier2.to_vec(),
_ => Vec::new(),
}
}
pub fn start_sampler(
self,
counters: Arc<HashMap<i32, Arc<ServiceCounters>>>,
interval: Duration,
) {
tokio::spawn(async move {
let mut tick = tokio::time::interval(interval);
loop {
tick.tick().await;
for (&svc_type, ctr) in counters.iter() {
let sample = ctr.snapshot_and_reset().await;
self.push_sample(svc_type, sample).await;
}
}
});
}
}
impl std::fmt::Debug for MetricsStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetricsStore").finish()
}
}
impl Default for MetricsStore {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_p95_basic() {
let mut vals: Vec<f64> = (1..=20).map(|v| v as f64).collect();
assert!((compute_p95(&mut vals) - 19.0).abs() < f64::EPSILON);
}
#[test]
fn compute_p95_empty() {
assert!((compute_p95(&mut []) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn compute_p95_single() {
assert!((compute_p95(&mut [42.0]) - 42.0).abs() < f64::EPSILON);
}
#[test]
fn ring_evicts_oldest() {
let mut ring = Ring::new(3);
for i in 0..5 {
ring.push(MetricSample {
ts: i,
active_conns: 0,
requests: 0,
failed_requests: 0,
latency_p95_ms: 0.0,
});
}
let v = ring.to_vec();
assert_eq!(v.len(), 3);
assert_eq!(v[0].ts, 2);
assert_eq!(v[2].ts, 4);
}
#[test]
fn aggregate_applies_rules() {
let samples = vec![
MetricSample {
ts: 100,
active_conns: 10,
requests: 50,
failed_requests: 2,
latency_p95_ms: 3.5,
},
MetricSample {
ts: 200,
active_conns: 20,
requests: 60,
failed_requests: 3,
latency_p95_ms: 7.1,
},
];
let agg = aggregate(&samples);
assert_eq!(agg.ts, 200); assert_eq!(agg.active_conns, 15); assert_eq!(agg.requests, 110); assert_eq!(agg.failed_requests, 5); assert!((agg.latency_p95_ms - 7.1).abs() < f64::EPSILON); }
#[tokio::test]
async fn push_and_query() {
let store = MetricsStore::new();
let sample = MetricSample {
ts: 1000,
active_conns: 5,
requests: 100,
failed_requests: 1,
latency_p95_ms: 2.0,
};
store.push_sample(1, sample).await;
let tier0 = store.query(1, 0).await;
assert_eq!(tier0.len(), 1);
assert_eq!(tier0[0].ts, 1000);
assert!(store.query(1, 1).await.is_empty());
}
#[tokio::test]
async fn tier0_to_tier1_rollup() {
let store = MetricsStore::new();
for i in 0..15 {
store
.push_sample(
1,
MetricSample {
ts: i * 60,
active_conns: 10,
requests: 100,
failed_requests: 1,
latency_p95_ms: 5.0,
},
)
.await;
}
let tier1 = store.query(1, 1).await;
assert_eq!(tier1.len(), 1);
assert_eq!(tier1[0].active_conns, 10); assert_eq!(tier1[0].requests, 1500); assert_eq!(tier1[0].failed_requests, 15); assert!((tier1[0].latency_p95_ms - 5.0).abs() < f64::EPSILON);
}
#[tokio::test]
async fn service_counters_snapshot() {
let ctr = ServiceCounters::new();
ctr.inc_conns();
ctr.inc_conns();
ctr.record_request(true, 1.0).await;
ctr.record_request(false, 10.0).await;
ctr.record_request(true, 5.0).await;
let snap = ctr.snapshot_and_reset().await;
assert_eq!(snap.active_conns, 2);
assert_eq!(snap.requests, 3);
assert_eq!(snap.failed_requests, 1);
assert!((snap.latency_p95_ms - 10.0).abs() < f64::EPSILON);
let snap2 = ctr.snapshot_and_reset().await;
assert_eq!(snap2.requests, 0);
assert_eq!(snap2.failed_requests, 0);
assert!((snap2.latency_p95_ms - 0.0).abs() < f64::EPSILON);
assert_eq!(snap2.active_conns, 2);
}
}