Skip to main content

monitrs_core/model/
host.rs

1//! System identity and environment.
2
3use core::time::Duration;
4use std::time::SystemTime;
5
6use crate::model::{Confidence, MetricState};
7
8/// Whether we appear to be running on hardware, in a VM, or in a container.
9///
10/// §7.5 requires this to be *clearly labelled heuristic*, which is why
11/// [`HostEnvironment`] carries both the evidence and a [`Confidence`] and why
12/// there is no `BareMetal` variant — absence of container and VM evidence is not
13/// proof of bare metal.
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
17pub enum EnvironmentKind {
18    /// No container or virtualization evidence was found.
19    #[default]
20    NoEvidenceFound,
21    /// Container evidence was found, e.g. a Docker or Kubernetes cgroup path.
22    Container,
23    /// Virtualization evidence was found, e.g. a hypervisor DMI string.
24    VirtualMachine,
25}
26
27impl EnvironmentKind {
28    /// Lower-case label.
29    #[must_use]
30    pub const fn label(self) -> &'static str {
31        match self {
32            Self::NoEvidenceFound => "no container/VM evidence",
33            Self::Container => "container",
34            Self::VirtualMachine => "virtual machine",
35        }
36    }
37}
38
39/// A heuristic environment classification together with its evidence.
40#[derive(Clone, Debug, PartialEq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42pub struct HostEnvironment {
43    /// What we think this is.
44    pub kind: EnvironmentKind,
45    /// What led to that conclusion, e.g. `"/proc/1/cgroup names docker"`.
46    ///
47    /// Rendered next to the classification so the user can judge it themselves.
48    pub evidence: Box<str>,
49    /// How much the evidence supports the conclusion.
50    pub confidence: Confidence,
51}
52
53/// System identity, mostly from the slow sampling tier (§8.6).
54#[derive(Clone, Debug, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize))]
56pub struct HostSnapshot {
57    /// Host name.
58    pub hostname: MetricState<Box<str>>,
59    /// OS name, e.g. `macOS` or `Debian GNU/Linux`.
60    pub os_name: MetricState<Box<str>>,
61    /// OS version.
62    pub os_version: MetricState<Box<str>>,
63    /// Kernel version.
64    pub kernel_version: MetricState<Box<str>>,
65    /// Target architecture. Known at compile time, so never unavailable.
66    pub arch: &'static str,
67    /// CPU model string.
68    pub cpu_brand: MetricState<Box<str>>,
69    /// Time since boot.
70    pub uptime: MetricState<Duration>,
71    /// Wall-clock boot time.
72    pub boot_time: MetricState<SystemTime>,
73    /// Heuristic container/VM classification.
74    pub environment: MetricState<HostEnvironment>,
75}
76
77impl HostSnapshot {
78    /// A snapshot with nothing resolved yet.
79    #[must_use]
80    pub const fn warming_up() -> Self {
81        Self {
82            hostname: MetricState::WarmingUp,
83            os_name: MetricState::WarmingUp,
84            os_version: MetricState::WarmingUp,
85            kernel_version: MetricState::WarmingUp,
86            arch: std::env::consts::ARCH,
87            cpu_brand: MetricState::WarmingUp,
88            uptime: MetricState::WarmingUp,
89            boot_time: MetricState::WarmingUp,
90            environment: MetricState::WarmingUp,
91        }
92    }
93
94    /// The host name for the header, or a neutral placeholder.
95    ///
96    /// §5.5 puts the host name in the title bar; an empty title would look
97    /// broken, and "unknown" is honest.
98    #[must_use]
99    pub fn display_hostname(&self) -> &str {
100        self.hostname
101            .displayable()
102            .map_or("unknown", |(name, _)| name)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn arch_is_known_at_compile_time_and_never_unavailable() {
112        let host = HostSnapshot::warming_up();
113        assert!(!host.arch.is_empty());
114        assert!(
115            [
116                "aarch64",
117                "x86_64",
118                "arm",
119                "x86",
120                "powerpc64",
121                "riscv64",
122                "s390x",
123                "loongarch64"
124            ]
125            .contains(&host.arch),
126            "unexpected arch {}",
127            host.arch
128        );
129    }
130
131    #[test]
132    fn an_unresolved_hostname_renders_as_unknown_not_as_an_empty_title() {
133        let host = HostSnapshot::warming_up();
134        assert_eq!(host.display_hostname(), "unknown");
135    }
136
137    #[test]
138    fn a_stale_hostname_is_still_displayable() {
139        let mut host = HostSnapshot::warming_up();
140        host.hostname = MetricState::Available("dev-mbp".into()).into_stale(Duration::from_secs(5));
141        assert_eq!(host.display_hostname(), "dev-mbp");
142    }
143
144    #[test]
145    fn absence_of_evidence_is_not_reported_as_bare_metal() {
146        assert_eq!(EnvironmentKind::default(), EnvironmentKind::NoEvidenceFound);
147        assert!(
148            EnvironmentKind::NoEvidenceFound
149                .label()
150                .contains("evidence")
151        );
152    }
153
154    #[test]
155    fn an_environment_classification_carries_its_evidence_and_confidence() {
156        let env = HostEnvironment {
157            kind: EnvironmentKind::Container,
158            evidence: "/proc/1/cgroup names docker".into(),
159            confidence: Confidence::High,
160        };
161        assert!(!env.evidence.is_empty());
162        assert_eq!(env.confidence, Confidence::High);
163    }
164}