feat(mini): send p4/p6 presence flags and connectivity metric from hbc_mini.py

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M
This commit is contained in:
2026-09-22 04:31:22 -04:00
co-authored by Claude Sonnet 5
parent 997279dce8
commit 3639464ba6
2 changed files with 116 additions and 17 deletions
+41 -17
View File
@@ -10,14 +10,20 @@ Built-in plugins (always available):
os_info — OS name, kernel, distro, Python version
ping_monitor — ICMP ping RTT to configured hosts
nagios_runner — run Nagios-compatible check scripts
network_monitor — per-interface TX/RX bytes/s (Linux); per-protocol connectivity (all platforms)
Linux-only plugins (via /proc, no extra tools needed):
cpu_monitor — CPU %, load average, core count
memory_monitor — RAM and swap usage
network_monitor — per-interface TX/RX bytes/s
Cross-platform (via df subprocess):
disk_monitor — per-mount usage and capacity
Presence probe (built in): reports per protocol (IPv4/IPv6) whether the host has internet
connectivity, as p4/p6 flags on every heartbeat, so hbd stops treating a missing IPv6 (or
IPv4) path as an outage. Config, in the network_monitor section of ~/.hbc.json:
probe_enabled (default true), probe_v4 (default 1.1.1.1), probe_v6 (default
2606:4700:4700::1111; IP literals only), probe_interval (default 60 s)
"""
import argparse
@@ -969,7 +975,7 @@ def _read_net_dev() -> Dict[str, Tuple[int, int]]:
class NetworkMonitorPlugin(MonitorPlugin):
name = "network_monitor"
description = "Network I/O rates via /proc/net/dev (Linux)"
description = "Network I/O rates via /proc/net/dev (Linux) and per-protocol internet connectivity"
interval = 300
def __init__(self, config: Optional[Dict[str, Any]] = None):
@@ -978,19 +984,25 @@ class NetworkMonitorPlugin(MonitorPlugin):
self.interval = cfg.get("interval", 300)
self.skip_ifaces: set = set(cfg.get("skip_interfaces", ["lo"]))
self._prev: Optional[Tuple[float, Dict[str, Tuple[int, int]]]] = None
self._have_dev = False
async def initialize(self) -> bool:
if platform.system() != "Linux":
self.skip_reason = "Linux only (/proc/net/dev not available)"
dev = _read_net_dev() if platform.system() == "Linux" else {}
self._have_dev = bool(dev)
if not self._have_dev and _probe is None:
self.skip_reason = "no /proc/net/dev (Linux only) and the connectivity probe is disabled"
return False
dev = _read_net_dev()
if not dev:
self.skip_reason = "/proc/net/dev not readable"
return False
self._prev = (time.time(), dev) # prime first delta
if self._have_dev:
self._prev = (time.time(), dev) # prime first delta
return True
async def _collect_metrics(self) -> Dict[str, Any]:
data: Dict[str, Any] = await self._collect_interfaces() if self._have_dev else {}
if _probe is not None:
data["connectivity"] = _probe.as_metric()
return data
async def _collect_interfaces(self) -> Dict[str, Any]:
now = time.time()
curr = _read_net_dev()
if not curr or self._prev is None:
@@ -1251,15 +1263,21 @@ async def _handle_update(conn: AsyncConnection):
# Heartbeat sender
# ---------------------------------------------------------------------------
async def _heartbeat_sender(conn: AsyncConnection, interval: int):
def _build_heartbeat_msg(conn: AsyncConnection, interval: int,
probe: Optional[_Connectivity] = None) -> Dict[str, Any]:
"""HTB payload; carries p4/p6 presence flags when the probe knows them."""
msg: Dict[str, Any] = {"acks": conn.ackcount, "rtt": conn.rtts[-1], "interval": interval}
if probe is not None:
msg.update(probe.flags())
return msg
async def _heartbeat_sender(conn: AsyncConnection, interval: int,
probe: Optional[_Connectivity] = None):
log = logging.getLogger("hbc.hb")
while _running:
try:
await conn.sendto({
"acks": conn.ackcount,
"rtt": conn.rtts[-1],
"interval": interval,
})
await conn.sendto(_build_heartbeat_msg(conn, interval, probe))
except Exception as e:
log.error("send: %s", e)
await _sleep(interval)
@@ -1367,7 +1385,7 @@ def _reconfigure_syslog(level: int):
# ---------------------------------------------------------------------------
async def _async_main(args, cfg: Dict[str, Any]) -> int:
global _running, _shutdown_event, _active_tasks, send_shutdown
global _running, _shutdown_event, _active_tasks, send_shutdown, _probe
_running = True
_shutdown_event = asyncio.Event()
_active_tasks = []
@@ -1424,6 +1442,10 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
c.close()
return 0
# Per-protocol internet presence probe (shared with the network_monitor plugin)
net_cfg = dict((cfg.get("plugins") or {}).get("network_monitor") or cfg.get("network_monitor") or {})
_probe = _Connectivity.from_config(net_cfg) if net_cfg.get("probe_enabled", True) else None
plugins = await _load_plugins(cfg)
loop = asyncio.get_event_loop()
@@ -1437,8 +1459,10 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
loop.add_signal_handler(signal.SIGHUP, _sighup)
if _probe is not None:
_active_tasks.append(asyncio.create_task(_probe.run()))
for conn in connections:
_active_tasks.append(asyncio.create_task(_heartbeat_sender(conn, interval)))
_active_tasks.append(asyncio.create_task(_heartbeat_sender(conn, interval, _probe)))
if plugins and connections:
_active_tasks.append(asyncio.create_task(_plugin_collector(connections[0], plugins)))