This commit is contained in:
Mateusz Gruszczyński committed 2026-10-06 15:31:59 +02:00
1 parent f07429c267
commit 5d2aaeabe6
11 files changed
+302 -14

No files matched your search

+208 -2
View File
@@ -1,6 +1,31 @@
use super::View;
use std::fs;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinuxEnvironment {
Lxc,
Docker,
Podman,
SystemdNspawn,
Wsl,
Vm,
Host,
}
impl LinuxEnvironment {
fn label(self) -> &'static str {
match self {
Self::Lxc => "LXC",
Self::Docker => "Docker",
Self::Podman => "Podman",
Self::SystemdNspawn => "systemd-nspawn",
Self::Wsl => "WSL",
Self::Vm => "VM",
Self::Host => "Host",
}
}
}
fn key_value(text: &str, name: &str) -> Option<u64> {
text.lines().find_map(|line| {
let (key, rest) = line.split_once(':')?;
@@ -10,7 +35,180 @@ fn key_value(text: &str, name: &str) -> Option<u64> {
})
}
fn read_text(path: &str) -> String {
fs::read(path)
.ok()
.map(|bytes| String::from_utf8_lossy(&bytes).replace('\0', "\n"))
.unwrap_or_default()
}
fn lxcfs_proc_override(mountinfo: &str) -> bool {
mountinfo.lines().any(|line| {
line.contains("lxcfs")
&& (line.contains(" /proc/uptime ") || line.contains(" /proc/loadavg "))
})
}
fn classify_environment(
systemd_container: &str,
pid1_environment: &str,
cgroup: &str,
mountinfo: &str,
dmi: &str,
hypervisor: &str,
osrelease: &str,
dockerenv: bool,
containerenv: bool,
) -> LinuxEnvironment {
let systemd = systemd_container.trim().to_ascii_lowercase();
let pid1 = pid1_environment.to_ascii_lowercase();
let cgroup = cgroup.to_ascii_lowercase();
let dmi = dmi.to_ascii_lowercase();
let hypervisor = hypervisor.trim().to_ascii_lowercase();
let osrelease = osrelease.to_ascii_lowercase();
if systemd.starts_with("lxc")
|| pid1.lines().any(|line| line.trim() == "container=lxc")
|| cgroup.contains("lxc.payload")
|| cgroup.contains("/lxc/")
|| cgroup.contains("machine-lxc\\x2d")
|| lxcfs_proc_override(mountinfo)
{
return LinuxEnvironment::Lxc;
}
if dockerenv
|| systemd == "docker"
|| pid1.lines().any(|line| line.trim() == "container=docker")
|| cgroup.contains("/docker/")
|| cgroup.contains("docker-")
{
return LinuxEnvironment::Docker;
}
if containerenv
|| systemd == "podman"
|| pid1.lines().any(|line| line.trim() == "container=podman")
|| cgroup.contains("libpod")
{
return LinuxEnvironment::Podman;
}
if systemd == "systemd-nspawn" || cgroup.contains("machine.slice/machine-") {
return LinuxEnvironment::SystemdNspawn;
}
if osrelease.contains("microsoft") || osrelease.contains("wsl") {
return LinuxEnvironment::Wsl;
}
if !hypervisor.is_empty()
|| [
"kvm",
"qemu",
"vmware",
"virtualbox",
"virtual machine",
"xen",
"bochs",
"bhyve",
"parallels",
"amazon ec2",
"google compute engine",
"openstack",
"rhev",
"nutanix",
]
.iter()
.any(|marker| dmi.contains(marker))
{
return LinuxEnvironment::Vm;
}
LinuxEnvironment::Host
}
fn detect_environment() -> LinuxEnvironment {
let systemd_container = read_text("/run/systemd/container");
let pid1_environment = read_text("/proc/1/environ");
let cgroup = format!(
"{}\n{}",
read_text("/proc/1/cgroup"),
read_text("/proc/self/cgroup")
);
let mountinfo = read_text("/proc/self/mountinfo");
let dmi = [
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
"/sys/class/dmi/id/bios_vendor",
]
.into_iter()
.map(read_text)
.collect::<Vec<_>>()
.join("\n");
let hypervisor = read_text("/sys/hypervisor/type");
let osrelease = read_text("/proc/sys/kernel/osrelease");
classify_environment(
&systemd_container,
&pid1_environment,
&cgroup,
&mountinfo,
&dmi,
&hypervisor,
&osrelease,
std::path::Path::new("/.dockerenv").exists(),
std::path::Path::new("/run/.containerenv").exists(),
)
}
fn parse_load_average(text: &str) -> Option<[f64; 3]> {
let mut values = text.split_whitespace().take(3).map(str::parse::<f64>);
let load = [
values.next()?.ok()?,
values.next()?.ok()?,
values.next()?.ok()?,
];
load.iter()
.all(|value| value.is_finite() && *value >= 0.0)
.then_some(load)
}
fn process_start_ticks(stat: &str) -> Option<u64> {
let end_of_name = stat.rfind(')')?;
stat.get(end_of_name + 1..)?
.split_whitespace()
.nth(19)?
.parse()
.ok()
}
fn boottime_seconds() -> Option<f64> {
let mut value = std::mem::MaybeUninit::<libc::timespec>::uninit();
let rc = unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, value.as_mut_ptr()) };
if rc != 0 {
return None;
}
let value = unsafe { value.assume_init() };
Some(value.tv_sec as f64 + value.tv_nsec as f64 / 1_000_000_000.0)
}
fn clock_ticks_per_second() -> Option<f64> {
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
(ticks > 0).then_some(ticks as f64)
}
fn lxc_uptime_seconds() -> Option<u64> {
let stat = fs::read_to_string("/proc/1/stat").ok()?;
let started = process_start_ticks(&stat)? as f64 / clock_ticks_per_second()?;
let age = boottime_seconds()? - started;
(age.is_finite() && age >= 0.0).then_some(age.floor() as u64)
}
fn lxc_load_average() -> Option<[f64; 3]> {
fs::read_to_string("/proc/loadavg")
.ok()
.and_then(|text| parse_load_average(&text))
}
pub(crate) fn augment(view: &mut View) {
let environment = detect_environment();
view.environment = Some(environment.label());
if let Ok(stat) = fs::read_to_string("/proc/stat") {
view.logical_cpus = Some(
stat.lines()
@@ -23,8 +221,16 @@ pub(crate) fn augment(view: &mut View) {
.filter(|count| *count > 0);
}
// Linux-specific augmentation preserves fields and units that existed before
// the cross-platform library adapter. Generic CPU/load/network/process
// metrics are deliberately collected by the shared API in mod.rs.
// the cross-platform library adapter. LXC additionally overrides uptime/load
// with namespace-visible data so the dashboard does not report the host values.
if environment == LinuxEnvironment::Lxc {
if let Some(uptime) = lxc_uptime_seconds() {
view.system_uptime_seconds = Some(uptime);
}
if let Some(load) = lxc_load_average() {
view.load_average = Some(load);
}
}
if let Ok(mem) = fs::read_to_string("/proc/meminfo") {
view.memory_total_bytes = key_value(&mem, "MemTotal").map(|n| n.saturating_mul(1024));
view.memory_used_bytes = view
+56
View File
@@ -12,4 +12,60 @@ mod tests {
);
assert_eq!(key_value("", "VmRSS"), None);
}
#[test]
fn parses_load_average_from_proc() {
assert_eq!(
parse_load_average("0.25 1.50 2.75 1/100 123\n"),
Some([0.25, 1.5, 2.75])
);
assert_eq!(parse_load_average("bad data"), None);
}
#[test]
fn parses_pid_one_start_time_with_spaces_in_name() {
let prefix = "1 (init process) S";
let middle = (4..22).map(|value| value.to_string()).collect::<Vec<_>>();
let stat = format!("{prefix} {} 4242 0 0", middle.join(" "));
assert_eq!(process_start_ticks(&stat), Some(4242));
}
#[test]
fn lxc_detection_has_priority_over_vm_metadata() {
assert_eq!(
classify_environment(
"lxc",
"container=lxc\n",
"0::/",
"",
"QEMU",
"",
"6.8.0",
false,
false,
),
LinuxEnvironment::Lxc
);
}
#[test]
fn detects_vm_and_plain_host_without_changing_metrics_policy() {
assert_eq!(
classify_environment("", "", "", "", "QEMU Standard PC", "", "", false, false),
LinuxEnvironment::Vm
);
assert_eq!(
classify_environment("", "", "", "", "Dell PowerEdge", "", "", false, false),
LinuxEnvironment::Host
);
}
#[test]
fn lxcfs_proc_mount_identifies_lxc_environment() {
let mountinfo = "100 99 0:55 / /proc/loadavg rw - fuse.lxcfs lxcfs rw\n";
assert_eq!(
classify_environment("", "", "", mountinfo, "", "", "", false, false),
LinuxEnvironment::Lxc
);
}
}
+1
View File
@@ -21,6 +21,7 @@ pub struct NetworkInterfaceView {
pub struct View {
pub source: &'static str,
pub platform: &'static str,
pub environment: Option<&'static str>,
pub sampled_at_ms: u64,
pub cpu_percent: Option<f64>,
pub process_cpu_percent: Option<f64>,