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 {
148 use std::time::SystemTime;
149 let nanos = SystemTime::now()
150 .duration_since(SystemTime::UNIX_EPOCH)
151 .map(|d| d.subsec_nanos())
152 .unwrap_or(0);
153 (nanos as f64 % 1000.0) / 1000.0
154}
155
156#[derive(Default)]
158pub struct AnalyticsConfigBuilder {
159 config: AnalyticsConfig,
160}
161
162impl AnalyticsConfigBuilder {
163 pub fn enabled(mut self, enabled: bool) -> Self {
164 self.config.enabled = enabled;
165 self
166 }
167
168 pub fn max_latency_samples(mut self, max: usize) -> Self {
169 self.config.max_latency_samples = max;
170 self
171 }
172
173 pub fn max_recent_errors(mut self, max: usize) -> Self {
174 self.config.max_recent_errors = max;
175 self
176 }
177
178 pub fn throughput_window(mut self, secs: u64) -> Self {
179 self.config.throughput_window_secs = secs;
180 self
181 }
182
183 pub fn enable_endpoint_metrics(mut self, enabled: bool) -> Self {
184 self.config.enable_endpoint_metrics = enabled;
185 self
186 }
187
188 pub fn max_endpoints(mut self, max: usize) -> Self {
189 self.config.max_endpoints = max;
190 self
191 }
192
193 pub fn enable_rate_limit_tracking(mut self, enabled: bool) -> Self {
194 self.config.enable_rate_limit_tracking = enabled;
195 self
196 }
197
198 pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
199 self.config.exclude_paths.push(path.into());
200 self
201 }
202
203 pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
204 self.config.exclude_paths = paths;
205 self
206 }
207
208 pub fn include_query_params(mut self, include: bool) -> Self {
209 self.config.include_query_params = include;
210 self
211 }
212
213 pub fn sampling_rate(mut self, rate: f64) -> Self {
214 self.config.sampling_rate = rate.clamp(0.0, 1.0);
215 self
216 }
217
218 pub fn track_clients(mut self, track: bool) -> Self {
219 self.config.track_clients = track;
220 self
221 }
222
223 pub fn max_rate_limit_clients(mut self, max: usize) -> Self {
224 self.config.max_rate_limit_clients = max;
225 self
226 }
227
228 pub fn build(self) -> AnalyticsConfig {
229 self.config
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_default_config() {
239 let config = AnalyticsConfig::default();
240 assert!(config.enabled);
241 assert_eq!(config.sampling_rate, 1.0);
242 }
243
244 #[test]
245 fn test_exclude_paths() {
246 let config = AnalyticsConfig::default();
247 assert!(config.should_exclude("/health"));
248 assert!(config.should_exclude("/healthz"));
249 assert!(!config.should_exclude("/api/users"));
250 }
251
252 #[test]
253 fn test_builder() {
254 let config = AnalyticsConfig::builder()
255 .enabled(true)
256 .sampling_rate(0.5)
257 .max_latency_samples(5000)
258 .exclude_path("/internal")
259 .build();
260
261 assert!(config.enabled);
262 assert_eq!(config.sampling_rate, 0.5);
263 assert_eq!(config.max_latency_samples, 5000);
264 assert!(config.should_exclude("/internal"));
265 }
266}
267