feat(monitors): unify ping/dns/http into one Monitor entity + custom targets
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:
+28
-33
@@ -12,9 +12,8 @@ from email.message import EmailMessage
|
||||
from sqlalchemy import and_, case, func, select
|
||||
|
||||
from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertEvent
|
||||
from steward.models.hosts import Host
|
||||
from steward.models.metrics import PluginMetric
|
||||
from steward.models.monitors import PingResult, PingStatus
|
||||
from steward.models.monitors import Monitor, MonitorResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,32 +27,28 @@ async def build_report_data(app) -> dict:
|
||||
cutoff_24h = now - timedelta(hours=24)
|
||||
|
||||
async with app.db_sessionmaker() as db:
|
||||
# ── Uptime summary (7d) ───────────────────────────────────────────────
|
||||
host_result = await db.execute(
|
||||
select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name)
|
||||
)
|
||||
hosts = host_result.scalars().all()
|
||||
# ── Uptime summary (7d) — per enabled monitor of any type ─────────────
|
||||
monitors = (await db.execute(
|
||||
select(Monitor).where(Monitor.enabled.is_(True)).order_by(Monitor.name)
|
||||
)).scalars().all()
|
||||
|
||||
uptime_rows = await db.execute(
|
||||
select(
|
||||
PingResult.host_id,
|
||||
func.count(PingResult.id).label("total"),
|
||||
func.sum(case((PingResult.status == PingStatus.up, 1), else_=0)).label("up"),
|
||||
MonitorResult.monitor_id,
|
||||
func.count(MonitorResult.id).label("total"),
|
||||
func.sum(case((MonitorResult.is_up.is_(True), 1), else_=0)).label("up"),
|
||||
)
|
||||
.where(PingResult.probed_at >= cutoff_7d)
|
||||
.group_by(PingResult.host_id)
|
||||
.where(MonitorResult.checked_at >= cutoff_7d)
|
||||
.group_by(MonitorResult.monitor_id)
|
||||
)
|
||||
uptime_map: dict[str, float | None] = {}
|
||||
for row in uptime_rows:
|
||||
pct = round(float(row.up) / float(row.total) * 100, 2) if row.total else None
|
||||
uptime_map[row.host_id] = pct
|
||||
uptime_map[row.monitor_id] = pct
|
||||
|
||||
uptime_summary = []
|
||||
for h in hosts:
|
||||
uptime_summary.append({
|
||||
"name": h.name,
|
||||
"pct_7d": uptime_map.get(h.id),
|
||||
})
|
||||
uptime_summary = [
|
||||
{"name": m.name, "pct_7d": uptime_map.get(m.id)} for m in monitors
|
||||
]
|
||||
|
||||
# ── Active alerts ─────────────────────────────────────────────────────
|
||||
active_result = await db.execute(
|
||||
@@ -112,27 +107,27 @@ async def build_report_data(app) -> dict:
|
||||
for row in top_routers_result
|
||||
]
|
||||
|
||||
# ── Hosts currently down ──────────────────────────────────────────────
|
||||
latest_ping_subq = (
|
||||
select(PingResult.host_id, func.max(PingResult.probed_at).label("max_at"))
|
||||
.group_by(PingResult.host_id)
|
||||
# ── Monitors currently down (latest result is_up = false) ─────────────
|
||||
latest_subq = (
|
||||
select(MonitorResult.monitor_id, func.max(MonitorResult.checked_at).label("max_at"))
|
||||
.group_by(MonitorResult.monitor_id)
|
||||
.subquery()
|
||||
)
|
||||
down_result = await db.execute(
|
||||
select(Host.name)
|
||||
.join(PingResult, PingResult.host_id == Host.id)
|
||||
select(Monitor.name)
|
||||
.join(MonitorResult, MonitorResult.monitor_id == Monitor.id)
|
||||
.join(
|
||||
latest_ping_subq,
|
||||
latest_subq,
|
||||
and_(
|
||||
PingResult.host_id == latest_ping_subq.c.host_id,
|
||||
PingResult.probed_at == latest_ping_subq.c.max_at,
|
||||
MonitorResult.monitor_id == latest_subq.c.monitor_id,
|
||||
MonitorResult.checked_at == latest_subq.c.max_at,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
Host.ping_enabled.is_(True),
|
||||
PingResult.status == PingStatus.down,
|
||||
Monitor.enabled.is_(True),
|
||||
MonitorResult.is_up.is_(False),
|
||||
)
|
||||
.order_by(Host.name)
|
||||
.order_by(Monitor.name)
|
||||
)
|
||||
hosts_down = [row.name for row in down_result]
|
||||
|
||||
@@ -157,7 +152,7 @@ def _render_report_text(data: dict) -> str:
|
||||
# ── Hosts currently down ──────────────────────────────────────────────────
|
||||
down = data["hosts_down"]
|
||||
if down:
|
||||
lines.append(f"⚠ {len(down)} host(s) currently DOWN: {', '.join(down)}")
|
||||
lines.append(f"⚠ {len(down)} monitor(s) currently DOWN: {', '.join(down)}")
|
||||
lines.append("")
|
||||
|
||||
# ── Uptime summary ────────────────────────────────────────────────────────
|
||||
@@ -168,7 +163,7 @@ def _render_report_text(data: dict) -> str:
|
||||
pct_str = f"{pct:.2f}%" if pct is not None else "no data"
|
||||
lines.append(f" {entry['name']:<32} {pct_str}")
|
||||
if not data["uptime_summary"]:
|
||||
lines.append(" (no ping-enabled hosts)")
|
||||
lines.append(" (no monitors configured)")
|
||||
lines.append("")
|
||||
|
||||
# ── Active alerts ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user