feat: show suffix-matched metric coverage in host info threshold table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 09:18:49 -04:00
parent b64a2a9313
commit 9e389736f8
3 changed files with 72 additions and 2 deletions
+20
View File
@@ -143,6 +143,25 @@ def _build_host_info(host, threshold_checker=None) -> dict:
thresholds = None thresholds = None
if threshold_checker is not None: if threshold_checker is not None:
raw = threshold_checker.get_thresholds_for_host(host.name) raw = threshold_checker.get_thresholds_for_host(host.name)
# Build reverse coverage: which metric paths suffix-match to each threshold.
# Mirrors the logic in ThresholdChecker._find_threshold.
coverage: dict = {}
for plugin_name, samples in host.plugin_data.items():
if not samples:
continue
_, pdata = samples[-1]
for field_name in pdata:
full_path = f"{plugin_name}.{field_name}"
if full_path in raw:
continue # exact match — the threshold IS this metric
parts = field_name.split("_")
for i in range(1, len(parts)):
candidate = f"{plugin_name}." + "_".join(parts[i:])
if candidate in raw:
coverage.setdefault(candidate, []).append(full_path)
break
thresholds = sorted( thresholds = sorted(
[ [
{ {
@@ -150,6 +169,7 @@ def _build_host_info(host, threshold_checker=None) -> dict:
"warning": tc.warning, "warning": tc.warning,
"critical": tc.critical, "critical": tc.critical,
"operator": tc.operator.value, "operator": tc.operator.value,
"covers": sorted(coverage.get(tc.metric_path, [])),
} }
for tc in raw.values() for tc in raw.values()
], ],
+6 -1
View File
@@ -411,6 +411,7 @@
} }
.info-note { color: #888; font-style: italic; } .info-note { color: #888; font-style: italic; }
.info-loading { color: #bbb; font-style: italic; } .info-loading { color: #bbb; font-style: italic; }
.threshold-covers { font-size: 0.85em; color: #777; font-style: italic; }
</style> </style>
<body> <body>
@@ -588,8 +589,12 @@
for (const t of data.thresholds) { for (const t of data.thresholds) {
const w = t.warning != null ? escHtml(String(t.warning)) : '—'; const w = t.warning != null ? escHtml(String(t.warning)) : '—';
const c = t.critical != null ? escHtml(String(t.critical)) : '—'; const c = t.critical != null ? escHtml(String(t.critical)) : '—';
let metricCell = escHtml(t.metric);
if (t.covers && t.covers.length > 0) {
metricCell += `<br><span class="threshold-covers">↳ ${t.covers.map(escHtml).join(', ')}</span>`;
}
html += `<tr> html += `<tr>
<td class="key">${escHtml(t.metric)}</td> <td class="key">${metricCell}</td>
<td>${escHtml(t.operator)}</td> <td>${escHtml(t.operator)}</td>
<td>${w}</td> <td>${w}</td>
<td>${c}</td> <td>${c}</td>
+46 -1
View File
@@ -11,12 +11,13 @@ class _FakeConn:
class _FakeHost: class _FakeHost:
def __init__(self, name="myhost", owner=None, managers=None, def __init__(self, name="myhost", owner=None, managers=None,
connections=None, os_data=None): connections=None, os_data=None, plugin_data=None):
self.name = name self.name = name
self.owner = owner self.owner = owner
self.managers = managers or [] self.managers = managers or []
self.connections = connections or {} self.connections = connections or {}
self._os_data = os_data self._os_data = os_data
self.plugin_data = plugin_data or {}
def get_latest_plugin_data(self, plugin_name): def get_latest_plugin_data(self, plugin_name):
if plugin_name == "os_info" and self._os_data is not None: if plugin_name == "os_info" and self._os_data is not None:
@@ -127,3 +128,47 @@ def test_build_host_info_nagios_operator_serialized():
host = _FakeHost() host = _FakeHost()
result = _build_host_info(host, threshold_checker=checker) result = _build_host_info(host, threshold_checker=checker)
assert result["thresholds"][0]["operator"] == "nagios" assert result["thresholds"][0]["operator"] == "nagios"
def test_build_host_info_covers_suffix_matched_metrics():
"""memory_monitor.percent threshold covers swap_percent via suffix match."""
from hbd.server.threshold import ThresholdConfig
tc_pct = ThresholdConfig("memory_monitor.percent", warning=85.0, critical=95.0)
checker = MagicMock()
checker.get_thresholds_for_host.return_value = {"memory_monitor.percent": tc_pct}
host = _FakeHost(
connections={},
os_data=None,
)
# Simulate plugin_data with both percent and swap_percent fields
host.plugin_data = {
"memory_monitor": [(1234567890.0, {
"percent": 80.0,
"swap_percent": 25.0,
"available_mb": 2000,
})]
}
result = _build_host_info(host, threshold_checker=checker)
assert result["thresholds"] is not None
t = result["thresholds"][0]
assert t["metric"] == "memory_monitor.percent"
assert t["covers"] == ["memory_monitor.swap_percent"]
def test_build_host_info_covers_empty_when_exact_matches_only():
"""No covers when all plugin fields match their threshold exactly."""
from hbd.server.threshold import ThresholdConfig
tc_pct = ThresholdConfig("memory_monitor.percent", warning=85.0, critical=95.0)
checker = MagicMock()
checker.get_thresholds_for_host.return_value = {"memory_monitor.percent": tc_pct}
host = _FakeHost()
host.plugin_data = {
"memory_monitor": [(1234567890.0, {"percent": 80.0})]
}
result = _build_host_info(host, threshold_checker=checker)
t = result["thresholds"][0]
assert t["covers"] == []