Skip to main content

fastmetrics_process/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3#![deny(unsafe_code)]
4#![deny(unused_crate_dependencies)]
5#![cfg_attr(docsrs, feature(doc_cfg))]
6
7use std::{process, sync::LazyLock};
8
9use fastmetrics::{
10    error::Result,
11    metrics::{
12        counter::LazyCounter,
13        gauge::{ConstGauge, LazyGauge},
14        lazy_group::LazyGroup,
15    },
16    registry::{Register, Registry, Unit},
17};
18use parking_lot::Mutex;
19use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
20
21/// A set of process metrics aligned with Prometheus' standard naming conventions.
22///
23/// This type implements [`fastmetrics::registry::Register`].
24///
25/// To get the standard Prometheus-style metric names (`process_*`), register into
26/// `registry.subsystem("process")?`.
27#[derive(Clone)]
28pub struct ProcessMetrics {
29    pid: ConstGauge<i64>,
30    cpu_seconds_total: LazyCounter<f64>,
31    cpu_usage_percent: LazyGauge<f32>,
32    resident_memory_bytes: LazyGauge<i64>,
33    virtual_memory_bytes: LazyGauge<i64>,
34    start_time_seconds: LazyGauge<i64>,
35    run_time_seconds: LazyGauge<i64>,
36    open_fds: LazyGauge<i64>,
37    max_fds: LazyGauge<i64>,
38    threads: LazyGauge<i64>,
39}
40
41static PROCESS_SAMPLER: LazyLock<ProcessSampler> = LazyLock::new(ProcessSampler::new);
42
43impl Default for ProcessMetrics {
44    fn default() -> Self {
45        let group: LazyGroup<ProcessSample> = LazyGroup::new(|| PROCESS_SAMPLER.sample());
46        Self {
47            pid: ConstGauge::new(PROCESS_SAMPLER.pid.as_u32() as i64),
48            cpu_seconds_total: group.counter(|s| s.cpu_seconds_total),
49            cpu_usage_percent: group.gauge(|s| s.cpu_usage_percent),
50            resident_memory_bytes: group.gauge(|s| s.resident_memory_bytes),
51            virtual_memory_bytes: group.gauge(|s| s.virtual_memory_bytes),
52            start_time_seconds: group.gauge(|s| s.start_time_seconds),
53            run_time_seconds: group.gauge(|s| s.run_time_seconds),
54            open_fds: group.gauge(|s| s.open_fds),
55            max_fds: group.gauge(|s| s.max_fds),
56            threads: group.gauge(|s| s.threads),
57        }
58    }
59}
60
61impl Register for ProcessMetrics {
62    fn register(&self, registry: &mut Registry) -> Result<()> {
63        registry.register("pid", "Process ID.", self.pid.clone())?;
64        registry.register_with_unit(
65            "cpu",
66            "Total user and system CPU time spent in seconds.",
67            Unit::Seconds,
68            self.cpu_seconds_total.clone(),
69        )?;
70        registry.register(
71            "cpu_usage_percent",
72            "CPU usage of the process in percent.",
73            self.cpu_usage_percent.clone(),
74        )?;
75        registry.register_with_unit(
76            "resident_memory",
77            "Resident memory size in bytes.",
78            Unit::Bytes,
79            self.resident_memory_bytes.clone(),
80        )?;
81        registry.register_with_unit(
82            "virtual_memory",
83            "Virtual memory size in bytes.",
84            Unit::Bytes,
85            self.virtual_memory_bytes.clone(),
86        )?;
87        registry.register_with_unit(
88            "start_time",
89            "Start time of the process since unix epoch in seconds.",
90            Unit::Seconds,
91            self.start_time_seconds.clone(),
92        )?;
93        registry.register_with_unit(
94            "run_time",
95            "Process run time in seconds.",
96            Unit::Seconds,
97            self.run_time_seconds.clone(),
98        )?;
99        registry.register("open_fds", "Number of open file descriptors.", self.open_fds.clone())?;
100        registry.register(
101            "max_fds",
102            "Maximum number of open file descriptors.",
103            self.max_fds.clone(),
104        )?;
105        registry.register(
106            "threads",
107            "Number of OS threads in the process.",
108            self.threads.clone(),
109        )?;
110        Ok(())
111    }
112}
113
114#[derive(Clone, Copy, Default)]
115struct ProcessSample {
116    cpu_seconds_total: f64,
117    cpu_usage_percent: f32,
118    resident_memory_bytes: i64,
119    virtual_memory_bytes: i64,
120    start_time_seconds: i64,
121    run_time_seconds: i64,
122    open_fds: i64,
123    max_fds: i64,
124    threads: i64,
125}
126
127struct ProcessSampler {
128    pid: Pid,
129    system: Mutex<System>,
130}
131
132impl ProcessSampler {
133    fn new() -> Self {
134        let pid = Pid::from_u32(process::id());
135        let mut system = System::new();
136
137        sample(&mut system, pid);
138
139        Self { pid, system: Mutex::new(system) }
140    }
141
142    fn sample(&self) -> ProcessSample {
143        let mut system = self.system.lock();
144        sample(&mut system, self.pid)
145    }
146}
147
148fn sample(system: &mut System, pid: Pid) -> ProcessSample {
149    system.refresh_processes_specifics(
150        ProcessesToUpdate::Some(&[pid]),
151        true,
152        ProcessRefreshKind::everything(),
153    );
154
155    let Some(process) = system.process(pid) else {
156        return ProcessSample::default();
157    };
158
159    ProcessSample {
160        cpu_seconds_total: process.accumulated_cpu_time() as f64 / 1_000.0,
161        cpu_usage_percent: process.cpu_usage(),
162        resident_memory_bytes: u64_to_i64_saturating(process.memory()),
163        virtual_memory_bytes: u64_to_i64_saturating(process.virtual_memory()),
164        start_time_seconds: u64_to_i64_saturating(process.start_time()),
165        run_time_seconds: u64_to_i64_saturating(process.run_time()),
166        open_fds: process.open_files().unwrap_or(0) as i64,
167        max_fds: process.open_files_limit().unwrap_or(0) as i64,
168        threads: process.tasks().map(|t| t.len()).unwrap_or(0) as i64,
169    }
170}
171
172#[inline]
173fn u64_to_i64_saturating(v: u64) -> i64 {
174    if v > i64::MAX as u64 { i64::MAX } else { v as i64 }
175}