35f658b573
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
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""Single scheduled task that runs every due Monitor of any type."""
|
|
from __future__ import annotations
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import select
|
|
|
|
from steward.core.alerts import record_metric
|
|
from steward.models.monitors import Monitor, MonitorResult
|
|
from .runner import run_monitor
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def run_due_monitors(app) -> None:
|
|
"""Check every enabled Monitor whose interval has elapsed, persist results.
|
|
|
|
Per-monitor `check_interval_seconds` overrides the global
|
|
MONITORS_POLL_INTERVAL (0 = use global), mirroring how the scheduler fires
|
|
on the global cadence but each monitor decides if it is actually due.
|
|
"""
|
|
global_interval = int(app.config.get("MONITORS_POLL_INTERVAL", 60))
|
|
now = datetime.now(timezone.utc)
|
|
|
|
async with app.db_sessionmaker() as session:
|
|
monitors = list((await session.execute(
|
|
select(Monitor).where(Monitor.enabled.is_(True))
|
|
)).scalars())
|
|
|
|
due: list[Monitor] = []
|
|
for m in monitors:
|
|
interval = m.check_interval_seconds or global_interval
|
|
if m.last_checked_at is None or (now - m.last_checked_at).total_seconds() >= interval:
|
|
due.append(m)
|
|
if not due:
|
|
return
|
|
|
|
pairs = await asyncio.gather(
|
|
*[run_monitor(m) for m in due], return_exceptions=True
|
|
)
|
|
check_time = datetime.now(timezone.utc)
|
|
|
|
async with app.db_sessionmaker() as session:
|
|
async with session.begin():
|
|
for monitor, res in zip(due, pairs):
|
|
if isinstance(res, Exception):
|
|
logger.error("Monitor %r check errored: %s", monitor.name, res)
|
|
continue
|
|
session.add(MonitorResult(
|
|
monitor_id=monitor.id,
|
|
checked_at=check_time,
|
|
is_up=res["is_up"],
|
|
response_ms=res["response_ms"],
|
|
status_code=res["status_code"],
|
|
resolved_ip=res["resolved_ip"],
|
|
content_matched=res["content_matched"],
|
|
tls_expires_at=res["tls_expires_at"],
|
|
error_msg=res["error_msg"],
|
|
))
|
|
db_monitor = await session.get(Monitor, monitor.id)
|
|
if db_monitor:
|
|
db_monitor.last_checked_at = check_time
|
|
|
|
# Alert pipeline — keyed by monitor type (source) + name.
|
|
await record_metric(
|
|
session=session, source_module=monitor.type,
|
|
resource_name=monitor.name, metric_name="is_up",
|
|
value=1.0 if res["is_up"] else 0.0,
|
|
)
|
|
if res["response_ms"] is not None:
|
|
await record_metric(
|
|
session=session, source_module=monitor.type,
|
|
resource_name=monitor.name, metric_name="response_ms",
|
|
value=res["response_ms"],
|
|
)
|