This commit is contained in:
Mateusz Gruszczyński committed 2026-10-19 15:40:37 +02:00
1 parent 34fb534e02
commit b2b5b45d8a
22 files changed
+2146 -107

No files matched your search

+1
View File
@@ -2,6 +2,7 @@ use serde::Serialize;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Default)]
pub struct Metrics {
pub summary_rate_sampler: RateSampler,
pub admin_requests: AtomicU64,
pub admin_response_headers: AtomicU64,
pub admin_header_duration_us: AtomicU64,
+100
View File
@@ -0,0 +1,100 @@
// Bounded pull-driven rate sampling for the JSON summary. No per-client state,
// background task, or unbounded history. A cached sample is shared by readers.
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, Default, Serialize)]
pub struct SummaryRates {
pub requests_per_second: Option<f64>,
pub errors_per_second: Option<f64>,
pub bytes_in_per_second: Option<f64>,
pub bytes_out_per_second: Option<f64>,
pub sample_interval_seconds: Option<f64>,
}
#[derive(Debug, Clone, Copy)]
struct SummaryCounters {
requests: u64,
errors: u64,
bytes_in: u64,
bytes_out: u64,
}
#[derive(Default)]
struct RateState {
baseline: Option<(Instant, SummaryCounters)>,
latest: SummaryRates,
}
#[derive(Default)]
pub struct RateSampler(Mutex<RateState>);
impl RateSampler {
fn sample(&self, counters: SummaryCounters, now: Instant) -> SummaryRates {
let mut state = self.0.lock().unwrap_or_else(|error| error.into_inner());
if let Some((previous_at, previous)) = state.baseline {
let elapsed = now.saturating_duration_since(previous_at);
// Do not turn rapid parallel polls into zero/spiky rates. Reuse
// the last measured rate until a useful sampling interval passes.
if elapsed < Duration::from_secs(1) {
return state.latest;
}
let continuous = elapsed <= Duration::from_secs(30)
&& counters.requests >= previous.requests
&& counters.errors >= previous.errors
&& counters.bytes_in >= previous.bytes_in
&& counters.bytes_out >= previous.bytes_out;
if continuous {
let seconds = elapsed.as_secs_f64();
state.latest = SummaryRates {
requests_per_second: Some((counters.requests - previous.requests) as f64 / seconds),
errors_per_second: Some((counters.errors - previous.errors) as f64 / seconds),
bytes_in_per_second: Some((counters.bytes_in - previous.bytes_in) as f64 / seconds),
bytes_out_per_second: Some((counters.bytes_out - previous.bytes_out) as f64 / seconds),
sample_interval_seconds: Some(seconds),
};
} else {
// Long polling gaps or counter resets invalidate the baseline.
state.latest = SummaryRates::default();
}
}
state.baseline = Some((now, counters));
state.latest
}
}
impl Metrics {
pub fn summary_rates(&self) -> SummaryRates {
self.summary_rate_sampler.sample(SummaryCounters {
requests: self.requests.load(Ordering::Relaxed),
errors: self.errors.load(Ordering::Relaxed),
bytes_in: self.bytes_in.load(Ordering::Relaxed),
bytes_out: self.bytes_out.load(Ordering::Relaxed),
}, Instant::now())
}
}
#[cfg(test)]
mod summary_rate_tests {
use super::*;
#[test]
fn sampled_rates_handle_first_read_short_intervals_long_gaps_and_resets() {
let sampler = RateSampler::default();
let at = Instant::now();
let counts = |requests, errors, bytes_in, bytes_out| SummaryCounters {
requests, errors, bytes_in, bytes_out
};
assert!(sampler.sample(counts(10, 2, 100, 200), at).requests_per_second.is_none());
let rate = sampler.sample(counts(20, 4, 300, 500), at + Duration::from_secs(2));
assert_eq!(rate.requests_per_second, Some(5.0));
assert_eq!(rate.errors_per_second, Some(1.0));
assert_eq!(rate.bytes_in_per_second, Some(100.0));
assert_eq!(rate.bytes_out_per_second, Some(150.0));
assert_eq!(rate.sample_interval_seconds, Some(2.0));
assert_eq!(sampler.sample(counts(21, 4, 310, 520), at + Duration::from_millis(2300)).requests_per_second, Some(5.0));
assert!(sampler.sample(counts(22, 4, 330, 550), at + Duration::from_secs(40)).requests_per_second.is_none());
assert!(sampler.sample(counts(1, 0, 0, 0), at + Duration::from_secs(42)).requests_per_second.is_none());
assert_eq!(sampler.sample(counts(5, 0, 200, 300), at + Duration::from_secs(44)).requests_per_second, Some(2.0));
}
}