pub struct Heartbeat {Show 13 fields
pub pc_id: String,
pub at: DateTime<Utc>,
pub agent_version: String,
pub hostname: Option<String>,
pub os_family: Option<String>,
pub agent_cpu_pct: Option<f64>,
pub agent_rss_bytes: Option<i64>,
pub agent_disk_read_bytes: Option<i64>,
pub agent_disk_written_bytes: Option<i64>,
pub quarantined_versions: Vec<String>,
pub last_logon_user: Option<String>,
pub last_logon_display_name: Option<String>,
pub command_keys: Option<Vec<String>>,
}Expand description
Liveness ping every agent sends on a 30 s cadence (see
inventory_interval / heartbeat_interval in agent_config).
hostname and os_family are enriched baseline facts so the
SPA agents page has something to show as soon as the agent
boots — even when the full WMI-driven HwInventory hasn’t been
(or can’t be) collected. Both stay Option<String> so older
agents that don’t send them still deserialize cleanly.
Fields§
§pc_id: String§at: DateTime<Utc>§agent_version: String§hostname: Option<String>§os_family: Option<String>Coarse OS bucket from std::env::consts::OS — "windows",
"linux", "macos". Rich OS metadata still flows through
the inventory path; this is just the “agent is alive on a
agent_cpu_pct: Option<f64>Agent process CPU usage, in percent-of-one-core (a process
fully pinning one core reports 100; one pinning two cores
reports 200). This is sysinfo’s convention — closer to
top than to Windows Task Manager (which normalises by
total cores, so a 1-core peg on an 8-core box shows up as
~12.5 % in TM). Divide by host core count if you want a
host-normalised view. None is published on the very first
heartbeat after process start, because sysinfo’s CPU% needs
two consecutive samples to diff — populating it would
always report 0.0 there and risk an operator misreading
“agent isn’t doing anything”.
agent_rss_bytes: Option<i64>Agent process resident set size in bytes — sysinfo’s
Process::memory(), which on Windows is
PROCESS_MEMORY_COUNTERS_EX::WorkingSetSize (full working
set, shared + private). Closest Task Manager column is
“Working set (memory)”, NOT “Memory (private working set)”
which would be PrivateUsage and sysinfo exposes
separately as virtual_memory().
agent_disk_read_bytes: Option<i64>Absolute bytes the agent process has read from disk since it started. Wire format is cumulative (not delta) so dropped / out-of-order heartbeats don’t poison rate math for any client that wants to derive a rate by diffing successive snapshots. Today neither the backend projector nor the SPA does that diff — they just store and render the cumulative value. Future SPA work or an exporter can compute rate without a schema change.
agent_disk_written_bytes: Option<i64>Absolute bytes the agent process has written to disk since
it started. Same shape as agent_disk_read_bytes.
quarantined_versions: Vec<String>#582 Phase 2: versions this agent’s boot sentinel rolled back
after they crash-looped on boot. The self-update path refuses
to (re-)deploy any version listed here, so the SPA’s rollout
view can flag “PC-X failed to adopt target 0.43.51” — the
fleet-wide signal that a rollout is bad. Empty (the common
case) is skipped on the wire; older agents simply omit it and
#[serde(default)] leaves it empty.
last_logon_user: Option<String>Most-recently signed-in account on this host, read from the
Windows LogonUI registry key
(HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\LastLoggedOnUser).
This is the DOMAIN\sam (or .\user) login name the sign-in
screen last used; it survives logoff, so it’s populated even
when no one is currently signed in. None on a never-signed-in
host and on non-Windows agents (read_hklm_value returns None
off-Windows) — see #655 for the cross-platform follow-up — so
older agents keep sending valid heartbeats either way.
last_logon_display_name: Option<String>Display name paired with Self::last_logon_user, from
LogonUI\LastLoggedOnDisplayName (e.g. "Yamada Taro"). None
when unavailable.
command_keys: Option<Vec<String>>#1165: the command-signing key ids this agent currently trusts.
Reported so “which machines still trust the old key” is answerable. Without it, retiring a key is a guess: an agent that never received the replacement rejects every command at stage 3, and there is no way to know it was going to before it does.
Option<Vec<_>> rather than a plain Vec with
skip_serializing_if = "Vec::is_empty" — the shape
Self::quarantined_versions uses — because empty is the state this
exists to surface. Skipping an empty list would put “this agent holds
no keys” and “this agent is too old to say” on the wire as the same
thing, and they need opposite responses: provision the first one, and
upgrade the second before you can even ask. So:
None— the agent predates this field. Unknown, not empty.Some([])— reporting, and holds nothing. This is the work queue.Some([kid, ..])— what it will actually accept right now.
It reports the in-memory ring, not the registry. Those differ between a key landing on disk and the reload that picks it up (#1186), and the useful answer is what this agent would accept if a command arrived now — reporting the file would describe a machine that does not exist yet.