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#[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}