greggd 1.0.3

Lightweight Linux, macOS, and Windows metrics daemon that exposes a read-only JSON API for the gregg client.
Documentation
//! Native metrics collection.
//!
//! The collector boundary isolates platform-specific sampling from the daemon
//! sampler and the HTTP surface. The shared trait is implemented by per-OS
//! modules that read their own native kernel or user-space interfaces and
//! return a normalized, daemon-internal sample. The sampler in phase 4 owns
//! cadence, clock, and snapshot publication.
//!
//! # Design rules
//!
//! - The collector never spawns external commands. Linux uses procfs and
//!   sysinfo interfaces; macOS uses Mach and sysctl APIs behind a contained
//!   FFI module added in phase 3.
//! - The collector never owns a clock. The daemon samples call
//!   [`SystemCollector::sample`] and stamp [`StatusSnapshot::observed_at_unix_ms`]
//!   in the sampler.
//! - All percentage normalization, counter-delta handling, and warming-up
//!   state live behind the trait, not in the protocol crate.
//! - Errors are typed so the daemon can distinguish a warming baseline from a
//!   hard collector failure when reporting health.

use gregg_protocol::v2::{
    CommitMetrics, CpuMetricsV2, DriveMetrics, MetricCapabilitiesV2, StatusPayloadV2,
    StatusSnapshotV2, SwapMetrics as SwapMetricsV2, SCHEMA_VERSION_V2,
};
use gregg_protocol::{
    CpuMetrics, LoadAverage, MemoryMetrics, MetricCapabilities, StatusSnapshot, SwapMetrics,
    SystemIdentity,
};

mod drives;

#[cfg(target_os = "linux")]
pub mod linux;

#[cfg(target_os = "macos")]
pub mod macos;

#[cfg(target_os = "windows")]
pub mod windows;

pub mod error;

/// Normalized metric sample produced by a [`SystemCollector`].
///
/// The struct is daemon-internal: it carries fields that do not appear on the
/// wire so collectors can express transient states (warming, counter reset)
/// without polluting the protocol. The daemon sampler maps it losslessly into
/// a [`StatusSnapshot`] once it is ready for publication.
#[derive(Debug, Clone, PartialEq)]
pub struct CollectedMetrics {
    /// Logical CPU core count. Always `> 0` for a successfully collected
    /// identity snapshot.
    pub logical_cores: u32,
    /// Aggregate CPU busy percentage derived from a counter interval. `None`
    /// while warming up or immediately after a counter reset.
    pub cpu_usage_pct: Option<f32>,
    /// Aggregate Linux CPU I/O-wait percentage. Always `None` for non-Linux
    /// collectors; on Linux it is `Some` once a valid interval exists.
    pub cpu_iowait_pct: Option<f32>,
    /// Load averages parsed verbatim from the platform source.
    pub load: LoadAverage,
    /// Physical memory utilization.
    pub memory: MemoryMetrics,
    /// Swap utilization.
    pub swap: SwapMetrics,
    /// Windows commit charge. `None` on Linux/macOS; `Some` on Windows
    /// when the collector reports commit metrics.
    pub commit: Option<CommitMetrics>,
    /// Optional bounded native drive capacity data. `None` means enumeration
    /// was unavailable; an empty list means it succeeded with no eligible
    /// local filesystems.
    pub drives: Option<Vec<DriveMetrics>>,
}

impl CollectedMetrics {
    /// Convert this sample into a wire [`StatusSnapshot`].
    ///
    /// The caller (the daemon sampler) is responsible for filling in
    /// `schema_version`, `observed_at_unix_ms`, and `sample_interval_ms`. CPU
    /// `usage_pct` and `iowait_pct` are coalesced from `Option` into either a
    /// concrete value or a protocol-compatible zero with the right capability
    /// flag set. Callers should not publish a snapshot while
    /// [`Self::cpu_usage_pct`] is `None`; the function performs the coalesce
    /// defensively so the result is always wire-valid for the daemon's
    /// platform capabilities.
    #[must_use]
    pub fn into_snapshot(
        self,
        schema_version: u16,
        observed_at_unix_ms: u64,
        sample_interval_ms: u64,
        capabilities: MetricCapabilities,
        system: SystemIdentity,
    ) -> StatusSnapshot {
        let cpu_usage_pct = self.cpu_usage_pct.unwrap_or(0.0);
        let cpu_usage_pct = if cpu_usage_pct.is_finite() {
            cpu_usage_pct
        } else {
            0.0
        };
        let cpu_iowait_pct = if capabilities.cpu_iowait {
            Some(self.cpu_iowait_pct.unwrap_or(0.0))
        } else {
            None
        };
        StatusSnapshot {
            schema_version,
            observed_at_unix_ms,
            sample_interval_ms,
            capabilities,
            system,
            cpu: CpuMetrics {
                logical_cores: self.logical_cores,
                usage_pct: cpu_usage_pct,
                iowait_pct: cpu_iowait_pct,
            },
            load: self.load,
            memory: self.memory,
            swap: self.swap,
        }
    }

    /// Convert this sample into a wire [`StatusSnapshotV2`].
    ///
    /// The caller (the daemon sampler) is responsible for filling in
    /// `observed_at_unix_ms` and `sample_interval_ms`. CPU usage and
    /// iowait are coalesced defensively. Optional metrics (load, swap,
    /// commit) are set according to the v2 capability flags.
    #[must_use]
    pub fn into_snapshot_v2(
        self,
        observed_at_unix_ms: u64,
        sample_interval_ms: u64,
        capabilities: MetricCapabilitiesV2,
        system: SystemIdentity,
    ) -> StatusSnapshotV2 {
        let cpu_usage_pct = self.cpu_usage_pct.unwrap_or(0.0);
        let cpu_usage_pct = if cpu_usage_pct.is_finite() {
            cpu_usage_pct
        } else {
            0.0
        };
        let cpu_iowait_pct = if capabilities.cpu_iowait {
            Some(self.cpu_iowait_pct.unwrap_or(0.0))
        } else {
            None
        };

        let load = if capabilities.load_average {
            Some(self.load)
        } else {
            None
        };

        let swap = if capabilities.swap {
            #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
            let usage_pct = if self.swap.total_bytes == 0 {
                0.0
            } else {
                let pct = (self.swap.used_bytes as f64) * 100.0 / (self.swap.total_bytes as f64);
                (pct as f32).clamp(0.0, 100.0)
            };
            Some(SwapMetricsV2 {
                used_bytes: self.swap.used_bytes,
                total_bytes: self.swap.total_bytes,
                usage_pct,
            })
        } else {
            None
        };

        StatusSnapshotV2 {
            schema_version: SCHEMA_VERSION_V2,
            observed_at_unix_ms,
            sample_interval_ms,
            capabilities,
            system,
            cpu: CpuMetricsV2 {
                logical_cores: self.logical_cores,
                usage_pct: cpu_usage_pct,
                iowait_pct: cpu_iowait_pct,
            },
            load,
            memory: self.memory,
            swap,
            commit: self.commit,
        }
    }

    /// Convert this sample into the flat v2 status payload, preserving drive
    /// availability semantics for the client.
    #[must_use]
    pub fn into_status_payload_v2(
        self,
        observed_at_unix_ms: u64,
        sample_interval_ms: u64,
        capabilities: MetricCapabilitiesV2,
        system: SystemIdentity,
    ) -> StatusPayloadV2 {
        let drives = self.drives.clone();
        let snapshot = self.into_snapshot_v2(
            observed_at_unix_ms,
            sample_interval_ms,
            capabilities,
            system,
        );
        StatusPayloadV2 { snapshot, drives }
    }
}

/// Shared collector contract implemented by every platform-specific collector.
///
/// The contract is intentionally minimal: it owns identity collection and one
/// incremental sample. The daemon sampler owns cadence and clock.
pub trait SystemCollector: Send {
    /// Read identity fields once and cache them inside the collector.
    ///
    /// Identity is expected to be stable for the lifetime of the daemon, but
    /// re-reading is permitted if the host's identity changes (for example a
    /// hostname rename).
    fn identity(&self) -> Result<SystemIdentity, error::CollectError>;

    /// Take one native sample.
    ///
    /// The first call after construction is expected to return
    /// [`error::CollectErrorKind::Warming`] because percentage metrics
    /// require a second reading. Once two valid samples exist the collector
    /// returns normalized [`CollectedMetrics`].
    fn sample(&mut self) -> Result<CollectedMetrics, error::CollectError>;

    /// Per-platform metric capability flags.
    fn capabilities(&self) -> MetricCapabilities;

    /// Per-platform metric capability flags for schema version 2.
    ///
    /// The default implementation derives v2 capabilities from v1
    /// capabilities. Platform collectors may override this if v2
    /// capabilities differ from v1.
    fn capabilities_v2(&self) -> MetricCapabilitiesV2 {
        let v1 = self.capabilities();
        MetricCapabilitiesV2 {
            cpu_iowait: v1.cpu_iowait,
            load_average: true,
            swap: true,
            memory_commit: false,
        }
    }

    /// Whether this collector supports producing a v1 `StatusSnapshot`.
    ///
    /// Returns `true` by default. Windows returns `false` because v1
    /// requires non-optional `load` and `swap` fields that have no
    /// meaningful representation on Windows. The sampler skips v1 snapshot
    /// production when this returns `false`, causing `/v1/status` to
    /// return 404.
    fn supports_v1_snapshot(&self) -> bool {
        true
    }
}