Skip to main content

mocopr_core/
monitoring.rs

1// Comprehensive monitoring and observability system for MoCoPr
2// This provides production-ready monitoring capabilities
3
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6use std::time::{Duration, Instant, SystemTime};
7use tokio::sync::RwLock;
8use tracing::{debug, error, warn};
9
10/// Health check status
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub enum HealthStatus {
13    /// System is healthy
14    Healthy,
15    /// System is degraded but operational
16    Degraded,
17    /// System is unhealthy and may not function correctly
18    Unhealthy,
19    /// System is in unknown state
20    Unknown,
21}
22
23/// Individual health check result
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct HealthCheckResult {
26    /// Name of the health check
27    pub name: String,
28    /// Status of the health check
29    pub status: HealthStatus,
30    /// Optional message providing details
31    pub message: Option<String>,
32    /// Time when the check was performed
33    pub timestamp: SystemTime,
34    /// Duration the check took to complete
35    pub duration: Duration,
36}
37
38/// Overall system health report
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct HealthReport {
41    /// Overall status (worst of all individual checks)
42    pub status: HealthStatus,
43    /// Individual check results
44    pub checks: Vec<HealthCheckResult>,
45    /// Time when the report was generated
46    pub timestamp: SystemTime,
47    /// Total time to generate the report
48    pub total_duration: Duration,
49}
50
51/// Trait for implementing health checks
52#[async_trait::async_trait]
53pub trait HealthCheck: Send + Sync {
54    /// Name of the health check
55    fn name(&self) -> &str;
56
57    /// Perform the health check
58    async fn check(&self) -> HealthCheckResult;
59}
60
61/// Performance metrics for monitoring
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct PerformanceMetrics {
64    /// Total number of requests processed
65    pub total_requests: u64,
66    /// Number of successful requests
67    pub successful_requests: u64,
68    /// Number of failed requests
69    pub failed_requests: u64,
70    /// Average response time in milliseconds
71    pub avg_response_time_ms: f64,
72    /// 95th percentile response time in milliseconds
73    pub p95_response_time_ms: f64,
74    /// 99th percentile response time in milliseconds
75    pub p99_response_time_ms: f64,
76    /// Current active connections
77    pub active_connections: u64,
78    /// Memory usage in bytes
79    pub memory_usage_bytes: u64,
80    /// CPU usage percentage
81    pub cpu_usage_percent: f64,
82    /// Timestamp when metrics were collected
83    pub timestamp: SystemTime,
84}
85
86/// Request metrics for tracking individual operations
87#[derive(Debug, Clone)]
88pub struct RequestMetrics {
89    /// Request start time
90    pub start_time: Instant,
91    /// Request method/operation
92    pub method: String,
93    /// Request success status
94    pub success: bool,
95    /// Response time
96    pub response_time: Duration,
97    /// Error message if failed
98    pub error_message: Option<String>,
99}
100
101/// Comprehensive monitoring system
102pub struct MonitoringSystem {
103    /// Registered health checks
104    health_checks: Arc<RwLock<Vec<Box<dyn HealthCheck>>>>,
105    /// Performance metrics
106    metrics: Arc<RwLock<PerformanceMetrics>>,
107    /// Recent response times for percentile calculations
108    response_times: Arc<RwLock<Vec<Duration>>>,
109    /// Configuration
110    config: MonitoringConfig,
111}
112
113/// Configuration for monitoring system
114#[derive(Debug, Clone)]
115pub struct MonitoringConfig {
116    /// Maximum number of response times to keep in memory
117    pub max_response_times: usize,
118    /// Health check interval
119    pub health_check_interval: Duration,
120    /// Enable detailed logging
121    pub detailed_logging: bool,
122}
123
124impl Default for MonitoringConfig {
125    fn default() -> Self {
126        Self {
127            max_response_times: 10000,
128            health_check_interval: Duration::from_secs(30),
129            detailed_logging: true,
130        }
131    }
132}
133
134impl MonitoringSystem {
135    /// Create a new monitoring system
136    pub fn new(config: MonitoringConfig) -> Self {
137        Self {
138            health_checks: Arc::new(RwLock::new(Vec::new())),
139            metrics: Arc::new(RwLock::new(PerformanceMetrics::default())),
140            response_times: Arc::new(RwLock::new(Vec::new())),
141            config,
142        }
143    }
144
145    /// Register a health check
146    pub async fn register_health_check(&self, check: Box<dyn HealthCheck>) {
147        let mut checks = self.health_checks.write().await;
148        checks.push(check);
149    }
150
151    /// Run all health checks and generate a report
152    pub async fn health_check(&self) -> HealthReport {
153        let start_time = Instant::now();
154        let mut results = Vec::new();
155        let mut overall_status = HealthStatus::Healthy;
156
157        let checks = self.health_checks.read().await;
158
159        for check in checks.iter() {
160            let result = check.check().await;
161
162            // Update overall status (worst case)
163            match (&overall_status, &result.status) {
164                (HealthStatus::Healthy, HealthStatus::Degraded) => {
165                    overall_status = HealthStatus::Degraded
166                }
167                (HealthStatus::Healthy | HealthStatus::Degraded, HealthStatus::Unhealthy) => {
168                    overall_status = HealthStatus::Unhealthy
169                }
170                (
171                    HealthStatus::Healthy | HealthStatus::Degraded | HealthStatus::Unhealthy,
172                    HealthStatus::Unknown,
173                ) => overall_status = HealthStatus::Unknown,
174                _ => {}
175            }
176
177            results.push(result);
178        }
179
180        let total_duration = start_time.elapsed();
181
182        HealthReport {
183            status: overall_status,
184            checks: results,
185            timestamp: SystemTime::now(),
186            total_duration,
187        }
188    }
189
190    /// Record a request for metrics
191    pub async fn record_request(&self, request: RequestMetrics) {
192        let mut metrics = self.metrics.write().await;
193        let mut response_times = self.response_times.write().await;
194
195        // Update basic counters
196        metrics.total_requests += 1;
197        if request.success {
198            metrics.successful_requests += 1;
199        } else {
200            metrics.failed_requests += 1;
201        }
202
203        // Update response times
204        response_times.push(request.response_time);
205
206        // Keep only recent response times
207        let current_len = response_times.len();
208        if current_len > self.config.max_response_times {
209            response_times.drain(0..current_len - self.config.max_response_times);
210        }
211
212        // Calculate percentiles
213        let mut sorted_times = response_times.clone();
214        sorted_times.sort();
215
216        if !sorted_times.is_empty() {
217            let avg_ms = sorted_times.iter().sum::<Duration>().as_secs_f64() * 1000.0
218                / sorted_times.len() as f64;
219            let p95_idx = (sorted_times.len() as f64 * 0.95) as usize;
220            let p99_idx = (sorted_times.len() as f64 * 0.99) as usize;
221
222            metrics.avg_response_time_ms = avg_ms;
223            metrics.p95_response_time_ms = sorted_times
224                .get(p95_idx)
225                .unwrap_or(&Duration::ZERO)
226                .as_secs_f64()
227                * 1000.0;
228            metrics.p99_response_time_ms = sorted_times
229                .get(p99_idx)
230                .unwrap_or(&Duration::ZERO)
231                .as_secs_f64()
232                * 1000.0;
233        }
234
235        // Update timestamp
236        metrics.timestamp = SystemTime::now();
237
238        // Log request if detailed logging is enabled
239        if self.config.detailed_logging {
240            if request.success {
241                debug!(
242                    "Request completed: {} in {:?}",
243                    request.method, request.response_time
244                );
245            } else {
246                warn!(
247                    "Request failed: {} in {:?} - {}",
248                    request.method,
249                    request.response_time,
250                    request
251                        .error_message
252                        .unwrap_or_else(|| "Unknown error".to_string())
253                );
254            }
255        }
256    }
257
258    /// Get current performance metrics
259    pub async fn get_metrics(&self) -> PerformanceMetrics {
260        self.metrics.read().await.clone()
261    }
262
263    /// Start periodic health checks
264    pub async fn start_periodic_health_checks(&self) {
265        let health_checks = self.health_checks.clone();
266        let interval = self.config.health_check_interval;
267
268        tokio::spawn(async move {
269            let mut interval_timer = tokio::time::interval(interval);
270
271            loop {
272                interval_timer.tick().await;
273
274                let checks = health_checks.read().await;
275                for check in checks.iter() {
276                    let result = check.check().await;
277
278                    match result.status {
279                        HealthStatus::Healthy => {
280                            debug!("Health check '{}' passed", result.name);
281                        }
282                        HealthStatus::Degraded => {
283                            warn!(
284                                "Health check '{}' degraded: {}",
285                                result.name,
286                                result.message.unwrap_or_else(|| "No details".to_string())
287                            );
288                        }
289                        HealthStatus::Unhealthy => {
290                            error!(
291                                "Health check '{}' failed: {}",
292                                result.name,
293                                result.message.unwrap_or_else(|| "No details".to_string())
294                            );
295                        }
296                        HealthStatus::Unknown => {
297                            warn!(
298                                "Health check '{}' status unknown: {}",
299                                result.name,
300                                result.message.unwrap_or_else(|| "No details".to_string())
301                            );
302                        }
303                    }
304                }
305            }
306        });
307    }
308
309    /// Update system resource metrics
310    pub async fn update_system_metrics(&self, active_connections: u64) {
311        let mut metrics = self.metrics.write().await;
312        metrics.active_connections = active_connections;
313
314        // Update system resource usage
315        #[cfg(target_os = "linux")]
316        {
317            if let Ok(usage) = self.get_system_usage().await {
318                metrics.memory_usage_bytes = usage.memory_bytes;
319                metrics.cpu_usage_percent = usage.cpu_percent;
320            }
321        }
322    }
323
324    /// Get system resource usage (Linux only)
325    #[cfg(target_os = "linux")]
326    async fn get_system_usage(&self) -> Result<SystemUsage, Box<dyn std::error::Error>> {
327        use std::fs;
328
329        // Read memory usage from /proc/self/status
330        let status = fs::read_to_string("/proc/self/status")?;
331        let memory_kb = status
332            .lines()
333            .find(|line| line.starts_with("VmRSS:"))
334            .and_then(|line| line.split_whitespace().nth(1))
335            .and_then(|s| s.parse::<u64>().ok())
336            .unwrap_or(0);
337
338        // Read CPU usage from /proc/self/stat
339        let stat = fs::read_to_string("/proc/self/stat")?;
340        let fields: Vec<&str> = stat.split_whitespace().collect();
341        let utime = fields
342            .get(13)
343            .and_then(|s| s.parse::<u64>().ok())
344            .unwrap_or(0);
345        let stime = fields
346            .get(14)
347            .and_then(|s| s.parse::<u64>().ok())
348            .unwrap_or(0);
349
350        // Simple CPU usage calculation (this is a simplified version)
351        let cpu_percent = ((utime + stime) as f64 / 100.0) * 0.1; // Rough estimate
352
353        Ok(SystemUsage {
354            memory_bytes: memory_kb * 1024,
355            cpu_percent,
356        })
357    }
358}
359
360#[cfg(target_os = "linux")]
361struct SystemUsage {
362    memory_bytes: u64,
363    cpu_percent: f64,
364}
365
366impl Default for PerformanceMetrics {
367    fn default() -> Self {
368        Self {
369            total_requests: 0,
370            successful_requests: 0,
371            failed_requests: 0,
372            avg_response_time_ms: 0.0,
373            p95_response_time_ms: 0.0,
374            p99_response_time_ms: 0.0,
375            active_connections: 0,
376            memory_usage_bytes: 0,
377            cpu_usage_percent: 0.0,
378            timestamp: SystemTime::now(),
379        }
380    }
381}
382
383/// Built-in health check for basic system status
384pub struct BasicHealthCheck {
385    name: String,
386}
387
388impl BasicHealthCheck {
389    /// Create a new HTTP health check
390    pub fn new(name: String) -> Self {
391        Self { name }
392    }
393}
394
395#[async_trait::async_trait]
396impl HealthCheck for BasicHealthCheck {
397    fn name(&self) -> &str {
398        &self.name
399    }
400
401    async fn check(&self) -> HealthCheckResult {
402        let start_time = Instant::now();
403
404        // Basic system health check
405        let status = if std::env::var("HEALTH_CHECK_FAIL").is_ok() {
406            HealthStatus::Unhealthy
407        } else {
408            HealthStatus::Healthy
409        };
410
411        let duration = start_time.elapsed();
412
413        HealthCheckResult {
414            name: self.name.clone(),
415            status,
416            message: Some("Basic system health check".to_string()),
417            timestamp: SystemTime::now(),
418            duration,
419        }
420    }
421}
422
423/// Health check for file system access
424pub struct FileSystemHealthCheck {
425    test_path: std::path::PathBuf,
426}
427
428impl FileSystemHealthCheck {
429    /// Create a new file health check
430    pub fn new(test_path: std::path::PathBuf) -> Self {
431        Self { test_path }
432    }
433}
434
435#[async_trait::async_trait]
436impl HealthCheck for FileSystemHealthCheck {
437    fn name(&self) -> &str {
438        "filesystem"
439    }
440
441    async fn check(&self) -> HealthCheckResult {
442        let start_time = Instant::now();
443
444        let (status, message) = match std::fs::metadata(&self.test_path) {
445            Ok(_) => (HealthStatus::Healthy, "File system accessible".to_string()),
446            Err(e) => (
447                HealthStatus::Unhealthy,
448                format!("File system check failed: {}", e),
449            ),
450        };
451
452        let duration = start_time.elapsed();
453
454        HealthCheckResult {
455            name: "filesystem".to_string(),
456            status,
457            message: Some(message),
458            timestamp: SystemTime::now(),
459            duration,
460        }
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use std::time::Duration;
468
469    #[tokio::test]
470    async fn test_monitoring_system() {
471        let config = MonitoringConfig::default();
472        let monitoring = MonitoringSystem::new(config);
473
474        // Register a health check
475        let health_check = Box::new(BasicHealthCheck::new("test".to_string()));
476        monitoring.register_health_check(health_check).await;
477
478        // Run health check
479        let report = monitoring.health_check().await;
480        assert_eq!(report.status, HealthStatus::Healthy);
481        assert_eq!(report.checks.len(), 1);
482
483        // Record a request
484        let request = RequestMetrics {
485            start_time: Instant::now(),
486            method: "test_method".to_string(),
487            success: true,
488            response_time: Duration::from_millis(100),
489            error_message: None,
490        };
491
492        monitoring.record_request(request).await;
493
494        // Check metrics
495        let metrics = monitoring.get_metrics().await;
496        assert_eq!(metrics.total_requests, 1);
497        assert_eq!(metrics.successful_requests, 1);
498        assert_eq!(metrics.failed_requests, 0);
499    }
500
501    #[tokio::test]
502    async fn test_health_check_aggregation() {
503        let config = MonitoringConfig::default();
504        let monitoring = MonitoringSystem::new(config);
505
506        // Register multiple health checks with different statuses
507        let healthy_check = Box::new(BasicHealthCheck::new("healthy".to_string()));
508        let degraded_check = Box::new(FileSystemHealthCheck::new(std::path::PathBuf::from(
509            "/nonexistent",
510        )));
511
512        monitoring.register_health_check(healthy_check).await;
513        monitoring.register_health_check(degraded_check).await;
514
515        // Run health check
516        let report = monitoring.health_check().await;
517
518        // Overall status should be the worst individual status
519        assert_eq!(report.checks.len(), 2);
520        // The overall status will be unhealthy due to the nonexistent path
521        assert_eq!(report.status, HealthStatus::Unhealthy);
522    }
523}