feat(monitors): unify ping/dns/http into one Monitor entity + custom targets
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m17s
CI / publish (push) Successful in 1m10s

Collapse the three former check types into a single core `Monitor` entity
with one management surface (/monitors), one result table (monitor_results),
and a single scheduled task. Every type can now watch a free-standing custom
destination (optional host_id) — not just a registered Host.

- models: Monitor + MonitorResult replace PingResult/DnsResult; Host loses its
  ping/dns facet columns (now Monitor rows linked by host_id).
- checks: monitors/{ping,dns,http}.py pure probes + runner.run_monitor
  dispatcher; one monitor_check scheduler with a per-monitor due-filter.
- status: single monitor_status_source replaces the three sources.
- UI: /monitors blueprint (type-aware add/edit/list/widget); host hub shows a
  host's linked monitors + "add monitor for this host"; nav + widget registry
  + alert metric catalog rewired. http plugin folded into core and removed.
- migration 0022 merges the http branch, data-migrates host facets +
  http_monitors + all three result histories, drops the old tables/columns.

Resolves the per-host ping/dns auto-attach issue (#275): monitors are now
explicit, never auto-added to every host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
2026-06-18 08:56:13 -04:00
parent 591706bd39
commit 35f658b573
53 changed files with 1628 additions and 1839 deletions
+15 -10
View File
@@ -6,7 +6,6 @@ from sqlalchemy import select
from steward.auth.middleware import require_role
from steward.ansible import sources as src_module
from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertOperator, MaintenanceWindow
from steward.models.hosts import Host
from steward.models.metrics import PluginMetric
from steward.models.users import UserRole
@@ -14,15 +13,18 @@ alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts")
_OPERATORS = [(op.value, op.value) for op in AlertOperator]
# Static catalog: source_module → available metric names
# Static catalog: source_module → available metric names.
# Unified monitors emit metrics under their TYPE (icmp/tcp/dns/http), each with
# is_up (1.0 up / 0.0 down) and response_ms (where the probe times it).
METRIC_CATALOG: dict[str, list[str]] = {
"ping": ["up", "response_time_ms"],
"dns": ["resolved", "ip_changed"],
"icmp": ["is_up", "response_ms"],
"tcp": ["is_up", "response_ms"],
"dns": ["is_up", "response_ms"],
"http": ["is_up", "response_ms"],
"traefik": ["request_rate", "error_rate", "latency_p50_ms", "latency_p95_ms",
"latency_p99_ms", "response_bytes_rate", "cert_expiry_days"],
"unifi": ["is_up", "latency_ms", "total_clients"],
"docker": ["cpu_pct", "mem_pct"],
"http": ["is_up", "response_ms"],
"host_agent": [
"cpu_pct", "mem_used_pct", "mem_available_bytes", "swap_used_bytes",
"disk_used_pct_worst", "load_1m", "load_5m", "load_15m", "uptime_secs",
@@ -38,8 +40,8 @@ _SOURCE_MODULES = list(METRIC_CATALOG.keys())
async def _get_resources(db, source_module: str) -> list[str]:
"""Return sorted distinct resource names for a source module.
For ping/dns, supplements with host names so the list is populated
even before any metrics have been recorded.
For monitor types (icmp/tcp/dns/http), supplements with monitor names so
the list is populated even before any metrics have been recorded.
"""
result = await db.execute(
select(PluginMetric.resource_name)
@@ -49,9 +51,12 @@ async def _get_resources(db, source_module: str) -> list[str]:
)
resources: set[str] = {r for (r,) in result}
if source_module in ("ping", "dns"):
host_result = await db.execute(select(Host.name).order_by(Host.name))
for (name,) in host_result:
if source_module in ("icmp", "tcp", "dns", "http"):
from steward.models.monitors import Monitor
mon_result = await db.execute(
select(Monitor.name).where(Monitor.type == source_module).order_by(Monitor.name)
)
for (name,) in mon_result:
resources.add(name)
return sorted(resources)