feat(host_agent): collect network, disk I/O, per-core CPU, temps, memory PSI
Extends the push agent (v1.2.0) with the Netdata-style signals the operator wants: per-core CPU, per-interface network throughput and per-disk I/O (rates derived in-agent from monotonic /proc counter deltas, with counter-reset clamping), hardware temperatures (/sys/class/hwmon), memory-pressure PSI (/proc/pressure), and cached/buffers memory breakdown. All stdlib-only. Server side needs no migration — these land as additional rows in the shared PluginMetric table via _expand_sample_to_metrics. Per-resource series use a ':' in resource_name (host:net:eth0, host:core0, host:temp:Package) so the existing fleet widget's ':' filter ignores them; host-level totals/max are emitted at the bare host resource. New sample keys are optional, so older agents keep ingesting unchanged. Unit tests for the new parsers, rate/reset logic, and the expander contract. Data foundation for the Netdata-style host view (task #867). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,60 @@ def _expand_sample_to_metrics(
|
||||
if sample.get("storage"):
|
||||
row("disk_used_pct_worst", host_name, worst_pct)
|
||||
|
||||
# Per-core CPU. Sub-resources carry a ':' so the fleet widget filters them out.
|
||||
for i, core_pct in enumerate(sample.get("cpu_cores") or []):
|
||||
if core_pct is not None:
|
||||
row("cpu_pct", f"{host_name}:core{i}", core_pct)
|
||||
|
||||
# Richer memory breakdown.
|
||||
if mem.get("cached_bytes") is not None:
|
||||
row("mem_cached_bytes", host_name, mem["cached_bytes"])
|
||||
if mem.get("buffers_bytes") is not None:
|
||||
row("mem_buffers_bytes", host_name, mem["buffers_bytes"])
|
||||
|
||||
# Network throughput — per interface plus a host-level total for the fleet view.
|
||||
net_rx_total = net_tx_total = 0.0
|
||||
for iface in sample.get("net") or []:
|
||||
rx, tx = iface.get("rx_bps", 0.0), iface.get("tx_bps", 0.0)
|
||||
res = f"{host_name}:net:{iface['iface']}"
|
||||
row("net_rx_bps", res, rx)
|
||||
row("net_tx_bps", res, tx)
|
||||
net_rx_total += rx
|
||||
net_tx_total += tx
|
||||
if sample.get("net"):
|
||||
row("net_rx_bps", host_name, net_rx_total)
|
||||
row("net_tx_bps", host_name, net_tx_total)
|
||||
|
||||
# Disk I/O — per device plus host-level total.
|
||||
dr_total = dw_total = 0.0
|
||||
for dev in sample.get("diskio") or []:
|
||||
rd, wr = dev.get("read_bps", 0.0), dev.get("write_bps", 0.0)
|
||||
res = f"{host_name}:diskio:{dev['device']}"
|
||||
row("disk_read_bps", res, rd)
|
||||
row("disk_write_bps", res, wr)
|
||||
dr_total += rd
|
||||
dw_total += wr
|
||||
if sample.get("diskio"):
|
||||
row("disk_read_bps", host_name, dr_total)
|
||||
row("disk_write_bps", host_name, dw_total)
|
||||
|
||||
# Temperatures — per sensor plus the host max for at-a-glance.
|
||||
max_temp: float | None = None
|
||||
for t in sample.get("temps") or []:
|
||||
c = t.get("celsius")
|
||||
if c is None:
|
||||
continue
|
||||
row("temp_c", f"{host_name}:temp:{t['label']}", c)
|
||||
if max_temp is None or c > max_temp:
|
||||
max_temp = c
|
||||
if max_temp is not None:
|
||||
row("temp_c_max", host_name, max_temp)
|
||||
|
||||
# Pressure stall information (PSI) — host-level gauges (mem/cpu/io some|full).
|
||||
for k, v in (sample.get("psi") or {}).items():
|
||||
if isinstance(v, (int, float)):
|
||||
row(f"psi_{k}", host_name, v)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user