use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metric {
pub name: String,
pub value: f64,
pub timestamp: u64,
pub tags: HashMap<String, String>,
}
impl Metric {
pub fn new(name: String, value: f64) -> Self {
Self {
name,
value,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
tags: HashMap::new(),
}
}
pub fn with_tags(mut self, tags: HashMap<String, String>) -> Self {
self.tags = tags;
self
}
pub fn with_timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
pub start: u64,
pub end: u64,
}
impl TimeRange {
pub fn new(start: u64, end: u64) -> Self {
Self { start, end }
}
pub fn last_seconds(seconds: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
start: now - seconds,
end: now,
}
}
pub fn contains(&self, timestamp: u64) -> bool {
timestamp >= self.start && timestamp <= self.end
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AggregationType {
Sum,
Average,
Min,
Max,
Count,
Latest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatedMetric {
pub name: String,
pub value: f64,
pub aggregation_type: AggregationType,
pub time_range: TimeRange,
pub sample_count: usize,
}
#[derive(Debug, Clone)]
pub struct MetricsCollector {
metrics: HashMap<String, Vec<Metric>>,
max_metrics_per_name: usize,
auto_cleanup: bool,
max_metric_age: u64,
}
impl MetricsCollector {
pub fn new() -> Self {
Self {
metrics: HashMap::new(),
max_metrics_per_name: 1000,
auto_cleanup: true,
max_metric_age: 3600, }
}
pub fn with_config(config: MetricsConfig) -> Self {
Self {
metrics: HashMap::new(),
max_metrics_per_name: config.max_metrics_per_name,
auto_cleanup: config.auto_cleanup,
max_metric_age: config.max_metric_age,
}
}
pub fn record(&mut self, metric: Metric) {
let name = metric.name.clone();
self.metrics.entry(name.clone()).or_insert_with(Vec::new).push(metric);
if self.auto_cleanup {
self.cleanup_old_metrics(&name);
}
if let Some(metrics) = self.metrics.get_mut(&name) {
if metrics.len() > self.max_metrics_per_name {
metrics.drain(0..metrics.len() - self.max_metrics_per_name);
}
}
}
pub fn get_metrics(&self, name: &str, time_range: &TimeRange) -> Vec<&Metric> {
self.metrics
.get(name)
.map(|metrics| {
metrics
.iter()
.filter(|m| time_range.contains(m.timestamp))
.collect()
})
.unwrap_or_default()
}
pub fn get_all_metrics(&self, name: &str) -> Vec<&Metric> {
self.metrics
.get(name)
.map(|metrics| metrics.iter().collect())
.unwrap_or_default()
}
pub fn aggregate_metrics(
&self,
name: &str,
time_range: &TimeRange,
aggregation_type: AggregationType,
) -> Option<AggregatedMetric> {
let metrics = self.get_metrics(name, time_range);
if metrics.is_empty() {
return None;
}
let value = match aggregation_type {
AggregationType::Sum => metrics.iter().map(|m| m.value).sum(),
AggregationType::Average => {
let sum: f64 = metrics.iter().map(|m| m.value).sum();
sum / metrics.len() as f64
}
AggregationType::Min => metrics.iter().map(|m| m.value).fold(f64::INFINITY, f64::min),
AggregationType::Max => metrics.iter().map(|m| m.value).fold(f64::NEG_INFINITY, f64::max),
AggregationType::Count => metrics.len() as f64,
AggregationType::Latest => metrics.last().unwrap().value,
};
Some(AggregatedMetric {
name: name.to_string(),
value,
aggregation_type,
time_range: time_range.clone(),
sample_count: metrics.len(),
})
}
pub fn get_metric_names(&self) -> Vec<String> {
self.metrics.keys().cloned().collect()
}
pub fn clear(&mut self) {
self.metrics.clear();
}
pub fn clear_metrics(&mut self, name: &str) {
self.metrics.remove(name);
}
fn cleanup_old_metrics(&mut self, name: &str) {
let cutoff_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
- self.max_metric_age;
if let Some(metrics) = self.metrics.get_mut(name) {
metrics.retain(|m| m.timestamp >= cutoff_time);
}
}
pub fn total_metrics_count(&self) -> usize {
self.metrics.values().map(|v| v.len()).sum()
}
pub fn unique_metric_count(&self) -> usize {
self.metrics.len()
}
}
impl Default for MetricsCollector {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub max_metrics_per_name: usize,
pub auto_cleanup: bool,
pub max_metric_age: u64,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
max_metrics_per_name: 1000,
auto_cleanup: true,
max_metric_age: 3600, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_creation() {
let metric = Metric::new("test_metric".to_string(), 42.0);
assert_eq!(metric.name, "test_metric");
assert_eq!(metric.value, 42.0);
assert!(!metric.tags.is_empty() || metric.tags.is_empty()); }
#[test]
fn test_metric_with_tags() {
let mut tags = HashMap::new();
tags.insert("service".to_string(), "api".to_string());
tags.insert("version".to_string(), "1.0".to_string());
let metric = Metric::new("test_metric".to_string(), 42.0).with_tags(tags.clone());
assert_eq!(metric.tags, tags);
}
#[test]
fn test_time_range() {
let range = TimeRange::new(1000, 2000);
assert!(range.contains(1500));
assert!(!range.contains(500));
assert!(!range.contains(2500));
}
#[test]
fn test_metrics_collector() {
let mut collector = MetricsCollector::new();
collector.record(Metric::new("cpu_usage".to_string(), 75.0));
collector.record(Metric::new("memory_usage".to_string(), 60.0));
collector.record(Metric::new("cpu_usage".to_string(), 80.0));
assert_eq!(collector.unique_metric_count(), 2);
assert_eq!(collector.total_metrics_count(), 3);
let cpu_metrics = collector.get_all_metrics("cpu_usage");
assert_eq!(cpu_metrics.len(), 2);
let time_range = TimeRange::last_seconds(3600);
let avg_cpu = collector.aggregate_metrics("cpu_usage", &time_range, AggregationType::Average);
assert!(avg_cpu.is_some());
assert_eq!(avg_cpu.unwrap().value, 77.5); }
#[test]
fn test_aggregation_types() {
let mut collector = MetricsCollector::new();
collector.record(Metric::new("test".to_string(), 10.0));
collector.record(Metric::new("test".to_string(), 20.0));
collector.record(Metric::new("test".to_string(), 30.0));
let time_range = TimeRange::last_seconds(3600);
let sum = collector.aggregate_metrics("test", &time_range, AggregationType::Sum);
assert_eq!(sum.unwrap().value, 60.0);
let avg = collector.aggregate_metrics("test", &time_range, AggregationType::Average);
assert_eq!(avg.unwrap().value, 20.0);
let min = collector.aggregate_metrics("test", &time_range, AggregationType::Min);
assert_eq!(min.unwrap().value, 10.0);
let max = collector.aggregate_metrics("test", &time_range, AggregationType::Max);
assert_eq!(max.unwrap().value, 30.0);
let count = collector.aggregate_metrics("test", &time_range, AggregationType::Count);
assert_eq!(count.unwrap().value, 3.0);
}
}