diff --git a/hbd/server/http.py b/hbd/server/http.py index 5b720ac..78f9947 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -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() ], diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html index 8a340b1..5171553 100644 --- a/hbd/server/templates/plugins.html +++ b/hbd/server/templates/plugins.html @@ -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; } @@ -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 += `
↳ ${t.covers.map(escHtml).join(', ')}`; + } html += ` - ${escHtml(t.metric)} + ${metricCell} ${escHtml(t.operator)} ${w} ${c} diff --git a/tests/test_http_host_info.py b/tests/test_http_host_info.py index 39b2171..9a5df61 100644 --- a/tests/test_http_host_info.py +++ b/tests/test_http_host_info.py @@ -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"] == []