Skip to main content

resource_tracker/metrics/
mod.rs

1pub mod cpu;
2pub mod disk;
3pub mod disk_mount;
4pub mod disk_type;
5pub mod gpu;
6pub mod host;
7pub mod memory;
8pub mod network;
9
10pub use cpu::CpuMetrics;
11pub use disk::DiskMetrics;
12pub use disk_mount::DiskMountMetrics;
13pub use disk_type::DiskType;
14pub use gpu::GpuMetrics;
15pub use host::{CloudInfo, HostInfo};
16pub use memory::MemoryMetrics;
17pub use network::NetworkMetrics;
18
19use serde::{Deserialize, Serialize};
20
21/// A single point-in-time observation of all tracked resources.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Sample {
24    /// Unix timestamp (seconds) when this sample was taken.
25    pub timestamp_secs: u64,
26    /// Actual elapsed milliseconds since the previous sample was collected.
27    /// None for the very first real sample (no prior collection to compare against).
28    /// Included in JSON output when present; not a CSV column.
29    /// Use this -- not the configured interval -- when converting rates to per-interval counts.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub actual_interval_ms: Option<u64>,
32    /// Optional job label supplied via CLI or config file.
33    pub job_name: Option<String>,
34    /// Root PID of the process tree being tracked, if any.
35    /// Carried here so the CSV serializer can emit `process_pid` without
36    /// needing access to `Config`.
37    pub tracked_pid: Option<i32>,
38    pub cpu: CpuMetrics,
39    pub memory: MemoryMetrics,
40    pub network: Vec<NetworkMetrics>,
41    pub disk: Vec<DiskMetrics>,
42    /// One entry per detected GPU/NPU/TPU. Empty on CPU-only hosts.
43    pub gpu: Vec<GpuMetrics>,
44}
45
46// ---------------------------------------------------------------------------
47// Unit tests
48// ---------------------------------------------------------------------------
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::metrics::CpuMetrics;
54
55    fn minimal_sample(actual_ms: Option<u64>) -> Sample {
56        Sample {
57            timestamp_secs: 1_700_000_000,
58            actual_interval_ms: actual_ms,
59            job_name: None,
60            tracked_pid: None,
61            cpu: CpuMetrics::default(),
62            memory: MemoryMetrics::default(),
63            network: vec![],
64            disk: vec![],
65            gpu: vec![],
66        }
67    }
68
69    // T-SAMPLE-01: actual_interval_ms is present in JSON when Some.
70    // Downstream consumers (e.g. dashboards) rely on this field to compute
71    // accurate per-interval rates when the scheduler did not wake the tracker
72    // on time.
73    #[test]
74    fn test_actual_interval_ms_present_in_json_when_some() {
75        let sample = minimal_sample(Some(1042));
76        let v = serde_json::to_value(&sample).expect("serialize failed");
77        let ms = v["actual_interval_ms"]
78            .as_u64()
79            .expect("actual_interval_ms must be a number in JSON when Some");
80        assert_eq!(
81            ms, 1042,
82            "actual_interval_ms value must round-trip through JSON"
83        );
84    }
85
86    // T-SAMPLE-02: actual_interval_ms is absent from JSON when None.
87    // The first real sample has no prior baseline; the field must not appear
88    // rather than emitting a JSON null (skip_serializing_if = "Option::is_none").
89    #[test]
90    fn test_actual_interval_ms_absent_from_json_when_none() {
91        let sample = minimal_sample(None);
92        let v = serde_json::to_value(&sample).expect("serialize failed");
93        assert!(
94            v["actual_interval_ms"].is_null(),
95            "actual_interval_ms must not appear in JSON when None, got: {:?}",
96            v["actual_interval_ms"]
97        );
98    }
99
100    // T-SAMPLE-03: timestamp_secs round-trips through JSON as a positive integer.
101    // This documents the guarantee that the field encodes wall-clock Unix seconds
102    // and is not affected by the configured or actual interval.
103    #[test]
104    fn test_timestamp_secs_round_trips_through_json() {
105        let sample = minimal_sample(Some(999));
106        let v = serde_json::to_value(&sample).expect("serialize failed");
107        let ts = v["timestamp_secs"]
108            .as_u64()
109            .expect("timestamp_secs must be a u64 in JSON");
110        assert_eq!(ts, 1_700_000_000, "timestamp_secs must round-trip exactly");
111    }
112}