Skip to content

Commit 0bd9266

Browse files
authored
Merge pull request #53 from AMDmi3/freebsd
Add FreeBSD support
2 parents f854e88 + 5024f5e commit 0bd9266

File tree

5 files changed

+165
-16
lines changed

5 files changed

+165
-16
lines changed

.github/workflows/test.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,25 @@ jobs:
103103
token: ${{ secrets.CODECOV_TOKEN }}
104104
files: lcov.info,lcov.use-gauge-on-cpu-seconds-total.info
105105
env_vars: RUNNER
106+
107+
test-cross-platform-freebsd:
108+
runs-on: ubuntu-latest
109+
steps:
110+
- uses: actions/checkout@v4
111+
- uses: actions/cache@v4
112+
with:
113+
path: |
114+
~/.cargo/bin/
115+
~/.cargo/registry/index/
116+
~/.cargo/registry/cache/
117+
~/.cargo/git/db/
118+
target/
119+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
120+
- name: Cross-platform test
121+
uses: cross-platform-actions/[email protected]
122+
with:
123+
operating_system: freebsd
124+
version: '14.1'
125+
run: |
126+
sudo pkg update && sudo pkg install -y rust cmake rust-bindgen-cli
127+
cargo test

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ rlimit = "0.10.0"
4242
once_cell = "1.13.1"
4343
procfs = { version = "0.16.0", default-features = false }
4444

45+
[target.'cfg(target_os = "freebsd")'.dependencies]
46+
libc = "0.2.159"
47+
4548
[target.'cfg(target_os = "windows")'.dependencies.windows]
4649
version = "0.57.0"
4750
features = [

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
# ⏱ metrics-process
99

1010
This crate provides a [Prometheus]-style [process metrics] collector for the
11-
[metrics] crate, supporting Linux, macOS, and Windows. The collector code is
12-
manually rewritten in Rust from the official Prometheus client for Go
13-
([client_golang]).
11+
[metrics] crate, supporting Linux, macOS, Windows, and FreeBSD. The original
12+
collector code was manually rewritten in Rust from the official Prometheus
13+
client for Go ([client_golang]), FreeBSD support was written from scratch.
1414

1515
[Prometheus]: https://prometheus.io/
1616
[process metrics]: https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics
@@ -41,17 +41,17 @@ Go ([client_golang]) provides.
4141
> Prior to version 2.0.0, the `process_cpu_seconds_total` metric was Gauge instead of Counter.
4242
> Enable `use-gauge-on-cpu-seconds-total` feature to use the previous behavior.
4343
44-
| Metric name | Linux | macOS | Windows |
45-
| ---------------------------------- | ----- | ----- | ------- |
46-
| `process_cpu_seconds_total` | x | x | x |
47-
| `process_open_fds` | x | x | x |
48-
| `process_max_fds` | x | x | x |
49-
| `process_virtual_memory_bytes` | x | x | x |
50-
| `process_virtual_memory_max_bytes` | x | x | |
51-
| `process_resident_memory_bytes` | x | x | x |
52-
| `process_heap_bytes` | | | |
53-
| `process_start_time_seconds` | x | x | x |
54-
| `process_threads` | x | x | |
44+
| Metric name | Linux | macOS | Windows | FreeBSD |
45+
| ---------------------------------- | ----- | ----- | ------- | ------- |
46+
| `process_cpu_seconds_total` | x | x | x | x |
47+
| `process_open_fds` | x | x | x | x |
48+
| `process_max_fds` | x | x | x | x |
49+
| `process_virtual_memory_bytes` | x | x | x | x |
50+
| `process_virtual_memory_max_bytes` | x | x | | x |
51+
| `process_resident_memory_bytes` | x | x | x | x |
52+
| `process_heap_bytes` | | | | |
53+
| `process_start_time_seconds` | x | x | x | x |
54+
| `process_threads` | x | x | | x |
5555

5656
> [!NOTE]
5757
>

src/collector.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
#[cfg_attr(target_os = "macos", path = "collector/macos.rs")]
22
#[cfg_attr(target_os = "linux", path = "collector/linux.rs")]
33
#[cfg_attr(target_os = "windows", path = "collector/windows.rs")]
4+
#[cfg_attr(target_os = "freebsd", path = "collector/freebsd.rs")]
45
#[allow(unused_attributes)]
56
#[cfg_attr(feature = "dummy", path = "collector/dummy.rs")]
67
mod collector;
78

89
#[cfg(all(
910
not(feature = "dummy"),
10-
not(any(target_os = "macos", target_os = "linux", target_os = "windows"))
11+
not(any(
12+
target_os = "macos",
13+
target_os = "linux",
14+
target_os = "windows",
15+
target_os = "freebsd"
16+
))
1117
))]
1218
compile_error!(
1319
"A feature \"dummy\" must be enabled to compile this crate on non supported platforms."
@@ -52,7 +58,12 @@ mod tests {
5258
}
5359
}
5460

