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(
65            "cpu_seconds_total",
66            "Total user and system CPU time spent in seconds.",
67            self.cpu_seconds_total.clone(),
68        )?;
69        registry.register(
70            "cpu_usage_percent",
71            "CPU usage of the process in percent.",
72            self.cpu_usage_percent.clone(),
73        )?;
74        registry.register_with_unit(
75            "resident_memory",
76            "Resident memory size in bytes.",
77            Unit::Bytes,
78            self.resident_memory_bytes.clone(),
79        )?;
80        registry.register_with_unit(
81            "virtual_memory",
82            "Virtual memory size in bytes.",
83            Unit::Bytes,
84            self.virtual_memory_bytes.clone(),
85        )?;
86        registry.register_with_unit(
87            "start_time",
88            "Start time of the process since unix epoch in seconds.",
89            Unit::Seconds,
90            self.start_time_seconds.clone(),
91        )?;
92        registry.register_with_unit(
93            "run_time",
94            "Process run time in seconds.",
95            Unit::Seconds,
96            self.run_time_seconds.clone(),
97        )?;
98        registry.register("open_fds", "Number of open file descriptors.", self.open_fds.clone())?;
99        registry.register(
100            "max_fds",
101            "Maximum number of open file descriptors.",
102            self.max_fds.clone(),
103        )?;
104        registry.register(
105            "threads",
106            "Number of OS threads in the process.",
107            self.threads.clone(),
108        )?;
109        Ok(())
110    }
111}
112
113#[derive(Clone, Copy, Default)]
114struct ProcessSample {
115    cpu_seconds_total: f64,
116    cpu_usage_percent: f32,
117    resident_memory_bytes: i64,
118    virtual_memory_bytes: i64,
119    start_time_seconds: i64,
120    run_time_seconds: i64,
121    open_fds: i64,
122    max_fds: i64,
123    threads: i64,
124}
125
126struct ProcessSampler {
127    pid: Pid,
128    system: Mutex<System>,
129}
130
131impl ProcessSampler {
132    fn new() -> Self {
133        let pid = Pid::from_u32(process::id());
134        let mut system = System::new();
135
136        sample(&mut system, pid);
137
138        Self { pid, system: Mutex::new(system) }
139    }
140
141    fn sample(&self) -> ProcessSample {
142        let mut system = self.system.lock();
143        sample(&mut system, self.pid)
144    }
145}
146
147fn sample(system: &mut System, pid: Pid) -> ProcessSample {
148    system.refresh_processes_specifics(
149        ProcessesToUpdate::Some(&[pid]),
150        true,
151        ProcessRefreshKind::everything(),
152    );
153
154    let Some(process) = system.process(pid) else {
155        return ProcessSample::default();
156    };
157
158    ProcessSample {
159        cpu_seconds_total: process.accumulated_cpu_time() as f64 / 1_000.0,
160        cpu_usage_percent: process.cpu_usage(),
161        resident_memory_bytes: u64_to_i64_saturating(process.memory()),
162        virtual_memory_bytes: u64_to_i64_saturating(process.virtual_memory()),
163        start_time_seconds: u64_to_i64_saturating(process.start_time()),
164        run_time_seconds: u64_to_i64_saturating(process.run_time()),
165        open_fds: process.open_files().unwrap_or(0) as i64,
166        max_fds: process.open_files_limit().unwrap_or(0) as i64,
167        threads: process.tasks().map(|t| t.len()).unwrap_or(0) as i64,
168    }
169}
170
171#[inline]
172fn u64_to_i64_saturating(v: u64) -> i64 {
173    if v > i64::MAX as u64 { i64::MAX } else { v as i64 }
174}