Skip to main content

hyperi_rustlib/metrics/
process.rs

1// Project:   hyperi-rustlib
2// File:      src/metrics/process.rs
3// Purpose:   Process-level metrics collection
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Process-level metrics collection.
10
11use std::sync::Arc;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
15
16/// Process metrics collector.
17#[derive(Debug, Clone)]
18pub struct ProcessMetrics {
19    namespace: String,
20    system: Arc<std::sync::Mutex<System>>,
21    pid: sysinfo::Pid,
22    start_time: f64,
23}
24
25impl ProcessMetrics {
26    /// Create a new process metrics collector.
27    #[must_use]
28    pub fn new(namespace: &str) -> Self {
29        let pid = sysinfo::Pid::from_u32(std::process::id());
30        let system = System::new_with_specifics(
31            RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
32        );
33
34        let start_time = SystemTime::now()
35            .duration_since(UNIX_EPOCH)
36            .map_or(0.0, |d| d.as_secs_f64());
37
38        let this = Self {
39            namespace: namespace.to_string(),
40            system: Arc::new(std::sync::Mutex::new(system)),
41            pid,
42            start_time,
43        };
44
45        // Register metric descriptions
46        this.register_metrics();
47        this
48    }
49
50    /// Register metric descriptions.
51    fn register_metrics(&self) {
52        let ns = &self.namespace;
53
54        metrics::describe_gauge!(
55            format!("{ns}_process_cpu_seconds_total"),
56            "Total user and system CPU time spent in seconds".to_string()
57        );
58        metrics::describe_gauge!(
59            format!("{ns}_process_resident_memory_bytes"),
60            "Resident memory size in bytes".to_string()
61        );
62        metrics::describe_gauge!(
63            format!("{ns}_process_virtual_memory_bytes"),
64            "Virtual memory size in bytes".to_string()
65        );
66        metrics::describe_gauge!(
67            format!("{ns}_process_open_fds"),
68            "Number of open file descriptors".to_string()
69        );
70        metrics::describe_gauge!(
71            format!("{ns}_process_start_time_seconds"),
72            "Start time of the process since unix epoch in seconds".to_string()
73        );
74    }
75
76    /// Update process metrics.
77    pub fn update(&self) {
78        // Recover from a poisoned lock rather than panicking: a panic in a
79        // prior update must not turn metrics collection into a repeat-panic.
80        // Observability degrades, it does not crash.
81        let mut system = self.system.lock().unwrap_or_else(|e| e.into_inner());
82        system.refresh_processes_specifics(
83            ProcessesToUpdate::Some(&[self.pid]),
84            true,
85            ProcessRefreshKind::everything(),
86        );
87
88        if let Some(process) = system.process(self.pid) {
89            let ns = &self.namespace;
90
91            // CPU time (approximate - sysinfo gives percentage, not total time)
92            let cpu_usage = f64::from(process.cpu_usage());
93            metrics::gauge!(format!("{ns}_process_cpu_seconds_total")).set(cpu_usage);
94
95            // Memory
96            let rss = process.memory();
97            let virtual_mem = process.virtual_memory();
98            metrics::gauge!(format!("{ns}_process_resident_memory_bytes")).set(rss as f64);
99            metrics::gauge!(format!("{ns}_process_virtual_memory_bytes")).set(virtual_mem as f64);
100
101            // File descriptors (Linux-specific)
102            #[cfg(target_os = "linux")]
103            {
104                if let Ok(fds) = count_open_fds() {
105                    metrics::gauge!(format!("{ns}_process_open_fds")).set(fds as f64);
106                }
107            }
108
109            // Start time
110            metrics::gauge!(format!("{ns}_process_start_time_seconds")).set(self.start_time);
111        }
112    }
113}
114
115/// Count open file descriptors (Linux only).
116#[cfg(target_os = "linux")]
117fn count_open_fds() -> std::io::Result<usize> {
118    let fd_dir = format!("/proc/{}/fd", std::process::id());
119    std::fs::read_dir(fd_dir).map(|entries| entries.count())
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_process_metrics_new() {
128        let pm = ProcessMetrics::new("test");
129        assert_eq!(pm.namespace, "test");
130        assert!(pm.start_time > 0.0);
131    }
132
133    #[test]
134    fn test_process_metrics_update() {
135        let pm = ProcessMetrics::new("test");
136        // Should not panic
137        pm.update();
138    }
139}