use crate::error::{CoreError, CoreResult};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant, SystemTime};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardConfig {
pub title: String,
pub refresh_interval: Duration,
pub retention_period: Duration,
pub max_data_points: usize,
pub enable_web_interface: bool,
pub web_port: u16,
pub enable_rest_api: bool,
pub api_token: Option<String>,
pub enable_alerts: bool,
pub theme: DashboardTheme,
pub auto_save_interval: Duration,
}
impl Default for DashboardConfig {
fn default() -> Self {
Self {
title: "Performance Dashboard".to_string(),
refresh_interval: Duration::from_secs(5),
retention_period: Duration::from_secs(7 * 24 * 60 * 60), max_data_points: 1000,
enable_web_interface: true,
web_port: 8080,
enable_rest_api: true,
api_token: None,
enable_alerts: true,
theme: DashboardTheme::Dark,
auto_save_interval: Duration::from_secs(60),
}
}
}
impl DashboardConfig {
pub fn with_title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
self.refresh_interval = interval;
self
}
pub fn with_retention_period(mut self, period: Duration) -> Self {
self.retention_period = period;
self
}
pub fn with_web_interface(mut self, port: u16) -> Self {
self.enable_web_interface = true;
self.web_port = port;
self
}
pub fn with_api_token(mut self, token: &str) -> Self {
self.api_token = Some(token.to_string());
self
}
pub fn with_theme(mut self, theme: DashboardTheme) -> Self {
self.theme = theme;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DashboardTheme {
Light,
Dark,
HighContrast,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChartType {
LineChart,
AreaChart,
BarChart,
GaugeChart,
PieChart,
Heatmap,
ScatterPlot,
Histogram,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MetricSource {
SystemCpu,
SystemMemory,
NetworkIO,
DiskIO,
Application(String),
Custom(String),
Database(String),
Cache(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Widget {
pub id: String,
pub title: String,
pub chart_type: ChartType,
pub metric_source: MetricSource,
pub layout: WidgetLayout,
pub alert_config: Option<AlertConfig>,
pub refresh_interval: Option<Duration>,
pub color_scheme: Vec<String>,
pub display_options: DisplayOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WidgetLayout {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Default for WidgetLayout {
fn default() -> Self {
Self {
x: 0,
y: 0,
width: 4,
height: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertConfig {
pub threshold: f64,
pub condition: AlertCondition,
pub severity: AlertSeverity,
pub notification_channels: Vec<NotificationChannel>,
pub cooldown_period: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertCondition {
GreaterThan,
LessThan,
EqualTo,
NotEqualTo,
RateOfChange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum AlertSeverity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NotificationChannel {
Email(String),
Slack(String),
Webhook(String),
Console,
File(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayOptions {
pub show_labels: bool,
pub show_grid: bool,
pub show_legend: bool,
pub enable_animation: bool,
pub number_format: NumberFormat,
pub time_format: String,
}
impl Default for DisplayOptions {
fn default() -> Self {
Self {
show_labels: true,
show_grid: true,
show_legend: true,
enable_animation: true,
number_format: NumberFormat::Auto,
time_format: "%H:%M:%S".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NumberFormat {
Auto,
Integer,
Decimal(u8),
Percentage,
Scientific,
Bytes,
}
impl Widget {
pub fn new() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
title: "New Widget".to_string(),
chart_type: ChartType::LineChart,
metric_source: MetricSource::SystemCpu,
layout: WidgetLayout::default(),
alert_config: None,
refresh_interval: None,
color_scheme: vec![
"#007acc".to_string(),
"#ff6b35".to_string(),
"#00b894".to_string(),
"#fdcb6e".to_string(),
"#e84393".to_string(),
],
display_options: DisplayOptions::default(),
}
}
pub fn with_title(mut self, title: &str) -> Self {
self.title = title.to_string();
self
}
pub fn with_chart_type(mut self, charttype: ChartType) -> Self {
self.chart_type = charttype;
self
}
pub fn with_metric_source(mut self, source: MetricSource) -> Self {
self.metric_source = source;
self
}
pub const fn with_layout(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
self.layout = WidgetLayout {
x,
y,
width,
height,
};
self
}
pub fn with_alert_threshold(mut self, threshold: f64) -> Self {
self.alert_config = Some(AlertConfig {
threshold,
condition: AlertCondition::GreaterThan,
severity: AlertSeverity::Warning,
notification_channels: vec![NotificationChannel::Console],
cooldown_period: Duration::from_secs(300), });
self
}
pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
self.refresh_interval = Some(interval);
self
}
pub fn with_colors(mut self, colors: Vec<&str>) -> Self {
self.color_scheme = colors.into_iter().map(|s| s.to_string()).collect();
self
}
}
impl Default for Widget {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricDataPoint {
pub timestamp: SystemTime,
pub value: f64,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricTimeSeries {
pub name: String,
pub data_points: VecDeque<MetricDataPoint>,
pub last_update: SystemTime,
}
impl MetricTimeSeries {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
data_points: VecDeque::new(),
last_update: SystemTime::now(),
}
}
pub fn add_point(&mut self, value: f64, metadata: Option<HashMap<String, String>>) {
let point = MetricDataPoint {
timestamp: SystemTime::now(),
value,
metadata: metadata.unwrap_or_default(),
};
self.data_points.push_back(point);
self.last_update = SystemTime::now();
}
pub fn latest_value(&self) -> Option<f64> {
self.data_points.back().map(|p| p.value)
}
pub fn average_value(&self, duration: Duration) -> Option<f64> {
let cutoff = SystemTime::now() - duration;
let values: Vec<f64> = self
.data_points
.iter()
.filter(|p| p.timestamp >= cutoff)
.map(|p| p.value)
.collect();
if values.is_empty() {
None
} else {
Some(values.iter().sum::<f64>() / values.len() as f64)
}
}
pub fn cleanup(&mut self, retention_period: Duration, maxpoints: usize) {
let cutoff = SystemTime::now() - retention_period;
while let Some(front) = self.data_points.front() {
if front.timestamp < cutoff {
self.data_points.pop_front();
} else {
break;
}
}
while self.data_points.len() > maxpoints {
self.data_points.pop_front();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardAlert {
pub id: String,
pub widget_id: String,
pub message: String,
pub severity: AlertSeverity,
pub triggered_at: SystemTime,
pub current_value: f64,
pub threshold_value: f64,
pub status: AlertStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AlertStatus {
Active,
Acknowledged,
Resolved,
}
pub struct PerformanceDashboard {
config: DashboardConfig,
widgets: HashMap<String, Widget>,
metrics: Arc<RwLock<HashMap<String, MetricTimeSeries>>>,
alerts: Arc<Mutex<HashMap<String, DashboardAlert>>>,
state: DashboardState,
last_update: Instant,
web_server_handle: Option<WebServerHandle>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DashboardState {
Stopped,
Running,
Paused,
Error,
}
pub struct WebServerHandle {
pub address: String,
pub port: u16,
pub running: Arc<Mutex<bool>>,
}
impl PerformanceDashboard {
pub fn new(config: DashboardConfig) -> CoreResult<Self> {
Ok(Self {
config,
widgets: HashMap::new(),
metrics: Arc::new(RwLock::new(HashMap::new())),
alerts: Arc::new(Mutex::new(HashMap::new())),
state: DashboardState::Stopped,
last_update: Instant::now(),
web_server_handle: None,
})
}
pub fn add_widget(&mut self, widget: Widget) -> CoreResult<String> {
let widget_id = widget.id.clone();
self.widgets.insert(widget_id.clone(), widget);
let metrics_name = format!("widget_{widget_id}");
if let Ok(mut metrics) = self.metrics.write() {
metrics.insert(metrics_name, MetricTimeSeries::new(&widget_id));
}
Ok(widget_id)
}
pub fn remove_widget(&mut self, widgetid: &str) -> CoreResult<()> {
self.widgets.remove(widgetid);
let metrics_name = format!("widget_{widgetid}");
if let Ok(mut metrics) = self.metrics.write() {
metrics.remove(&metrics_name);
}
Ok(())
}
pub fn start(&mut self) -> CoreResult<()> {
if self.state == DashboardState::Running {
return Ok(());
}
if self.config.enable_web_interface {
self.start_web_server()?;
}
self.state = DashboardState::Running;
self.last_update = Instant::now();
Ok(())
}
pub fn stop(&mut self) -> CoreResult<()> {
if let Some(ref handle) = self.web_server_handle {
if let Ok(mut running) = handle.running.lock() {
*running = false;
}
}
self.state = DashboardState::Stopped;
Ok(())
}
pub fn update_metric(&mut self, source: &MetricSource, value: f64) -> CoreResult<()> {
let metricname = self.metric_source_to_name(source);
if let Ok(mut metrics) = self.metrics.write() {
let time_series = metrics
.entry(metricname.clone())
.or_insert_with(|| MetricTimeSeries::new(&metricname));
time_series.add_point(value, None);
time_series.cleanup(self.config.retention_period, self.config.max_data_points);
}
self.check_alerts(&metricname, value)?;
self.last_update = Instant::now();
Ok(())
}
pub fn get_metrics(&self) -> CoreResult<HashMap<String, MetricTimeSeries>> {
self.metrics
.read()
.map(|metrics| metrics.clone())
.map_err(|_| CoreError::from(std::io::Error::other("Failed to read metrics")))
}
pub fn get_statistics(&self) -> DashboardStatistics {
let metrics = self.metrics.read().expect("Operation failed");
let alerts = self.alerts.lock().expect("Operation failed");
DashboardStatistics {
total_widgets: self.widgets.len(),
total_metrics: metrics.len(),
active_alerts: alerts
.values()
.filter(|a| a.status == AlertStatus::Active)
.count(),
last_update: self.last_update,
uptime: self.last_update.elapsed(),
state: self.state,
}
}
pub fn export_config(&self) -> CoreResult<String> {
{
let export_data = DashboardExport {
config: self.config.clone(),
widgets: self.widgets.values().cloned().collect(),
created_at: SystemTime::now(),
};
serde_json::to_string_pretty(&export_data).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to serialize dashboard config: {e}"
)))
})
}
#[cfg(not(feature = "serde"))]
{
Ok(format!("title: {}", self.config.title))
}
}
pub fn import_configuration(&mut self, configjson: &str) -> CoreResult<()> {
{
let import_data: DashboardExport = serde_json::from_str(configjson).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to parse dashboard config: {e}"
)))
})?;
self.config = import_data.config;
self.widgets.clear();
for widget in import_data.widgets {
self.widgets.insert(widget.id.clone(), widget);
}
Ok(())
}
#[cfg(not(feature = "serde"))]
{
let _ = configjson; Err(CoreError::from(std::io::Error::other(
"Serde feature not enabled for configuration import",
)))
}
}
fn check_alerts(&self, metricname: &str, value: f64) -> CoreResult<()> {
for widget in self.widgets.values() {
if let Some(ref alert_config) = widget.alert_config {
let widget_metric = self.metric_source_to_name(&widget.metric_source);
if widget_metric == metricname {
let triggered = match alert_config.condition {
AlertCondition::GreaterThan => value > alert_config.threshold,
AlertCondition::LessThan => value < alert_config.threshold,
AlertCondition::EqualTo => {
(value - alert_config.threshold).abs() < f64::EPSILON
}
AlertCondition::NotEqualTo => {
(value - alert_config.threshold).abs() > f64::EPSILON
}
AlertCondition::RateOfChange => {
false
}
};
if triggered {
let alert = DashboardAlert {
id: uuid::Uuid::new_v4().to_string(),
widget_id: widget.id.clone(),
message: format!(
"Alert triggered for '{}': value {:.2} {} threshold {:.2}",
widget.title,
value,
match alert_config.condition {
AlertCondition::GreaterThan => "exceeds",
AlertCondition::LessThan => "below",
_ => "meets",
},
alert_config.threshold
),
severity: alert_config.severity,
triggered_at: SystemTime::now(),
current_value: value,
threshold_value: alert_config.threshold,
status: AlertStatus::Active,
};
if let Ok(mut alerts) = self.alerts.lock() {
alerts.insert(alert.id.clone(), alert.clone());
}
self.send_alert_notifications(&alert, alert_config)?;
}
}
}
}
Ok(())
}
fn send_alert_notifications(
&self,
alert: &DashboardAlert,
config: &AlertConfig,
) -> CoreResult<()> {
for channel in &config.notification_channels {
match channel {
NotificationChannel::Console => {
println!("[DASHBOARD ALERT] {message}", message = alert.message);
}
NotificationChannel::File(path) => {
use std::fs::OpenOptions;
use std::io::Write;
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
writeln!(
file,
"[{}] {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
alert.message
)
.ok();
}
}
#[cfg(feature = "observability_http")]
NotificationChannel::Webhook(url) => {
let _ = url; }
#[cfg(not(feature = "observability_http"))]
NotificationChannel::Webhook(_) => {
}
NotificationChannel::Email(_) | NotificationChannel::Slack(_) => {
}
}
}
Ok(())
}
fn metric_source_to_name(&self, source: &MetricSource) -> String {
match source {
MetricSource::SystemCpu => "system.cpu".to_string(),
MetricSource::SystemMemory => "system.memory".to_string(),
MetricSource::NetworkIO => "system.network_io".to_string(),
MetricSource::DiskIO => "system.disk_io".to_string(),
MetricSource::Application(name) => format!("app.{name}"),
MetricSource::Custom(name) => format!("custom.{name}"),
MetricSource::Database(name) => format!("db.{name}"),
MetricSource::Cache(name) => format!("cache.{name}"),
}
}
fn start_web_server(&mut self) -> CoreResult<()> {
let handle = WebServerHandle {
address: "0.0.0.0".to_string(),
port: self.config.web_port,
running: Arc::new(Mutex::new(true)),
};
self.web_server_handle = Some(handle);
println!(
"Dashboard web interface started at http://localhost:{}/dashboard",
self.config.web_port
);
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardExport {
pub config: DashboardConfig,
pub widgets: Vec<Widget>,
pub created_at: SystemTime,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct DashboardStatistics {
pub total_widgets: usize,
pub total_metrics: usize,
pub active_alerts: usize,
#[cfg_attr(feature = "serde", serde(skip))]
pub last_update: Instant,
pub uptime: Duration,
pub state: DashboardState,
}
impl Default for DashboardStatistics {
fn default() -> Self {
Self {
total_widgets: 0,
total_metrics: 0,
active_alerts: 0,
last_update: Instant::now(),
uptime: Duration::from_secs(0),
state: DashboardState::Stopped,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dashboard_creation() {
let config = DashboardConfig::default()
.with_title("Test Dashboard")
.with_refresh_interval(Duration::from_secs(10));
let dashboard = PerformanceDashboard::new(config);
assert!(dashboard.is_ok());
let dashboard = dashboard.expect("Operation failed");
assert_eq!(dashboard.config.title, "Test Dashboard");
assert_eq!(dashboard.config.refresh_interval, Duration::from_secs(10));
}
#[test]
fn test_widget_creation_and_addition() {
let config = DashboardConfig::default();
let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
let widget = Widget::new()
.with_title("CPU Usage")
.with_chart_type(ChartType::LineChart)
.with_metric_source(MetricSource::SystemCpu)
.with_alert_threshold(80.0);
let widget_id = dashboard.add_widget(widget).expect("Operation failed");
assert!(!widget_id.is_empty());
assert_eq!(dashboard.widgets.len(), 1);
}
#[test]
fn test_metric_updates() {
let config = DashboardConfig::default();
let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
let widget = Widget::new().with_metric_source(MetricSource::SystemCpu);
dashboard.add_widget(widget).expect("Operation failed");
let result = dashboard.update_metric(&MetricSource::SystemCpu, 75.5);
assert!(result.is_ok());
let metrics = dashboard.get_metrics().expect("Operation failed");
let cpu_metric = metrics.get("system.cpu");
assert!(cpu_metric.is_some());
let cpu_metric = cpu_metric.expect("Operation failed");
assert_eq!(cpu_metric.latest_value(), Some(75.5));
}
#[test]
fn test_metric_time_series() {
let mut ts = MetricTimeSeries::new("test_metric");
ts.add_point(10.0, None);
ts.add_point(20.0, None);
ts.add_point(30.0, None);
assert_eq!(ts.latest_value(), Some(30.0));
assert_eq!(ts.data_points.len(), 3);
ts.cleanup(Duration::from_secs(1), 2);
assert_eq!(ts.data_points.len(), 2);
}
#[test]
fn test_alert_configuration() {
let widget = Widget::new()
.with_title("Memory Usage")
.with_alert_threshold(90.0);
assert!(widget.alert_config.is_some());
let alert_config = widget.alert_config.expect("Operation failed");
assert_eq!(alert_config.threshold, 90.0);
assert_eq!(alert_config.condition, AlertCondition::GreaterThan);
assert_eq!(alert_config.severity, AlertSeverity::Warning);
}
#[test]
fn test_dashboard_statistics() {
let config = DashboardConfig::default();
let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
for i in 0..3 {
let widget = Widget::new().with_title(&format!("Widget {i}"));
dashboard.add_widget(widget).expect("Operation failed");
}
let stats = dashboard.get_statistics();
assert_eq!(stats.total_widgets, 3);
assert_eq!(stats.active_alerts, 0);
assert_eq!(stats.state, DashboardState::Stopped);
}
#[test]
fn test_dashboard_config_builder() {
let config = DashboardConfig::default()
.with_title("Custom Dashboard")
.with_refresh_interval(Duration::from_secs(30))
.with_retention_period(Duration::from_secs(14 * 24 * 60 * 60)) .with_web_interface(9090)
.with_api_token("test-token")
.with_theme(DashboardTheme::Light);
assert_eq!(config.title, "Custom Dashboard");
assert_eq!(config.refresh_interval, Duration::from_secs(30));
assert_eq!(
config.retention_period,
Duration::from_secs(14 * 24 * 60 * 60)
); assert_eq!(config.web_port, 9090);
assert_eq!(config.api_token, Some("test-token".to_string()));
assert_eq!(config.theme, DashboardTheme::Light);
}
#[test]
fn test_widget_layout() {
let widget = Widget::new().with_layout(2, 3, 6, 4);
assert_eq!(widget.layout.x, 2);
assert_eq!(widget.layout.y, 3);
assert_eq!(widget.layout.width, 6);
assert_eq!(widget.layout.height, 4);
}
}