armature_analytics/
lib.rs

1//! API Analytics Module for Armature Framework
2//!
3//! Provides comprehensive API usage tracking, rate limit insights, and error monitoring.
4//!
5//! ## Features
6//!
7//! - **Request Metrics**: Track requests per endpoint, method, and status code
8//! - **Latency Tracking**: P50, P90, P95, P99 latency percentiles
9//! - **Error Rates**: Monitor error rates by endpoint and error type
10//! - **Rate Limit Insights**: Track rate limit hits, rejections, and usage patterns
11//! - **Throughput Monitoring**: Requests per second, minute, hour
12//! - **Real-time Dashboard**: JSON endpoint for analytics data
13//!
14//! ## Quick Start
15//!
16//! ```rust,ignore
17//! use armature_analytics::{Analytics, AnalyticsMiddleware};
18//! use armature_core::Application;
19//!
20//! let analytics = Analytics::new(AnalyticsConfig::default());
21//!
22//! let app = Application::new(container, router)
23//!     .middleware(AnalyticsMiddleware::new(analytics.clone()));
24//!
25//! // Access analytics endpoint
26//! // GET /api/_analytics -> JSON dashboard data
27//! ```
28//!
29//! ## Architecture
30//!
31//! ```text
32//! ┌─────────────────────────────────────────────────────────────┐
33//! │                        Requests                              │
34//! └─────────────────────────┬───────────────────────────────────┘
35//!                           │
36//!                           ▼
37//! ┌─────────────────────────────────────────────────────────────┐
38//! │                 AnalyticsMiddleware                          │
39//! │  - Captures request/response metadata                        │
40//! │  - Records timing, status, errors                            │
41//! └─────────────────────────┬───────────────────────────────────┘
42//!                           │
43//!                           ▼
44//! ┌─────────────────────────────────────────────────────────────┐
45//! │                    MetricsCollector                          │
46//! │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
47//! │  │ Requests │ │ Latency  │ │  Errors  │ │Rate Limit│       │
48//! │  │ Counter  │ │Histogram │ │ Tracker  │ │ Insights │       │
49//! │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
50//! └─────────────────────────┬───────────────────────────────────┘
51//!                           │
52//!                           ▼
53//! ┌─────────────────────────────────────────────────────────────┐
54//! │                    Export Backends                           │
55//! │  ┌──────────┐ ┌──────────┐ ┌──────────┐                     │
56//! │  │   JSON   │ │Prometheus│ │  Custom  │                     │
57//! │  └──────────┘ └──────────┘ └──────────┘                     │
58//! └─────────────────────────────────────────────────────────────┘
59//! ```
60
61mod collector;
62mod config;
63mod error;
64mod insights;
65mod metrics;
66mod middleware;
67
68pub use collector::*;
69pub use config::*;
70pub use error::*;
71pub use insights::*;
72pub use metrics::*;
73pub use middleware::*;
74
75use chrono::{DateTime, Utc};
76use serde::{Deserialize, Serialize};
77use std::collections::HashMap;
78use std::sync::Arc;
79use std::time::Duration;
80
81/// Main analytics instance
82///
83/// Thread-safe analytics collector that can be shared across handlers.
84#[derive(Clone)]
85pub struct Analytics {
86    inner: Arc<AnalyticsInner>,
87}
88
89struct AnalyticsInner {
90    config: AnalyticsConfig,
91    collector: MetricsCollector,
92    started_at: DateTime<Utc>,
93}
94
95impl Analytics {
96    /// Create a new analytics instance
97    pub fn new(config: AnalyticsConfig) -> Self {
98        Self {
99            inner: Arc::new(AnalyticsInner {
100                config,
101                collector: MetricsCollector::new(),
102                started_at: Utc::now(),
103            }),
104        }
105    }
106
107    /// Record a request
108    pub fn record_request(&self, record: RequestRecord) {
109        self.inner.collector.record_request(record);
110    }
111
112    /// Record a rate limit event
113    pub fn record_rate_limit(&self, event: RateLimitEvent) {
114        self.inner.collector.record_rate_limit(event);
115    }
116
117    /// Record an error
118    pub fn record_error(&self, error: ErrorRecord) {
119        self.inner.collector.record_error(error);
120    }
121
122    /// Get current analytics snapshot
123    pub fn snapshot(&self) -> AnalyticsSnapshot {
124        let collector = &self.inner.collector;
125
126        AnalyticsSnapshot {
127            timestamp: Utc::now(),
128            uptime_seconds: (Utc::now() - self.inner.started_at).num_seconds() as u64,
129            requests: collector.request_metrics(),
130            latency: collector.latency_metrics(),
131            errors: collector.error_metrics(),
132            rate_limits: collector.rate_limit_metrics(),
133            endpoints: collector.endpoint_metrics(),
134            throughput: collector.throughput_metrics(),
135        }
136    }
137
138    /// Get JSON dashboard data
139    pub fn dashboard_json(&self) -> String {
140        serde_json::to_string_pretty(&self.snapshot()).unwrap_or_else(|_| "{}".to_string())
141    }
142
143    /// Reset all metrics
144    pub fn reset(&self) {
145        self.inner.collector.reset();
146    }
147
148    /// Get the configuration
149    pub fn config(&self) -> &AnalyticsConfig {
150        &self.inner.config
151    }
152}
153
154// =============================================================================
155// Request Recording
156// =============================================================================
157
158/// Record of a single request for analytics
159#[derive(Debug, Clone)]
160pub struct RequestRecord {
161    /// HTTP method
162    pub method: String,
163    /// Request path (normalized)
164    pub path: String,
165    /// HTTP status code
166    pub status: u16,
167    /// Request duration
168    pub duration: Duration,
169    /// Request timestamp
170    pub timestamp: DateTime<Utc>,
171    /// Client identifier (IP, user ID, etc.)
172    pub client_id: Option<String>,
173    /// Response body size in bytes
174    pub response_size: Option<u64>,
175    /// Whether the request was authenticated
176    pub authenticated: bool,
177    /// Custom tags for filtering
178    pub tags: HashMap<String, String>,
179}
180
181impl RequestRecord {
182    /// Create a new request record
183    pub fn new(method: impl Into<String>, path: impl Into<String>, status: u16, duration: Duration) -> Self {
184        Self {
185            method: method.into(),
186            path: path.into(),
187            status,
188            duration,
189            timestamp: Utc::now(),
190            client_id: None,
191            response_size: None,
192            authenticated: false,
193            tags: HashMap::new(),
194        }
195    }
196
197    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
198        self.client_id = Some(client_id.into());
199        self
200    }
201
202    pub fn with_response_size(mut self, size: u64) -> Self {
203        self.response_size = Some(size);
204        self
205    }
206
207    pub fn with_authenticated(mut self, authenticated: bool) -> Self {
208        self.authenticated = authenticated;
209        self
210    }
211
212    pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
213        self.tags.insert(key.into(), value.into());
214        self
215    }
216
217    /// Check if request was successful (2xx)
218    pub fn is_success(&self) -> bool {
219        self.status >= 200 && self.status < 300
220    }
221
222    /// Check if request was a client error (4xx)
223    pub fn is_client_error(&self) -> bool {
224        self.status >= 400 && self.status < 500
225    }
226
227    /// Check if request was a server error (5xx)
228    pub fn is_server_error(&self) -> bool {
229        self.status >= 500
230    }
231}
232
233// =============================================================================
234// Rate Limit Events
235// =============================================================================
236
237/// Rate limit event types
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
239pub enum RateLimitEventType {
240    /// Request was allowed
241    Allowed,
242    /// Request was rate limited
243    Limited,
244    /// Near the limit (warning threshold)
245    Warning,
246}
247
248/// Record of a rate limit event
249#[derive(Debug, Clone)]
250pub struct RateLimitEvent {
251    /// Client identifier
252    pub client_id: String,
253    /// Event type
254    pub event_type: RateLimitEventType,
255    /// Current request count
256    pub current_count: u64,
257    /// Maximum allowed requests
258    pub limit: u64,
259    /// Time window in seconds
260    pub window_seconds: u64,
261    /// Endpoint affected
262    pub endpoint: Option<String>,
263    /// Timestamp
264    pub timestamp: DateTime<Utc>,
265}
266
267impl RateLimitEvent {
268    pub fn allowed(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
269        Self {
270            client_id: client_id.into(),
271            event_type: RateLimitEventType::Allowed,
272            current_count: current,
273            limit,
274            window_seconds: window,
275            endpoint: None,
276            timestamp: Utc::now(),
277        }
278    }
279
280    pub fn limited(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
281        Self {
282            client_id: client_id.into(),
283            event_type: RateLimitEventType::Limited,
284            current_count: current,
285            limit,
286            window_seconds: window,
287            endpoint: None,
288            timestamp: Utc::now(),
289        }
290    }
291
292    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
293        self.endpoint = Some(endpoint.into());
294        self
295    }
296
297    /// Calculate utilization percentage
298    pub fn utilization(&self) -> f64 {
299        if self.limit == 0 {
300            0.0
301        } else {
302            (self.current_count as f64 / self.limit as f64) * 100.0
303        }
304    }
305}
306
307// =============================================================================
308// Error Recording
309// =============================================================================
310
311/// Record of an error for analytics
312#[derive(Debug, Clone)]
313pub struct ErrorRecord {
314    /// Error type/code
315    pub error_type: String,
316    /// Error message
317    pub message: String,
318    /// HTTP status code
319    pub status: Option<u16>,
320    /// Endpoint where error occurred
321    pub endpoint: Option<String>,
322    /// Stack trace (if available)
323    pub stack_trace: Option<String>,
324    /// Timestamp
325    pub timestamp: DateTime<Utc>,
326    /// Additional context
327    pub context: HashMap<String, String>,
328}
329
330impl ErrorRecord {
331    pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
332        Self {
333            error_type: error_type.into(),
334            message: message.into(),
335            status: None,
336            endpoint: None,
337            stack_trace: None,
338            timestamp: Utc::now(),
339            context: HashMap::new(),
340        }
341    }
342
343    pub fn with_status(mut self, status: u16) -> Self {
344        self.status = Some(status);
345        self
346    }
347
348    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
349        self.endpoint = Some(endpoint.into());
350        self
351    }
352
353    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
354        self.context.insert(key.into(), value.into());
355        self
356    }
357}
358
359// =============================================================================
360// Analytics Snapshot
361// =============================================================================
362
363/// Complete analytics snapshot for dashboard/export
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct AnalyticsSnapshot {
366    /// Snapshot timestamp
367    pub timestamp: DateTime<Utc>,
368    /// Uptime in seconds
369    pub uptime_seconds: u64,
370    /// Request metrics
371    pub requests: RequestMetrics,
372    /// Latency metrics
373    pub latency: LatencyMetrics,
374    /// Error metrics
375    pub errors: ErrorMetrics,
376    /// Rate limit metrics
377    pub rate_limits: RateLimitMetrics,
378    /// Per-endpoint metrics
379    pub endpoints: Vec<EndpointMetrics>,
380    /// Throughput metrics
381    pub throughput: ThroughputMetrics,
382}
383
384/// Request metrics summary
385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
386pub struct RequestMetrics {
387    /// Total requests
388    pub total: u64,
389    /// Successful requests (2xx)
390    pub success: u64,
391    /// Client errors (4xx)
392    pub client_errors: u64,
393    /// Server errors (5xx)
394    pub server_errors: u64,
395    /// Requests by method
396    pub by_method: HashMap<String, u64>,
397    /// Requests by status code
398    pub by_status: HashMap<u16, u64>,
399}
400
401impl RequestMetrics {
402    /// Calculate success rate as percentage
403    pub fn success_rate(&self) -> f64 {
404        if self.total == 0 {
405            100.0
406        } else {
407            (self.success as f64 / self.total as f64) * 100.0
408        }
409    }
410
411    /// Calculate error rate as percentage
412    pub fn error_rate(&self) -> f64 {
413        if self.total == 0 {
414            0.0
415        } else {
416            ((self.client_errors + self.server_errors) as f64 / self.total as f64) * 100.0
417        }
418    }
419}
420
421/// Latency metrics with percentiles
422#[derive(Debug, Clone, Default, Serialize, Deserialize)]
423pub struct LatencyMetrics {
424    /// Average latency in milliseconds
425    pub avg_ms: f64,
426    /// Minimum latency in milliseconds
427    pub min_ms: f64,
428    /// Maximum latency in milliseconds
429    pub max_ms: f64,
430    /// 50th percentile (median)
431    pub p50_ms: f64,
432    /// 90th percentile
433    pub p90_ms: f64,
434    /// 95th percentile
435    pub p95_ms: f64,
436    /// 99th percentile
437    pub p99_ms: f64,
438    /// Sample count
439    pub samples: u64,
440}
441
442/// Error metrics summary
443#[derive(Debug, Clone, Default, Serialize, Deserialize)]
444pub struct ErrorMetrics {
445    /// Total errors
446    pub total: u64,
447    /// Errors by type
448    pub by_type: HashMap<String, u64>,
449    /// Errors by status code
450    pub by_status: HashMap<u16, u64>,
451    /// Recent errors (last N)
452    pub recent: Vec<ErrorSummary>,
453}
454
455/// Summary of a recent error
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct ErrorSummary {
458    pub error_type: String,
459    pub message: String,
460    pub count: u64,
461    pub last_seen: DateTime<Utc>,
462}
463
464/// Rate limit metrics
465#[derive(Debug, Clone, Default, Serialize, Deserialize)]
466pub struct RateLimitMetrics {
467    /// Total rate limit checks
468    pub total_checks: u64,
469    /// Requests that were allowed
470    pub allowed: u64,
471    /// Requests that were limited
472    pub limited: u64,
473    /// Unique clients rate limited
474    pub unique_clients_limited: u64,
475    /// Average utilization percentage
476    pub avg_utilization: f64,
477    /// Top rate-limited clients
478    pub top_limited_clients: Vec<ClientRateLimitInfo>,
479}
480
481/// Rate limit info for a specific client
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct ClientRateLimitInfo {
484    pub client_id: String,
485    pub times_limited: u64,
486    pub last_limited: DateTime<Utc>,
487}
488
489/// Per-endpoint metrics
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct EndpointMetrics {
492    /// Endpoint path
493    pub path: String,
494    /// HTTP method
495    pub method: String,
496    /// Total requests
497    pub requests: u64,
498    /// Error count
499    pub errors: u64,
500    /// Average latency in milliseconds
501    pub avg_latency_ms: f64,
502    /// P99 latency in milliseconds
503    pub p99_latency_ms: f64,
504    /// Error rate percentage
505    pub error_rate: f64,
506}
507
508/// Throughput metrics
509#[derive(Debug, Clone, Default, Serialize, Deserialize)]
510pub struct ThroughputMetrics {
511    /// Requests per second (current)
512    pub requests_per_second: f64,
513    /// Requests in the last minute
514    pub requests_last_minute: u64,
515    /// Requests in the last hour
516    pub requests_last_hour: u64,
517    /// Peak requests per second
518    pub peak_rps: f64,
519    /// Average response size in bytes
520    pub avg_response_size: u64,
521    /// Total data transferred in bytes
522    pub total_bytes_transferred: u64,
523}
524
525// =============================================================================
526// Tests
527// =============================================================================
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    #[test]
534    fn test_request_record() {
535        let record = RequestRecord::new("GET", "/api/users", 200, Duration::from_millis(50))
536            .with_client_id("user-123")
537            .with_response_size(1024)
538            .with_authenticated(true)
539            .with_tag("version", "v1");
540
541        assert!(record.is_success());
542        assert!(!record.is_client_error());
543        assert!(!record.is_server_error());
544        assert_eq!(record.client_id, Some("user-123".to_string()));
545    }
546
547    #[test]
548    fn test_rate_limit_event() {
549        let event = RateLimitEvent::limited("client-1", 100, 100, 60);
550        assert_eq!(event.utilization(), 100.0);
551
552        let event = RateLimitEvent::allowed("client-2", 50, 100, 60);
553        assert_eq!(event.utilization(), 50.0);
554    }
555
556    #[test]
557    fn test_request_metrics() {
558        let metrics = RequestMetrics {
559            total: 100,
560            success: 90,
561            client_errors: 8,
562            server_errors: 2,
563            ..Default::default()
564        };
565
566        assert_eq!(metrics.success_rate(), 90.0);
567        assert_eq!(metrics.error_rate(), 10.0);
568    }
569
570    #[test]
571    fn test_analytics_creation() {
572        let analytics = Analytics::new(AnalyticsConfig::default());
573        let snapshot = analytics.snapshot();
574
575        assert_eq!(snapshot.requests.total, 0);
576        assert_eq!(snapshot.errors.total, 0);
577    }
578}
579