use crate::error::{ObservabilityError, Result};
use chrono::{DateTime, Duration, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPoint {
pub timestamp: DateTime<Utc>,
pub value: f64,
pub labels: HashMap<String, String>,
}
impl DataPoint {
pub fn now(value: f64) -> Self {
Self {
timestamp: Utc::now(),
value,
labels: HashMap::new(),
}
}
pub fn with_timestamp(timestamp: DateTime<Utc>, value: f64) -> Self {
Self {
timestamp,
value,
labels: HashMap::new(),
}
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct TimeSeries {
pub name: String,
pub description: String,
pub unit: MetricUnit,
data: VecDeque<DataPoint>,
max_size: usize,
retention: Duration,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum MetricUnit {
None,
Count,
Bytes,
Kilobytes,
Megabytes,
Gigabytes,
Milliseconds,
Seconds,
Percent,
RequestsPerSecond,
OperationsPerSecond,
PixelsPerSecond,
}
impl TimeSeries {
pub fn new(name: impl Into<String>, description: impl Into<String>, unit: MetricUnit) -> Self {
Self {
name: name.into(),
description: description.into(),
unit,
data: VecDeque::new(),
max_size: 10000,
retention: Duration::hours(24),
}
}
pub fn with_max_size(mut self, max_size: usize) -> Self {
self.max_size = max_size;
self
}
pub fn with_retention(mut self, retention: Duration) -> Self {
self.retention = retention;
self
}
pub fn add(&mut self, point: DataPoint) {
let cutoff = Utc::now() - self.retention;
while let Some(front) = self.data.front() {
if front.timestamp < cutoff {
self.data.pop_front();
} else {
break;
}
}
if self.data.len() >= self.max_size {
self.data.pop_front();
}
self.data.push_back(point);
}
pub fn add_value(&mut self, value: f64) {
self.add(DataPoint::now(value));
}
pub fn range(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> Vec<&DataPoint> {
self.data
.iter()
.filter(|p| p.timestamp >= start && p.timestamp <= end)
.collect()
}
pub fn last_n(&self, n: usize) -> Vec<&DataPoint> {
self.data.iter().rev().take(n).rev().collect()
}
pub fn latest(&self) -> Option<&DataPoint> {
self.data.back()
}
pub fn statistics(&self) -> Option<TimeSeriesStats> {
if self.data.is_empty() {
return None;
}
let values: Vec<f64> = self.data.iter().map(|p| p.value).collect();
let count = values.len();
let sum: f64 = values.iter().sum();
let mean = sum / count as f64;
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count as f64;
let std_dev = variance.sqrt();
let min = values.iter().copied().fold(f64::INFINITY, |a, b| a.min(b));
let max = values
.iter()
.copied()
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
let mut sorted = values;
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p50 = percentile(&sorted, 50.0);
let p90 = percentile(&sorted, 90.0);
let p95 = percentile(&sorted, 95.0);
let p99 = percentile(&sorted, 99.0);
Some(TimeSeriesStats {
count,
sum,
mean,
std_dev,
min,
max,
p50,
p90,
p95,
p99,
})
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn clear(&mut self) {
self.data.clear();
}
}
fn percentile(sorted: &[f64], p: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
if sorted.len() == 1 {
return sorted[0];
}
let idx = (p / 100.0 * (sorted.len() - 1) as f64).round() as usize;
let idx = idx.min(sorted.len() - 1);
sorted[idx]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesStats {
pub count: usize,
pub sum: f64,
pub mean: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
pub p50: f64,
pub p90: f64,
pub p95: f64,
pub p99: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum AggregationType {
Sum,
Average,
Min,
Max,
Count,
Rate,
Percentile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregationConfig {
pub method: AggregationType,
pub window: Duration,
pub step: Duration,
pub percentile_value: Option<f64>,
}
impl Default for AggregationConfig {
fn default() -> Self {
Self {
method: AggregationType::Average,
window: Duration::minutes(1),
step: Duration::seconds(10),
percentile_value: None,
}
}
}
#[derive(Debug)]
pub struct MetricAggregator {
config: AggregationConfig,
series: TimeSeries,
aggregated: RwLock<VecDeque<DataPoint>>,
max_aggregated: usize,
}
impl MetricAggregator {
pub fn new(series: TimeSeries, config: AggregationConfig) -> Self {
Self {
config,
series,
aggregated: RwLock::new(VecDeque::new()),
max_aggregated: 1000,
}
}
pub fn add(&mut self, point: DataPoint) {
self.series.add(point);
self.maybe_aggregate();
}
pub fn add_value(&mut self, value: f64) {
self.series.add_value(value);
self.maybe_aggregate();
}
fn maybe_aggregate(&self) {
let mut aggregated = self.aggregated.write();
let now = Utc::now();
let should_aggregate = match aggregated.back() {
Some(last) => (now - last.timestamp) >= self.config.step,
None => !self.series.is_empty(),
};
if should_aggregate {
let window_start = now - self.config.window;
let points = self.series.range(window_start, now);
if !points.is_empty() {
let value = self.compute_aggregation(&points);
let point = DataPoint::now(value);
if aggregated.len() >= self.max_aggregated {
aggregated.pop_front();
}
aggregated.push_back(point);
}
}
}
fn compute_aggregation(&self, points: &[&DataPoint]) -> f64 {
if points.is_empty() {
return 0.0;
}
match self.config.method {
AggregationType::Sum => points.iter().map(|p| p.value).sum(),
AggregationType::Average => {
let sum: f64 = points.iter().map(|p| p.value).sum();
sum / points.len() as f64
}
AggregationType::Min => points
.iter()
.map(|p| p.value)
.fold(f64::INFINITY, |a, b| a.min(b)),
AggregationType::Max => points
.iter()
.map(|p| p.value)
.fold(f64::NEG_INFINITY, |a, b| a.max(b)),
AggregationType::Count => points.len() as f64,
AggregationType::Rate => {
if points.len() < 2 {
return 0.0;
}
let first = &points[0];
let last = &points[points.len() - 1];
let duration = (last.timestamp - first.timestamp).num_seconds() as f64;
if duration <= 0.0 {
return 0.0;
}
(last.value - first.value) / duration
}
AggregationType::Percentile => {
let p = self.config.percentile_value.unwrap_or(95.0);
let mut values: Vec<f64> = points.iter().map(|pt| pt.value).collect();
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
percentile(&values, p)
}
}
}
pub fn aggregated(&self) -> Vec<DataPoint> {
self.aggregated.read().iter().cloned().collect()
}
pub fn latest_aggregated(&self) -> Option<DataPoint> {
self.aggregated.read().back().cloned()
}
pub fn statistics(&self) -> Option<TimeSeriesStats> {
self.series.statistics()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum WidgetType {
LineChart,
AreaChart,
BarChart,
Gauge,
SingleStat,
Table,
Heatmap,
Histogram,
PieChart,
Sparkline,
Text,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WidgetDisplay {
pub title: String,
pub description: Option<String>,
pub position: (u32, u32),
pub size: (u32, u32),
pub background_color: Option<String>,
pub text_color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThresholdConfig {
pub value: f64,
pub color: String,
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Widget {
pub id: String,
pub widget_type: WidgetType,
pub display: WidgetDisplay,
pub metrics: Vec<String>,
pub aggregation: Option<AggregationConfig>,
pub thresholds: Vec<ThresholdConfig>,
pub refresh_interval: u32,
pub options: HashMap<String, serde_json::Value>,
}
impl Widget {
pub fn new(id: impl Into<String>, widget_type: WidgetType, title: impl Into<String>) -> Self {
Self {
id: id.into(),
widget_type,
display: WidgetDisplay {
title: title.into(),
description: None,
position: (0, 0),
size: (4, 4),
background_color: None,
text_color: None,
},
metrics: Vec::new(),
aggregation: None,
thresholds: Vec::new(),
refresh_interval: 30,
options: HashMap::new(),
}
}
pub fn with_position(mut self, x: u32, y: u32) -> Self {
self.display.position = (x, y);
self
}
pub fn with_size(mut self, width: u32, height: u32) -> Self {
self.display.size = (width, height);
self
}
pub fn with_metric(mut self, metric: impl Into<String>) -> Self {
self.metrics.push(metric.into());
self
}
pub fn with_aggregation(mut self, config: AggregationConfig) -> Self {
self.aggregation = Some(config);
self
}
pub fn with_threshold(mut self, value: f64, color: impl Into<String>) -> Self {
self.thresholds.push(ThresholdConfig {
value,
color: color.into(),
label: None,
});
self
}
pub fn with_refresh_interval(mut self, seconds: u32) -> Self {
self.refresh_interval = seconds;
self
}
pub fn with_option(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.options.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ComparisonOperator {
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Equal,
NotEqual,
}
impl ComparisonOperator {
pub fn evaluate(&self, actual: f64, threshold: f64) -> bool {
match self {
ComparisonOperator::GreaterThan => actual > threshold,
ComparisonOperator::GreaterThanOrEqual => actual >= threshold,
ComparisonOperator::LessThan => actual < threshold,
ComparisonOperator::LessThanOrEqual => actual <= threshold,
ComparisonOperator::Equal => (actual - threshold).abs() < f64::EPSILON,
ComparisonOperator::NotEqual => (actual - threshold).abs() >= f64::EPSILON,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertSeverity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertThreshold {
pub name: String,
pub metric: String,
pub operator: ComparisonOperator,
pub threshold: f64,
pub severity: AlertSeverity,
pub duration: Duration,
pub message: String,
pub enabled: bool,
}
impl AlertThreshold {
pub fn new(
name: impl Into<String>,
metric: impl Into<String>,
operator: ComparisonOperator,
threshold: f64,
) -> Self {
Self {
name: name.into(),
metric: metric.into(),
operator,
threshold,
severity: AlertSeverity::Warning,
duration: Duration::minutes(1),
message: String::new(),
enabled: true,
}
}
pub fn with_severity(mut self, severity: AlertSeverity) -> Self {
self.severity = severity;
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn check(&self, value: f64) -> bool {
self.enabled && self.operator.evaluate(value, self.threshold)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
pub id: String,
pub threshold_name: String,
pub metric: String,
pub value: f64,
pub threshold_value: f64,
pub severity: AlertSeverity,
pub message: String,
pub triggered_at: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub struct AlertManager {
thresholds: RwLock<Vec<AlertThreshold>>,
active_alerts: RwLock<HashMap<String, Alert>>,
history: RwLock<VecDeque<Alert>>,
max_history: usize,
violation_starts: RwLock<HashMap<String, DateTime<Utc>>>,
}
impl Default for AlertManager {
fn default() -> Self {
Self::new()
}
}
impl AlertManager {
pub fn new() -> Self {
Self {
thresholds: RwLock::new(Vec::new()),
active_alerts: RwLock::new(HashMap::new()),
history: RwLock::new(VecDeque::new()),
max_history: 1000,
violation_starts: RwLock::new(HashMap::new()),
}
}
pub fn add_threshold(&self, threshold: AlertThreshold) {
self.thresholds.write().push(threshold);
}
pub fn remove_threshold(&self, name: &str) {
self.thresholds.write().retain(|t| t.name != name);
}
pub fn check_metric(&self, metric: &str, value: f64) -> Vec<Alert> {
let thresholds = self.thresholds.read();
let now = Utc::now();
let mut new_alerts = Vec::new();
for threshold in thresholds.iter().filter(|t| t.metric == metric) {
let alert_key = format!("{}:{}", threshold.name, threshold.metric);
if threshold.check(value) {
let mut violation_starts = self.violation_starts.write();
let start = violation_starts.entry(alert_key.clone()).or_insert(now);
if now - *start >= threshold.duration {
let mut active = self.active_alerts.write();
if !active.contains_key(&alert_key) {
let alert = Alert {
id: uuid::Uuid::new_v4().to_string(),
threshold_name: threshold.name.clone(),
metric: metric.to_string(),
value,
threshold_value: threshold.threshold,
severity: threshold.severity,
message: threshold.message.clone(),
triggered_at: now,
resolved_at: None,
};
active.insert(alert_key.clone(), alert.clone());
new_alerts.push(alert);
}
}
} else {
self.violation_starts.write().remove(&alert_key);
if let Some(mut alert) = self.active_alerts.write().remove(&alert_key) {
alert.resolved_at = Some(now);
let mut history = self.history.write();
if history.len() >= self.max_history {
history.pop_front();
}
history.push_back(alert);
}
}
}
new_alerts
}
pub fn active_alerts(&self) -> Vec<Alert> {
self.active_alerts.read().values().cloned().collect()
}
pub fn history(&self) -> Vec<Alert> {
self.history.read().iter().cloned().collect()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum TrendDirection {
Increasing,
Decreasing,
Stable,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendAnalysis {
pub metric: String,
pub direction: TrendDirection,
pub slope: f64,
pub r_squared: f64,
pub projected_value: Option<f64>,
pub confidence_interval: Option<(f64, f64)>,
}
#[derive(Debug)]
pub struct TrendAnalyzer {
min_data_points: usize,
stability_threshold: f64,
}
impl Default for TrendAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl TrendAnalyzer {
pub fn new() -> Self {
Self {
min_data_points: 10,
stability_threshold: 0.01,
}
}
pub fn with_min_data_points(mut self, min: usize) -> Self {
self.min_data_points = min;
self
}
pub fn with_stability_threshold(mut self, threshold: f64) -> Self {
self.stability_threshold = threshold;
self
}
pub fn analyze(&self, series: &TimeSeries) -> Result<TrendAnalysis> {
let points = series.last_n(self.min_data_points.max(series.len()));
if points.len() < self.min_data_points {
return Ok(TrendAnalysis {
metric: series.name.clone(),
direction: TrendDirection::Unknown,
slope: 0.0,
r_squared: 0.0,
projected_value: None,
confidence_interval: None,
});
}
let (slope, intercept, r_squared) = self.linear_regression(&points)?;
let direction = if slope.abs() < self.stability_threshold {
TrendDirection::Stable
} else if slope > 0.0 {
TrendDirection::Increasing
} else {
TrendDirection::Decreasing
};
let n = points.len() as f64;
let projected = intercept + slope * (n + 1.0);
let projected_value = Some(projected);
let values: Vec<f64> = points.iter().map(|p| p.value).collect();
let mean: f64 = values.iter().sum::<f64>() / n;
let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
let margin = 1.96 * std_dev / n.sqrt(); let confidence_interval = Some((projected - margin, projected + margin));
Ok(TrendAnalysis {
metric: series.name.clone(),
direction,
slope,
r_squared,
projected_value,
confidence_interval,
})
}
fn linear_regression(&self, points: &[&DataPoint]) -> Result<(f64, f64, f64)> {
let n = points.len() as f64;
if n < 2.0 {
return Err(ObservabilityError::Other(
"Insufficient data points for regression".to_string(),
));
}
let x_values: Vec<f64> = (0..points.len()).map(|i| i as f64).collect();
let y_values: Vec<f64> = points.iter().map(|p| p.value).collect();
let x_mean = x_values.iter().sum::<f64>() / n;
let y_mean = y_values.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for i in 0..points.len() {
let x_diff = x_values[i] - x_mean;
let y_diff = y_values[i] - y_mean;
numerator += x_diff * y_diff;
denominator += x_diff * x_diff;
}
let slope = if denominator.abs() < f64::EPSILON {
0.0
} else {
numerator / denominator
};
let intercept = y_mean - slope * x_mean;
let ss_tot: f64 = y_values.iter().map(|y| (y - y_mean).powi(2)).sum();
let ss_res: f64 = y_values
.iter()
.zip(x_values.iter())
.map(|(y, x)| (y - (slope * x + intercept)).powi(2))
.sum();
let r_squared = if ss_tot.abs() < f64::EPSILON {
0.0
} else {
1.0 - (ss_res / ss_tot)
};
Ok((slope, intercept, r_squared))
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ResourceType {
Cpu,
Memory,
DiskIo,
NetworkIo,
Gpu,
GpuMemory,
FileDescriptors,
Threads,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceSnapshot {
pub timestamp: DateTime<Utc>,
pub resource_type: ResourceType,
pub utilization: f64,
pub used: u64,
pub total: u64,
pub metadata: HashMap<String, String>,
}
#[derive(Debug)]
pub struct ResourceTracker {
resources: RwLock<HashMap<ResourceType, VecDeque<ResourceSnapshot>>>,
max_history: usize,
thresholds: RwLock<HashMap<ResourceType, f64>>,
}
impl Default for ResourceTracker {
fn default() -> Self {
Self::new()
}
}
impl ResourceTracker {
pub fn new() -> Self {
Self {
resources: RwLock::new(HashMap::new()),
max_history: 1000,
thresholds: RwLock::new(HashMap::new()),
}
}
pub fn set_threshold(&self, resource_type: ResourceType, threshold: f64) {
self.thresholds.write().insert(resource_type, threshold);
}
pub fn record(&self, snapshot: ResourceSnapshot) {
let mut resources = self.resources.write();
let history = resources.entry(snapshot.resource_type).or_default();
if history.len() >= self.max_history {
history.pop_front();
}
history.push_back(snapshot);
}
pub fn record_cpu(&self, utilization: f64, used_cores: u64, total_cores: u64) {
self.record(ResourceSnapshot {
timestamp: Utc::now(),
resource_type: ResourceType::Cpu,
utilization,
used: used_cores,
total: total_cores,
metadata: HashMap::new(),
});
}
pub fn record_memory(&self, used_bytes: u64, total_bytes: u64) {
let utilization = if total_bytes > 0 {
(used_bytes as f64 / total_bytes as f64) * 100.0
} else {
0.0
};
self.record(ResourceSnapshot {
timestamp: Utc::now(),
resource_type: ResourceType::Memory,
utilization,
used: used_bytes,
total: total_bytes,
metadata: HashMap::new(),
});
}
pub fn latest(&self, resource_type: ResourceType) -> Option<ResourceSnapshot> {
self.resources
.read()
.get(&resource_type)
.and_then(|h| h.back().cloned())
}
pub fn history(&self, resource_type: ResourceType) -> Vec<ResourceSnapshot> {
self.resources
.read()
.get(&resource_type)
.map(|h| h.iter().cloned().collect())
.unwrap_or_default()
}
pub fn exceeding_threshold(&self) -> Vec<(ResourceType, f64)> {
let resources = self.resources.read();
let thresholds = self.thresholds.read();
let mut exceeding = Vec::new();
for (resource_type, threshold) in thresholds.iter() {
if let Some(history) = resources.get(resource_type)
&& let Some(latest) = history.back()
&& latest.utilization > *threshold
{
exceeding.push((*resource_type, latest.utilization));
}
}
exceeding
}
pub fn average_utilization(&self, resource_type: ResourceType) -> Option<f64> {
let resources = self.resources.read();
resources.get(&resource_type).and_then(|history| {
if history.is_empty() {
None
} else {
let sum: f64 = history.iter().map(|s| s.utilization).sum();
Some(sum / history.len() as f64)
}
})
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum VisualizationFormat {
Json,
PrometheusText,
OpenMetrics,
Csv,
InfluxLineProtocol,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomMetric {
pub name: String,
pub description: String,
pub metric_type: CustomMetricType,
pub unit: MetricUnit,
pub labels: HashMap<String, String>,
pub value: f64,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CustomMetricType {
Counter,
Gauge,
Histogram,
Summary,
}
impl CustomMetric {
pub fn new(name: impl Into<String>, metric_type: CustomMetricType, value: f64) -> Self {
Self {
name: name.into(),
description: String::new(),
metric_type,
unit: MetricUnit::None,
labels: HashMap::new(),
value,
timestamp: Utc::now(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_unit(mut self, unit: MetricUnit) -> Self {
self.unit = unit;
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
pub fn format(&self, format: VisualizationFormat) -> String {
match format {
VisualizationFormat::Json => {
serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
}
VisualizationFormat::PrometheusText => {
let labels = if self.labels.is_empty() {
String::new()
} else {
let label_str: Vec<String> = self
.labels
.iter()
.map(|(k, v)| format!("{}=\"{}\"", k, v))
.collect();
format!("{{{}}}", label_str.join(","))
};
format!("{}{} {}", self.name, labels, self.value)
}
VisualizationFormat::OpenMetrics => {
let type_str = match self.metric_type {
CustomMetricType::Counter => "counter",
CustomMetricType::Gauge => "gauge",
CustomMetricType::Histogram => "histogram",
CustomMetricType::Summary => "summary",
};
format!(
"# TYPE {} {}\n# HELP {} {}\n{} {}",
self.name, type_str, self.name, self.description, self.name, self.value
)
}
VisualizationFormat::Csv => {
format!(
"{},{},{},{}",
self.name,
self.value,
self.timestamp.to_rfc3339(),
serde_json::to_string(&self.labels).unwrap_or_else(|_| "{}".to_string())
)
}
VisualizationFormat::InfluxLineProtocol => {
let tags = if self.labels.is_empty() {
String::new()
} else {
let tag_str: Vec<String> = self
.labels
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
format!(",{}", tag_str.join(","))
};
format!(
"{}{} value={} {}",
self.name,
tags,
self.value,
self.timestamp.timestamp_nanos_opt().unwrap_or(0)
)
}
}
}
}
#[derive(Debug)]
pub struct MetricRegistry {
metrics: RwLock<HashMap<String, CustomMetric>>,
history: RwLock<HashMap<String, VecDeque<CustomMetric>>>,
max_history: usize,
}
impl Default for MetricRegistry {
fn default() -> Self {
Self::new()
}
}
impl MetricRegistry {
pub fn new() -> Self {
Self {
metrics: RwLock::new(HashMap::new()),
history: RwLock::new(HashMap::new()),
max_history: 1000,
}
}
pub fn register(&self, metric: CustomMetric) {
let name = metric.name.clone();
self.metrics.write().insert(name.clone(), metric.clone());
let mut history = self.history.write();
let hist = history.entry(name).or_default();
if hist.len() >= self.max_history {
hist.pop_front();
}
hist.push_back(metric);
}
pub fn get(&self, name: &str) -> Option<CustomMetric> {
self.metrics.read().get(name).cloned()
}
pub fn all(&self) -> Vec<CustomMetric> {
self.metrics.read().values().cloned().collect()
}
pub fn get_history(&self, name: &str) -> Vec<CustomMetric> {
self.history
.read()
.get(name)
.map(|h| h.iter().cloned().collect())
.unwrap_or_default()
}
pub fn export(&self, format: VisualizationFormat) -> String {
let metrics = self.metrics.read();
match format {
VisualizationFormat::Json => {
let values: Vec<&CustomMetric> = metrics.values().collect();
serde_json::to_string_pretty(&values).unwrap_or_else(|_| "[]".to_string())
}
VisualizationFormat::Csv => {
let mut output = String::from("name,value,timestamp,labels\n");
for metric in metrics.values() {
output.push_str(&metric.format(format));
output.push('\n');
}
output
}
_ => metrics
.values()
.map(|m| m.format(format))
.collect::<Vec<_>>()
.join("\n"),
}
}
}
#[derive(Debug)]
pub struct PerformanceDashboard {
pub name: String,
pub description: String,
widgets: RwLock<Vec<Widget>>,
time_series: RwLock<HashMap<String, TimeSeries>>,
pub alert_manager: AlertManager,
pub trend_analyzer: TrendAnalyzer,
pub resource_tracker: ResourceTracker,
pub metric_registry: MetricRegistry,
}
impl PerformanceDashboard {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: String::new(),
widgets: RwLock::new(Vec::new()),
time_series: RwLock::new(HashMap::new()),
alert_manager: AlertManager::new(),
trend_analyzer: TrendAnalyzer::new(),
resource_tracker: ResourceTracker::new(),
metric_registry: MetricRegistry::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn add_widget(&self, widget: Widget) {
self.widgets.write().push(widget);
}
pub fn remove_widget(&self, id: &str) {
self.widgets.write().retain(|w| w.id != id);
}
pub fn widgets(&self) -> Vec<Widget> {
self.widgets.read().clone()
}
pub fn time_series(
&self,
name: impl Into<String>,
description: impl Into<String>,
unit: MetricUnit,
) -> String {
let name = name.into();
let mut series_map = self.time_series.write();
if !series_map.contains_key(&name) {
series_map.insert(name.clone(), TimeSeries::new(&name, description, unit));
}
name
}
pub fn add_value(&self, series_name: &str, value: f64) {
let mut series_map = self.time_series.write();
if let Some(series) = series_map.get_mut(series_name) {
series.add_value(value);
}
}
pub fn get_statistics(&self, series_name: &str) -> Option<TimeSeriesStats> {
self.time_series
.read()
.get(series_name)
.and_then(|s| s.statistics())
}
pub fn analyze_trend(&self, series_name: &str) -> Result<TrendAnalysis> {
let series_map = self.time_series.read();
let series = series_map.get(series_name).ok_or_else(|| {
ObservabilityError::NotFound(format!("Time series '{}' not found", series_name))
})?;
self.trend_analyzer.analyze(series)
}
pub fn summary(&self) -> DashboardSummary {
let widgets = self.widgets.read();
let series = self.time_series.read();
let active_alerts = self.alert_manager.active_alerts();
let exceeding = self.resource_tracker.exceeding_threshold();
DashboardSummary {
name: self.name.clone(),
widget_count: widgets.len(),
time_series_count: series.len(),
active_alert_count: active_alerts.len(),
resources_exceeding_threshold: exceeding.len(),
critical_alerts: active_alerts
.iter()
.filter(|a| a.severity == AlertSeverity::Critical)
.count(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardSummary {
pub name: String,
pub widget_count: usize,
pub time_series_count: usize,
pub active_alert_count: usize,
pub resources_exceeding_threshold: usize,
pub critical_alerts: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_series() {
let mut series = TimeSeries::new("test_metric", "Test metric", MetricUnit::Milliseconds);
for i in 0..100 {
series.add_value(i as f64);
}
assert_eq!(series.len(), 100);
assert!(series.statistics().is_some());
let stats = series.statistics().expect("Stats should exist");
assert_eq!(stats.count, 100);
assert!((stats.mean - 49.5).abs() < 0.1);
}
#[test]
fn test_data_point_with_labels() {
let point = DataPoint::now(42.0)
.with_label("region", "us-west")
.with_label("service", "api");
assert_eq!(point.value, 42.0);
assert_eq!(point.labels.len(), 2);
assert_eq!(point.labels.get("region"), Some(&"us-west".to_string()));
}
#[test]
fn test_widget_builder() {
let widget = Widget::new("cpu_gauge", WidgetType::Gauge, "CPU Usage")
.with_position(0, 0)
.with_size(4, 4)
.with_metric("cpu_percent")
.with_threshold(80.0, "#FFA500")
.with_threshold(95.0, "#FF0000")
.with_refresh_interval(5);
assert_eq!(widget.id, "cpu_gauge");
assert_eq!(widget.display.title, "CPU Usage");
assert_eq!(widget.thresholds.len(), 2);
assert_eq!(widget.refresh_interval, 5);
}
#[test]
fn test_alert_threshold() {
let threshold = AlertThreshold::new(
"high_cpu",
"cpu_percent",
ComparisonOperator::GreaterThan,
80.0,
)
.with_severity(AlertSeverity::Warning)
.with_message("CPU usage is high");
assert!(threshold.check(85.0));
assert!(!threshold.check(75.0));
}
#[test]
fn test_trend_analyzer() {
let mut series = TimeSeries::new("increasing", "Increasing metric", MetricUnit::Count);
for i in 0..50 {
series.add(DataPoint::with_timestamp(
Utc::now() - Duration::seconds(50 - i),
i as f64 * 2.0,
));
}
let analyzer = TrendAnalyzer::new().with_min_data_points(5);
let analysis = analyzer.analyze(&series).expect("Analysis should succeed");
assert_eq!(analysis.direction, TrendDirection::Increasing);
assert!(analysis.slope > 0.0);
}
#[test]
fn test_resource_tracker() {
let tracker = ResourceTracker::new();
tracker.set_threshold(ResourceType::Cpu, 80.0);
tracker.record_cpu(75.0, 6, 8);
assert!(tracker.exceeding_threshold().is_empty());
tracker.record_cpu(85.0, 7, 8);
let exceeding = tracker.exceeding_threshold();
assert_eq!(exceeding.len(), 1);
assert_eq!(exceeding[0].0, ResourceType::Cpu);
}
#[test]
fn test_custom_metric_formats() {
let metric = CustomMetric::new("request_count", CustomMetricType::Counter, 42.0)
.with_description("Total request count")
.with_label("service", "api")
.with_unit(MetricUnit::Count);
let prometheus_text = metric.format(VisualizationFormat::PrometheusText);
assert!(prometheus_text.contains("request_count"));
assert!(prometheus_text.contains("42"));
let json = metric.format(VisualizationFormat::Json);
assert!(json.contains("request_count"));
}
#[test]
fn test_metric_registry() {
let registry = MetricRegistry::new();
registry.register(CustomMetric::new(
"cpu_usage",
CustomMetricType::Gauge,
75.5,
));
registry.register(CustomMetric::new(
"memory_usage",
CustomMetricType::Gauge,
8_589_934_592.0,
));
assert_eq!(registry.all().len(), 2);
assert!(registry.get("cpu_usage").is_some());
let export = registry.export(VisualizationFormat::PrometheusText);
assert!(export.contains("cpu_usage"));
assert!(export.contains("memory_usage"));
}
#[test]
fn test_performance_dashboard() {
let dashboard = PerformanceDashboard::new("Test Dashboard")
.with_description("Testing dashboard functionality");
dashboard.add_widget(Widget::new("w1", WidgetType::Gauge, "CPU"));
dashboard.add_widget(Widget::new("w2", WidgetType::LineChart, "Latency"));
assert_eq!(dashboard.widgets().len(), 2);
let series_name =
dashboard.time_series("latency", "Request latency", MetricUnit::Milliseconds);
for i in 0..20 {
dashboard.add_value(&series_name, i as f64 * 10.0);
}
let stats = dashboard.get_statistics(&series_name);
assert!(stats.is_some());
let summary = dashboard.summary();
assert_eq!(summary.widget_count, 2);
assert_eq!(summary.time_series_count, 1);
}
#[test]
fn test_aggregation_config() {
let config = AggregationConfig {
method: AggregationType::Average,
window: Duration::minutes(5),
step: Duration::seconds(30),
percentile_value: None,
};
assert_eq!(config.method, AggregationType::Average);
}
#[test]
fn test_comparison_operators() {
assert!(ComparisonOperator::GreaterThan.evaluate(10.0, 5.0));
assert!(!ComparisonOperator::GreaterThan.evaluate(5.0, 10.0));
assert!(ComparisonOperator::LessThan.evaluate(5.0, 10.0));
assert!(!ComparisonOperator::LessThan.evaluate(10.0, 5.0));
assert!(ComparisonOperator::Equal.evaluate(5.0, 5.0));
assert!(ComparisonOperator::NotEqual.evaluate(5.0, 10.0));
}
#[test]
fn test_percentile_calculation() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
assert_eq!(percentile(&values, 50.0), 6.0);
assert_eq!(percentile(&values, 0.0), 1.0);
assert_eq!(percentile(&values, 100.0), 10.0);
}
}