1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AnalyticsConfig {
8 pub enabled: bool,
10 pub max_latency_samples: usize,
12 pub max_recent_errors: usize,
14 pub throughput_window_secs: u64,
16 pub enable_endpoint_metrics: bool,
18 pub max_endpoints: usize,
20 pub enable_rate_limit_tracking: bool,
22 pub exclude_paths: Vec<String>,
24 pub include_query_params: bool,
26 pub sampling_rate: f64,
28 pub track_clients: bool,
30 pub max_rate_limit_clients: usize,
32}
33
34impl Default for AnalyticsConfig {
35 fn default() -> Self {
36 Self {
37 enabled: true,
38 max_latency_samples: 10_000,
39 max_recent_errors: 100,
40 throughput_window_secs: 60,
41 enable_endpoint_metrics: true,
42 max_endpoints: 500,
43 enable_rate_limit_tracking: true,
44 exclude_paths: vec![
45 "/health".to_string(),
46 "/healthz".to_string(),
47 "/ready".to_string(),
48 "/metrics".to_string(),
49 ],
50 include_query_params: false,
51 sampling_rate: 1.0,
52 track_clients: true,
53 max_rate_limit_clients: 1000,
54 }
55 }
56}
57
58impl AnalyticsConfig {
59 pub fn builder() -> AnalyticsConfigBuilder {
61 AnalyticsConfigBuilder::default()
62 }
63
64 pub fn development() -> Self {
66 Self {
67 enabled: true,
68 max_latency_samples: 50_000,
69 max_recent_errors: 500,
70 throughput_window_secs: 60,
71 enable_endpoint_metrics: true,
72 max_endpoints: 1000,
73 enable_rate_limit_tracking: true,
74 exclude_paths: vec![],
75 include_query_params: true,
76 sampling_rate: 1.0,
77 track_clients: true,
78 max_rate_limit_clients: 5000,
79 }
80 }
81
82 pub fn production() -> Self {
84 Self {
85 enabled: true,
86 max_latency_samples: 10_000,
87 max_recent_errors: 100,
88 throughput_window_secs: 60,
89 enable_endpoint_metrics: true,
90 max_endpoints: 500,
91 enable_rate_limit_tracking: true,
92 exclude_paths: vec![
93 "/health".to_string(),
94 "/healthz".to_string(),
95 "/ready".to_string(),
96 "/metrics".to_string(),
97 "/favicon.ico".to_string(),
98 ],
99 include_query_params: false,
100 sampling_rate: 1.0,
101 track_clients: true,
102 max_rate_limit_clients: 1000,
103 }
104 }
105
106 pub fn minimal() -> Self {
108 Self {
109 enabled: true,
110 max_latency_samples: 1_000,
111 max_recent_errors: 20,
112 throughput_window_secs: 60,
113 enable_endpoint_metrics: false,
114 max_endpoints: 100,
115 enable_rate_limit_tracking: false,
116 exclude_paths: vec![
117 "/health".to_string(),
118 "/healthz".to_string(),
119 "/ready".to_string(),
120 "/metrics".to_string(),
121 ],
122 include_query_params: false,
123 sampling_rate: 0.1, track_clients: false,
125 max_rate_limit_clients: 100,
126 }
127 }
128
129 pub fn should_exclude(&self, path: &str) -> bool {
131 self.exclude_paths.iter().any(|p| path.starts_with(p))
132 }
133
134 pub fn should_sample(&self) -> bool {
136 if self.sampling_rate >= 1.0 {
137 return true;
138 }
139 if self.sampling_rate <= 0.0 {
140 return false;
141 }
142 rand_float() < self.sampling_rate
143 }
144}
145
146fn rand_float() -> f64 {
154 use std::cell::Cell;
155 use std::sync::atomic::{AtomicU64, Ordering};
156 use std::time::{SystemTime, UNIX_EPOCH};
157
158 static SEED_COUNTER: AtomicU64 = AtomicU64::new(0);
161
162 thread_local! {
163 static STATE: Cell<u64> = Cell::new({
164 let nanos = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .map(|d| d.as_nanos() as u64)
167 .unwrap_or(0);
168 let counter = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
169 let mut s = nanos
171 ^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15)
172 ^ 0xD1B5_4A32_D192_ED03;
173 if s == 0 {
174 s = 0x9E37_79B9_7F4A_7C15;
175 }
176 s
177 });
178 }
179
180 STATE.with(|state| {
181 let mut x = state.get();
182 x ^= x >> 12;
184 x ^= x << 25;
185 x ^= x >> 27;
186 state.set(x);
187 let v = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
188 ((v >> 11) as f64) / ((1u64 << 53) as f64)
190 })
191}
192
193#[derive(Default)]
195pub struct AnalyticsConfigBuilder {
196 config: AnalyticsConfig,
197}
198
199impl AnalyticsConfigBuilder {
200 pub fn enabled(mut self, enabled: bool) -> Self {
201 self.config.enabled = enabled;
202 self
203 }
204
205 pub fn max_latency_samples(mut self, max: usize) -> Self {
206 self.config.max_latency_samples = max;
207 self
208 }
209
210 pub fn max_recent_errors(mut self, max: usize) -> Self {
211 self.config.max_recent_errors = max;
212 self
213 }
214
215 pub fn throughput_window(mut self, secs: u64) -> Self {
216 self.config.throughput_window_secs = secs;
217 self
218 }
219
220 pub fn enable_endpoint_metrics(mut self, enabled: bool) -> Self {
221 self.config.enable_endpoint_metrics = enabled;
222 self
223 }
224
225 pub fn max_endpoints(mut self, max: usize) -> Self {
226 self.config.max_endpoints = max;
227 self
228 }
229
230 pub fn enable_rate_limit_tracking(mut self, enabled: bool) -> Self {
231 self.config.enable_rate_limit_tracking = enabled;
232 self
233 }
234
235 pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
236 self.config.exclude_paths.push(path.into());
237 self
238 }
239
240 pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
241 self.config.exclude_paths = paths;
242 self
243 }
244
245 pub fn include_query_params(mut self, include: bool) -> Self {
246 self.config.include_query_params = include;
247 self
248 }
249
250 pub fn sampling_rate(mut self, rate: f64) -> Self {
251 self.config.sampling_rate = rate.clamp(0.0, 1.0);
252 self
253 }
254
255 pub fn track_clients(mut self, track: bool) -> Self {
256 self.config.track_clients = track;
257 self
258 }
259
260 pub fn max_rate_limit_clients(mut self, max: usize) -> Self {
261 self.config.max_rate_limit_clients = max;
262 self
263 }
264
265 pub fn build(self) -> AnalyticsConfig {
266 self.config
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_default_config() {
276 let config = AnalyticsConfig::default();
277 assert!(config.enabled);
278 assert_eq!(config.sampling_rate, 1.0);
279 }
280
281 #[test]
282 fn test_exclude_paths() {
283 let config = AnalyticsConfig::default();
284 assert!(config.should_exclude("/health"));
285 assert!(config.should_exclude("/healthz"));
286 assert!(!config.should_exclude("/api/users"));
287 }
288
289 #[test]
290 fn test_builder() {
291 let config = AnalyticsConfig::builder()
292 .enabled(true)
293 .sampling_rate(0.5)
294 .max_latency_samples(5000)
295 .exclude_path("/internal")
296 .build();
297
298 assert!(config.enabled);
299 assert_eq!(config.sampling_rate, 0.5);
300 assert_eq!(config.max_latency_samples, 5000);
301 assert!(config.should_exclude("/internal"));
302 }
303
304 #[test]
308 fn test_sampling_is_roughly_uniform() {
309 let config = AnalyticsConfig::builder().sampling_rate(0.5).build();
310 let n = 20_000;
311 let sampled = (0..n).filter(|_| config.should_sample()).count();
312 let ratio = sampled as f64 / n as f64;
313 assert!(
314 (0.45..=0.55).contains(&ratio),
315 "expected ~50% sampling, got {:.3}",
316 ratio
317 );
318 }
319
320 #[test]
321 fn test_rand_float_in_range_and_varies() {
322 let mut seen = std::collections::HashSet::new();
323 for _ in 0..1000 {
324 let v = rand_float();
325 assert!((0.0..1.0).contains(&v));
326 seen.insert(v.to_bits());
327 }
328 assert!(
330 seen.len() > 500,
331 "rand_float not varied enough: {}",
332 seen.len()
333 );
334 }
335}