refactor: move host, docker, alert and monitor queries into service modules
MCP-3 of milestone 461 "MCP server: Steward as an LLM-inspectable, LLM-operable hub". The /mcp tools run outside Quart's request pipeline, so the queries they share with the pages now live in plain functions that take an open session: - steward/hosts/service.py: uptime, status, monitors, agent metrics, agent overview (stale threshold passed in, not read from current_app), CPU sparks. - plugins/docker/service.py: bucket, container_history, host_map, list_containers, container_events, query_logs. - steward/alerts/service.py: active_alerts (also used by core/reports), list_rules, list_maintenance_windows. - steward/monitors/service.py: monitor_rows (was routes._rows_data). Routes are repointed and render the same data. One behavior fix rides along: the container-log search now escapes % and _ so they match literally instead of acting as LIKE wildcards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,13 +10,12 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from quart import Blueprint, current_app, render_template, request, redirect, url_for
|
||||
from sqlalchemy import select
|
||||
|
||||
from steward.auth.middleware import require_role
|
||||
from steward.models.users import UserRole
|
||||
from steward.models.monitors import Monitor, MonitorType, MONITOR_TYPE_VALUES
|
||||
from steward.core.status import _last_n_results, _uptime_by_monitor, _heartbeat_title
|
||||
from steward.core.time_range import parse_range, DEFAULT_RANGE
|
||||
from steward.core.time_range import DEFAULT_RANGE
|
||||
from steward.monitors import service as monitor_svc
|
||||
|
||||
monitors_bp = Blueprint("monitors", __name__, url_prefix="/monitors")
|
||||
|
||||
@@ -52,41 +51,6 @@ def _normalise_target(mtype: str, target: str) -> str:
|
||||
return target
|
||||
|
||||
|
||||
async def _rows_data(range_key: str):
|
||||
"""Per-monitor display data for the list / widget."""
|
||||
since, range_key = parse_range(range_key)
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
monitors = list((await db.execute(
|
||||
select(Monitor).order_by(Monitor.name)
|
||||
)).scalars())
|
||||
ids = [m.id for m in monitors]
|
||||
recent = await _last_n_results(db, ids)
|
||||
uptime = await _uptime_by_monitor(db, ids)
|
||||
|
||||
# Map the selected range to the uptime bucket we surface in the list.
|
||||
bucket = {"24h": "24h", "7d": "7d", "30d": "30d"}.get(range_key, "24h")
|
||||
now = datetime.now(timezone.utc)
|
||||
data = []
|
||||
for m in monitors:
|
||||
rows = recent.get(m.id, [])
|
||||
latest = rows[-1] if rows else None
|
||||
tls_days = None
|
||||
if latest and latest.tls_expires_at:
|
||||
tls_days = (latest.tls_expires_at - now).total_seconds() / 86400.0
|
||||
data.append({
|
||||
"monitor": m,
|
||||
"latest": latest,
|
||||
"heartbeat": [{"state": "up" if r.is_up else "down",
|
||||
"title": _heartbeat_title(m.type, r)} for r in rows],
|
||||
"uptime_pct": uptime.get(m.id, {}).get(bucket),
|
||||
"tls_days": tls_days,
|
||||
})
|
||||
up = sum(1 for d in data if d["latest"] and d["latest"].is_up)
|
||||
down = sum(1 for d in data if d["latest"] and not d["latest"].is_up)
|
||||
pending = sum(1 for d in data if not d["latest"])
|
||||
return data, up, down, pending, range_key
|
||||
|
||||
|
||||
@monitors_bp.get("/")
|
||||
@require_role(UserRole.viewer)
|
||||
async def index():
|
||||
@@ -102,9 +66,9 @@ async def index():
|
||||
@monitors_bp.get("/rows")
|
||||
@require_role(UserRole.viewer)
|
||||
async def rows():
|
||||
data, up, down, pending, range_key = await _rows_data(
|
||||
request.args.get("range", DEFAULT_RANGE)
|
||||
)
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
data, up, down, pending, range_key = await monitor_svc.monitor_rows(
|
||||
db, request.args.get("range", DEFAULT_RANGE))
|
||||
return await render_template(
|
||||
"monitors/rows.html",
|
||||
monitor_data=data, up=up, down=down, pending=pending, range_key=range_key,
|
||||
@@ -202,7 +166,8 @@ async def widget():
|
||||
limit = max(1, min(50, int(request.args.get("limit", 10) or 10)))
|
||||
widget_id = request.args.get("wid", "0")
|
||||
|
||||
data, up, down, pending, _ = await _rows_data(DEFAULT_RANGE)
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
data, up, down, pending, _ = await monitor_svc.monitor_rows(db, DEFAULT_RANGE)
|
||||
if type_filter in MONITOR_TYPE_VALUES:
|
||||
data = [d for d in data if d["monitor"].type == type_filter]
|
||||
if show_down_only:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Monitor queries shared by the monitors pages and the MCP tools.
|
||||
|
||||
Each function takes an open session, with no Quart request context, so the
|
||||
routes and the /mcp tools (which run outside Quart's pipeline) call the same
|
||||
code.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from steward.core.status import _heartbeat_title, _last_n_results, _uptime_by_monitor
|
||||
from steward.core.time_range import parse_range
|
||||
from steward.models.monitors import Monitor
|
||||
|
||||
|
||||
async def monitor_rows(db, range_key: str):
|
||||
"""Per-monitor state: latest result, heartbeat strip, uptime and TLS days.
|
||||
|
||||
Returns (rows, up, down, pending, range_key). `range_key` picks the uptime
|
||||
window surfaced (24h/7d/30d; anything else falls back to 24h) and comes
|
||||
back normalised.
|
||||
"""
|
||||
_since, range_key = parse_range(range_key)
|
||||
monitors = list((await db.execute(select(Monitor).order_by(Monitor.name))).scalars())
|
||||
ids = [m.id for m in monitors]
|
||||
recent = await _last_n_results(db, ids)
|
||||
uptime = await _uptime_by_monitor(db, ids)
|
||||
|
||||
# Map the selected range to the uptime bucket we surface in the list.
|
||||
bucket = {"24h": "24h", "7d": "7d", "30d": "30d"}.get(range_key, "24h")
|
||||
now = datetime.now(timezone.utc)
|
||||
data = []
|
||||
for m in monitors:
|
||||
rows = recent.get(m.id, [])
|
||||
latest = rows[-1] if rows else None
|
||||
tls_days = None
|
||||
if latest and latest.tls_expires_at:
|
||||
tls_days = (latest.tls_expires_at - now).total_seconds() / 86400.0
|
||||
data.append({
|
||||
"monitor": m,
|
||||
"latest": latest,
|
||||
"heartbeat": [{"state": "up" if r.is_up else "down",
|
||||
"title": _heartbeat_title(m.type, r)} for r in rows],
|
||||
"uptime_pct": uptime.get(m.id, {}).get(bucket),
|
||||
"tls_days": tls_days,
|
||||
})
|
||||
up = sum(1 for d in data if d["latest"] and d["latest"].is_up)
|
||||
down = sum(1 for d in data if d["latest"] and not d["latest"].is_up)
|
||||
pending = sum(1 for d in data if not d["latest"])
|
||||
return data, up, down, pending, range_key
|
||||
Reference in New Issue
Block a user