scirs2_core/profiling/
dashboards.rs

1//! # Performance Dashboards System
2//!
3//! Enterprise-grade performance monitoring dashboards with real-time visualization
4//! and historical trend analysis for production environments. Provides comprehensive
5//! performance insights with customizable metrics and alerting capabilities.
6//!
7//! ## Features
8//!
9//! - Real-time performance visualization with live updates
10//! - Historical trend analysis with configurable time ranges
11//! - Customizable dashboard layouts and widgets
12//! - Interactive charts and graphs with drill-down capabilities
13//! - Performance alerting with threshold-based notifications
14//! - Multi-dimensional metrics aggregation and filtering
15//! - Export capabilities for reports and presentations
16//! - Integration with external monitoring systems
17//! - Web-based dashboard interface with REST API
18//! - Mobile-responsive design for on-the-go monitoring
19//!
20//! ## Example
21//!
22//! ```rust
23//! use scirs2_core::profiling::dashboards::{
24//!     PerformanceDashboard, DashboardConfig, Widget, ChartType, MetricSource
25//! };
26//!
27//! // Create a performance dashboard
28//! let config = DashboardConfig::default()
29//!     .with_title("Production System Performance")
30//!     .with_refresh_interval(std::time::Duration::from_secs(30))
31//!     .with_retention_period(std::time::Duration::from_secs(30 * 24 * 60 * 60));
32//!
33//! let mut dashboard = PerformanceDashboard::new(config)?;
34//!
35//! // Add performance widgets
36//! dashboard.add_widget(Widget::new()
37//!     .with_title("CPU Usage")
38//!     .with_chart_type(ChartType::LineChart)
39//!     .with_metric_source(MetricSource::SystemCpu)
40//!     .with_alert_threshold(80.0)
41//! )?;
42//!
43//! dashboard.add_widget(Widget::new()
44//!     .with_title("Memory Usage")
45//!     .with_chart_type(ChartType::AreaChart)
46//!     .with_metric_source(MetricSource::SystemMemory)
47//! )?;
48//!
49//! // Start the dashboard
50//! dashboard.start()?;
51//!
52//! // Dashboard will now continuously update with real-time metrics
53//! // Access via web interface at http://localhost:8080/dashboard
54//! # Ok::<(), Box<dyn std::error::Error>>(())
55//! ```
56
57use crate::error::{CoreError, CoreResult};
58use std::collections::{HashMap, VecDeque};
59use std::sync::{Arc, Mutex, RwLock};
60use std::time::{Duration, Instant, SystemTime};
61
62use serde::{Deserialize, Serialize};
63
64/// Dashboard configuration
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DashboardConfig {
67    /// Dashboard title
68    pub title: String,
69    /// Refresh interval for real-time updates
70    pub refresh_interval: Duration,
71    /// Data retention period
72    pub retention_period: Duration,
73    /// Maximum number of data points to keep
74    pub max_data_points: usize,
75    /// Enable web interface
76    pub enable_web_interface: bool,
77    /// Web server port
78    pub web_port: u16,
79    /// Enable REST API
80    pub enable_rest_api: bool,
81    /// API authentication token
82    pub api_token: Option<String>,
83    /// Enable real-time alerts
84    pub enable_alerts: bool,
85    /// Dashboard theme
86    pub theme: DashboardTheme,
87    /// Auto-save interval for persistence
88    pub auto_save_interval: Duration,
89}
90
91impl Default for DashboardConfig {
92    fn default() -> Self {
93        Self {
94            title: "Performance Dashboard".to_string(),
95            refresh_interval: Duration::from_secs(5),
96            retention_period: Duration::from_secs(7 * 24 * 60 * 60), // 7 days
97            max_data_points: 1000,
98            enable_web_interface: true,
99            web_port: 8080,
100            enable_rest_api: true,
101            api_token: None,
102            enable_alerts: true,
103            theme: DashboardTheme::Dark,
104            auto_save_interval: Duration::from_secs(60),
105        }
106    }
107}
108
109impl DashboardConfig {
110    /// Set dashboard title
111    pub fn with_title(mut self, title: &str) -> Self {
112        self.title = title.to_string();
113        self
114    }
115
116    /// Set refresh interval
117    pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
118        self.refresh_interval = interval;
119        self
120    }
121
122    /// Set data retention period
123    pub fn with_retention_period(mut self, period: Duration) -> Self {
124        self.retention_period = period;
125        self
126    }
127
128    /// Enable web interface on specific port
129    pub fn with_web_interface(mut self, port: u16) -> Self {
130        self.enable_web_interface = true;
131        self.web_port = port;
132        self
133    }
134
135    /// Set API authentication token
136    pub fn with_api_token(mut self, token: &str) -> Self {
137        self.api_token = Some(token.to_string());
138        self
139    }
140
141    /// Set dashboard theme
142    pub fn with_theme(mut self, theme: DashboardTheme) -> Self {
143        self.theme = theme;
144        self
145    }
146}
147
148/// Dashboard theme options
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum DashboardTheme {
151    /// Light theme
152    Light,
153    /// Dark theme
154    Dark,
155    /// High contrast theme
156    HighContrast,
157    /// Custom theme
158    Custom,
159}
160
161/// Chart types for dashboard widgets
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum ChartType {
164    /// Line chart for time series data
165    LineChart,
166    /// Area chart for cumulative data
167    AreaChart,
168    /// Bar chart for categorical data
169    BarChart,
170    /// Gauge chart for single values
171    GaugeChart,
172    /// Pie chart for proportional data
173    PieChart,
174    /// Heatmap for matrix data
175    Heatmap,
176    /// Scatter plot for correlation analysis
177    ScatterPlot,
178    /// Histogram for distribution analysis
179    Histogram,
180}
181
182/// Metric data sources
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub enum MetricSource {
185    /// System CPU usage
186    SystemCpu,
187    /// System memory usage
188    SystemMemory,
189    /// Network I/O
190    NetworkIO,
191    /// Disk I/O
192    DiskIO,
193    /// Application-specific metrics
194    Application(String),
195    /// Custom metric query
196    Custom(String),
197    /// Database metrics
198    Database(String),
199    /// Cache metrics
200    Cache(String),
201}
202
203/// Dashboard widget configuration
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct Widget {
206    /// Widget ID
207    pub id: String,
208    /// Widget title
209    pub title: String,
210    /// Chart type
211    pub chart_type: ChartType,
212    /// Data source
213    pub metric_source: MetricSource,
214    /// Position and size
215    pub layout: WidgetLayout,
216    /// Alert configuration
217    pub alert_config: Option<AlertConfig>,
218    /// Refresh interval (overrides dashboard default)
219    pub refresh_interval: Option<Duration>,
220    /// Color scheme
221    pub color_scheme: Vec<String>,
222    /// Display options
223    pub display_options: DisplayOptions,
224}
225
226/// Widget layout and positioning
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct WidgetLayout {
229    /// X position (grid units)
230    pub x: u32,
231    /// Y position (grid units)
232    pub y: u32,
233    /// Width (grid units)
234    pub width: u32,
235    /// Height (grid units)
236    pub height: u32,
237}
238
239impl Default for WidgetLayout {
240    fn default() -> Self {
241        Self {
242            x: 0,
243            y: 0,
244            width: 4,
245            height: 3,
246        }
247    }
248}
249
250/// Alert configuration for widgets
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct AlertConfig {
253    /// Alert threshold value
254    pub threshold: f64,
255    /// Alert condition
256    pub condition: AlertCondition,
257    /// Alert severity
258    pub severity: AlertSeverity,
259    /// Notification channels
260    pub notification_channels: Vec<NotificationChannel>,
261    /// Cooldown period to prevent spam
262    pub cooldown_period: Duration,
263}
264
265/// Alert conditions
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267pub enum AlertCondition {
268    /// Greater than threshold
269    GreaterThan,
270    /// Less than threshold
271    LessThan,
272    /// Equal to threshold
273    EqualTo,
274    /// Not equal to threshold
275    NotEqualTo,
276    /// Rate of change exceeds threshold
277    RateOfChange,
278}
279
280/// Alert severity levels
281#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
282pub enum AlertSeverity {
283    /// Informational alert
284    Info,
285    /// Warning alert
286    Warning,
287    /// Error alert
288    Error,
289    /// Critical alert
290    Critical,
291}
292
293/// Notification channels for alerts
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub enum NotificationChannel {
296    /// Email notification
297    Email(String),
298    /// Slack webhook
299    Slack(String),
300    /// Custom webhook
301    Webhook(String),
302    /// Console log
303    Console,
304    /// File log
305    File(String),
306}
307
308/// Display options for widgets
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct DisplayOptions {
311    /// Show data labels
312    pub show_labels: bool,
313    /// Show grid lines
314    pub show_grid: bool,
315    /// Show legend
316    pub show_legend: bool,
317    /// Animation enabled
318    pub enable_animation: bool,
319    /// Number format
320    pub number_format: NumberFormat,
321    /// Time format for time series
322    pub time_format: String,
323}
324
325impl Default for DisplayOptions {
326    fn default() -> Self {
327        Self {
328            show_labels: true,
329            show_grid: true,
330            show_legend: true,
331            enable_animation: true,
332            number_format: NumberFormat::Auto,
333            time_format: "%H:%M:%S".to_string(),
334        }
335    }
336}
337
338/// Number formatting options
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
340pub enum NumberFormat {
341    /// Automatic formatting
342    Auto,
343    /// Integer formatting
344    Integer,
345    /// Decimal formatting with specified precision
346    Decimal(u8),
347    /// Percentage formatting
348    Percentage,
349    /// Scientific notation
350    Scientific,
351    /// Bytes formatting (KB, MB, GB)
352    Bytes,
353}
354
355impl Widget {
356    /// Create a new widget with default settings
357    pub fn new() -> Self {
358        Self {
359            id: uuid::Uuid::new_v4().to_string(),
360            title: "New Widget".to_string(),
361            chart_type: ChartType::LineChart,
362            metric_source: MetricSource::SystemCpu,
363            layout: WidgetLayout::default(),
364            alert_config: None,
365            refresh_interval: None,
366            color_scheme: vec![
367                "#007acc".to_string(),
368                "#ff6b35".to_string(),
369                "#00b894".to_string(),
370                "#fdcb6e".to_string(),
371                "#e84393".to_string(),
372            ],
373            display_options: DisplayOptions::default(),
374        }
375    }
376
377    /// Set widget title
378    pub fn with_title(mut self, title: &str) -> Self {
379        self.title = title.to_string();
380        self
381    }
382
383    /// Set chart type
384    pub fn with_chart_type(mut self, charttype: ChartType) -> Self {
385        self.chart_type = charttype;
386        self
387    }
388
389    /// Set metric source
390    pub fn with_metric_source(mut self, source: MetricSource) -> Self {
391        self.metric_source = source;
392        self
393    }
394
395    /// Set widget layout
396    pub const fn with_layout(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
397        self.layout = WidgetLayout {
398            x,
399            y,
400            width,
401            height,
402        };
403        self
404    }
405
406    /// Set alert threshold
407    pub fn with_alert_threshold(mut self, threshold: f64) -> Self {
408        self.alert_config = Some(AlertConfig {
409            threshold,
410            condition: AlertCondition::GreaterThan,
411            severity: AlertSeverity::Warning,
412            notification_channels: vec![NotificationChannel::Console],
413            cooldown_period: Duration::from_secs(300), // 5 minutes
414        });
415        self
416    }
417
418    /// Set custom refresh interval
419    pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
420        self.refresh_interval = Some(interval);
421        self
422    }
423
424    /// Set color scheme
425    pub fn with_colors(mut self, colors: Vec<&str>) -> Self {
426        self.color_scheme = colors.into_iter().map(|s| s.to_string()).collect();
427        self
428    }
429}
430
431impl Default for Widget {
432    fn default() -> Self {
433        Self::new()
434    }
435}
436
437/// Metric data point
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct MetricDataPoint {
440    /// Timestamp
441    pub timestamp: SystemTime,
442    /// Metric value
443    pub value: f64,
444    /// Optional metadata
445    pub metadata: HashMap<String, String>,
446}
447
448/// Time series data for a metric
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct MetricTimeSeries {
451    /// Metric name
452    pub name: String,
453    /// Data points
454    pub data_points: VecDeque<MetricDataPoint>,
455    /// Last update time
456    pub last_update: SystemTime,
457}
458
459impl MetricTimeSeries {
460    /// Create a new time series
461    pub fn new(name: &str) -> Self {
462        Self {
463            name: name.to_string(),
464            data_points: VecDeque::new(),
465            last_update: SystemTime::now(),
466        }
467    }
468
469    /// Add a data point
470    pub fn add_point(&mut self, value: f64, metadata: Option<HashMap<String, String>>) {
471        let point = MetricDataPoint {
472            timestamp: SystemTime::now(),
473            value,
474            metadata: metadata.unwrap_or_default(),
475        };
476
477        self.data_points.push_back(point);
478        self.last_update = SystemTime::now();
479    }
480
481    /// Get latest value
482    pub fn latest_value(&self) -> Option<f64> {
483        self.data_points.back().map(|p| p.value)
484    }
485
486    /// Get average value over time range
487    pub fn average_value(&self, duration: Duration) -> Option<f64> {
488        let cutoff = SystemTime::now() - duration;
489        let values: Vec<f64> = self
490            .data_points
491            .iter()
492            .filter(|p| p.timestamp >= cutoff)
493            .map(|p| p.value)
494            .collect();
495
496        if values.is_empty() {
497            None
498        } else {
499            Some(values.iter().sum::<f64>() / values.len() as f64)
500        }
501    }
502
503    /// Clean old data points
504    pub fn cleanup(&mut self, retention_period: Duration, maxpoints: usize) {
505        let cutoff = SystemTime::now() - retention_period;
506
507        // Remove old data points
508        while let Some(front) = self.data_points.front() {
509            if front.timestamp < cutoff {
510                self.data_points.pop_front();
511            } else {
512                break;
513            }
514        }
515
516        // Limit maximum number of points
517        while self.data_points.len() > maxpoints {
518            self.data_points.pop_front();
519        }
520    }
521}
522
523/// Dashboard alert
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct DashboardAlert {
526    /// Alert ID
527    pub id: String,
528    /// Widget ID that triggered the alert
529    pub widget_id: String,
530    /// Alert message
531    pub message: String,
532    /// Alert severity
533    pub severity: AlertSeverity,
534    /// Trigger time
535    pub triggered_at: SystemTime,
536    /// Current value that triggered the alert
537    pub current_value: f64,
538    /// Threshold value
539    pub threshold_value: f64,
540    /// Alert status
541    pub status: AlertStatus,
542}
543
544/// Alert status
545#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
546pub enum AlertStatus {
547    /// Alert is active
548    Active,
549    /// Alert is acknowledged
550    Acknowledged,
551    /// Alert is resolved
552    Resolved,
553}
554
555/// Performance dashboard
556pub struct PerformanceDashboard {
557    /// Dashboard configuration
558    config: DashboardConfig,
559    /// Dashboard widgets
560    widgets: HashMap<String, Widget>,
561    /// Metric time series data
562    metrics: Arc<RwLock<HashMap<String, MetricTimeSeries>>>,
563    /// Active alerts
564    alerts: Arc<Mutex<HashMap<String, DashboardAlert>>>,
565    /// Dashboard state
566    state: DashboardState,
567    /// Last update time
568    last_update: Instant,
569    /// Web server handle (if enabled)
570    web_server_handle: Option<WebServerHandle>,
571}
572
573/// Dashboard state
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575pub enum DashboardState {
576    /// Dashboard is stopped
577    Stopped,
578    /// Dashboard is running
579    Running,
580    /// Dashboard is paused
581    Paused,
582    /// Dashboard has encountered an error
583    Error,
584}
585
586/// Web server handle for dashboard
587pub struct WebServerHandle {
588    /// Server address
589    pub address: String,
590    /// Server port
591    pub port: u16,
592    /// Running state
593    pub running: Arc<Mutex<bool>>,
594}
595
596impl PerformanceDashboard {
597    /// Create a new performance dashboard
598    pub fn new(config: DashboardConfig) -> CoreResult<Self> {
599        Ok(Self {
600            config,
601            widgets: HashMap::new(),
602            metrics: Arc::new(RwLock::new(HashMap::new())),
603            alerts: Arc::new(Mutex::new(HashMap::new())),
604            state: DashboardState::Stopped,
605            last_update: Instant::now(),
606            web_server_handle: None,
607        })
608    }
609
610    /// Add a widget to the dashboard
611    pub fn add_widget(&mut self, widget: Widget) -> CoreResult<String> {
612        let widget_id = widget.id.clone();
613        self.widgets.insert(widget_id.clone(), widget);
614
615        // Initialize metric time series for this widget
616        let metrics_name = format!("widget_{widget_id}");
617        if let Ok(mut metrics) = self.metrics.write() {
618            metrics.insert(metrics_name, MetricTimeSeries::new(&widget_id));
619        }
620
621        Ok(widget_id)
622    }
623
624    /// Remove a widget from the dashboard
625    pub fn remove_widget(&mut self, widgetid: &str) -> CoreResult<()> {
626        self.widgets.remove(widgetid);
627
628        // Remove associated metric data
629        let metrics_name = format!("widget_{widgetid}");
630        if let Ok(mut metrics) = self.metrics.write() {
631            metrics.remove(&metrics_name);
632        }
633
634        Ok(())
635    }
636
637    /// Start the dashboard
638    pub fn start(&mut self) -> CoreResult<()> {
639        if self.state == DashboardState::Running {
640            return Ok(());
641        }
642
643        // Start web server if enabled
644        if self.config.enable_web_interface {
645            self.start_web_server()?;
646        }
647
648        self.state = DashboardState::Running;
649        self.last_update = Instant::now();
650
651        Ok(())
652    }
653
654    /// Stop the dashboard
655    pub fn stop(&mut self) -> CoreResult<()> {
656        // Stop web server if running
657        if let Some(ref handle) = self.web_server_handle {
658            if let Ok(mut running) = handle.running.lock() {
659                *running = false;
660            }
661        }
662
663        self.state = DashboardState::Stopped;
664        Ok(())
665    }
666
667    /// Update dashboard with new metric data
668    pub fn update_metric(&mut self, source: &MetricSource, value: f64) -> CoreResult<()> {
669        let metricname = self.metric_source_to_name(source);
670
671        if let Ok(mut metrics) = self.metrics.write() {
672            let time_series = metrics
673                .entry(metricname.clone())
674                .or_insert_with(|| MetricTimeSeries::new(&metricname));
675
676            time_series.add_point(value, None);
677
678            // Clean up old data
679            time_series.cleanup(self.config.retention_period, self.config.max_data_points);
680        }
681
682        // Check for alerts
683        self.check_alerts(&metricname, value)?;
684
685        self.last_update = Instant::now();
686        Ok(())
687    }
688
689    /// Get current metrics data
690    pub fn get_metrics(&self) -> CoreResult<HashMap<String, MetricTimeSeries>> {
691        self.metrics
692            .read()
693            .map(|metrics| metrics.clone())
694            .map_err(|_| CoreError::from(std::io::Error::other("Failed to read metrics")))
695    }
696
697    /// Get dashboard statistics
698    pub fn get_statistics(&self) -> DashboardStatistics {
699        let metrics = self.metrics.read().expect("Operation failed");
700        let alerts = self.alerts.lock().expect("Operation failed");
701
702        DashboardStatistics {
703            total_widgets: self.widgets.len(),
704            total_metrics: metrics.len(),
705            active_alerts: alerts
706                .values()
707                .filter(|a| a.status == AlertStatus::Active)
708                .count(),
709            last_update: self.last_update,
710            uptime: self.last_update.elapsed(),
711            state: self.state,
712        }
713    }
714
715    /// Export dashboard configuration
716    pub fn export_config(&self) -> CoreResult<String> {
717        {
718            let export_data = DashboardExport {
719                config: self.config.clone(),
720                widgets: self.widgets.values().cloned().collect(),
721                created_at: SystemTime::now(),
722            };
723
724            serde_json::to_string_pretty(&export_data).map_err(|e| {
725                CoreError::from(std::io::Error::other(format!(
726                    "Failed to serialize dashboard config: {e}"
727                )))
728            })
729        }
730        #[cfg(not(feature = "serde"))]
731        {
732            Ok(format!("title: {}", self.config.title))
733        }
734    }
735
736    /// Import dashboard configuration
737    pub fn import_configuration(&mut self, configjson: &str) -> CoreResult<()> {
738        {
739            let import_data: DashboardExport = serde_json::from_str(configjson).map_err(|e| {
740                CoreError::from(std::io::Error::other(format!(
741                    "Failed to parse dashboard config: {e}"
742                )))
743            })?;
744
745            self.config = import_data.config;
746            self.widgets.clear();
747
748            for widget in import_data.widgets {
749                self.widgets.insert(widget.id.clone(), widget);
750            }
751
752            Ok(())
753        }
754        #[cfg(not(feature = "serde"))]
755        {
756            let _ = config_json; // Suppress unused variable warning
757            Err(CoreError::from(std::io::Error::other(
758                "Serde feature not enabled for configuration import",
759            )))
760        }
761    }
762
763    /// Check for alert conditions
764    fn check_alerts(&self, metricname: &str, value: f64) -> CoreResult<()> {
765        for widget in self.widgets.values() {
766            if let Some(ref alert_config) = widget.alert_config {
767                let widget_metric = self.metric_source_to_name(&widget.metric_source);
768
769                if widget_metric == metricname {
770                    let triggered = match alert_config.condition {
771                        AlertCondition::GreaterThan => value > alert_config.threshold,
772                        AlertCondition::LessThan => value < alert_config.threshold,
773                        AlertCondition::EqualTo => {
774                            (value - alert_config.threshold).abs() < f64::EPSILON
775                        }
776                        AlertCondition::NotEqualTo => {
777                            (value - alert_config.threshold).abs() > f64::EPSILON
778                        }
779                        AlertCondition::RateOfChange => {
780                            // Would implement rate of change calculation here
781                            false
782                        }
783                    };
784
785                    if triggered {
786                        let alert = DashboardAlert {
787                            id: uuid::Uuid::new_v4().to_string(),
788                            widget_id: widget.id.clone(),
789                            message: format!(
790                                "Alert triggered for '{}': value {:.2} {} threshold {:.2}",
791                                widget.title,
792                                value,
793                                match alert_config.condition {
794                                    AlertCondition::GreaterThan => "exceeds",
795                                    AlertCondition::LessThan => "below",
796                                    _ => "meets",
797                                },
798                                alert_config.threshold
799                            ),
800                            severity: alert_config.severity,
801                            triggered_at: SystemTime::now(),
802                            current_value: value,
803                            threshold_value: alert_config.threshold,
804                            status: AlertStatus::Active,
805                        };
806
807                        if let Ok(mut alerts) = self.alerts.lock() {
808                            alerts.insert(alert.id.clone(), alert.clone());
809                        }
810
811                        // Send notifications
812                        self.send_alert_notifications(&alert, alert_config)?;
813                    }
814                }
815            }
816        }
817
818        Ok(())
819    }
820
821    /// Send alert notifications
822    fn send_alert_notifications(
823        &self,
824        alert: &DashboardAlert,
825        config: &AlertConfig,
826    ) -> CoreResult<()> {
827        for channel in &config.notification_channels {
828            match channel {
829                NotificationChannel::Console => {
830                    println!("[DASHBOARD ALERT] {message}", message = alert.message);
831                }
832                NotificationChannel::File(path) => {
833                    use std::fs::OpenOptions;
834                    use std::io::Write;
835
836                    if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
837                        writeln!(
838                            file,
839                            "[{}] {}",
840                            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
841                            alert.message
842                        )
843                        .ok();
844                    }
845                }
846                #[cfg(feature = "observability_http")]
847                NotificationChannel::Webhook(url) => {
848                    // Would implement webhook notification here
849                    let _ = url; // Suppress unused warning
850                }
851                #[cfg(not(feature = "observability_http"))]
852                NotificationChannel::Webhook(_) => {
853                    // Webhook notifications require HTTP feature
854                }
855                NotificationChannel::Email(_) | NotificationChannel::Slack(_) => {
856                    // Would implement email/slack notifications here in production
857                }
858            }
859        }
860
861        Ok(())
862    }
863
864    /// Convert metric source to internal metric name
865    fn metric_source_to_name(&self, source: &MetricSource) -> String {
866        match source {
867            MetricSource::SystemCpu => "system.cpu".to_string(),
868            MetricSource::SystemMemory => "system.memory".to_string(),
869            MetricSource::NetworkIO => "system.network_io".to_string(),
870            MetricSource::DiskIO => "system.disk_io".to_string(),
871            MetricSource::Application(name) => format!("app.{name}"),
872            MetricSource::Custom(name) => format!("custom.{name}"),
873            MetricSource::Database(name) => format!("db.{name}"),
874            MetricSource::Cache(name) => format!("cache.{name}"),
875        }
876    }
877
878    /// Start web server for dashboard interface
879    fn start_web_server(&mut self) -> CoreResult<()> {
880        // In a full implementation, this would start an actual web server
881        // For now, we'll simulate it
882
883        let handle = WebServerHandle {
884            address: "0.0.0.0".to_string(),
885            port: self.config.web_port,
886            running: Arc::new(Mutex::new(true)),
887        };
888
889        self.web_server_handle = Some(handle);
890
891        println!(
892            "Dashboard web interface started at http://localhost:{}/dashboard",
893            self.config.web_port
894        );
895
896        Ok(())
897    }
898}
899
900/// Dashboard export/import structure
901#[derive(Debug, Clone, Serialize, Deserialize)]
902pub struct DashboardExport {
903    /// Dashboard configuration
904    pub config: DashboardConfig,
905    /// Widget configurations
906    pub widgets: Vec<Widget>,
907    /// Export timestamp
908    pub created_at: SystemTime,
909}
910
911/// Dashboard statistics
912#[derive(Debug, Clone)]
913#[cfg_attr(feature = "serde", derive(Serialize))]
914pub struct DashboardStatistics {
915    /// Total number of widgets
916    pub total_widgets: usize,
917    /// Total number of metrics
918    pub total_metrics: usize,
919    /// Number of active alerts
920    pub active_alerts: usize,
921    /// Last update time
922    #[cfg_attr(feature = "serde", serde(skip))]
923    pub last_update: Instant,
924    /// Dashboard uptime
925    pub uptime: Duration,
926    /// Current state
927    pub state: DashboardState,
928}
929
930impl Default for DashboardStatistics {
931    fn default() -> Self {
932        Self {
933            total_widgets: 0,
934            total_metrics: 0,
935            active_alerts: 0,
936            last_update: Instant::now(),
937            uptime: Duration::from_secs(0),
938            state: DashboardState::Stopped,
939        }
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    #[test]
948    fn test_dashboard_creation() {
949        let config = DashboardConfig::default()
950            .with_title("Test Dashboard")
951            .with_refresh_interval(Duration::from_secs(10));
952
953        let dashboard = PerformanceDashboard::new(config);
954        assert!(dashboard.is_ok());
955
956        let dashboard = dashboard.expect("Operation failed");
957        assert_eq!(dashboard.config.title, "Test Dashboard");
958        assert_eq!(dashboard.config.refresh_interval, Duration::from_secs(10));
959    }
960
961    #[test]
962    fn test_widget_creation_and_addition() {
963        let config = DashboardConfig::default();
964        let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
965
966        let widget = Widget::new()
967            .with_title("CPU Usage")
968            .with_chart_type(ChartType::LineChart)
969            .with_metric_source(MetricSource::SystemCpu)
970            .with_alert_threshold(80.0);
971
972        let widget_id = dashboard.add_widget(widget).expect("Operation failed");
973        assert!(!widget_id.is_empty());
974        assert_eq!(dashboard.widgets.len(), 1);
975    }
976
977    #[test]
978    fn test_metric_updates() {
979        let config = DashboardConfig::default();
980        let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
981
982        // Add a widget
983        let widget = Widget::new().with_metric_source(MetricSource::SystemCpu);
984
985        dashboard.add_widget(widget).expect("Operation failed");
986
987        // Update metric
988        let result = dashboard.update_metric(&MetricSource::SystemCpu, 75.5);
989        assert!(result.is_ok());
990
991        // Check that metric was recorded
992        let metrics = dashboard.get_metrics().expect("Operation failed");
993        let cpu_metric = metrics.get("system.cpu");
994        assert!(cpu_metric.is_some());
995
996        let cpu_metric = cpu_metric.expect("Operation failed");
997        assert_eq!(cpu_metric.latest_value(), Some(75.5));
998    }
999
1000    #[test]
1001    fn test_metric_time_series() {
1002        let mut ts = MetricTimeSeries::new("test_metric");
1003
1004        ts.add_point(10.0, None);
1005        ts.add_point(20.0, None);
1006        ts.add_point(30.0, None);
1007
1008        assert_eq!(ts.latest_value(), Some(30.0));
1009        assert_eq!(ts.data_points.len(), 3);
1010
1011        // Test cleanup
1012        ts.cleanup(Duration::from_secs(1), 2);
1013        assert_eq!(ts.data_points.len(), 2);
1014    }
1015
1016    #[test]
1017    fn test_alert_configuration() {
1018        let widget = Widget::new()
1019            .with_title("Memory Usage")
1020            .with_alert_threshold(90.0);
1021
1022        assert!(widget.alert_config.is_some());
1023
1024        let alert_config = widget.alert_config.expect("Operation failed");
1025        assert_eq!(alert_config.threshold, 90.0);
1026        assert_eq!(alert_config.condition, AlertCondition::GreaterThan);
1027        assert_eq!(alert_config.severity, AlertSeverity::Warning);
1028    }
1029
1030    #[test]
1031    fn test_dashboard_statistics() {
1032        let config = DashboardConfig::default();
1033        let mut dashboard = PerformanceDashboard::new(config).expect("Operation failed");
1034
1035        // Add some widgets
1036        for i in 0..3 {
1037            let widget = Widget::new().with_title(&format!("Widget {i}"));
1038            dashboard.add_widget(widget).expect("Operation failed");
1039        }
1040
1041        let stats = dashboard.get_statistics();
1042        assert_eq!(stats.total_widgets, 3);
1043        assert_eq!(stats.active_alerts, 0);
1044        assert_eq!(stats.state, DashboardState::Stopped);
1045    }
1046
1047    #[test]
1048    fn test_dashboard_config_builder() {
1049        let config = DashboardConfig::default()
1050            .with_title("Custom Dashboard")
1051            .with_refresh_interval(Duration::from_secs(30))
1052            .with_retention_period(Duration::from_secs(14 * 24 * 60 * 60)) // 14 days
1053            .with_web_interface(9090)
1054            .with_api_token("test-token")
1055            .with_theme(DashboardTheme::Light);
1056
1057        assert_eq!(config.title, "Custom Dashboard");
1058        assert_eq!(config.refresh_interval, Duration::from_secs(30));
1059        assert_eq!(
1060            config.retention_period,
1061            Duration::from_secs(14 * 24 * 60 * 60)
1062        ); // 14 days
1063        assert_eq!(config.web_port, 9090);
1064        assert_eq!(config.api_token, Some("test-token".to_string()));
1065        assert_eq!(config.theme, DashboardTheme::Light);
1066    }
1067
1068    #[test]
1069    fn test_widget_layout() {
1070        let widget = Widget::new().with_layout(2, 3, 6, 4);
1071
1072        assert_eq!(widget.layout.x, 2);
1073        assert_eq!(widget.layout.y, 3);
1074        assert_eq!(widget.layout.width, 6);
1075        assert_eq!(widget.layout.height, 4);
1076    }
1077}