Skip to main content

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//! │                    JSON Snapshot / Dashboard                 │
55//! │  `Analytics::snapshot()` / `Analytics::dashboard_json()`     │
56//! └─────────────────────────────────────────────────────────────┘
57//! ```
58
59mod 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/// Main analytics instance
80///
81/// Thread-safe analytics collector that can be shared across handlers.
82#[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    /// Create a new analytics instance
95    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    /// Record a request
107    pub fn record_request(&self, record: RequestRecord) {
108        self.inner.collector.record_request(record);
109    }
110
111    /// Record a rate limit event
112    pub fn record_rate_limit(&self, event: RateLimitEvent) {
113        self.inner.collector.record_rate_limit(event);
114    }
115
116    /// Record an error
117    pub fn record_error(&self, error: ErrorRecord) {
118        self.inner.collector.record_error(error);
119    }
120
121    /// Get current analytics snapshot
122    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    /// Get JSON dashboard data
138    pub fn dashboard_json(&self) -> String {
139        serde_json::to_string_pretty(&self.snapshot()).unwrap_or_else(|_| "{}".to_string())
140    }
141
142    /// Reset all metrics
143    pub fn reset(&self) {
144        self.inner.collector.reset();
145    }
146
147    /// Get the configuration
148    pub fn config(&self) -> &AnalyticsConfig {
149        &self.inner.config
150    }
151}
152
153// =============================================================================
154// Request Recording
155// =============================================================================
156
157/// Record of a single request for analytics
158#[derive(Debug, Clone)]
159pub struct RequestRecord {
160    /// HTTP method
161    pub method: String,
162    /// Request path (normalized)
163    pub path: String,
164    /// HTTP status code
165    pub status: u16,
166    /// Request duration
167    pub duration: Duration,
168    /// Request timestamp
169    pub timestamp: DateTime<Utc>,
170    /// Client identifier (IP, user ID, etc.)
171    pub client_id: Option<String>,
172    /// Response body size in bytes
173    pub response_size: Option<u64>,
174    /// Whether the request was authenticated
175    pub authenticated: bool,
176    /// Custom tags for filtering
177    pub tags: HashMap<String, String>,
178}
179
180impl RequestRecord {
181    /// Create a new request record
182    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    /// Check if request was successful (2xx)
222    pub fn is_success(&self) -> bool {
223        self.status >= 200 && self.status < 300
224    }
225
226    /// Check if request was a client error (4xx)
227    pub fn is_client_error(&self) -> bool {
228        self.status >= 400 && self.status < 500
229    }
230
231    /// Check if request was a server error (5xx)
232    pub fn is_server_error(&self) -> bool {
233        self.status >= 500
234    }
235}
236
237// =============================================================================
238// Rate Limit Events
239// =============================================================================
240
241/// Rate limit event types
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum RateLimitEventType {
244    /// Request was allowed
245    Allowed,
246    /// Request was rate limited
247    Limited,
248    /// Near the limit (warning threshold)
249    Warning,
250}
251
252/// Record of a rate limit event
253#[derive(Debug, Clone)]
254pub struct RateLimitEvent {
255    /// Client identifier
256    pub client_id: String,
257    /// Event type
258    pub event_type: RateLimitEventType,
259    /// Current request count
260    pub current_count: u64,
261    /// Maximum allowed requests
262    pub limit: u64,
263    /// Time window in seconds
264    pub window_seconds: u64,
265    /// Endpoint affected
266    pub endpoint: Option<String>,
267    /// Timestamp
268    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    /// Calculate utilization percentage
302    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// =============================================================================
312// Error Recording
313// =============================================================================
314
315/// Record of an error for analytics
316#[derive(Debug, Clone)]
317pub struct ErrorRecord {
318    /// Error type/code
319    pub error_type: String,
320    /// Error message
321    pub message: String,
322    /// HTTP status code
323    pub status: Option<u16>,
324    /// Endpoint where error occurred
325    pub endpoint: Option<String>,
326    /// Stack trace (if available)
327    pub stack_trace: Option<String>,
328    /// Timestamp
329    pub timestamp: DateTime<Utc>,
330    /// Additional context
331    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// =============================================================================
364// Analytics Snapshot
365// =============================================================================
366
367/// Complete analytics snapshot for dashboard/export
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct AnalyticsSnapshot {
370    /// Snapshot timestamp
371    pub timestamp: DateTime<Utc>,
372    /// Uptime in seconds
373    pub uptime_seconds: u64,
374    /// Request metrics
375    pub requests: RequestMetrics,
376    /// Latency metrics
377    pub latency: LatencyMetrics,
378    /// Error metrics
379    pub errors: ErrorMetrics,
380    /// Rate limit metrics
381    pub rate_limits: RateLimitMetrics,
382    /// Per-endpoint metrics
383    pub endpoints: Vec<EndpointMetrics>,
384    /// Throughput metrics
385    pub throughput: ThroughputMetrics,
386}
387
388/// Request metrics summary
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
390pub struct RequestMetrics {
391    /// Total requests
392    pub total: u64,
393    /// Successful requests (2xx)
394    pub success: u64,
395    /// Client errors (4xx)
396    pub client_errors: u64,
397    /// Server errors (5xx)
398    pub server_errors: u64,
399    /// Requests by method
400    pub by_method: HashMap<String, u64>,
401    /// Requests by status code
402    pub by_status: HashMap<u16, u64>,
403}
404
405impl RequestMetrics {
406    /// Calculate success rate as percentage
407    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    /// Calculate error rate as percentage
416    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/// Latency metrics with percentiles
426#[derive(Debug, Clone, Default, Serialize, Deserialize)]
427pub struct LatencyMetrics {
428    /// Average latency in milliseconds
429    pub avg_ms: f64,
430    /// Minimum latency in milliseconds
431    pub min_ms: f64,
432    /// Maximum latency in milliseconds
433    pub max_ms: f64,
434    /// 50th percentile (median)
435    pub p50_ms: f64,
436    /// 90th percentile
437    pub p90_ms: f64,
438    /// 95th percentile
439    pub p95_ms: f64,
440    /// 99th percentile
441    pub p99_ms: f64,
442    /// Sample count
443    pub samples: u64,
444}
445
446/// Error metrics summary
447#[derive(Debug, Clone, Default, Serialize, Deserialize)]
448pub struct ErrorMetrics {
449    /// Total errors
450    pub total: u64,
451    /// Errors by type
452    pub by_type: HashMap<String, u64>,
453    /// Errors by status code
454    pub by_status: HashMap<u16, u64>,
455    /// Recent errors (last N)
456    pub recent: Vec<ErrorSummary>,
457}
458
459/// Summary of a recent error
460#[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/// Rate limit metrics
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct RateLimitMetrics {
471    /// Total rate limit checks
472    pub total_checks: u64,
473    /// Requests that were allowed
474    pub allowed: u64,
475    /// Requests that were limited
476    pub limited: u64,
477    /// Unique clients rate limited
478    pub unique_clients_limited: u64,
479    /// Average utilization percentage
480    pub avg_utilization: f64,
481    /// Top rate-limited clients
482    pub top_limited_clients: Vec<ClientRateLimitInfo>,
483}
484
485/// Rate limit info for a specific client
486#[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/// Per-endpoint metrics
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct EndpointMetrics {
496    /// Endpoint path
497    pub path: String,
498    /// HTTP method
499    pub method: String,
500    /// Total requests
501    pub requests: u64,
502    /// Error count
503    pub errors: u64,
504    /// Average latency in milliseconds
505    pub avg_latency_ms: f64,
506    /// P99 latency in milliseconds
507    pub p99_latency_ms: f64,
508    /// Error rate percentage
509    pub error_rate: f64,
510}
511
512/// Throughput metrics
513#[derive(Debug, Clone, Default, Serialize, Deserialize)]
514pub struct ThroughputMetrics {
515    /// Requests per second (current)
516    pub requests_per_second: f64,
517    /// Requests in the last minute
518    pub requests_last_minute: u64,
519    /// Requests in the last hour
520    pub requests_last_hour: u64,
521    /// Peak requests per second
522    pub peak_rps: f64,
523    /// Average response size in bytes
524    pub avg_response_size: u64,
525    /// Total data transferred in bytes
526    pub total_bytes_transferred: u64,
527}
528
529// =============================================================================
530// Tests
531// =============================================================================
532
533#[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    // Regression: Analytics::new used to hardcode MetricsCollector::new(),
584    // ignoring the config's capacity knobs. A low max_endpoints must be honored.
585    #[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}