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
|
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()
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user