use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticsConfig {
pub enabled: bool,
pub max_latency_samples: usize,
pub max_recent_errors: usize,
pub throughput_window_secs: u64,
pub enable_endpoint_metrics: bool,
pub max_endpoints: usize,
pub enable_rate_limit_tracking: bool,
pub exclude_paths: Vec<String>,
pub include_query_params: bool,
pub sampling_rate: f64,
pub track_clients: bool,
pub max_rate_limit_clients: usize,
}
impl Default for AnalyticsConfig {
fn default() -> Self {
Self {
enabled: true,
max_latency_samples: 10_000,
max_recent_errors: 100,
throughput_window_secs: 60,
enable_endpoint_metrics: true,
max_endpoints: 500,
enable_rate_limit_tracking: true,
exclude_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/ready".to_string(),
"/metrics".to_string(),
],
include_query_params: false,
sampling_rate: 1.0,
track_clients: true,
max_rate_limit_clients: 1000,
}
}
}
impl AnalyticsConfig {
pub fn builder() -> AnalyticsConfigBuilder {
AnalyticsConfigBuilder::default()
}
pub fn development() -> Self {
Self {
enabled: true,
max_latency_samples: 50_000,
max_recent_errors: 500,
throughput_window_secs: 60,
enable_endpoint_metrics: true,
max_endpoints: 1000,
enable_rate_limit_tracking: true,
exclude_paths: vec![],
include_query_params: true,
sampling_rate: 1.0,
track_clients: true,
max_rate_limit_clients: 5000,
}
}
pub fn production() -> Self {
Self {
enabled: true,
max_latency_samples: 10_000,
max_recent_errors: 100,
throughput_window_secs: 60,
enable_endpoint_metrics: true,
max_endpoints: 500,
enable_rate_limit_tracking: true,
exclude_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/ready".to_string(),
"/metrics".to_string(),
"/favicon.ico".to_string(),
],
include_query_params: false,
sampling_rate: 1.0,
track_clients: true,
max_rate_limit_clients: 1000,
}
}
pub fn minimal() -> Self {
Self {
enabled: true,
max_latency_samples: 1_000,
max_recent_errors: 20,
throughput_window_secs: 60,
enable_endpoint_metrics: false,
max_endpoints: 100,
enable_rate_limit_tracking: false,
exclude_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/ready".to_string(),
"/metrics".to_string(),
],
include_query_params: false,
sampling_rate: 0.1, track_clients: false,
max_rate_limit_clients: 100,
}
}
pub fn should_exclude(&self, path: &str) -> bool {
self.exclude_paths.iter().any(|p| path.starts_with(p))
}
pub fn should_sample(&self) -> bool {
if self.sampling_rate >= 1.0 {
return true;
}
if self.sampling_rate <= 0.0 {
return false;
}
rand_float() < self.sampling_rate
}
}
fn rand_float() -> f64 {
use std::cell::Cell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
thread_local! {
static STATE: Cell<u64> = Cell::new({
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut s = nanos
^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15)
^ 0xD1B5_4A32_D192_ED03;
if s == 0 {
s = 0x9E37_79B9_7F4A_7C15;
}
s
});
}
STATE.with(|state| {
let mut x = state.get();
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
state.set(x);
let v = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
((v >> 11) as f64) / ((1u64 << 53) as f64)
})
}
#[derive(Default)]
pub struct AnalyticsConfigBuilder {
config: AnalyticsConfig,
}
impl AnalyticsConfigBuilder {
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn max_latency_samples(mut self, max: usize) -> Self {
self.config.max_latency_samples = max;
self
}
pub fn max_recent_errors(mut self, max: usize) -> Self {
self.config.max_recent_errors = max;
self
}
pub fn throughput_window(mut self, secs: u64) -> Self {
self.config.throughput_window_secs = secs;
self
}
pub fn enable_endpoint_metrics(mut self, enabled: bool) -> Self {
self.config.enable_endpoint_metrics = enabled;
self
}
pub fn max_endpoints(mut self, max: usize) -> Self {
self.config.max_endpoints = max;
self
}
pub fn enable_rate_limit_tracking(mut self, enabled: bool) -> Self {
self.config.enable_rate_limit_tracking = enabled;
self
}
pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
self.config.exclude_paths.push(path.into());
self
}
pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
self.config.exclude_paths = paths;
self
}
pub fn include_query_params(mut self, include: bool) -> Self {
self.config.include_query_params = include;
self
}
pub fn sampling_rate(mut self, rate: f64) -> Self {
self.config.sampling_rate = rate.clamp(0.0, 1.0);
self
}
pub fn track_clients(mut self, track: bool) -> Self {
self.config.track_clients = track;
self
}
pub fn max_rate_limit_clients(mut self, max: usize) -> Self {
self.config.max_rate_limit_clients = max;
self
}
pub fn build(self) -> AnalyticsConfig {
self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = AnalyticsConfig::default();
assert!(config.enabled);
assert_eq!(config.sampling_rate, 1.0);
}
#[test]
fn test_exclude_paths() {
let config = AnalyticsConfig::default();
assert!(config.should_exclude("/health"));
assert!(config.should_exclude("/healthz"));
assert!(!config.should_exclude("/api/users"));
}
#[test]
fn test_builder() {
let config = AnalyticsConfig::builder()
.enabled(true)
.sampling_rate(0.5)
.max_latency_samples(5000)
.exclude_path("/internal")
.build();
assert!(config.enabled);
assert_eq!(config.sampling_rate, 0.5);
assert_eq!(config.max_latency_samples, 5000);
assert!(config.should_exclude("/internal"));
}
#[test]
fn test_sampling_is_roughly_uniform() {
let config = AnalyticsConfig::builder().sampling_rate(0.5).build();
let n = 20_000;
let sampled = (0..n).filter(|_| config.should_sample()).count();
let ratio = sampled as f64 / n as f64;
assert!(
(0.45..=0.55).contains(&ratio),
"expected ~50% sampling, got {:.3}",
ratio
);
}
#[test]
fn test_rand_float_in_range_and_varies() {
let mut seen = std::collections::HashSet::new();
for _ in 0..1000 {
let v = rand_float();
assert!((0.0..1.0).contains(&v));
seen.insert(v.to_bits());
}
assert!(
seen.len() > 500,
"rand_float not varied enough: {}",
seen.len()
);
}
}