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
+11 -46
View File
@@ -115,8 +115,7 @@ def create_app(
from .auth.routes import auth_bp
from .dashboard.routes import dashboard_bp
from .hosts.routes import hosts_bp
from .ping.routes import ping_bp
from .dns.routes import dns_bp
from .monitors.routes import monitors_bp
from .status.routes import status_bp
from .alerts.routes import alerts_bp
from .ansible.routes import ansible_bp
@@ -127,8 +126,7 @@ def create_app(
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(hosts_bp)
app.register_blueprint(ping_bp)
app.register_blueprint(dns_bp)
app.register_blueprint(monitors_bp)
app.register_blueprint(status_bp)
app.register_blueprint(alerts_bp)
app.register_blueprint(ansible_bp)
@@ -136,11 +134,10 @@ def create_app(
app.register_blueprint(settings_bp)
app.register_blueprint(audit_bp)
# Register the core (ping/DNS) status sources for the unified Status page.
# Plugins register their own sources from setup() (e.g. http). Idempotent.
from .core.status import register_status_source, ping_status_source, dns_status_source
register_status_source(ping_status_source)
register_status_source(dns_status_source)
# Register the unified Monitor status source for the Status page. Plugins
# may register additional sources from setup(). Idempotent.
from .core.status import register_status_source, monitor_status_source
register_status_source(monitor_status_source)
# Publish the Ansible "run a playbook" capability so plugins (e.g. host_agent
# auto-deploy) can drive runs without importing the runner. Ansible is core,
@@ -230,35 +227,9 @@ def _register_core_tasks(app: Quart) -> None:
poll_interval = app.config.get("MONITORS_POLL_INTERVAL", 60)
cleanup_interval = 3600 # hourly
async def run_ping_monitors():
from sqlalchemy import select
from .models.hosts import Host
from .monitors.ping import ping_check
async with app.db_sessionmaker() as session:
async with session.begin():
result = await session.execute(
select(Host).where(Host.ping_enabled.is_(True))
)
for host in result.scalars().all():
try:
await ping_check(host, session)
except Exception:
app.logger.exception(f"Ping check failed for host {host.name!r}")
async def run_dns_monitors():
from sqlalchemy import select
from .models.hosts import Host
from .monitors.dns import dns_check
async with app.db_sessionmaker() as session:
async with session.begin():
result = await session.execute(
select(Host).where(Host.dns_enabled.is_(True))
)
for host in result.scalars().all():
try:
await dns_check(host, session)
except Exception:
app.logger.exception(f"DNS check failed for host {host.name!r}")
async def run_monitor_checks():
from .monitors.scheduler import run_due_monitors
await run_due_monitors(app)
async def run_cleanup():
from .core.cleanup import run_cleanup as _cleanup
@@ -276,14 +247,8 @@ def _register_core_tasks(app: Quart) -> None:
run_on_startup=False,
),
ScheduledTask(
name="ping_monitor",
coro_factory=run_ping_monitors,
interval_seconds=poll_interval,
run_on_startup=True,
),
ScheduledTask(
name="dns_monitor",
coro_factory=run_dns_monitors,
name="monitor_check",
coro_factory=run_monitor_checks,
interval_seconds=poll_interval,
run_on_startup=True,
),