1mod collector;
60mod config;
61mod error;
62mod insights;
63mod metrics;
64mod middleware;
65
66pub use collector::*;
67pub use config::*;
68pub use error::*;
69pub use insights::*;
70pub use metrics::*;
71pub use middleware::*;
72
73use chrono::{DateTime, Utc};
74use serde::{Deserialize, Serialize};
75use std::collections::HashMap;
76use std::sync::Arc;
77use std::time::Duration;
78
79#[derive(Clone)]
83pub struct Analytics {
84 inner: Arc<AnalyticsInner>,
85}
86
87struct AnalyticsInner {
88 config: AnalyticsConfig,
89 collector: MetricsCollector,
90 started_at: DateTime<Utc>,
91}
92
93impl Analytics {
94 pub fn new(config: AnalyticsConfig) -> Self {
96 let collector = MetricsCollector::from_config(&config);
97 Self {
98 inner: Arc::new(AnalyticsInner {
99 config,
100 collector,
101 started_at: Utc::now(),
102 }),
103 }
104 }
105
106 pub fn record_request(&self, record: RequestRecord) {
108 self.inner.collector.record_request(record);
109 }
110
111 pub fn record_rate_limit(&self, event: RateLimitEvent) {
113 self.inner.collector.record_rate_limit(event);
114 }
115
116 pub fn record_error(&self, error: ErrorRecord) {
118 self.inner.collector.record_error(error);
119 }
120
121 pub fn snapshot(&self) -> AnalyticsSnapshot {
123 let collector = &self.inner.collector;
124
125 AnalyticsSnapshot {
126 timestamp: Utc::now(),
127 uptime_seconds: (Utc::now() - self.inner.started_at).num_seconds() as u64,
128 requests: collector.request_metrics(),
129 latency: collector.latency_metrics(),
130 errors: collector.error_metrics(),
131 rate_limits: collector.rate_limit_metrics(),
132 endpoints: collector.endpoint_metrics(),
133 throughput: collector.throughput_metrics(),
134 }
135 }
136
137 pub fn dashboard_json(&self) -> String {
139 serde_json::to_string_pretty(&self.snapshot()).unwrap_or_else(|_| "{}".to_string())
140 }
141
142 pub fn reset(&self) {
144 self.inner.collector.reset();
145 }
146
147 pub fn config(&self) -> &AnalyticsConfig {
149 &self.inner.config
150 }
151}
152
153#[derive(Debug, Clone)]
159pub struct RequestRecord {
160 pub method: String,
162 pub path: String,
164 pub status: u16,
166 pub duration: Duration,
168 pub timestamp: DateTime<Utc>,
170 pub client_id: Option<String>,
172 pub response_size: Option<u64>,
174 pub authenticated: bool,
176 pub tags: HashMap<String, String>,
178}
179
180impl RequestRecord {
181 pub fn new(
183 method: impl Into<String>,
184 path: impl Into<String>,
185 status: u16,
186 duration: Duration,
187 ) -> Self {
188 Self {
189 method: method.into(),
190 path: path.into(),
191 status,
192 duration,
193 timestamp: Utc::now(),
194 client_id: None,
195 response_size: None,
196 authenticated: false,
197 tags: HashMap::new(),
198 }
199 }
200
201 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
202 self.client_id = Some(client_id.into());
203 self
204 }
205
206 pub fn with_response_size(mut self, size: u64) -> Self {
207 self.response_size = Some(size);
208 self
209 }
210
211 pub fn with_authenticated(mut self, authenticated: bool) -> Self {
212 self.authenticated = authenticated;
213 self
214 }
215
216 pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
217 self.tags.insert(key.into(), value.into());
218 self
219 }
220
221 pub fn is_success(&self) -> bool {
223 self.status >= 200 && self.status < 300
224 }
225
226 pub fn is_client_error(&self) -> bool {
228 self.status >= 400 && self.status < 500
229 }
230
231 pub fn is_server_error(&self) -> bool {
233 self.status >= 500
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum RateLimitEventType {
244 Allowed,
246 Limited,
248 Warning,
250}
251
252#[derive(Debug, Clone)]
254pub struct RateLimitEvent {
255 pub client_id: String,
257 pub event_type: RateLimitEventType,
259 pub current_count: u64,
261 pub limit: u64,
263 pub window_seconds: u64,
265 pub endpoint: Option<String>,
267 pub timestamp: DateTime<Utc>,
269}
270
271impl RateLimitEvent {
272 pub fn allowed(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
273 Self {
274 client_id: client_id.into(),
275 event_type: RateLimitEventType::Allowed,
276 current_count: current,
277 limit,
278 window_seconds: window,
279 endpoint: None,
280 timestamp: Utc::now(),
281 }
282 }
283
284 pub fn limited(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
285 Self {
286 client_id: client_id.into(),
287 event_type: RateLimitEventType::Limited,
288 current_count: current,
289 limit,
290 window_seconds: window,
291 endpoint: None,
292 timestamp: Utc::now(),
293 }
294 }
295
296 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
297 self.endpoint = Some(endpoint.into());
298 self
299 }
300
301 pub fn utilization(&self) -> f64 {
303 if self.limit == 0 {
304 0.0
305 } else {
306 (self.current_count as f64 / self.limit as f64) * 100.0
307 }
308 }
309}
310
311#[derive(Debug, Clone)]
317pub struct ErrorRecord {
318 pub error_type: String,
320 pub message: String,
322 pub status: Option<u16>,
324 pub endpoint: Option<String>,
326 pub stack_trace: Option<String>,
328 pub timestamp: DateTime<Utc>,
330 pub context: HashMap<String, String>,
332}
333
334impl ErrorRecord {
335 pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
336 Self {
337 error_type: error_type.into(),
338 message: message.into(),
339 status: None,
340 endpoint: None,
341 stack_trace: None,
342 timestamp: Utc::now(),
343 context: HashMap::new(),
344 }
345 }
346
347 pub fn with_status(mut self, status: u16) -> Self {
348 self.status = Some(status);
349 self
350 }
351
352 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
353 self.endpoint = Some(endpoint.into());
354 self
355 }
356
357 pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
358 self.context.insert(key.into(), value.into());
359 self
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct AnalyticsSnapshot {
370 pub timestamp: DateTime<Utc>,
372 pub uptime_seconds: u64,
374 pub requests: RequestMetrics,
376 pub latency: LatencyMetrics,
378 pub errors: ErrorMetrics,
380 pub rate_limits: RateLimitMetrics,
382 pub endpoints: Vec<EndpointMetrics>,
384 pub throughput: ThroughputMetrics,
386}
387
388#[derive(Debug, Clone, Default, Serialize, Deserialize)]
390pub struct RequestMetrics {
391 pub total: u64,
393 pub success: u64,
395 pub client_errors: u64,
397 pub server_errors: u64,
399 pub by_method: HashMap<String, u64>,
401 pub by_status: HashMap<u16, u64>,
403}
404
405impl RequestMetrics {
406 pub fn success_rate(&self) -> f64 {
408 if self.total == 0 {
409 100.0
410 } else {
411 (self.success as f64 / self.total as f64) * 100.0
412 }
413 }
414
415 pub fn error_rate(&self) -> f64 {
417 if self.total == 0 {
418 0.0
419 } else {
420 ((self.client_errors + self.server_errors) as f64 / self.total as f64) * 100.0
421 }
422 }
423}
424
425#[derive(Debug, Clone, Default, Serialize, Deserialize)]
427pub struct LatencyMetrics {
428 pub avg_ms: f64,
430 pub min_ms: f64,
432 pub max_ms: f64,
434 pub p50_ms: f64,
436 pub p90_ms: f64,
438 pub p95_ms: f64,
440 pub p99_ms: f64,
442 pub samples: u64,
444}
445
446#[derive(Debug, Clone, Default, Serialize, Deserialize)]
448pub struct ErrorMetrics {
449 pub total: u64,
451 pub by_type: HashMap<String, u64>,
453 pub by_status: HashMap<u16, u64>,
455 pub recent: Vec<ErrorSummary>,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct ErrorSummary {
462 pub error_type: String,
463 pub message: String,
464 pub count: u64,
465 pub last_seen: DateTime<Utc>,
466}
467
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct RateLimitMetrics {
471 pub total_checks: u64,
473 pub allowed: u64,
475 pub limited: u64,
477 pub unique_clients_limited: u64,
479 pub avg_utilization: f64,
481 pub top_limited_clients: Vec<ClientRateLimitInfo>,
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct ClientRateLimitInfo {
488 pub client_id: String,
489 pub times_limited: u64,
490 pub last_limited: DateTime<Utc>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct EndpointMetrics {
496 pub path: String,
498 pub method: String,
500 pub requests: u64,
502 pub errors: u64,
504 pub avg_latency_ms: f64,
506 pub p99_latency_ms: f64,
508 pub error_rate: f64,
510}
511
512#[derive(Debug, Clone, Default, Serialize, Deserialize)]
514pub struct ThroughputMetrics {
515 pub requests_per_second: f64,
517 pub requests_last_minute: u64,
519 pub requests_last_hour: u64,
521 pub peak_rps: f64,
523 pub avg_response_size: u64,
525 pub total_bytes_transferred: u64,
527}
528
529#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_request_record() {
539 let record = RequestRecord::new("GET", "/api/users", 200, Duration::from_millis(50))
540 .with_client_id("user-123")
541 .with_response_size(1024)
542 .with_authenticated(true)
543 .with_tag("version", "v1");
544
545 assert!(record.is_success());
546 assert!(!record.is_client_error());
547 assert!(!record.is_server_error());
548 assert_eq!(record.client_id, Some("user-123".to_string()));
549 }
550
551 #[test]
552 fn test_rate_limit_event() {
553 let event = RateLimitEvent::limited("client-1", 100, 100, 60);
554 assert_eq!(event.utilization(), 100.0);
555
556 let event = RateLimitEvent::allowed("client-2", 50, 100, 60);
557 assert_eq!(event.utilization(), 50.0);
558 }
559
560 #[test]
561 fn test_request_metrics() {
562 let metrics = RequestMetrics {
563 total: 100,
564 success: 90,
565 client_errors: 8,
566 server_errors: 2,
567 ..Default::default()
568 };
569
570 assert_eq!(metrics.success_rate(), 90.0);
571 assert_eq!(metrics.error_rate(), 10.0);
572 }
573
574 #[test]
575 fn test_analytics_creation() {
576 let analytics = Analytics::new(AnalyticsConfig::default());
577 let snapshot = analytics.snapshot();
578
579 assert_eq!(snapshot.requests.total, 0);
580 assert_eq!(snapshot.errors.total, 0);
581 }
582
583 #[test]
586 fn test_analytics_new_respects_capacity_knobs() {
587 let config = AnalyticsConfig::builder().max_endpoints(2).build();
588 let analytics = Analytics::new(config);
589
590 for i in 0..5 {
591 analytics.record_request(RequestRecord::new(
592 "GET",
593 format!("/endpoint-{i}"),
594 200,
595 Duration::from_millis(1),
596 ));
597 }
598
599 let snapshot = analytics.snapshot();
600 assert_eq!(
601 snapshot.endpoints.len(),
602 2,
603 "max_endpoints from config must be respected"
604 );
605 }
606}