Skip to main content

torsh_package/
monitoring.rs

1//! Package Monitoring and Analytics
2//!
3//! This module provides comprehensive monitoring and analytics capabilities for
4//! package operations, including usage metrics, performance analytics, resource
5//! monitoring, and real-time alerting for production observability.
6//!
7//! # Features
8//!
9//! - **Usage Metrics**: Track package downloads, uploads, and access patterns
10//! - **Performance Analytics**: Monitor operation durations, compression ratios, throughput
11//! - **Resource Monitoring**: Track memory, disk, bandwidth, and CPU usage
12//! - **Time-Series Data**: Collect and aggregate metrics over time
13//! - **Real-time Alerting**: Trigger alerts based on configurable thresholds
14//! - **Analytics Reports**: Generate comprehensive analytics reports
15//! - **User Activity**: Track user-level and organization-level statistics
16//! - **Geographic Analytics**: Monitor usage patterns by region
17//!
18//! # Examples
19//!
20//! ```rust
21//! use torsh_package::monitoring::{MetricsCollector, MetricType, AlertThreshold};
22//! use std::time::Duration;
23//!
24//! // Create a metrics collector
25//! let mut collector = MetricsCollector::new();
26//!
27//! // Record package operations
28//! collector.record_download("my-package", "1.0.0", Duration::from_secs(2));
29//! collector.record_upload("other-package", "2.0.0", 1024 * 1024 * 50); // 50 MB
30//!
31//! // Configure alerting
32//! collector.set_alert_threshold(
33//!     MetricType::DownloadTime,
34//!     AlertThreshold::Maximum(Duration::from_secs(10)),
35//! );
36//!
37//! // Generate analytics report
38//! let report = collector.generate_report();
39//! println!("Total downloads: {}", report.total_downloads);
40//! ```
41
42use chrono::{DateTime, Duration as ChronoDuration, Utc};
43use serde::{Deserialize, Serialize};
44use std::collections::{HashMap, VecDeque};
45use std::time::Duration;
46
47/// Metric types tracked by the monitoring system
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub enum MetricType {
50    /// Package download operation
51    Download,
52    /// Package upload operation
53    Upload,
54    /// Package access/read operation
55    Access,
56    /// Package creation operation
57    Creation,
58    /// Package deletion operation
59    Deletion,
60    /// Download duration
61    DownloadTime,
62    /// Upload duration
63    UploadTime,
64    /// Compression operation duration
65    CompressionTime,
66    /// Decompression operation duration
67    DecompressionTime,
68    /// Bandwidth usage in bytes
69    BandwidthUsage,
70    /// Storage usage in bytes
71    StorageUsage,
72    /// Memory usage in bytes
73    MemoryUsage,
74    /// CPU usage percentage
75    CpuUsage,
76    /// Error count
77    ErrorCount,
78}
79
80/// Alert threshold configuration
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub enum AlertThreshold {
83    /// Maximum value threshold
84    Maximum(Duration),
85    /// Maximum count within time window
86    MaxCount {
87        /// Maximum number of occurrences
88        count: usize,
89        /// Time window duration
90        window: ChronoDuration,
91    },
92    /// Maximum bytes threshold
93    MaxBytes(u64),
94    /// Maximum percentage threshold
95    MaxPercentage(f64),
96    /// Minimum value threshold (e.g., for uptime)
97    Minimum(Duration),
98}
99
100/// Alert severity level
101#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
102pub enum AlertSeverity {
103    /// Informational alert
104    Info,
105    /// Warning level alert
106    Warning,
107    /// Error level alert
108    Error,
109    /// Critical alert requiring immediate attention
110    Critical,
111}
112
113/// Alert triggered by threshold violation
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct Alert {
116    /// Alert severity
117    pub severity: AlertSeverity,
118    /// Metric type that triggered the alert
119    pub metric_type: MetricType,
120    /// Alert message
121    pub message: String,
122    /// Current value
123    pub current_value: String,
124    /// Threshold that was violated
125    pub threshold: String,
126    /// Timestamp when alert was triggered
127    pub timestamp: DateTime<Utc>,
128    /// Package ID if relevant
129    pub package_id: Option<String>,
130}
131
132/// Metric data point
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MetricPoint {
135    /// Metric type
136    pub metric_type: MetricType,
137    /// Timestamp of measurement
138    pub timestamp: DateTime<Utc>,
139    /// Numeric value
140    pub value: f64,
141    /// Package ID if relevant
142    pub package_id: Option<String>,
143    /// User ID if relevant
144    pub user_id: Option<String>,
145    /// Additional metadata
146    pub metadata: HashMap<String, String>,
147}
148
149/// Time-series data for a specific metric
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct TimeSeries {
152    /// Metric type
153    pub metric_type: MetricType,
154    /// Data points sorted by timestamp
155    pub points: Vec<MetricPoint>,
156    /// Aggregated statistics
157    pub stats: TimeSeriesStats,
158}
159
160/// Statistical summary of time-series data
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TimeSeriesStats {
163    /// Total number of data points
164    pub count: usize,
165    /// Sum of all values
166    pub sum: f64,
167    /// Minimum value
168    pub min: f64,
169    /// Maximum value
170    pub max: f64,
171    /// Average value
172    pub mean: f64,
173    /// Median value (50th percentile)
174    pub median: f64,
175    /// 95th percentile value
176    pub p95: f64,
177    /// 99th percentile value
178    pub p99: f64,
179}
180
181/// Package usage statistics
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct PackageStats {
184    /// Package ID
185    pub package_id: String,
186    /// Package version
187    pub version: String,
188    /// Total downloads
189    pub downloads: u64,
190    /// Total uploads
191    pub uploads: u64,
192    /// Total accesses
193    pub accesses: u64,
194    /// Total bandwidth (bytes)
195    pub bandwidth_bytes: u64,
196    /// Average download time (seconds)
197    pub avg_download_time: f64,
198    /// Total storage used (bytes)
199    pub storage_bytes: u64,
200    /// Unique users
201    pub unique_users: usize,
202    /// Error count
203    pub errors: u64,
204    /// Last access timestamp
205    pub last_access: Option<DateTime<Utc>>,
206}
207
208/// User activity statistics
209#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210pub struct UserStats {
211    /// User ID
212    pub user_id: String,
213    /// Total downloads
214    pub downloads: u64,
215    /// Total uploads
216    pub uploads: u64,
217    /// Unique packages accessed
218    pub unique_packages: usize,
219    /// Total bandwidth used (bytes)
220    pub bandwidth_bytes: u64,
221    /// First activity timestamp
222    pub first_activity: Option<DateTime<Utc>>,
223    /// Last activity timestamp
224    pub last_activity: Option<DateTime<Utc>>,
225}
226
227/// Geographic region statistics
228#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct RegionStats {
230    /// Region name
231    pub region: String,
232    /// Total requests
233    pub requests: u64,
234    /// Total bandwidth (bytes)
235    pub bandwidth_bytes: u64,
236    /// Average latency (milliseconds)
237    pub avg_latency_ms: f64,
238    /// Error rate percentage
239    pub error_rate: f64,
240}
241
242/// Comprehensive analytics report
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct AnalyticsReport {
245    /// Report generation timestamp
246    pub generated_at: DateTime<Utc>,
247    /// Report time range start
248    pub time_range_start: DateTime<Utc>,
249    /// Report time range end
250    pub time_range_end: DateTime<Utc>,
251    /// Total downloads across all packages
252    pub total_downloads: u64,
253    /// Total uploads across all packages
254    pub total_uploads: u64,
255    /// Total bandwidth used (bytes)
256    pub total_bandwidth_bytes: u64,
257    /// Total storage used (bytes)
258    pub total_storage_bytes: u64,
259    /// Total errors
260    pub total_errors: u64,
261    /// Per-package statistics
262    pub package_stats: Vec<PackageStats>,
263    /// Per-user statistics
264    pub user_stats: Vec<UserStats>,
265    /// Per-region statistics
266    pub region_stats: Vec<RegionStats>,
267    /// Active alerts
268    pub active_alerts: Vec<Alert>,
269    /// Top packages by downloads
270    pub top_packages: Vec<(String, u64)>,
271    /// Top users by activity
272    pub top_users: Vec<(String, u64)>,
273}
274
275/// Metrics collector for package monitoring
276///
277/// Collects, aggregates, and analyzes package operation metrics in real-time.
278/// Supports alerting, time-series data, and comprehensive analytics reporting.
279pub struct MetricsCollector {
280    /// Time-series data by metric type
281    time_series: HashMap<MetricType, Vec<MetricPoint>>,
282    /// Package statistics
283    package_stats: HashMap<String, PackageStats>,
284    /// User statistics
285    user_stats: HashMap<String, UserStats>,
286    /// Region statistics
287    region_stats: HashMap<String, RegionStats>,
288    /// Alert thresholds
289    alert_thresholds: HashMap<MetricType, AlertThreshold>,
290    /// Active alerts
291    active_alerts: VecDeque<Alert>,
292    /// Maximum alerts to keep
293    max_alerts: usize,
294    /// Maximum time-series points per metric
295    max_points_per_metric: usize,
296}
297
298impl MetricsCollector {
299    /// Create a new metrics collector
300    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    /// Set maximum number of alerts to retain
314    pub fn set_max_alerts(&mut self, max: usize) {
315        self.max_alerts = max;
316    }
317
318    /// Set maximum number of time-series points per metric
319    pub fn set_max_points_per_metric(&mut self, max: usize) {
320        self.max_points_per_metric = max;
321    }
322
323    /// Record a package download
324    pub fn record_download(&mut self, package_id: &str, version: &str, duration: Duration) {
325        // Record metric point
326        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        // Update package stats
343        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        // Update average download time
357        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        // Check for alerts
362        self.check_alert(
363            MetricType::DownloadTime,
364            duration.as_secs_f64(),
365            Some(package_id),
366        );
367    }
368
369    /// Record a package upload
370    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        // Update package stats
388        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        // Check for alerts
404        self.check_alert(
405            MetricType::BandwidthUsage,
406            size_bytes as f64,
407            Some(package_id),
408        );
409    }
410
411    /// Record a package access
412    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        // Update package stats
422        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        // Update user stats if user_id provided
436        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    /// Record an error
451    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        // Update package stats if package_id provided
464        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        // Check for alerts
473        self.check_alert(MetricType::ErrorCount, 1.0, package_id);
474    }
475
476    /// Record resource usage
477    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        // Check for alerts
508        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    /// Record a generic metric
514    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        // Trim old points if exceeding limit
535        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    /// Set an alert threshold for a metric type
542    pub fn set_alert_threshold(&mut self, metric_type: MetricType, threshold: AlertThreshold) {
543        self.alert_thresholds.insert(metric_type, threshold);
544    }
545
546    /// Get time-series data for a metric type
547    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    /// Get package statistics
559    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    /// Get user statistics
567    pub fn get_user_stats(&self, user_id: &str) -> Option<&UserStats> {
568        self.user_stats.get(user_id)
569    }
570
571    /// Get active alerts
572    pub fn get_active_alerts(&self) -> Vec<&Alert> {
573        self.active_alerts.iter().collect()
574    }
575
576    /// Clear all alerts
577    pub fn clear_alerts(&mut self) {
578        self.active_alerts.clear();
579    }
580
581    /// Generate comprehensive analytics report
582    pub fn generate_report(&self) -> AnalyticsReport {
583        let now = Utc::now();
584        let time_range_start = now - ChronoDuration::days(30); // Last 30 days
585
586        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        // Get top packages by downloads
597        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        // Get top users by activity
606        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    /// Export metrics to JSON
633    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    // Private helper methods
639
640    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                    // Count occurrences within time window
702                    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                // Trim old alerts
726                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        // This should trigger an alert
845        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        // Record more than max
902        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        // Record many errors
923        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        // Check severity increases with error count
931        let severities: Vec<AlertSeverity> = alerts.iter().map(|a| a.severity).collect();
932        assert!(severities.iter().any(|s| *s >= AlertSeverity::Warning));
933    }
934}