fix: settings thresholds show correct per-config metrics; misc hbc fixes

Settings page: pass threshold_checker to http.start so the Threshold
Configurations section has data. Use threshold_checker's already-parsed
ThresholdConfig objects instead of re-parsing the raw nested YAML.
Named (non-default) configs now display only their explicit overrides
via threshold_raw_configs, not the full merged set with defaults.

hbc/hbc_mini: send boot and shutdown messages on first connection only
to avoid duplicate packets when multiple servers are configured.
Replace print("Daemonizing...") with logging.info so output goes to
syslog in daemon mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 09:12:39 -04:00
parent 3a546a1e5c
commit c93dbdc0f4
5 changed files with 38 additions and 39 deletions
+3 -1
View File
@@ -474,6 +474,7 @@ async def cleanup(connections: List[AsyncConnection]):
logger.error(f"Error sending shutdown: {e}")
conn.close()
break # Only send shutdown on first connection to avoid duplicates
# Give messages time to send
await asyncio.sleep(0.5)
@@ -540,6 +541,7 @@ async def async_main(args, config):
boot_msg["acks"] = 0
for conn in connections:
await conn.sendto(boot_msg)
break # Only send message on first connection to avoid duplicates
if args.message and not args.daemon:
# Message-only mode
@@ -739,7 +741,7 @@ def main(argv=None):
# Daemonize if requested
if args.daemon:
print("Daemonizing...")
logging.info("Daemonizing...")
daemonize()
_reconfigure_logging_for_daemon(log_level)
logging.info(f"hbc starting, sending heartbeat to {', '.join(args.hosts)}")
+1 -1
View File
@@ -890,7 +890,7 @@ async def start(
tmpl = env.get_template("settings.html")
body = tmpl.render(
title="Settings - Heartbeat",
sections=settings_mod.get_settings_sections(config),
sections=settings_mod.get_settings_sections(config, threshold_checker=threshold_checker),
current_user=current_user.to_dict() if current_user else None,
active_page="settings",
)
+1
View File
@@ -255,6 +255,7 @@ async def _run_async(config, config_path=None):
config=config,
hbdclass=hbdclass,
tcss=None,
threshold_checker=threshold_checker,
verbose=config.get("verbose", False),
get_now=lambda: time.time(),
VER="",
+30 -37
View File
@@ -88,7 +88,7 @@ def _sanitize_channel(name, cfg):
# Public API
# ---------------------------------------------------------------------------
def get_settings_sections(config: dict) -> list:
def get_settings_sections(config: dict, threshold_checker=None) -> list:
"""Return ordered list of setting sections for the settings page.
Each section:
@@ -182,46 +182,39 @@ def get_settings_sections(config: dict) -> list:
})
# ---- Threshold configurations -----------------------------------------
def _parse_metric_row(metric_path, metric_cfg):
if not isinstance(metric_cfg, dict):
return None
def _tc_to_row(tc):
return {
"metric": metric_path,
"operator": metric_cfg.get("operator", ">"),
"warning": metric_cfg.get("warning"),
"critical": metric_cfg.get("critical"),
"hysteresis": metric_cfg.get("hysteresis"),
"count": metric_cfg.get("count", 1),
"enabled": metric_cfg.get("enabled", True),
"metric": tc.metric_path,
"operator": tc.operator.value,
"warning": tc.warning,
"critical": tc.critical,
"hysteresis": tc.hysteresis,
"count": tc.count,
"enabled": tc.enabled,
}
threshold_config_list = []
raw_tconfigs = config.get("threshold_configs") or {}
if raw_tconfigs:
for cfg_name, cfg_data in sorted(raw_tconfigs.items()):
if not isinstance(cfg_data, dict):
continue
metrics = [
r for r in (
_parse_metric_row(mp, mc)
for mp, mc in (cfg_data.get("thresholds") or {}).items()
) if r
]
threshold_config_list.append({
"name": cfg_name,
"metrics": sorted(metrics, key=lambda m: m["metric"]),
})
elif config.get("thresholds"):
metrics = [
r for r in (
_parse_metric_row(mp, mc)
for mp, mc in config["thresholds"].items()
) if r
]
threshold_config_list.append({
"name": "default",
"metrics": sorted(metrics, key=lambda m: m["metric"]),
})
if threshold_checker is not None:
if threshold_checker.threshold_configs:
for cfg_name, cfg_metrics in sorted(threshold_checker.threshold_configs.items()):
# For the default config use the merged effective set;
# for named overrides use only the explicitly defined metrics
# (threshold_raw_configs) so inherited defaults are not repeated.
if cfg_name == "default":
display_metrics = cfg_metrics
else:
display_metrics = threshold_checker.threshold_raw_configs.get(cfg_name, cfg_metrics)
metrics = sorted(
[_tc_to_row(tc) for tc in display_metrics.values()],
key=lambda m: m["metric"],
)
threshold_config_list.append({"name": cfg_name, "metrics": metrics})
elif threshold_checker.thresholds:
metrics = sorted(
[_tc_to_row(tc) for tc in threshold_checker.thresholds.values()],
key=lambda m: m["metric"],
)
threshold_config_list.append({"name": "default", "metrics": metrics})
# ---- Hosts summary ----------------------------------------------------
hosts_list = []
+3
View File
@@ -1054,6 +1054,7 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
bmsg["msg"] = args.message
for c in connections:
await c.sendto(bmsg)
break
if args.message and not args.daemon:
await asyncio.sleep(0.3)
for c in connections:
@@ -1090,6 +1091,8 @@ async def _async_main(args, cfg: Dict[str, Any]) -> int:
await conn.sendto({"shutdown": 1, "acks": conn.ackcount})
except Exception:
pass
break
for conn in connections:
conn.close()
await asyncio.sleep(0.3)
for plugin in plugins: