measured_process/
lib.rs

1//! Monitor a process.
2//!
3//! This crate only supports **Linux** platform.
4//!
5//! # Usage
6//!
7//! ```
8//! use measured::MetricGroup;
9//!
10//! #[derive(MetricGroup)]
11//! #[metric(new())]
12//! struct MyAppMetrics {
13//!     #[cfg(target_os = "linux")]
14//!     #[metric(namespace = "process")]
15//!     #[metric(init = measured_process::ProcessCollector::for_self())]
16//!     process: measured_process::ProcessCollector,
17//!
18//!     // other metrics
19//! }
20//!
21//! let metrics = MyAppMetrics::new();
22//!
23//! // when you run metrics.collect_group_into(...), you will sample procfs to get process stats.
24//!
25//! # drop(metrics);
26//! ```
27
28use std::sync::OnceLock;
29
30use libc::pid_t;
31use measured::{
32    MetricGroup,
33    metric::{
34        MetricEncoding,
35        gauge::{GaugeState, write_gauge},
36        group::Encoding,
37    },
38};
39
40// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
41// https://github.com/tikv/rust-prometheus/blob/f49c724df0e123520554664436da68e555593af0/src/process_collector.rs
42// With modifications by Conrad Ludgate for the transition to measured.
43
44/// A collector which exports the current state of process metrics including
45/// CPU, memory and file descriptor usage, thread count, as well as the process
46/// start time for the given process id.
47pub struct ProcessCollector {
48    pid: pid_t,
49    start_time: Option<i64>,
50}
51
52impl ProcessCollector {
53    /// Create a `ProcessCollector` with the given process id and namespace.
54    pub fn new(pid: pid_t) -> ProcessCollector {
55        // proc_start_time init once because it is immutable
56        let mut start_time = None;
57        #[cfg(target_os = "linux")]
58        if let Ok(boot_time) = procfs::boot_time_secs() {
59            if let Ok(stat) = procfs::process::Process::myself().and_then(|p| p.stat()) {
60                start_time = Some(stat.starttime as i64 / clk_tck() + boot_time as i64);
61            }
62        }
63
64        ProcessCollector { pid, start_time }
65    }
66
67    /// Return a `ProcessCollector` of the calling process.
68    pub fn for_self() -> ProcessCollector {
69        let pid = unsafe { libc::getpid() };
70        ProcessCollector::new(pid)
71    }
72}
73
74impl<Enc: Encoding> MetricGroup<Enc> for ProcessCollector
75where
76    GaugeState: MetricEncoding<Enc>,
77{
78    fn collect_group_into(&self, enc: &mut Enc) -> Result<(), Enc::Err> {
79        #[cfg(target_os = "linux")]
80        {
81            use measured::label::NoLabels;
82            use measured::metric::name::MetricName;
83
84            let Ok(p) = procfs::process::Process::new(self.pid) else {
85                // we can't construct a Process object, so there's no stats to gather
86                return Ok(());
87            };
88
89            // file descriptors
90            if let Ok(fd_count) = p.fd_count() {
91                let fd = MetricName::from_str("open_fds");
92                enc.write_help(fd, "Number of open file descriptors.")?;
93                write_gauge(enc, fd, NoLabels, fd_count as i64)?;
94            }
95            if let Ok(limits) = p.limits() {
96                if let procfs::process::LimitValue::Value(max) = limits.max_open_files.soft_limit {
97                    let fd = MetricName::from_str("max_fds");
98                    enc.write_help(fd, "Maximum number of open file descriptors.")?;
99                    write_gauge(enc, fd, NoLabels, max as i64)?;
100                }
101            }
102
103            if let Ok(stat) = p.stat() {
104                // memory
105                let vmm = MetricName::from_str("virtual_memory_bytes");
106                enc.write_help(vmm, "Virtual memory size in bytes.")?;
107                write_gauge(enc, vmm, NoLabels, stat.vsize as i64)?;
108
109                let rss = MetricName::from_str("resident_memory_bytes");
110                enc.write_help(rss, "Resident memory size in bytes.")?;
111                write_gauge(enc, rss, NoLabels, (stat.rss as i64) * pagesize())?;
112
113                // cpu
114                let cpu = MetricName::from_str("cpu_seconds_total");
115                enc.write_help(cpu, "Total user and system CPU time spent in seconds.")?;
116                write_gauge(
117                    enc,
118                    cpu,
119                    NoLabels,
120                    (stat.utime + stat.stime) as i64 / clk_tck(),
121                )?;
122
123                // threads
124                let threads = MetricName::from_str("threads");
125                enc.write_help(threads, "Number of OS threads in the process.")?;
126                write_gauge(enc, threads, NoLabels, stat.num_threads)?;
127            }
128
129            if let Some(start_time) = self.start_time {
130                let name = MetricName::from_str("start_time_seconds");
131                enc.write_help(
132                    name,
133                    "Start time of the process since unix epoch in seconds.",
134                )?;
135                write_gauge(enc, name, NoLabels, start_time)?;
136            }
137        }
138
139        Ok(())
140    }
141}
142
143fn clk_tck() -> i64 {
144    static CLK_TCK: OnceLock<i64> = OnceLock::new();
145    *CLK_TCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as i64)
146}
147
148fn pagesize() -> i64 {
149    static PAGESIZE: OnceLock<i64> = OnceLock::new();
150    *PAGESIZE.get_or_init(|| unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as i64)
151}