Files
bvandeusen a9e7baee6a
CI / lint (push) Successful in 3s
CI / unit (push) Failing after 8s
CI / integration (push) Successful in 2m17s
CI / publish (push) Has been skipped
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>
2026-06-16 10:11:30 -04:00

61 lines
2.5 KiB
Python

"""Unit tests for _expand_sample_to_metrics — sample dict → PluginMetric rows.
No DB: PluginMetric objects are constructed in-memory, so we assert the
(metric_name, resource_name) → value contract directly.
"""
from datetime import datetime, timezone
from plugins.host_agent.routes import _expand_sample_to_metrics
def _index(rows):
return {(r.metric_name, r.resource_name): r.value for r in rows}
def test_expand_emits_all_new_metric_families():
sample = {
"ts": "2026-06-16T00:00:00+00:00",
"cpu_pct": 12.5,
"cpu_cores": [10.0, 15.0],
"mem": {"total_bytes": 100, "available_bytes": 40, "swap_used_bytes": 5,
"cached_bytes": 20, "buffers_bytes": 3},
"load": {"1m": 0.1, "5m": 0.2, "15m": 0.3},
"uptime_secs": 1000,
"storage": [{"mount": "/", "total_bytes": 100, "used_bytes": 50}],
"net": [{"iface": "eth0", "rx_bps": 1000.0, "tx_bps": 500.0}],
"diskio": [{"device": "sda", "read_bps": 2000.0, "write_bps": 1000.0}],
"temps": [{"label": "Package", "celsius": 45.0}, {"label": "Core0", "celsius": 50.0}],
"psi": {"mem_some_avg10": 1.5, "cpu_some_avg10": 0.0},
}
idx = _index(_expand_sample_to_metrics(sample, "alpha", datetime.now(timezone.utc)))
# per-core CPU as sub-resources
assert idx[("cpu_pct", "alpha:core0")] == 10.0
assert idx[("cpu_pct", "alpha:core1")] == 15.0
# richer memory
assert idx[("mem_cached_bytes", "alpha")] == 20
assert idx[("mem_buffers_bytes", "alpha")] == 3
# network: per-iface + host total
assert idx[("net_rx_bps", "alpha:net:eth0")] == 1000.0
assert idx[("net_rx_bps", "alpha")] == 1000.0
# disk I/O: per-device + host total
assert idx[("disk_read_bps", "alpha:diskio:sda")] == 2000.0
assert idx[("disk_write_bps", "alpha")] == 1000.0
# temps: per-sensor + host max
assert idx[("temp_c", "alpha:temp:Package")] == 45.0
assert idx[("temp_c_max", "alpha")] == 50.0
# PSI
assert idx[("psi_mem_some_avg10", "alpha")] == 1.5
def test_expand_is_backcompat_with_old_agent_sample():
"""An old agent (no new keys) must still expand cleanly to legacy metrics."""
sample = {"ts": "x", "cpu_pct": 5.0, "mem": {"total_bytes": 10, "available_bytes": 5}}
rows = _expand_sample_to_metrics(sample, "h", datetime.now(timezone.utc))
names = {r.metric_name for r in rows}
assert "cpu_pct" in names
assert not any(
n.startswith(("net_", "disk_read", "disk_write", "psi_", "temp_", "mem_cached"))
for n in names
)