chie_core/
dashboard.rs

1//! Performance dashboard data endpoints.
2//!
3//! This module provides structured data for performance dashboards and monitoring UIs.
4//!
5//! # Features
6//!
7//! - System health status aggregation
8//! - Performance metrics summaries
9//! - Resource utilization snapshots
10//! - Historical trend data
11//! - Alert summaries
12//!
13//! # Example
14//!
15//! ```
16//! use chie_core::dashboard::{DashboardData, SystemStatus, PerformanceSnapshot};
17//!
18//! let mut dashboard = DashboardData::new();
19//!
20//! // Update metrics
21//! dashboard.update_storage(1024 * 1024, 10 * 1024 * 1024);
22//! dashboard.update_bandwidth(500 * 1024, 200 * 1024);
23//!
24//! // Get snapshot
25//! let snapshot = dashboard.snapshot();
26//! println!("System Status: {:?}", snapshot.system_status);
27//! println!("Storage Usage: {}%", snapshot.storage_usage_percent);
28//! ```
29
30use std::collections::VecDeque;
31use std::time::{Duration, SystemTime};
32
33/// System health status.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SystemStatus {
36    /// All systems operating normally.
37    Healthy,
38    /// Minor issues detected.
39    Degraded,
40    /// Significant problems detected.
41    Unhealthy,
42    /// Critical failures.
43    Critical,
44}
45
46impl SystemStatus {
47    /// Get a color code for the status (for UI display).
48    #[must_use]
49    #[inline]
50    pub const fn color_code(&self) -> &'static str {
51        match self {
52            Self::Healthy => "#22c55e",   // Green
53            Self::Degraded => "#f59e0b",  // Amber
54            Self::Unhealthy => "#ef4444", // Red
55            Self::Critical => "#991b1b",  // Dark red
56        }
57    }
58
59    /// Get a label for the status.
60    #[must_use]
61    #[inline]
62    pub const fn label(&self) -> &'static str {
63        match self {
64            Self::Healthy => "Healthy",
65            Self::Degraded => "Degraded",
66            Self::Unhealthy => "Unhealthy",
67            Self::Critical => "Critical",
68        }
69    }
70}
71
72/// Performance snapshot at a point in time.
73#[derive(Debug, Clone)]
74pub struct PerformanceSnapshot {
75    /// Timestamp of the snapshot.
76    pub timestamp: SystemTime,
77    /// Overall system status.
78    pub system_status: SystemStatus,
79    /// Storage used in bytes.
80    pub storage_used_bytes: u64,
81    /// Total storage capacity in bytes.
82    pub storage_total_bytes: u64,
83    /// Storage usage percentage (0-100).
84    pub storage_usage_percent: f64,
85    /// Bandwidth upload rate in bytes/sec.
86    pub bandwidth_upload_bps: u64,
87    /// Bandwidth download rate in bytes/sec.
88    pub bandwidth_download_bps: u64,
89    /// Average request latency in milliseconds.
90    pub avg_latency_ms: u64,
91    /// P95 latency in milliseconds.
92    pub p95_latency_ms: u64,
93    /// Active connections count.
94    pub active_connections: u32,
95    /// Requests served in the last period.
96    pub requests_served: u64,
97    /// Error count in the last period.
98    pub error_count: u64,
99    /// Cache hit rate percentage (0-100).
100    pub cache_hit_rate: f64,
101    /// Number of active alerts.
102    pub active_alerts: u32,
103}
104
105impl Default for PerformanceSnapshot {
106    fn default() -> Self {
107        Self {
108            timestamp: SystemTime::now(),
109            system_status: SystemStatus::Healthy,
110            storage_used_bytes: 0,
111            storage_total_bytes: 0,
112            storage_usage_percent: 0.0,
113            bandwidth_upload_bps: 0,
114            bandwidth_download_bps: 0,
115            avg_latency_ms: 0,
116            p95_latency_ms: 0,
117            active_connections: 0,
118            requests_served: 0,
119            error_count: 0,
120            cache_hit_rate: 0.0,
121            active_alerts: 0,
122        }
123    }
124}
125
126impl PerformanceSnapshot {
127    /// Create a new snapshot with current timestamp.
128    #[must_use]
129    #[inline]
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Determine system status based on metrics.
135    #[must_use]
136    pub fn determine_status(&self) -> SystemStatus {
137        // Critical conditions
138        if self.storage_usage_percent > 95.0 || self.error_count > 100 || self.active_alerts > 10 {
139            return SystemStatus::Critical;
140        }
141
142        // Unhealthy conditions
143        if self.storage_usage_percent > 90.0 || self.avg_latency_ms > 1000 || self.error_count > 50
144        {
145            return SystemStatus::Unhealthy;
146        }
147
148        // Degraded conditions
149        if self.storage_usage_percent > 75.0
150            || self.avg_latency_ms > 500
151            || self.error_count > 10
152            || self.cache_hit_rate < 50.0
153        {
154            return SystemStatus::Degraded;
155        }
156
157        SystemStatus::Healthy
158    }
159}
160
161/// Historical data point for trend analysis.
162#[derive(Debug, Clone)]
163pub struct DataPoint {
164    /// Timestamp of the data point.
165    pub timestamp: SystemTime,
166    /// The metric value.
167    pub value: f64,
168}
169
170impl DataPoint {
171    /// Create a new data point.
172    #[must_use]
173    #[inline]
174    pub fn new(value: f64) -> Self {
175        Self {
176            timestamp: SystemTime::now(),
177            value,
178        }
179    }
180
181    /// Get the age of this data point in seconds.
182    #[must_use]
183    #[inline]
184    pub fn age_secs(&self) -> u64 {
185        SystemTime::now()
186            .duration_since(self.timestamp)
187            .unwrap_or_default()
188            .as_secs()
189    }
190}
191
192/// Time series data for trend analysis.
193#[derive(Debug, Clone)]
194pub struct TimeSeries {
195    /// Data points.
196    points: VecDeque<DataPoint>,
197    /// Maximum number of points to retain.
198    max_points: usize,
199    /// Maximum age of points in seconds.
200    max_age_secs: u64,
201}
202
203impl TimeSeries {
204    /// Create a new time series.
205    #[must_use]
206    pub fn new(max_points: usize, max_age_secs: u64) -> Self {
207        Self {
208            points: VecDeque::with_capacity(max_points),
209            max_points,
210            max_age_secs,
211        }
212    }
213
214    /// Add a data point.
215    pub fn add(&mut self, value: f64) {
216        self.points.push_back(DataPoint::new(value));
217
218        // Trim old points by age
219        let cutoff = SystemTime::now() - Duration::from_secs(self.max_age_secs);
220        while let Some(point) = self.points.front() {
221            if point.timestamp < cutoff {
222                self.points.pop_front();
223            } else {
224                break;
225            }
226        }
227
228        // Trim by count
229        while self.points.len() > self.max_points {
230            self.points.pop_front();
231        }
232    }
233
234    /// Get all data points.
235    #[must_use]
236    #[inline]
237    pub fn points(&self) -> &VecDeque<DataPoint> {
238        &self.points
239    }
240
241    /// Get the most recent value.
242    #[must_use]
243    pub fn latest(&self) -> Option<f64> {
244        self.points.back().map(|p| p.value)
245    }
246
247    /// Get the average value.
248    #[must_use]
249    pub fn average(&self) -> Option<f64> {
250        if self.points.is_empty() {
251            return None;
252        }
253
254        let sum: f64 = self.points.iter().map(|p| p.value).sum();
255        Some(sum / self.points.len() as f64)
256    }
257
258    /// Get the minimum value.
259    #[must_use]
260    pub fn min(&self) -> Option<f64> {
261        self.points
262            .iter()
263            .map(|p| p.value)
264            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
265    }
266
267    /// Get the maximum value.
268    #[must_use]
269    pub fn max(&self) -> Option<f64> {
270        self.points
271            .iter()
272            .map(|p| p.value)
273            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
274    }
275
276    /// Get the number of points.
277    #[must_use]
278    #[inline]
279    pub fn len(&self) -> usize {
280        self.points.len()
281    }
282
283    /// Check if the series is empty.
284    #[must_use]
285    #[inline]
286    pub fn is_empty(&self) -> bool {
287        self.points.is_empty()
288    }
289}
290
291/// Dashboard data aggregator.
292pub struct DashboardData {
293    /// Current snapshot.
294    current: PerformanceSnapshot,
295    /// Storage usage history.
296    storage_history: TimeSeries,
297    /// Bandwidth history (upload).
298    bandwidth_upload_history: TimeSeries,
299    /// Bandwidth history (download).
300    bandwidth_download_history: TimeSeries,
301    /// Latency history.
302    latency_history: TimeSeries,
303    /// Error rate history.
304    error_history: TimeSeries,
305}
306
307impl Default for DashboardData {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl DashboardData {
314    /// Create a new dashboard data aggregator.
315    #[must_use]
316    pub fn new() -> Self {
317        // Keep 100 points, max 1 hour old
318        Self {
319            current: PerformanceSnapshot::new(),
320            storage_history: TimeSeries::new(100, 3600),
321            bandwidth_upload_history: TimeSeries::new(100, 3600),
322            bandwidth_download_history: TimeSeries::new(100, 3600),
323            latency_history: TimeSeries::new(100, 3600),
324            error_history: TimeSeries::new(100, 3600),
325        }
326    }
327
328    /// Update storage metrics.
329    pub fn update_storage(&mut self, used_bytes: u64, total_bytes: u64) {
330        self.current.storage_used_bytes = used_bytes;
331        self.current.storage_total_bytes = total_bytes;
332        self.current.storage_usage_percent = if total_bytes > 0 {
333            (used_bytes as f64 / total_bytes as f64) * 100.0
334        } else {
335            0.0
336        };
337
338        self.storage_history.add(self.current.storage_usage_percent);
339    }
340
341    /// Update bandwidth metrics.
342    pub fn update_bandwidth(&mut self, upload_bps: u64, download_bps: u64) {
343        self.current.bandwidth_upload_bps = upload_bps;
344        self.current.bandwidth_download_bps = download_bps;
345
346        self.bandwidth_upload_history.add(upload_bps as f64);
347        self.bandwidth_download_history.add(download_bps as f64);
348    }
349
350    /// Update latency metrics.
351    pub fn update_latency(&mut self, avg_ms: u64, p95_ms: u64) {
352        self.current.avg_latency_ms = avg_ms;
353        self.current.p95_latency_ms = p95_ms;
354
355        self.latency_history.add(avg_ms as f64);
356    }
357
358    /// Update connection metrics.
359    #[inline]
360    pub fn update_connections(&mut self, active: u32) {
361        self.current.active_connections = active;
362    }
363
364    /// Update request metrics.
365    #[inline]
366    pub fn update_requests(&mut self, served: u64) {
367        self.current.requests_served = served;
368    }
369
370    /// Update error metrics.
371    pub fn update_errors(&mut self, count: u64) {
372        self.current.error_count = count;
373        self.error_history.add(count as f64);
374    }
375
376    /// Update cache metrics.
377    #[inline]
378    pub fn update_cache(&mut self, hit_rate: f64) {
379        self.current.cache_hit_rate = hit_rate;
380    }
381
382    /// Update alert count.
383    #[inline]
384    pub fn update_alerts(&mut self, count: u32) {
385        self.current.active_alerts = count;
386    }
387
388    /// Get the current snapshot.
389    #[must_use]
390    pub fn snapshot(&self) -> PerformanceSnapshot {
391        let mut snapshot = self.current.clone();
392        snapshot.system_status = snapshot.determine_status();
393        snapshot.timestamp = SystemTime::now();
394        snapshot
395    }
396
397    /// Get storage usage trend.
398    #[must_use]
399    #[inline]
400    pub fn storage_trend(&self) -> &TimeSeries {
401        &self.storage_history
402    }
403
404    /// Get bandwidth upload trend.
405    #[must_use]
406    #[inline]
407    pub fn bandwidth_upload_trend(&self) -> &TimeSeries {
408        &self.bandwidth_upload_history
409    }
410
411    /// Get bandwidth download trend.
412    #[must_use]
413    #[inline]
414    pub fn bandwidth_download_trend(&self) -> &TimeSeries {
415        &self.bandwidth_download_history
416    }
417
418    /// Get latency trend.
419    #[must_use]
420    #[inline]
421    pub fn latency_trend(&self) -> &TimeSeries {
422        &self.latency_history
423    }
424
425    /// Get error trend.
426    #[must_use]
427    #[inline]
428    pub fn error_trend(&self) -> &TimeSeries {
429        &self.error_history
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_system_status_labels() {
439        assert_eq!(SystemStatus::Healthy.label(), "Healthy");
440        assert_eq!(SystemStatus::Degraded.label(), "Degraded");
441        assert_eq!(SystemStatus::Unhealthy.label(), "Unhealthy");
442        assert_eq!(SystemStatus::Critical.label(), "Critical");
443    }
444
445    #[test]
446    fn test_performance_snapshot_status() {
447        let mut snapshot = PerformanceSnapshot::new();
448
449        // Set cache_hit_rate to avoid triggering Degraded status (< 50.0 triggers degraded)
450        snapshot.cache_hit_rate = 80.0;
451
452        // Healthy
453        snapshot.storage_usage_percent = 50.0;
454        snapshot.avg_latency_ms = 100;
455        assert_eq!(snapshot.determine_status(), SystemStatus::Healthy);
456
457        // Degraded
458        snapshot.storage_usage_percent = 80.0;
459        assert_eq!(snapshot.determine_status(), SystemStatus::Degraded);
460
461        // Unhealthy
462        snapshot.storage_usage_percent = 92.0;
463        assert_eq!(snapshot.determine_status(), SystemStatus::Unhealthy);
464
465        // Critical
466        snapshot.storage_usage_percent = 96.0;
467        assert_eq!(snapshot.determine_status(), SystemStatus::Critical);
468    }
469
470    #[test]
471    fn test_time_series_basic() {
472        let mut ts = TimeSeries::new(10, 3600);
473        assert!(ts.is_empty());
474
475        ts.add(10.0);
476        ts.add(20.0);
477        ts.add(30.0);
478
479        assert_eq!(ts.len(), 3);
480        assert_eq!(ts.latest(), Some(30.0));
481        assert_eq!(ts.average(), Some(20.0));
482        assert_eq!(ts.min(), Some(10.0));
483        assert_eq!(ts.max(), Some(30.0));
484    }
485
486    #[test]
487    fn test_time_series_capacity() {
488        let mut ts = TimeSeries::new(5, 3600);
489
490        for i in 0..10 {
491            ts.add(i as f64);
492        }
493
494        // Should only keep the last 5
495        assert_eq!(ts.len(), 5);
496        assert_eq!(ts.latest(), Some(9.0));
497    }
498
499    #[test]
500    fn test_dashboard_data_storage() {
501        let mut dashboard = DashboardData::new();
502        dashboard.update_storage(5000, 10000);
503
504        let snapshot = dashboard.snapshot();
505        assert_eq!(snapshot.storage_used_bytes, 5000);
506        assert_eq!(snapshot.storage_total_bytes, 10000);
507        assert_eq!(snapshot.storage_usage_percent, 50.0);
508    }
509
510    #[test]
511    fn test_dashboard_data_bandwidth() {
512        let mut dashboard = DashboardData::new();
513        dashboard.update_bandwidth(1000, 2000);
514
515        let snapshot = dashboard.snapshot();
516        assert_eq!(snapshot.bandwidth_upload_bps, 1000);
517        assert_eq!(snapshot.bandwidth_download_bps, 2000);
518    }
519
520    #[test]
521    fn test_dashboard_data_latency() {
522        let mut dashboard = DashboardData::new();
523        dashboard.update_latency(100, 250);
524
525        let snapshot = dashboard.snapshot();
526        assert_eq!(snapshot.avg_latency_ms, 100);
527        assert_eq!(snapshot.p95_latency_ms, 250);
528    }
529
530    #[test]
531    fn test_dashboard_trends() {
532        let mut dashboard = DashboardData::new();
533
534        for i in 1..=5 {
535            dashboard.update_storage(i * 1000, 10000);
536        }
537
538        let trend = dashboard.storage_trend();
539        assert_eq!(trend.len(), 5);
540        assert_eq!(trend.latest(), Some(50.0));
541    }
542
543    #[test]
544    fn test_data_point_age() {
545        let point = DataPoint::new(42.0);
546        std::thread::sleep(std::time::Duration::from_millis(100));
547        assert!(point.age_secs() < 1);
548    }
549}