armature_analytics/
config.rs

1//! Analytics configuration
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for the analytics module
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AnalyticsConfig {
8    /// Enable analytics collection
9    pub enabled: bool,
10    /// Maximum number of latency samples to keep for percentile calculation
11    pub max_latency_samples: usize,
12    /// Maximum number of recent errors to keep
13    pub max_recent_errors: usize,
14    /// Time window for throughput calculation (in seconds)
15    pub throughput_window_secs: u64,
16    /// Enable per-endpoint metrics
17    pub enable_endpoint_metrics: bool,
18    /// Maximum number of endpoints to track
19    pub max_endpoints: usize,
20    /// Enable rate limit tracking
21    pub enable_rate_limit_tracking: bool,
22    /// Paths to exclude from analytics
23    pub exclude_paths: Vec<String>,
24    /// Whether to include query parameters in path tracking
25    pub include_query_params: bool,
26    /// Sampling rate (0.0 to 1.0, 1.0 = 100% of requests)
27    pub sampling_rate: f64,
28    /// Enable client identification tracking
29    pub track_clients: bool,
30    /// Maximum number of unique clients to track for rate limits
31    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    /// Create a new configuration builder
60    pub fn builder() -> AnalyticsConfigBuilder {
61        AnalyticsConfigBuilder::default()
62    }
63
64    /// Create configuration for development (verbose tracking)
65    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    /// Create configuration for production (optimized)
83    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    /// Create minimal configuration (low overhead)
107    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, // 10% sampling
124            track_clients: false,
125            max_rate_limit_clients: 100,
126        }
127    }
128
129    /// Check if a path should be excluded
130    pub fn should_exclude(&self, path: &str) -> bool {
131        self.exclude_paths.iter().any(|p| path.starts_with(p))
132    }
133
134    /// Check if this request should be sampled
135    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
146/// Simple random float generator (0.0 to 1.0)
147fn 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/// Builder for AnalyticsConfig
157#[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