mod collector;
mod config;
mod error;
mod insights;
mod metrics;
mod middleware;
pub use collector::*;
pub use config::*;
pub use error::*;
pub use insights::*;
pub use metrics::*;
pub use middleware::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct Analytics {
inner: Arc<AnalyticsInner>,
}
struct AnalyticsInner {
config: AnalyticsConfig,
collector: MetricsCollector,
started_at: DateTime<Utc>,
}
impl Analytics {
pub fn new(config: AnalyticsConfig) -> Self {
let collector = MetricsCollector::from_config(&config);
Self {
inner: Arc::new(AnalyticsInner {
config,
collector,
started_at: Utc::now(),
}),
}
}
pub fn record_request(&self, record: RequestRecord) {
self.inner.collector.record_request(record);
}
pub fn record_rate_limit(&self, event: RateLimitEvent) {
self.inner.collector.record_rate_limit(event);
}
pub fn record_error(&self, error: ErrorRecord) {
self.inner.collector.record_error(error);
}
pub fn snapshot(&self) -> AnalyticsSnapshot {
let collector = &self.inner.collector;
AnalyticsSnapshot {
timestamp: Utc::now(),
uptime_seconds: (Utc::now() - self.inner.started_at).num_seconds() as u64,
requests: collector.request_metrics(),
latency: collector.latency_metrics(),
errors: collector.error_metrics(),
rate_limits: collector.rate_limit_metrics(),
endpoints: collector.endpoint_metrics(),
throughput: collector.throughput_metrics(),
}
}
pub fn dashboard_json(&self) -> String {
serde_json::to_string_pretty(&self.snapshot()).unwrap_or_else(|_| "{}".to_string())
}
pub fn reset(&self) {
self.inner.collector.reset();
}
pub fn config(&self) -> &AnalyticsConfig {
&self.inner.config
}
}
#[derive(Debug, Clone)]
pub struct RequestRecord {
pub method: String,
pub path: String,
pub status: u16,
pub duration: Duration,
pub timestamp: DateTime<Utc>,
pub client_id: Option<String>,
pub response_size: Option<u64>,
pub authenticated: bool,
pub tags: HashMap<String, String>,
}
impl RequestRecord {
pub fn new(
method: impl Into<String>,
path: impl Into<String>,
status: u16,
duration: Duration,
) -> Self {
Self {
method: method.into(),
path: path.into(),
status,
duration,
timestamp: Utc::now(),
client_id: None,
response_size: None,
authenticated: false,
tags: HashMap::new(),
}
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_response_size(mut self, size: u64) -> Self {
self.response_size = Some(size);
self
}
pub fn with_authenticated(mut self, authenticated: bool) -> Self {
self.authenticated = authenticated;
self
}
pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.tags.insert(key.into(), value.into());
self
}
pub fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
pub fn is_client_error(&self) -> bool {
self.status >= 400 && self.status < 500
}
pub fn is_server_error(&self) -> bool {
self.status >= 500
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RateLimitEventType {
Allowed,
Limited,
Warning,
}
#[derive(Debug, Clone)]
pub struct RateLimitEvent {
pub client_id: String,
pub event_type: RateLimitEventType,
pub current_count: u64,
pub limit: u64,
pub window_seconds: u64,
pub endpoint: Option<String>,
pub timestamp: DateTime<Utc>,
}
impl RateLimitEvent {
pub fn allowed(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
Self {
client_id: client_id.into(),
event_type: RateLimitEventType::Allowed,
current_count: current,
limit,
window_seconds: window,
endpoint: None,
timestamp: Utc::now(),
}
}
pub fn limited(client_id: impl Into<String>, current: u64, limit: u64, window: u64) -> Self {
Self {
client_id: client_id.into(),
event_type: RateLimitEventType::Limited,
current_count: current,
limit,
window_seconds: window,
endpoint: None,
timestamp: Utc::now(),
}
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn utilization(&self) -> f64 {
if self.limit == 0 {
0.0
} else {
(self.current_count as f64 / self.limit as f64) * 100.0
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorRecord {
pub error_type: String,
pub message: String,
pub status: Option<u16>,
pub endpoint: Option<String>,
pub stack_trace: Option<String>,
pub timestamp: DateTime<Utc>,
pub context: HashMap<String, String>,
}
impl ErrorRecord {
pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
message: message.into(),
status: None,
endpoint: None,
stack_trace: None,
timestamp: Utc::now(),
context: HashMap::new(),
}
}
pub fn with_status(mut self, status: u16) -> Self {
self.status = Some(status);
self
}
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.context.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticsSnapshot {
pub timestamp: DateTime<Utc>,
pub uptime_seconds: u64,
pub requests: RequestMetrics,
pub latency: LatencyMetrics,
pub errors: ErrorMetrics,
pub rate_limits: RateLimitMetrics,
pub endpoints: Vec<EndpointMetrics>,
pub throughput: ThroughputMetrics,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RequestMetrics {
pub total: u64,
pub success: u64,
pub client_errors: u64,
pub server_errors: u64,
pub by_method: HashMap<String, u64>,
pub by_status: HashMap<u16, u64>,
}
impl RequestMetrics {
pub fn success_rate(&self) -> f64 {
if self.total == 0 {
100.0
} else {
(self.success as f64 / self.total as f64) * 100.0
}
}
pub fn error_rate(&self) -> f64 {
if self.total == 0 {
0.0
} else {
((self.client_errors + self.server_errors) as f64 / self.total as f64) * 100.0
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LatencyMetrics {
pub avg_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub p50_ms: f64,
pub p90_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
pub samples: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ErrorMetrics {
pub total: u64,
pub by_type: HashMap<String, u64>,
pub by_status: HashMap<u16, u64>,
pub recent: Vec<ErrorSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorSummary {
pub error_type: String,
pub message: String,
pub count: u64,
pub last_seen: DateTime<Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RateLimitMetrics {
pub total_checks: u64,
pub allowed: u64,
pub limited: u64,
pub unique_clients_limited: u64,
pub avg_utilization: f64,
pub top_limited_clients: Vec<ClientRateLimitInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientRateLimitInfo {
pub client_id: String,
pub times_limited: u64,
pub last_limited: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EndpointMetrics {
pub path: String,
pub method: String,
pub requests: u64,
pub errors: u64,
pub avg_latency_ms: f64,
pub p99_latency_ms: f64,
pub error_rate: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ThroughputMetrics {
pub requests_per_second: f64,
pub requests_last_minute: u64,
pub requests_last_hour: u64,
pub peak_rps: f64,
pub avg_response_size: u64,
pub total_bytes_transferred: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_record() {
let record = RequestRecord::new("GET", "/api/users", 200, Duration::from_millis(50))
.with_client_id("user-123")
.with_response_size(1024)
.with_authenticated(true)
.with_tag("version", "v1");
assert!(record.is_success());
assert!(!record.is_client_error());
assert!(!record.is_server_error());
assert_eq!(record.client_id, Some("user-123".to_string()));
}
#[test]
fn test_rate_limit_event() {
let event = RateLimitEvent::limited("client-1", 100, 100, 60);
assert_eq!(event.utilization(), 100.0);
let event = RateLimitEvent::allowed("client-2", 50, 100, 60);
assert_eq!(event.utilization(), 50.0);
}
#[test]
fn test_request_metrics() {
let metrics = RequestMetrics {
total: 100,
success: 90,
client_errors: 8,
server_errors: 2,
..Default::default()
};
assert_eq!(metrics.success_rate(), 90.0);
assert_eq!(metrics.error_rate(), 10.0);
}
#[test]
fn test_analytics_creation() {
let analytics = Analytics::new(AnalyticsConfig::default());
let snapshot = analytics.snapshot();
assert_eq!(snapshot.requests.total, 0);
assert_eq!(snapshot.errors.total, 0);
}
#[test]
fn test_analytics_new_respects_capacity_knobs() {
let config = AnalyticsConfig::builder().max_endpoints(2).build();
let analytics = Analytics::new(config);
for i in 0..5 {
analytics.record_request(RequestRecord::new(
"GET",
format!("/endpoint-{i}"),
200,
Duration::from_millis(1),
));
}
let snapshot = analytics.snapshot();
assert_eq!(
snapshot.endpoints.len(),
2,
"max_endpoints from config must be respected"
);
}
}