1use chrono::{DateTime, Duration as ChronoDuration, Utc};
43use serde::{Deserialize, Serialize};
44use std::collections::{HashMap, VecDeque};
45use std::time::Duration;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub enum MetricType {
50 Download,
52 Upload,
54 Access,
56 Creation,
58 Deletion,
60 DownloadTime,
62 UploadTime,
64 CompressionTime,
66 DecompressionTime,
68 BandwidthUsage,
70 StorageUsage,
72 MemoryUsage,
74 CpuUsage,
76 ErrorCount,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub enum AlertThreshold {
83 Maximum(Duration),
85 MaxCount {
87 count: usize,
89 window: ChronoDuration,
91 },
92 MaxBytes(u64),
94 MaxPercentage(f64),
96 Minimum(Duration),
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
102pub enum AlertSeverity {
103 Info,
105 Warning,
107 Error,
109 Critical,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct Alert {
116 pub severity: AlertSeverity,
118 pub metric_type: MetricType,
120 pub message: String,
122 pub current_value: String,
124 pub threshold: String,
126 pub timestamp: DateTime<Utc>,
128 pub package_id: Option<String>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MetricPoint {
135 pub metric_type: MetricType,
137 pub timestamp: DateTime<Utc>,
139 pub value: f64,
141 pub package_id: Option<String>,
143 pub user_id: Option<String>,
145 pub metadata: HashMap<String, String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TimeSeries {
152 pub metric_type: MetricType,
154 pub points: Vec<MetricPoint>,
156 pub stats: TimeSeriesStats,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TimeSeriesStats {
163 pub count: usize,
165 pub sum: f64,
167 pub min: f64,
169 pub max: f64,
171 pub mean: f64,
173 pub median: f64,
175 pub p95: f64,
177 pub p99: f64,
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct PackageStats {
184 pub package_id: String,
186 pub version: String,
188 pub downloads: u64,
190 pub uploads: u64,
192 pub accesses: u64,
194 pub bandwidth_bytes: u64,
196 pub avg_download_time: f64,
198 pub storage_bytes: u64,
200 pub unique_users: usize,
202 pub errors: u64,
204 pub last_access: Option<DateTime<Utc>>,
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210pub struct UserStats {
211 pub user_id: String,
213 pub downloads: u64,
215 pub uploads: u64,
217 pub unique_packages: usize,
219 pub bandwidth_bytes: u64,
221 pub first_activity: Option<DateTime<Utc>>,
223 pub last_activity: Option<DateTime<Utc>>,
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct RegionStats {
230 pub region: String,
232 pub requests: u64,
234 pub bandwidth_bytes: u64,
236 pub avg_latency_ms: f64,
238 pub error_rate: f64,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct AnalyticsReport {
245 pub generated_at: DateTime<Utc>,
247 pub time_range_start: DateTime<Utc>,
249 pub time_range_end: DateTime<Utc>,
251 pub total_downloads: u64,
253 pub total_uploads: u64,
255 pub total_bandwidth_bytes: u64,
257 pub total_storage_bytes: u64,
259 pub total_errors: u64,
261 pub package_stats: Vec<PackageStats>,
263 pub user_stats: Vec<UserStats>,
265 pub region_stats: Vec<RegionStats>,
267 pub active_alerts: Vec<Alert>,
269 pub top_packages: Vec<(String, u64)>,
271 pub top_users: Vec<(String, u64)>,
273}
274
275pub struct MetricsCollector {
280 time_series: HashMap<MetricType, Vec<MetricPoint>>,
282 package_stats: HashMap<String, PackageStats>,
284 user_stats: HashMap<String, UserStats>,
286 region_stats: HashMap<String, RegionStats>,
288 alert_thresholds: HashMap<MetricType, AlertThreshold>,
290 active_alerts: VecDeque<Alert>,
292 max_alerts: usize,
294 max_points_per_metric: usize,
296}
297
298impl MetricsCollector {
299 pub fn new() -> Self {
301 Self {
302 time_series: HashMap::new(),
303 package_stats: HashMap::new(),
304 user_stats: HashMap::new(),
305 region_stats: HashMap::new(),
306 alert_thresholds: HashMap::new(),
307 active_alerts: VecDeque::new(),
308 max_alerts: 1000,
309 max_points_per_metric: 10000,
310 }
311 }
312
313 pub fn set_max_alerts(&mut self, max: usize) {
315 self.max_alerts = max;
316 }
317
318 pub fn set_max_points_per_metric(&mut self, max: usize) {
320 self.max_points_per_metric = max;
321 }
322
323 pub fn record_download(&mut self, package_id: &str, version: &str, duration: Duration) {
325 self.record_metric(
327 MetricType::Download,
328 1.0,
329 Some(package_id.to_string()),
330 None,
331 HashMap::new(),
332 );
333
334 self.record_metric(
335 MetricType::DownloadTime,
336 duration.as_secs_f64(),
337 Some(package_id.to_string()),
338 None,
339 HashMap::new(),
340 );
341
342 let key = format!("{}:{}", package_id, version);
344 let stats = self
345 .package_stats
346 .entry(key.clone())
347 .or_insert_with(|| PackageStats {
348 package_id: package_id.to_string(),
349 version: version.to_string(),
350 ..Default::default()
351 });
352
353 stats.downloads += 1;
354 stats.last_access = Some(Utc::now());
355
356 stats.avg_download_time = (stats.avg_download_time * (stats.downloads - 1) as f64
358 + duration.as_secs_f64())
359 / stats.downloads as f64;
360
361 self.check_alert(
363 MetricType::DownloadTime,
364 duration.as_secs_f64(),
365 Some(package_id),
366 );
367 }
368
369 pub fn record_upload(&mut self, package_id: &str, version: &str, size_bytes: u64) {
371 self.record_metric(
372 MetricType::Upload,
373 1.0,
374 Some(package_id.to_string()),
375 None,
376 HashMap::new(),
377 );
378
379 self.record_metric(
380 MetricType::BandwidthUsage,
381 size_bytes as f64,
382 Some(package_id.to_string()),
383 None,
384 HashMap::new(),
385 );
386
387 let key = format!("{}:{}", package_id, version);
389 let stats = self
390 .package_stats
391 .entry(key)
392 .or_insert_with(|| PackageStats {
393 package_id: package_id.to_string(),
394 version: version.to_string(),
395 ..Default::default()
396 });
397
398 stats.uploads += 1;
399 stats.bandwidth_bytes += size_bytes;
400 stats.storage_bytes += size_bytes;
401 stats.last_access = Some(Utc::now());
402
403 self.check_alert(
405 MetricType::BandwidthUsage,
406 size_bytes as f64,
407 Some(package_id),
408 );
409 }
410
411 pub fn record_access(&mut self, package_id: &str, version: &str, user_id: Option<&str>) {
413 self.record_metric(
414 MetricType::Access,
415 1.0,
416 Some(package_id.to_string()),
417 user_id.map(|s| s.to_string()),
418 HashMap::new(),
419 );
420
421 let key = format!("{}:{}", package_id, version);
423 let stats = self
424 .package_stats
425 .entry(key)
426 .or_insert_with(|| PackageStats {
427 package_id: package_id.to_string(),
428 version: version.to_string(),
429 ..Default::default()
430 });
431
432 stats.accesses += 1;
433 stats.last_access = Some(Utc::now());
434
435 if let Some(uid) = user_id {
437 let user_stats = self
438 .user_stats
439 .entry(uid.to_string())
440 .or_insert_with(|| UserStats {
441 user_id: uid.to_string(),
442 first_activity: Some(Utc::now()),
443 ..Default::default()
444 });
445
446 user_stats.last_activity = Some(Utc::now());
447 }
448 }
449
450 pub fn record_error(&mut self, package_id: Option<&str>, error_type: &str) {
452 let mut metadata = HashMap::new();
453 metadata.insert("error_type".to_string(), error_type.to_string());
454
455 self.record_metric(
456 MetricType::ErrorCount,
457 1.0,
458 package_id.map(|s| s.to_string()),
459 None,
460 metadata,
461 );
462
463 if let Some(pid) = package_id {
465 for stats in self.package_stats.values_mut() {
466 if stats.package_id == pid {
467 stats.errors += 1;
468 }
469 }
470 }
471
472 self.check_alert(MetricType::ErrorCount, 1.0, package_id);
474 }
475
476 pub fn record_resource_usage(
478 &mut self,
479 memory_bytes: u64,
480 storage_bytes: u64,
481 cpu_percent: f64,
482 ) {
483 self.record_metric(
484 MetricType::MemoryUsage,
485 memory_bytes as f64,
486 None,
487 None,
488 HashMap::new(),
489 );
490
491 self.record_metric(
492 MetricType::StorageUsage,
493 storage_bytes as f64,
494 None,
495 None,
496 HashMap::new(),
497 );
498
499 self.record_metric(
500 MetricType::CpuUsage,
501 cpu_percent,
502 None,
503 None,
504 HashMap::new(),
505 );
506
507 self.check_alert(MetricType::MemoryUsage, memory_bytes as f64, None);
509 self.check_alert(MetricType::StorageUsage, storage_bytes as f64, None);
510 self.check_alert(MetricType::CpuUsage, cpu_percent, None);
511 }
512
513 pub fn record_metric(
515 &mut self,
516 metric_type: MetricType,
517 value: f64,
518 package_id: Option<String>,
519 user_id: Option<String>,
520 metadata: HashMap<String, String>,
521 ) {
522 let point = MetricPoint {
523 metric_type,
524 timestamp: Utc::now(),
525 value,
526 package_id,
527 user_id,
528 metadata,
529 };
530
531 let points = self.time_series.entry(metric_type).or_insert_with(Vec::new);
532 points.push(point);
533
534 if points.len() > self.max_points_per_metric {
536 let excess = points.len() - self.max_points_per_metric;
537 points.drain(0..excess);
538 }
539 }
540
541 pub fn set_alert_threshold(&mut self, metric_type: MetricType, threshold: AlertThreshold) {
543 self.alert_thresholds.insert(metric_type, threshold);
544 }
545
546 pub fn get_time_series(&self, metric_type: MetricType) -> Option<TimeSeries> {
548 self.time_series.get(&metric_type).map(|points| {
549 let stats = self.calculate_stats(points);
550 TimeSeries {
551 metric_type,
552 points: points.clone(),
553 stats,
554 }
555 })
556 }
557
558 pub fn get_package_stats(&self, package_id: &str) -> Vec<&PackageStats> {
560 self.package_stats
561 .values()
562 .filter(|stats| stats.package_id == package_id)
563 .collect()
564 }
565
566 pub fn get_user_stats(&self, user_id: &str) -> Option<&UserStats> {
568 self.user_stats.get(user_id)
569 }
570
571 pub fn get_active_alerts(&self) -> Vec<&Alert> {
573 self.active_alerts.iter().collect()
574 }
575
576 pub fn clear_alerts(&mut self) {
578 self.active_alerts.clear();
579 }
580
581 pub fn generate_report(&self) -> AnalyticsReport {
583 let now = Utc::now();
584 let time_range_start = now - ChronoDuration::days(30); let total_downloads = self.package_stats.values().map(|s| s.downloads).sum();
587
588 let total_uploads = self.package_stats.values().map(|s| s.uploads).sum();
589
590 let total_bandwidth_bytes = self.package_stats.values().map(|s| s.bandwidth_bytes).sum();
591
592 let total_storage_bytes = self.package_stats.values().map(|s| s.storage_bytes).sum();
593
594 let total_errors = self.package_stats.values().map(|s| s.errors).sum();
595
596 let mut top_packages: Vec<(String, u64)> = self
598 .package_stats
599 .values()
600 .map(|s| (s.package_id.clone(), s.downloads))
601 .collect();
602 top_packages.sort_by(|a, b| b.1.cmp(&a.1));
603 top_packages.truncate(10);
604
605 let mut top_users: Vec<(String, u64)> = self
607 .user_stats
608 .values()
609 .map(|s| (s.user_id.clone(), s.downloads + s.uploads))
610 .collect();
611 top_users.sort_by(|a, b| b.1.cmp(&a.1));
612 top_users.truncate(10);
613
614 AnalyticsReport {
615 generated_at: now,
616 time_range_start,
617 time_range_end: now,
618 total_downloads,
619 total_uploads,
620 total_bandwidth_bytes,
621 total_storage_bytes,
622 total_errors,
623 package_stats: self.package_stats.values().cloned().collect(),
624 user_stats: self.user_stats.values().cloned().collect(),
625 region_stats: self.region_stats.values().cloned().collect(),
626 active_alerts: self.active_alerts.iter().cloned().collect(),
627 top_packages,
628 top_users,
629 }
630 }
631
632 pub fn export_to_json(&self) -> Result<String, String> {
634 let report = self.generate_report();
635 serde_json::to_string_pretty(&report).map_err(|e| e.to_string())
636 }
637
638 fn calculate_stats(&self, points: &[MetricPoint]) -> TimeSeriesStats {
641 if points.is_empty() {
642 return TimeSeriesStats {
643 count: 0,
644 sum: 0.0,
645 min: 0.0,
646 max: 0.0,
647 mean: 0.0,
648 median: 0.0,
649 p95: 0.0,
650 p99: 0.0,
651 };
652 }
653
654 let mut values: Vec<f64> = points.iter().map(|p| p.value).collect();
655 values.sort_by(|a, b| {
656 a.partial_cmp(b)
657 .expect("metric values should be comparable")
658 });
659
660 let count = values.len();
661 let sum: f64 = values.iter().sum();
662 let min = values[0];
663 let max = values[count - 1];
664 let mean = sum / count as f64;
665
666 let median = if count % 2 == 0 {
667 (values[count / 2 - 1] + values[count / 2]) / 2.0
668 } else {
669 values[count / 2]
670 };
671
672 let p95_idx = ((count as f64) * 0.95) as usize;
673 let p99_idx = ((count as f64) * 0.99) as usize;
674 let p95 = values[p95_idx.min(count - 1)];
675 let p99 = values[p99_idx.min(count - 1)];
676
677 TimeSeriesStats {
678 count,
679 sum,
680 min,
681 max,
682 mean,
683 median,
684 p95,
685 p99,
686 }
687 }
688
689 fn check_alert(&mut self, metric_type: MetricType, value: f64, package_id: Option<&str>) {
690 if let Some(threshold) = self.alert_thresholds.get(&metric_type) {
691 let (should_alert, alert_value) = match threshold {
692 AlertThreshold::Maximum(max_duration) => {
693 (value > max_duration.as_secs_f64(), value)
694 }
695 AlertThreshold::MaxBytes(max_bytes) => (value > *max_bytes as f64, value),
696 AlertThreshold::MaxPercentage(max_percent) => (value > *max_percent, value),
697 AlertThreshold::Minimum(min_duration) => {
698 (value < min_duration.as_secs_f64(), value)
699 }
700 AlertThreshold::MaxCount { count, window } => {
701 let cutoff = Utc::now() - *window;
703 if let Some(points) = self.time_series.get(&metric_type) {
704 let recent_count = points.iter().filter(|p| p.timestamp > cutoff).count();
705 (recent_count > *count, recent_count as f64)
706 } else {
707 (false, 0.0)
708 }
709 }
710 };
711
712 if should_alert {
713 let alert = Alert {
714 severity: self.determine_severity(metric_type, alert_value),
715 metric_type,
716 message: format!("Threshold exceeded for {:?}", metric_type),
717 current_value: format!("{:.2}", alert_value),
718 threshold: format!("{:?}", threshold),
719 timestamp: Utc::now(),
720 package_id: package_id.map(|s| s.to_string()),
721 };
722
723 self.active_alerts.push_back(alert);
724
725 if self.active_alerts.len() > self.max_alerts {
727 self.active_alerts.pop_front();
728 }
729 }
730 }
731 }
732
733 fn determine_severity(&self, metric_type: MetricType, value: f64) -> AlertSeverity {
734 match metric_type {
735 MetricType::ErrorCount => {
736 if value > 100.0 {
737 AlertSeverity::Critical
738 } else if value > 50.0 {
739 AlertSeverity::Error
740 } else if value > 10.0 {
741 AlertSeverity::Warning
742 } else {
743 AlertSeverity::Info
744 }
745 }
746 MetricType::DownloadTime | MetricType::UploadTime => {
747 if value > 60.0 {
748 AlertSeverity::Critical
749 } else if value > 30.0 {
750 AlertSeverity::Error
751 } else if value > 10.0 {
752 AlertSeverity::Warning
753 } else {
754 AlertSeverity::Info
755 }
756 }
757 MetricType::CpuUsage | MetricType::MemoryUsage => {
758 if value > 90.0 {
759 AlertSeverity::Critical
760 } else if value > 80.0 {
761 AlertSeverity::Error
762 } else if value > 70.0 {
763 AlertSeverity::Warning
764 } else {
765 AlertSeverity::Info
766 }
767 }
768 _ => AlertSeverity::Info,
769 }
770 }
771}
772
773impl Default for MetricsCollector {
774 fn default() -> Self {
775 Self::new()
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782
783 #[test]
784 fn test_metrics_collector_creation() {
785 let collector = MetricsCollector::new();
786 let report = collector.generate_report();
787 assert_eq!(report.total_downloads, 0);
788 assert_eq!(report.total_uploads, 0);
789 }
790
791 #[test]
792 fn test_record_download() {
793 let mut collector = MetricsCollector::new();
794 collector.record_download("test-pkg", "1.0.0", Duration::from_secs(2));
795
796 let stats = collector.get_package_stats("test-pkg");
797 assert_eq!(stats.len(), 1);
798 assert_eq!(stats[0].downloads, 1);
799 assert_eq!(stats[0].avg_download_time, 2.0);
800 }
801
802 #[test]
803 fn test_record_upload() {
804 let mut collector = MetricsCollector::new();
805 collector.record_upload("test-pkg", "1.0.0", 1024 * 1024);
806
807 let stats = collector.get_package_stats("test-pkg");
808 assert_eq!(stats.len(), 1);
809 assert_eq!(stats[0].uploads, 1);
810 assert_eq!(stats[0].bandwidth_bytes, 1024 * 1024);
811 }
812
813 #[test]
814 fn test_record_access() {
815 let mut collector = MetricsCollector::new();
816 collector.record_access("test-pkg", "1.0.0", Some("alice"));
817
818 let stats = collector.get_package_stats("test-pkg");
819 assert_eq!(stats.len(), 1);
820 assert_eq!(stats[0].accesses, 1);
821
822 let user_stats = collector.get_user_stats("alice");
823 assert!(user_stats.is_some());
824 }
825
826 #[test]
827 fn test_record_error() {
828 let mut collector = MetricsCollector::new();
829 collector.record_upload("test-pkg", "1.0.0", 1024);
830 collector.record_error(Some("test-pkg"), "download_failed");
831
832 let stats = collector.get_package_stats("test-pkg");
833 assert_eq!(stats[0].errors, 1);
834 }
835
836 #[test]
837 fn test_alert_threshold() {
838 let mut collector = MetricsCollector::new();
839 collector.set_alert_threshold(
840 MetricType::DownloadTime,
841 AlertThreshold::Maximum(Duration::from_secs(5)),
842 );
843
844 collector.record_download("test-pkg", "1.0.0", Duration::from_secs(10));
846
847 let alerts = collector.get_active_alerts();
848 assert!(!alerts.is_empty());
849 assert_eq!(alerts[0].metric_type, MetricType::DownloadTime);
850 }
851
852 #[test]
853 fn test_time_series() {
854 let mut collector = MetricsCollector::new();
855
856 for i in 1..=10 {
857 collector.record_metric(MetricType::Download, i as f64, None, None, HashMap::new());
858 }
859
860 let ts = collector.get_time_series(MetricType::Download);
861 assert!(ts.is_some());
862
863 let ts = ts.unwrap();
864 assert_eq!(ts.stats.count, 10);
865 assert_eq!(ts.stats.min, 1.0);
866 assert_eq!(ts.stats.max, 10.0);
867 assert_eq!(ts.stats.mean, 5.5);
868 }
869
870 #[test]
871 fn test_generate_report() {
872 let mut collector = MetricsCollector::new();
873
874 collector.record_download("pkg1", "1.0.0", Duration::from_secs(2));
875 collector.record_download("pkg2", "1.0.0", Duration::from_secs(3));
876 collector.record_upload("pkg3", "1.0.0", 1024 * 1024);
877
878 let report = collector.generate_report();
879 assert_eq!(report.total_downloads, 2);
880 assert_eq!(report.total_uploads, 1);
881 assert!(report.total_bandwidth_bytes > 0);
882 }
883
884 #[test]
885 fn test_export_to_json() {
886 let mut collector = MetricsCollector::new();
887 collector.record_download("test-pkg", "1.0.0", Duration::from_secs(2));
888
889 let json = collector.export_to_json();
890 assert!(json.is_ok());
891 let json_str = json.unwrap();
892 assert!(json_str.contains("total_downloads"));
893 assert!(json_str.contains("test-pkg"));
894 }
895
896 #[test]
897 fn test_max_points_limit() {
898 let mut collector = MetricsCollector::new();
899 collector.set_max_points_per_metric(100);
900
901 for i in 0..200 {
903 collector.record_metric(MetricType::Download, i as f64, None, None, HashMap::new());
904 }
905
906 let ts = collector.get_time_series(MetricType::Download);
907 assert!(ts.is_some());
908 assert_eq!(ts.unwrap().points.len(), 100);
909 }
910
911 #[test]
912 fn test_alert_severity() {
913 let mut collector = MetricsCollector::new();
914 collector.set_alert_threshold(
915 MetricType::ErrorCount,
916 AlertThreshold::MaxCount {
917 count: 10,
918 window: ChronoDuration::minutes(5),
919 },
920 );
921
922 for _ in 0..15 {
924 collector.record_error(Some("test-pkg"), "error");
925 }
926
927 let alerts = collector.get_active_alerts();
928 assert!(!alerts.is_empty());
929
930 let severities: Vec<AlertSeverity> = alerts.iter().map(|a| a.severity).collect();
932 assert!(severities.iter().any(|s| *s >= AlertSeverity::Warning));
933 }
934}