fix(mcp): get_anomaly shows Steward's own restarts beside every anomaly, and a router's containers (#5862)
All 16 Traefik 4xx anomalies on 2026-10-15 came back with nothing around them, though 15 were Steward's own restarts. A router has no host, and every events source was read by host. - Core: each break of more than 90 s in Steward's once-a-minute health sample is a "steward" event beside every anomaly, placed or not. - Plugins: an optional mcp_resource_events(db, source, resource, since, until) for a resource with no host. Traefik's gives the container events of the swarm service behind a router, on any host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,3 +72,10 @@ def register_mcp_tools(register) -> None:
|
||||
async def mcp_overview(db) -> list[dict]:
|
||||
from .mcp_tools import mcp_overview as findings
|
||||
return await findings(db)
|
||||
|
||||
|
||||
async def mcp_resource_events(db, source: str, resource: str, since, until) -> list[dict]:
|
||||
if source != "traefik":
|
||||
return []
|
||||
from .mcp_tools import service_events
|
||||
return await service_events(db, resource, since, until)
|
||||
|
||||
@@ -22,6 +22,7 @@ from steward.mcp.tools._shape import (
|
||||
from steward.mcp.tools.overview import finding
|
||||
from steward.models.users import UserRole
|
||||
|
||||
from . import meaning
|
||||
from . import routes as page
|
||||
from .models import TraefikAccessSummary, TraefikCert, TraefikGlobalStat
|
||||
from .scheduler import error_page_services, router_domains
|
||||
@@ -316,6 +317,29 @@ async def mcp_overview(db) -> list[dict]:
|
||||
return router_findings(current) + cert_findings(served, now, thresholds)
|
||||
|
||||
|
||||
async def service_events(db, router: str, since, until) -> list[dict]:
|
||||
"""What happened to the containers of the swarm service behind a router
|
||||
(starts with their image, deaths, OOM kills, health changes), on every
|
||||
host: a router anomaly has no host of its own to read events by (#5862).
|
||||
[] without the Docker plugin, or when no single service matches."""
|
||||
from steward.core.plugin_manager import loaded_plugin_submodule
|
||||
from steward.mcp.tools._events import PER_SOURCE, event
|
||||
from steward.models.hosts import Host
|
||||
models = loaded_plugin_submodule("docker", "models")
|
||||
if models is None:
|
||||
return []
|
||||
service = meaning.swarm_service(router, await page.swarm_service_names(db))
|
||||
if service is None:
|
||||
return []
|
||||
e = models.DockerEvent
|
||||
rows = (await db.execute(
|
||||
select(e, Host.name).join(Host, Host.id == e.host_id)
|
||||
.where(e.container_name.startswith(f"{service}.", autoescape=True), e.at >= since, e.at <= until)
|
||||
.order_by(e.at).limit(PER_SOURCE))).all()
|
||||
return [event(ev.at, "container", f"{ev.container_name} {ev.event}" + (f" ({ev.detail})" if ev.detail else ""),
|
||||
subject=f"{host}/{ev.container_name}") for ev, host in rows]
|
||||
|
||||
|
||||
def register_tools(register) -> None:
|
||||
for fn in (list_routers, get_router, list_certificates, get_traffic):
|
||||
register(fn, role=UserRole.viewer, read_only=True)
|
||||
|
||||
Reference in New Issue
Block a user