Skip to main content

oxirs_gql/
system_monitor.rs

1//! System Resource Monitoring
2//!
3//! This module provides real-time system resource monitoring capabilities
4//! including memory usage, CPU usage, and other system metrics.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10use sysinfo::{Pid, System};
11use tokio::sync::RwLock;
12use tracing::{debug, warn};
13
14/// System resource metrics
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SystemMetrics {
17    /// Memory usage in MB for the current process
18    pub memory_usage_mb: f64,
19    /// CPU usage percentage for the current process (0.0 to 100.0)
20    pub cpu_usage_percent: f64,
21    /// Total system memory in MB
22    pub total_memory_mb: f64,
23    /// Available system memory in MB
24    pub available_memory_mb: f64,
25    /// System-wide CPU usage percentage
26    pub system_cpu_usage_percent: f64,
27    /// Number of CPU cores
28    pub cpu_cores: usize,
29    /// Current process ID
30    pub process_id: u32,
31    /// Timestamp when metrics were collected (duration since a reference point)
32    #[serde(with = "duration_serde")]
33    pub timestamp: Duration,
34}
35
36/// Serde module for Duration serialization
37mod duration_serde {
38    use super::*;
39    use serde::{Deserializer, Serializer};
40
41    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
42    where
43        S: Serializer,
44    {
45        serializer.serialize_u64(duration.as_millis() as u64)
46    }
47
48    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
49    where
50        D: Deserializer<'de>,
51    {
52        let millis = u64::deserialize(deserializer)?;
53        Ok(Duration::from_millis(millis))
54    }
55}
56
57impl Default for SystemMetrics {
58    fn default() -> Self {
59        Self {
60            memory_usage_mb: 0.0,
61            cpu_usage_percent: 0.0,
62            total_memory_mb: 0.0,
63            available_memory_mb: 0.0,
64            system_cpu_usage_percent: 0.0,
65            cpu_cores: std::thread::available_parallelism()
66                .map(|n| n.get())
67                .unwrap_or(1),
68            process_id: std::process::id(),
69            timestamp: Duration::from_secs(0),
70        }
71    }
72}
73
74/// Configuration for system monitoring
75#[derive(Debug, Clone)]
76pub struct SystemMonitorConfig {
77    /// How often to update system information
78    pub update_interval: Duration,
79    /// Enable detailed CPU per-core monitoring
80    pub enable_per_core_cpu: bool,
81    /// Enable memory breakdown monitoring
82    pub enable_memory_breakdown: bool,
83    /// Enable network monitoring
84    pub enable_network_monitoring: bool,
85    /// Enable disk I/O monitoring
86    pub enable_disk_monitoring: bool,
87}
88
89impl Default for SystemMonitorConfig {
90    fn default() -> Self {
91        Self {
92            update_interval: Duration::from_secs(1),
93            enable_per_core_cpu: false,
94            enable_memory_breakdown: true,
95            enable_network_monitoring: false,
96            enable_disk_monitoring: false,
97        }
98    }
99}
100
101/// Real-time system resource monitor
102pub struct SystemMonitor {
103    config: SystemMonitorConfig,
104    system: Arc<Mutex<System>>,
105    last_update: Arc<RwLock<Instant>>,
106    cached_metrics: Arc<RwLock<SystemMetrics>>,
107    process_id: u32,
108}
109
110impl Default for SystemMonitor {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl SystemMonitor {
117    /// Create a new system monitor
118    pub fn new() -> Self {
119        Self::with_config(SystemMonitorConfig::default())
120    }
121
122    /// Create a new system monitor with custom configuration
123    pub fn with_config(config: SystemMonitorConfig) -> Self {
124        // Use System::new() instead of System::new_all() for faster initialization
125        // new_all() scans all processes which can take minutes on busy systems
126        let mut system = System::new();
127        // Refresh only what we need
128        system.refresh_memory();
129        system.refresh_cpu_specifics(sysinfo::CpuRefreshKind::everything());
130
131        let process_id = std::process::id();
132
133        Self {
134            config,
135            system: Arc::new(Mutex::new(system)),
136            last_update: Arc::new(RwLock::new(Instant::now())),
137            cached_metrics: Arc::new(RwLock::new(SystemMetrics::default())),
138            process_id,
139        }
140    }
141
142    /// Get current system metrics (cached or fresh)
143    pub async fn get_metrics(&self) -> Result<SystemMetrics> {
144        let now = Instant::now();
145        let last_update = *self.last_update.read().await;
146
147        // Check if we need to update metrics
148        if now.duration_since(last_update) >= self.config.update_interval {
149            self.update_metrics().await?;
150        }
151
152        let metrics = self.cached_metrics.read().await.clone();
153        Ok(metrics)
154    }
155
156    /// Force update of system metrics
157    pub async fn update_metrics(&self) -> Result<()> {
158        let metrics = {
159            let mut system = self
160                .system
161                .lock()
162                .map_err(|_| anyhow::anyhow!("System mutex poisoned"))?;
163
164            // Refresh system information
165            system.refresh_cpu_all();
166            system.refresh_memory();
167            system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
168
169            self.collect_metrics(&mut system)?
170        };
171
172        // Update cached metrics and timestamp
173        {
174            let mut cached = self.cached_metrics.write().await;
175            *cached = metrics;
176        }
177
178        {
179            let mut last_update = self.last_update.write().await;
180            *last_update = Instant::now();
181        }
182
183        debug!("System metrics updated");
184        Ok(())
185    }
186
187    /// Collect current system metrics
188    fn collect_metrics(&self, system: &mut System) -> Result<SystemMetrics> {
189        // Get process-specific metrics
190        let (memory_usage_mb, cpu_usage_percent) =
191            if let Some(process) = system.process(Pid::from_u32(self.process_id)) {
192                let memory_bytes = process.memory();
193                let memory_mb = memory_bytes as f64 / 1024.0 / 1024.0;
194                let cpu_percent = process.cpu_usage() as f64;
195                (memory_mb, cpu_percent)
196            } else {
197                warn!("Could not find current process in system info");
198                (0.0, 0.0)
199            };
200
201        // Get system-wide metrics
202        let total_memory_mb = system.total_memory() as f64 / 1024.0 / 1024.0;
203        let available_memory_mb = system.available_memory() as f64 / 1024.0 / 1024.0;
204
205        // Calculate system-wide CPU usage
206        let system_cpu_usage_percent = system.global_cpu_usage() as f64;
207
208        let cpu_cores = system.cpus().len();
209
210        // Use duration since epoch as timestamp for consistency
211        let timestamp = std::time::SystemTime::now()
212            .duration_since(std::time::UNIX_EPOCH)
213            .unwrap_or(Duration::from_secs(0));
214
215        Ok(SystemMetrics {
216            memory_usage_mb,
217            cpu_usage_percent,
218            total_memory_mb,
219            available_memory_mb,
220            system_cpu_usage_percent,
221            cpu_cores,
222            process_id: self.process_id,
223            timestamp,
224        })
225    }
226
227    /// Get memory usage for the current process in MB
228    pub async fn get_memory_usage_mb(&self) -> Result<f64> {
229        let metrics = self.get_metrics().await?;
230        Ok(metrics.memory_usage_mb)
231    }
232
233    /// Get CPU usage for the current process as percentage (0.0 to 100.0)
234    pub async fn get_cpu_usage_percent(&self) -> Result<f64> {
235        let metrics = self.get_metrics().await?;
236        Ok(metrics.cpu_usage_percent)
237    }
238
239    /// Get a lightweight snapshot of current metrics (may be slightly stale)
240    pub async fn get_cached_metrics(&self) -> SystemMetrics {
241        self.cached_metrics.read().await.clone()
242    }
243
244    /// Start background monitoring task
245    pub async fn start_background_monitoring(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
246        let monitor = self.clone();
247
248        tokio::spawn(async move {
249            let mut interval = tokio::time::interval(monitor.config.update_interval);
250
251            loop {
252                interval.tick().await;
253
254                if let Err(e) = monitor.update_metrics().await {
255                    warn!("Failed to update system metrics: {}", e);
256                }
257            }
258        })
259    }
260}
261
262/// Helper function to get current memory usage (convenience method)
263pub async fn get_current_memory_usage_mb() -> f64 {
264    #[cfg(test)]
265    {
266        // In tests, return a dummy value to avoid slow system initialization
267        0.0
268    }
269
270    #[cfg(not(test))]
271    {
272        static MONITOR: std::sync::OnceLock<Arc<SystemMonitor>> = std::sync::OnceLock::new();
273
274        let monitor = MONITOR.get_or_init(|| Arc::new(SystemMonitor::new()));
275
276        monitor.get_memory_usage_mb().await.unwrap_or_else(|e| {
277            warn!("Failed to get memory usage: {}", e);
278            0.0
279        })
280    }
281}
282
283/// Helper function to get current CPU usage (convenience method)
284pub async fn get_current_cpu_usage_percent() -> f64 {
285    #[cfg(test)]
286    {
287        // In tests, return a dummy value to avoid slow system initialization
288        0.0
289    }
290
291    #[cfg(not(test))]
292    {
293        static MONITOR: std::sync::OnceLock<Arc<SystemMonitor>> = std::sync::OnceLock::new();
294
295        let monitor = MONITOR.get_or_init(|| Arc::new(SystemMonitor::new()));
296
297        monitor.get_cpu_usage_percent().await.unwrap_or_else(|e| {
298            warn!("Failed to get CPU usage: {}", e);
299            0.0
300        })
301    }
302}
303
304/// Calculate throughput in Mbps from requests per second and average response size
305pub fn calculate_throughput_mbps(requests_per_second: f64, avg_response_size_bytes: f64) -> f64 {
306    if requests_per_second <= 0.0 || avg_response_size_bytes <= 0.0 {
307        return 0.0;
308    }
309
310    // Convert bytes per second to megabits per second
311    let bytes_per_second = requests_per_second * avg_response_size_bytes;
312    let bits_per_second = bytes_per_second * 8.0;
313
314    bits_per_second / (1024.0 * 1024.0)
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use std::time::Duration;
321
322    #[tokio::test]
323    async fn test_system_monitor_creation() {
324        let monitor = SystemMonitor::new();
325
326        // Update metrics to ensure system info is refreshed
327        monitor.update_metrics().await.expect("should succeed");
328
329        // Should be able to get metrics
330        let metrics = monitor.get_metrics().await.expect("should succeed");
331
332        // Basic validation
333        assert!(metrics.cpu_cores > 0);
334        assert!(metrics.process_id > 0);
335        // Some CI systems might not report memory properly, so use >= 0.0
336        assert!(metrics.total_memory_mb >= 0.0);
337    }
338
339    #[tokio::test]
340    async fn test_memory_monitoring() {
341        let monitor = SystemMonitor::new();
342
343        let memory_usage = monitor.get_memory_usage_mb().await.expect("should succeed");
344
345        // Should have some memory usage (process should be using at least some memory)
346        assert!(memory_usage >= 0.0);
347    }
348
349    #[tokio::test]
350    async fn test_cpu_monitoring() {
351        let monitor = SystemMonitor::new();
352
353        // Update metrics to get CPU readings
354        monitor.update_metrics().await.expect("should succeed");
355
356        let cpu_usage = monitor
357            .get_cpu_usage_percent()
358            .await
359            .expect("should succeed");
360
361        // CPU usage should be between 0 and 100
362        assert!(cpu_usage >= 0.0);
363        assert!(cpu_usage <= 100.0);
364    }
365
366    #[test]
367    fn test_throughput_calculation() {
368        // Test normal case
369        let throughput = calculate_throughput_mbps(100.0, 1024.0); // 100 RPS, 1KB responses
370        assert!(throughput > 0.0);
371
372        // Test edge cases
373        assert_eq!(calculate_throughput_mbps(0.0, 1024.0), 0.0);
374        assert_eq!(calculate_throughput_mbps(100.0, 0.0), 0.0);
375        assert_eq!(calculate_throughput_mbps(-1.0, 1024.0), 0.0);
376    }
377
378    #[tokio::test]
379    async fn test_convenience_functions() {
380        // Test convenience functions
381        let memory = get_current_memory_usage_mb().await;
382        assert!(memory >= 0.0);
383
384        let cpu = get_current_cpu_usage_percent().await;
385        assert!(cpu >= 0.0);
386        assert!(cpu <= 100.0);
387    }
388
389    #[tokio::test]
390    async fn test_background_monitoring() {
391        let monitor = Arc::new(SystemMonitor::with_config(SystemMonitorConfig {
392            update_interval: Duration::from_millis(100),
393            ..SystemMonitorConfig::default()
394        }));
395
396        let handle = monitor.clone().start_background_monitoring().await;
397
398        // Wait a bit to let background monitoring run
399        tokio::time::sleep(Duration::from_millis(150)).await;
400
401        // Should have updated metrics
402        let metrics = monitor.get_cached_metrics().await;
403        assert!(metrics.cpu_cores > 0);
404
405        handle.abort();
406    }
407}