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