55-
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
61+
#[cfg(any(
62+
target_os = "macos",
63+
target_os = "linux",
64+
target_os = "windows",
65+
target_os = "freebsd"
66+
))]
5667
#[test]
5768
fn test_collect_internal_ok() {
5869
fibonacci(40);
@@ -73,6 +84,7 @@ mod tests {
7384
#[cfg(not(target_os = "macos"))]
7485
#[cfg(not(target_os = "linux"))]
7586
#[cfg(not(target_os = "windows"))]
87+
#[cfg(not(target_os = "freebsd"))]
7688
#[cfg(feature = "dummy")]
7789
#[test]
7890
fn test_collect_internal_ok_dummy() {

src/collector/freebsd.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use super::Metrics;
2+
3+
fn getrusage(who: libc::c_int) -> Option<libc::rusage> {
4+
let mut usage = std::mem::MaybeUninit::zeroed();
5+
// SAFETY: libc call; usage is valid pointer to rusage struct
6+
if unsafe { libc::getrusage(who, usage.as_mut_ptr()) } == 0 {
7+
// SAFETY: libc call was success, struct must be initialized
8+
Some(unsafe { usage.assume_init() })
9+
} else {
10+
None
11+
}
12+
}
13+
14+
fn getrlimit(resource: libc::c_int) -> Option<libc::rlimit> {
15+
let mut limit = std::mem::MaybeUninit::zeroed();
16+
// SAFETY: libc call; limit is valid pointer to rlimit struct
17+
if unsafe { libc::getrlimit(resource, limit.as_mut_ptr()) } == 0 {
18+
// SAFETY: libc call was success, struct must be initialized
19+
Some(unsafe { limit.assume_init() })
20+
} else {
21+
None
22+
}
23+
}
24+
25+
fn translate_rlim(rlim: libc::rlim_t) -> u64 {
26+
if rlim == libc::RLIM_INFINITY {
27+
0
28+
} else {
29+
rlim as u64
30+
}
31+
}
32+
33+
fn kinfo_getproc(pid: libc::pid_t) -> Option<libc::kinfo_proc> {
34+
// References:
35+
// kinfo_getproc() code from FreeBSD: https://github.com/freebsd/freebsd-src/blob/b22be3bbb2de75157c97d8baa01ce6cd654caddf/lib/libutil/kinfo_getproc.c
36+
// code from deno doing similar stuff: https://github.com/denoland/deno/blob/20ae8db50d7d48ad020b83ebe78dc0e9e9eab3b2/runtime/ops/os/mod.rs#L415
37+
let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
38+
39+
let mut kinfo_proc = std::mem::MaybeUninit::zeroed();
40+
let kinfo_proc_size = std::mem::size_of_val(&kinfo_proc) as libc::size_t;
41+
let mut data_size = kinfo_proc_size;
42+
43+
// SAFETY: libc call; mib is statically initialized, kinfo_proc is valid pointer
44+
// to kinfo_proc and data_size holds its size
45+
if unsafe {
46+
libc::sysctl(
47+
mib.as_ptr(),
48+
mib.len() as _,
49+
kinfo_proc.as_mut_ptr() as *mut libc::c_void,
50+
&mut data_size,
51+
std::ptr::null(),
52+
0,
53+
)
54+
} == 0
55+
&& data_size == kinfo_proc_size
56+
{
57+
// SAFETY: libc call was success and check for struct size passed, struct must be initialized
58+
Some(unsafe { kinfo_proc.assume_init() })
59+
} else {
60+
None
61+
}
62+
}
63+
64+
pub fn collect() -> Metrics {
65+
let mut metrics = Metrics::default();
66+
67+
if let Some(usage) = getrusage(libc::RUSAGE_SELF) {
68+
metrics.cpu_seconds_total = Some(
69+
(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) as f64
70+
+ (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec) as f64 / 1000000.0,
71+
);
72+
}
73+
74+
if let Some(limit_as) = getrlimit(libc::RLIMIT_AS) {
75+
metrics.virtual_memory_max_bytes = Some(translate_rlim(limit_as.rlim_cur));
76+
}
77+
78+
if let Some(limit_as) = getrlimit(libc::RLIMIT_NOFILE) {
79+
metrics.max_fds = Some(translate_rlim(limit_as.rlim_cur));
80+
}
81+
82+
// SAFETY: libc call
83+
let pid = unsafe { libc::getpid() };
84+
85+
if let Some(kinfo_proc) = kinfo_getproc(pid) {
86+
// struct kinfo_proc layout for reference
87+
// libc crate: https://docs.rs/libc/latest/x86_64-unknown-freebsd/libc/struct.kinfo_proc.html
88+
// FreeBSD: https://github.com/freebsd/freebsd-src/blob/b22be3bbb2de75157c97d8baa01ce6cd654caddf/lib/libutil/kinfo_getfile.c
89+
90+
// SAFETY: libc call
91+
let pagesize = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
92+
metrics.virtual_memory_bytes = Some(kinfo_proc.ki_size as u64);
93+
metrics.resident_memory_bytes = Some(kinfo_proc.ki_rssize as u64 * pagesize);
94+
use std::convert::TryInto as _;
95+
metrics.start_time_seconds = kinfo_proc.ki_start.tv_sec.try_into().ok();
96+
metrics.threads = kinfo_proc.ki_numthreads.try_into().ok();
97+
98+
// note that we can't access pointers in kinfo_proc as these point to kernel space
99+
}
100+
101+
// Alternative to this would be implementing kinfo_getfile() like interface, see
102+
// https://github.com/freebsd/freebsd-src/blob/b22be3bbb2de75157c97d8baa01ce6cd654caddf/lib/libutil/kinfo_getfile.c
103+
// it can be done similar to kinfo_getproc() implementation above, but is more cumbersome
104+
// because we have to parse structures of varying size frow raw memory. As long as
105+
// it's common to read /proc on Linux, it shuld be as ok to read /dev/fs (which
106+
// is roughly the same as /proc/self/fd) on FreeBSD.
107+
metrics.open_fds = std::fs::read_dir("/dev/fd")
108+
.ok()
109+
.map(|read_dir| read_dir.count() as u64);
110+
111+
metrics
112+
}

0 commit comments

Comments
 (0)