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:
@@ -143,6 +143,25 @@ def _build_host_info(host, threshold_checker=None) -> dict:
|
||||
thresholds = None
|
||||
if threshold_checker is not None:
|
||||
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(
|
||||
[
|
||||
{
|
||||
@@ -150,6 +169,7 @@ def _build_host_info(host, threshold_checker=None) -> dict:
|
||||
"warning": tc.warning,
|
||||
"critical": tc.critical,
|
||||
"operator": tc.operator.value,
|
||||
"covers": sorted(coverage.get(tc.metric_path, [])),
|
||||
}
|
||||
for tc in raw.values()
|
||||
],
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
}
|
||||
.info-note { color: #888; font-style: italic; }
|
||||
.info-loading { color: #bbb; font-style: italic; }
|
||||
.threshold-covers { font-size: 0.85em; color: #777; font-style: italic; }
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -588,8 +589,12 @@
|
||||
for (const t of data.thresholds) {
|
||||
const w = t.warning != null ? escHtml(String(t.warning)) : '—';
|
||||
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>
|
||||
<td class="key">${escHtml(t.metric)}</td>
|
||||
<td class="key">${metricCell}</td>
|
||||
<td>${escHtml(t.operator)}</td>
|
||||
<td>${w}</td>
|
||||
<td>${c}</td>
|
||||
|
||||
@@ -11,12 +11,13 @@ class _FakeConn:
|
||||
|
||||
class _FakeHost:
|
||||
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.owner = owner
|
||||
self.managers = managers or []
|
||||
self.connections = connections or {}
|
||||
self._os_data = os_data
|
||||
self.plugin_data = plugin_data or {}
|
||||
|
||||
def get_latest_plugin_data(self, plugin_name):
|
||||
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()
|
||||
result = _build_host_info(host, threshold_checker=checker)
|
||||
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"] == []
|
||||
|
||||
Reference in New Issue
Block a user