feat(mcp): search_container_logs takes a Swarm service's name and searches every task it has run (#5468)
#5468 "Log search by service name: searching "steward_app" refuses because every deploy makes a new container name". search_container_logs(container="steward_app") was refused, because no container carries the service's own name and five task containers partly matched. Now a service's name reaches every task container Steward holds anything for, live and removed (docker_svc.service_containers, as the service page's logs already do), so one search spans deploys. Each match carries the slot and task_id that wrote it, and the reply names the service and how many tasks were searched. A container named exactly what was asked still takes precedence. A miss that partly matches task containers now names their services. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,9 @@ def test_docker_and_agent_tools_over_seeded_rows(app):
|
||||
host_name, quiet_host = f"dock-{tag}", f"quiet-{tag}"
|
||||
web, db_, job, svc = f"web-{tag}", f"db-{tag}", f"job-{tag}", f"svc-{tag}"
|
||||
replaced = f"{svc}.1.old"
|
||||
# A service across a deploy (#5468): its live task, and the task the deploy replaced.
|
||||
api = f"api-{tag}"
|
||||
api_live, api_gone = f"{api}.1.{tag}{'a' * 17}", f"{api}.1.{tag}{'b' * 17}"
|
||||
removed = f"{svc}.2.gone{tag}" # a dead task Docker removed: logs and events only
|
||||
|
||||
def _ago(minutes):
|
||||
@@ -105,6 +108,21 @@ def test_docker_and_agent_tools_over_seeded_rows(app):
|
||||
"INSERT INTO docker_logs (id, host_id, container_name, ts, stream, line) "
|
||||
"VALUES (:id, :h, :n, :ts, 'stderr', 'FATAL: cannot open database')"),
|
||||
{"id": str(uuid.uuid4()), "h": hid, "n": removed, "ts": now - timedelta(minutes=13)})
|
||||
await s.execute(container_sql, {
|
||||
"h": hid, "n": api_live, "cid": uuid.uuid4().hex, "st": "running", "rc": 0,
|
||||
"started": now - timedelta(minutes=3), "now": now, "health": None,
|
||||
"exit": None, "proj": None, "svc": api, "task": f"{tag}{'a' * 17}"})
|
||||
await s.execute(text(
|
||||
"INSERT INTO docker_events (id, host_id, container_name, event, detail, at) "
|
||||
"VALUES (:id, :h, :n, 'start', '', :at)"),
|
||||
{"id": str(uuid.uuid4()), "h": hid, "n": api_gone, "at": now - timedelta(minutes=30)})
|
||||
for name, mins, line in ((api_gone, 20, "listening (old image)"),
|
||||
(api_live, 2, "listening (new image)")):
|
||||
await s.execute(text(
|
||||
"INSERT INTO docker_logs (id, host_id, container_name, ts, stream, line) "
|
||||
"VALUES (:id, :h, :n, :ts, 'stdout', :line)"),
|
||||
{"id": str(uuid.uuid4()), "h": hid, "n": name,
|
||||
"ts": now - timedelta(minutes=mins), "line": line})
|
||||
for i, (stream, line) in enumerate((
|
||||
("stdout", "starting up"), ("stdout", "connected to db"),
|
||||
("stderr", "ERROR: 100% of disk_used"), ("stdout", "retrying"),
|
||||
@@ -218,6 +236,17 @@ def test_docker_and_agent_tools_over_seeded_rows(app):
|
||||
with pytest.raises(ValueError, match="may have been removed"):
|
||||
await dtools.get_container(f"nothing-{tag}")
|
||||
|
||||
# ── a service's name searches every task it has run (#5468) ─────
|
||||
both = await dtools.search_container_logs(container=api, text="listening")
|
||||
assert [m["line"] for m in both["matches"]] == ["listening (new image)", "listening (old image)"]
|
||||
assert [(m["slot"], m["task_id"]) for m in both["matches"]] == [
|
||||
("1", f"{tag}{'a' * 17}"), ("1", f"{tag}{'b' * 17}")]
|
||||
assert (both["service"], both["tasks_searched"]) == (api, 2)
|
||||
assert (await dtools.search_container_logs(container=api, host=quiet_host))["matches"] == []
|
||||
# A container's own name still reads as that one container.
|
||||
one = await dtools.search_container_logs(container=api_live)
|
||||
assert [m["line"] for m in one["matches"]] == ["listening (new image)"] and "service" not in one
|
||||
|
||||
# ── swarm, disk ────────────────────────────────────────────────────
|
||||
sw = await dtools.list_swarm_services(limit=500)
|
||||
s = next(x for x in sw["services"] if x["service_name"] == svc)
|
||||
|
||||
Reference in New Issue
Block a user