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:
2026-10-15 15:58:28 -04:00
co-authored by Claude Opus 5.5
parent a24b9da69d
commit 91bb199d14
8 changed files with 139 additions and 10 deletions
+24
View File
@@ -103,3 +103,27 @@ def test_a_plugin_adds_events_through_its_hook(app):
assert "events_test" not in mcp.event_sources
finally:
mcp.event_sources.pop("events_test", None)
def test_a_plugin_adds_a_hostless_resources_events_through_its_hook(app):
async def events(db, source, resource, since, until):
return []
mcp = app.mcp_server
mcp.register_plugin("events_test", NS(mcp_resource_events=events))
try:
assert mcp.resource_event_sources["events_test"] is events
mcp.register_plugin("events_test", NS())
assert "events_test" not in mcp.resource_event_sources
finally:
mcp.resource_event_sources.pop("events_test", None)
def test_steward_not_recording_is_an_event_beside_every_anomaly():
"""A Steward restart between two of its once-a-minute samples (#5862)."""
ticks = [NOW + timedelta(minutes=m) for m in (0, 1, 2)] + [NOW + timedelta(minutes=4, seconds=10)]
[gap] = _events.gaps_in(ticks, NOW, NOW + timedelta(hours=1))
assert gap["kind"] == "steward" and gap["at"] == "2026-10-15T12:02:00Z"
assert gap["until"] == "2026-10-15T12:04:10Z" and "wasn't recording" in gap["what"]
assert _events.gaps_in(ticks, NOW + timedelta(minutes=5), NOW + timedelta(hours=1)) == []
assert _events.gaps_in([NOW + timedelta(minutes=m) for m in range(5)], NOW, NOW + timedelta(hours=1)) == []