pub struct Heartbeat {Show 14 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>>,
pub enforcing: Option<bool>,
}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 keys this agent currently trusts, each as
kid:fingerprint.
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.
The fingerprint half (#1229) answers a question the id cannot: do two
machines holding the same id hold the same key. The id is chosen by
whoever wrote the ring, so a mistyped paste, a same-kid re-mint, or a
hand-edited registry value produces a host that reports the expected id,
refuses every command once enforcement is on, and never self-heals —
the reload-on-unknown-key path (#1186) does not fire, because the key
is present, just wrong.
One flat string rather than a nested object on purpose. The projected
column is a JSON array queried with LIKE, because the read-only query
path rejects table-valued functions (json_each) — so
LIKE '%"backend-2026…:3f2a…"%' pins the exact key with the machinery
that already exists, while LIKE '%"backend-2026…:%' still asks the
id-only question. Nothing parses these back apart; they are matched.
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.
enforcing: Option<bool>#1250: whether this agent is refusing commands it cannot verify.
Self::command_keys made key distribution enumerable; this makes
enforcement enumerable, which nothing else does. It cannot be inferred
from the signature outcomes: in normal operation every command is
signed, so an enforcing host and a non-enforcing one both report
command_signature_ok. The two are observationally identical until
something unsigned arrives — which is exactly the event nobody wants to
stage across a fleet to find out. Nor from command_keys: a host can
hold a perfect ring and not be enforcing, which is what every machine is
doing today.
Three states, for the same reason as command_keys:
None— the agent predates this field. Unknown, not “no”.Some(false)— reporting, and not enforcing. This is the queue.Some(true)— refusing unverified commands right now.
The effective state, not the configured one: an agent declines to
enforce on an empty ring (refusing everything would include the command
that restores the keys), so a host in that state reports false however
its registry reads. Reporting the configured value would describe a
machine that does not exist — the same rule that makes command_keys
report memory rather than disk.