diff --git a/steward/mcp/tools/monitors.py b/steward/mcp/tools/monitors.py index 2224f82..2cbc693 100644 --- a/steward/mcp/tools/monitors.py +++ b/steward/mcp/tools/monitors.py @@ -6,6 +6,7 @@ import json from sqlalchemy import select from steward.core import renames +from steward.mcp._context import mcp_app from steward.models.hosts import Host from steward.models.monitors import MONITOR_TYPE_VALUES, Monitor from steward.models.users import UserRole @@ -27,6 +28,15 @@ def _state(m: Monitor, latest) -> str: return "up" if latest.is_up else "down" +def _interval(m: Monitor) -> dict: + """How often a monitor runs: its own interval, or the global one it + follows when it has none (#5433), and which.""" + if m.check_interval_seconds: + return {"check_interval_s": m.check_interval_seconds, "interval_from": "monitor"} + return {"check_interval_s": int(mcp_app().config.get("MONITORS_POLL_INTERVAL", 60)), + "interval_from": "global"} + + def _tls_days(latest) -> float | None: if latest is None or latest.tls_expires_at is None: return None @@ -112,8 +122,10 @@ async def get_monitor(monitor: str, failures: int = 10) -> dict: newest first, each with its error message. `heartbeat` is its last 30 checks oldest-first as a string, "+" up and "-" - down, to show a flapping check at a glance. Header values in an HTTP - monitor's configuration are redacted. + down, to show a flapping check at a glance. `check_interval_s` is how + often it runs; `interval_from` says whether that is its own setting + ("monitor") or the global monitors interval it follows ("global"). + Header values in an HTTP monitor's configuration are redacted. """ failures = clamp_limit(failures, 10, 50) async with db_session() as db: @@ -123,7 +135,7 @@ async def get_monitor(monitor: str, failures: int = 10) -> dict: failed = await monitor_svc.recent_failures(db, m.id, failures) return { **_row(m, st, host_names), - "check_interval_s": m.check_interval_seconds or None, + **_interval(m), "config": _config(m), "heartbeat": "".join("+" if r.is_up else "-" for r in st["recent"]), "recent_failures": [ @@ -156,7 +168,7 @@ def _saved(m: Monitor, host_names: dict[str, str]) -> dict: "target": redact_url(m.target), "host_name": host_names.get(m.host_id) if m.host_id else None, "enabled": m.enabled, - "check_interval_s": m.check_interval_seconds or None, + **_interval(m), "config": _config(m), "next": f'get_monitor(monitor="{m.id}")', } diff --git a/tests/integration/test_mcp_monitoring_writes.py b/tests/integration/test_mcp_monitoring_writes.py index f55ad42..6c773ac 100644 --- a/tests/integration/test_mcp_monitoring_writes.py +++ b/tests/integration/test_mcp_monitoring_writes.py @@ -76,6 +76,9 @@ def test_monitor_rule_ack_and_maintenance_writes(app): assert made["target"] == "https://nas.lan" and made["host_name"] == host_name assert made["config"]["headers"] == {"Authorization": ""} assert made["config"]["verify_ssl"] is True + # No interval of its own: it says which it follows, and how often (#5433). + assert (made["check_interval_s"], made["interval_from"]) == ( + int(app.config.get("MONITORS_POLL_INTERVAL", 60)), "global") # ── rules ────────────────────────────────────────────────────────── opts = await a.get_alert_rule_options("http") @@ -100,7 +103,9 @@ def test_monitor_rule_ack_and_maintenance_writes(app): assert renamed["carried"]["from"] == mon_name and renamed["carried"]["rules"] == 1 async with app.db_sessionmaker() as s: assert (await s.get(AlertRule, rule["rule_id"])).resource_name == f"{mon_name}-2" - assert "carried" not in await m.update_monitor(f"{mon_name}-2", check_interval_s=30) + own = await m.update_monitor(f"{mon_name}-2", check_interval_s=30) + assert "carried" not in own + assert (own["check_interval_s"], own["interval_from"]) == (30, "monitor") # Another http monitor may not take the same name: they would share one history. with pytest.raises(ValueError, match="already named"): await m.create_monitor(f"{mon_name}-2", "http", "other.lan")