From c93dbdc0f44bb97c675feb5c80b3cc05675db622 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Mon, 4 May 2026 09:12:39 -0400 Subject: [PATCH] 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 --- hbd/client/main.py | 4 ++- hbd/server/http.py | 2 +- hbd/server/main.py | 1 + hbd/server/settings.py | 67 +++++++++++++++++++----------------------- scripts/hbc_mini.py | 3 ++ 5 files changed, 38 insertions(+), 39 deletions(-) diff --git a/hbd/client/main.py b/hbd/client/main.py index 2418910..9587385 100644 --- a/hbd/client/main.py +++ b/hbd/client/main.py @@ -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)}") diff --git a/hbd/server/http.py b/hbd/server/http.py index c40e3fe..cdda8fd 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -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", ) diff --git a/hbd/server/main.py b/hbd/server/main.py index 95701bd..4ed1230 100644 --- a/hbd/server/main.py +++ b/hbd/server/main.py @@ -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="", diff --git a/hbd/server/settings.py b/hbd/server/settings.py index 39648d0..b442890 100644 --- a/hbd/server/settings.py +++ b/hbd/server/settings.py @@ -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 = [] diff --git a/scripts/hbc_mini.py b/scripts/hbc_mini.py index 7a0ba7c..5dd6e47 100755 --- a/scripts/hbc_mini.py +++ b/scripts/hbc_mini.py @@ -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: