Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenBSD support #59

Merged
merged 5 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,17 @@ jobs:
files: lcov.info,lcov.use-gauge-on-cpu-seconds-total.info
env_vars: RUNNER

test-cross-platform-freebsd:
test-cross-platform:
strategy:
matrix:
# don't forget to update current os versions, see https://github.com/cross-platform-actions/action?tab=readme-ov-file#supported-platforms
include:
- { os: freebsd, version: "14.1", prepare_cmd: "sudo pkg install -y rust cmake rust-bindgen-cli" }
# 7.6 is not supported by action@v0.25.0 yet
- { os: openbsd, version: "7.5", prepare_cmd: "sudo pkg_add rust cmake llvm%17; export LIBCLANG_PATH=/usr/local/llvm17/lib" }
fail-fast: false
runs-on: ubuntu-latest
name: "test (${{matrix.os}}-${{matrix.version}})"
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
Expand All @@ -116,12 +125,12 @@ jobs:
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: freebsd-14.1-cargo-${{ hashFiles('**/Cargo.lock') }}
key: ${{matrix.os}}-${{matrix.version}}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Cross-platform test
uses: cross-platform-actions/action@v0.25.0
with:
operating_system: freebsd
version: '14.1'
operating_system: ${{matrix.os}}
version: "${{matrix.version}}"
run: |
sudo pkg update && sudo pkg install -y rust cmake rust-bindgen-cli
cargo test
${{matrix.prepare_cmd}}
cargo test -- --nocapture
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ procfs = { version = "0.17.0", default-features = false }
[target.'cfg(target_os = "freebsd")'.dependencies]
libc = "0.2.159"

[target.'cfg(target_os = "openbsd")'.dependencies]
libc = "0.2.159"

[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.58.0"
features = [
Expand Down
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
# ⏱ metrics-process

This crate provides a [Prometheus]-style [process metrics] collector for the
[metrics] crate, supporting Linux, macOS, Windows, and FreeBSD. The original
collector code was manually rewritten in Rust from the official Prometheus
client for Go ([client_golang]), FreeBSD support was written from scratch.
[metrics] crate, supporting Linux, macOS, Windows, FreeBSD, and OpenBSD. The
original collector code was manually rewritten in Rust from the official
Prometheus client for Go ([client_golang]), \*BSD support code was written
from scratch.

[Prometheus]: https://prometheus.io/
[process metrics]: https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics
Expand Down Expand Up @@ -41,17 +42,17 @@ Go ([client_golang]) provides.
> Prior to version 2.0.0, the `process_cpu_seconds_total` metric was Gauge instead of Counter.
> Enable `use-gauge-on-cpu-seconds-total` feature to use the previous behavior.

| Metric name | Linux | macOS | Windows | FreeBSD |
| ---------------------------------- | ----- | ----- | ------- | ------- |
| `process_cpu_seconds_total` | x | x | x | x |
| `process_open_fds` | x | x | x | x |
| `process_max_fds` | x | x | x | x |
| `process_virtual_memory_bytes` | x | x | x | x |
| `process_virtual_memory_max_bytes` | x | x | | x |
| `process_resident_memory_bytes` | x | x | x | x |
| `process_heap_bytes` | | | | |
| `process_start_time_seconds` | x | x | x | x |
| `process_threads` | x | x | | x |
| Metric name | Linux | macOS | Windows | FreeBSD | OpenBSD |
| ---------------------------------- | ----- | ----- | ------- | ------- | ------- |
| `process_cpu_seconds_total` | x | x | x | x | x |
| `process_open_fds` | x | x | x | x | |
| `process_max_fds` | x | x | x | x | x |
| `process_virtual_memory_bytes` | x | x | x | x | |
| `process_virtual_memory_max_bytes` | x | x | | x | |
| `process_resident_memory_bytes` | x | x | x | x | x |
| `process_heap_bytes` | | | | | |
| `process_start_time_seconds` | x | x | x | x | x |
| `process_threads` | x | x | | x | |

> [!NOTE]
>
Expand Down
23 changes: 22 additions & 1 deletion src/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#[cfg_attr(target_os = "linux", path = "collector/linux.rs")]
#[cfg_attr(target_os = "windows", path = "collector/windows.rs")]
#[cfg_attr(target_os = "freebsd", path = "collector/freebsd.rs")]
#[cfg_attr(target_os = "openbsd", path = "collector/openbsd.rs")]
#[allow(unused_attributes)]
#[cfg_attr(feature = "dummy", path = "collector/dummy.rs")]
mod collector;
Expand All @@ -12,7 +13,8 @@ mod collector;
target_os = "macos",
target_os = "linux",
target_os = "windows",
target_os = "freebsd"
target_os = "freebsd",
target_os = "openbsd"
))
))]
compile_error!(
Expand Down Expand Up @@ -81,10 +83,29 @@ mod tests {
assert_matches!(m.threads, Some(_));
}

#[cfg(target_os = "openbsd")]
#[test]
fn test_collect_internal_ok_openbsd() {
// TODO: if more metrics is implemented for OpenBSD, merge this test into
// test_collect_internal_ok
fibonacci(40);
let m = collect();
dbg!(&m);
assert_matches!(m.cpu_seconds_total, Some(_));
assert_matches!(m.open_fds, None);
assert_matches!(m.max_fds, Some(_));
assert_matches!(m.virtual_memory_bytes, None);
assert_matches!(m.virtual_memory_max_bytes, None);
assert_matches!(m.resident_memory_bytes, Some(_));
assert_matches!(m.start_time_seconds, Some(_));
assert_matches!(m.threads, None);
}

#[cfg(not(target_os = "macos"))]
#[cfg(not(target_os = "linux"))]
#[cfg(not(target_os = "windows"))]
#[cfg(not(target_os = "freebsd"))]
#[cfg(not(target_os = "openbsd"))]
#[cfg(feature = "dummy")]
#[test]
fn test_collect_internal_ok_dummy() {
Expand Down
107 changes: 107 additions & 0 deletions src/collector/openbsd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::convert::TryInto as _;

use super::Metrics;

fn getrusage(who: libc::c_int) -> Option<libc::rusage> {
let mut usage = std::mem::MaybeUninit::zeroed();
// SAFETY: libc call; usage is valid pointer to rusage struct
if unsafe { libc::getrusage(who, usage.as_mut_ptr()) } == 0 {
// SAFETY: libc call was success, struct must be initialized
Some(unsafe { usage.assume_init() })
} else {
None
}
}

fn getrlimit(resource: libc::c_int) -> Option<libc::rlimit> {
let mut limit = std::mem::MaybeUninit::zeroed();
// SAFETY: libc call; limit is valid pointer to rlimit struct
if unsafe { libc::getrlimit(resource, limit.as_mut_ptr()) } == 0 {
// SAFETY: libc call was success, struct must be initialized
Some(unsafe { limit.assume_init() })
} else {
None
}
}

fn translate_rlim(rlim: libc::rlim_t) -> u64 {
if rlim == libc::RLIM_INFINITY {
0
} else {
rlim as u64
}
}
Comment on lines +27 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review handling of RLIM_INFINITY in translate_rlim function

In the translate_rlim function, mapping RLIM_INFINITY to 0 might be misleading, as 0 could be interpreted as no resource limit rather than an infinite limit. Consider using u64::MAX or a sentinel value that more clearly represents an infinite limit.

Apply this diff to adjust the handling:

fn translate_rlim(rlim: libc::rlim_t) -> u64 {
    if rlim == libc::RLIM_INFINITY {
-       0
+       u64::MAX
    } else {
        rlim as u64
    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn translate_rlim(rlim: libc::rlim_t) -> u64 {
if rlim == libc::RLIM_INFINITY {
0
} else {
rlim as u64
}
}
fn translate_rlim(rlim: libc::rlim_t) -> u64 {
if rlim == libc::RLIM_INFINITY {
u64::MAX
} else {
rlim as u64
}
}


fn kinfo_getproc(pid: libc::pid_t) -> Option<libc::kinfo_proc> {
let mut kinfo_proc = std::mem::MaybeUninit::zeroed();
let kinfo_proc_size = std::mem::size_of_val(&kinfo_proc) as libc::size_t;
let mut data_size = kinfo_proc_size;

// code from deno doing similar stuff: https://github.com/denoland/deno/blob/20ae8db50d7d48ad020b83ebe78dc0e9e9eab3b2/runtime/ops/os/mod.rs#L415
let mib = [
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_PID,
pid,
// this is required because MIB is array of ints, and is safe
// as long size of kinfo_proc structure doesn't exceed 2GB
kinfo_proc_size.try_into().unwrap(),
1,
];

Comment on lines +46 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential failure in try_into conversion

At line 48, kinfo_proc_size.try_into().unwrap() could panic if the conversion fails. Although the comment mentions safety due to the size constraint, it's safer to handle the Result explicitly to prevent possible panics.

Apply this diff to gracefully handle conversion errors:

let mib = [
    libc::CTL_KERN,
    libc::KERN_PROC,
    libc::KERN_PROC_PID,
    pid,
-   kinfo_proc_size.try_into().unwrap(),
+   match kinfo_proc_size.try_into() {
+       Ok(size) => size,
+       Err(_) => return None, // Handle the error appropriately
+   },
    1,
];

Alternatively, if you're confident the size will always fit, consider using expect with a descriptive message:

kinfo_proc_size.try_into().expect("kinfo_proc_size exceeds libc::c_int range"),

Committable suggestion was skipped due to low confidence.

// SAFETY: libc call; mib is statically initialized, kinfo_proc is valid pointer
// to kinfo_proc and data_size holds its size
if unsafe {
libc::sysctl(
mib.as_ptr(),
mib.len() as _,
kinfo_proc.as_mut_ptr() as *mut libc::c_void,
&mut data_size,
std::ptr::null_mut(),
0,
)
} == 0
&& data_size == kinfo_proc_size
{
// SAFETY: libc call was success and check for struct size passed, struct must be initialized
Some(unsafe { kinfo_proc.assume_init() })
} else {
None
}
}

pub fn collect() -> Metrics {
let mut metrics = Metrics::default();

// TODO: this is based on freebsd.rs, but lacks
// - virtual_memory_bytes (kinfo_proc::p_vm_map_size contains zero)
// - virtual_memory_max_bytes (openbsd lacks RLIMIT_AS)
// - threads (no corresponding field in kinfo_proc(
// - open_fds (no idea where to get it from)

if let Some(usage) = getrusage(libc::RUSAGE_SELF) {
metrics.cpu_seconds_total = Some(
(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) as f64
+ (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec) as f64 / 1000000.0,
);
}

if let Some(limit_as) = getrlimit(libc::RLIMIT_NOFILE) {
metrics.max_fds = Some(translate_rlim(limit_as.rlim_cur));
}

// SAFETY: libc call
let pid = unsafe { libc::getpid() };

if let Some(kinfo_proc) = kinfo_getproc(pid) {
// reference:
// https://github.com/openbsd/src/blob/782feb691bc15d1abd5f5c66fe3c0d336903a461/sys/sys/sysctl.h#L370

// SAFETY: libc call
let pagesize = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
metrics.resident_memory_bytes = Some(kinfo_proc.p_vm_rssize as u64 * pagesize);
metrics.start_time_seconds = Some(kinfo_proc.p_ustart_sec);
}

metrics
}