feat(docker): a removed container's logs and events stay reachable by name
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 50s
CI / unit (push) Successful in 1m5s
CI / publish (push) Skipped

#5010 "Logs of a removed container cannot be reached by name"

- get_container and search_container_logs fall back to the names Steward
  still holds logs or events for when no live container matches: the exact
  name from logs and events, or a part of it that matches one removed
  container's events.
- get_container answers with removed: true, last_seen_at, its history and
  events; search_container_logs marks container_removed and notes when it
  was last seen, and says when that is older than the range.
- A miss now says the container may have been removed, that Swarm replaces a
  dead task under a new name, and where to look instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 10:26:04 -04:00
co-authored by Claude Opus 5.5
parent e9341c5639
commit 190a5d1e80
3 changed files with 145 additions and 16 deletions
@@ -59,6 +59,7 @@ 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"
removed = f"{svc}.2.gone{tag}" # a dead task Docker removed: logs and events only
def _ago(minutes):
return (now - timedelta(minutes=minutes)).isoformat().replace("+00:00", "Z")
@@ -95,6 +96,14 @@ def test_docker_and_agent_tools_over_seeded_rows(app):
"INSERT INTO docker_events (id, host_id, container_name, event, detail, at) "
"VALUES (:id, :h, :n, 'die', 'exit 1', :at)"),
{"id": str(uuid.uuid4()), "h": hid, "n": name, "at": now - timedelta(minutes=10)})
await s.execute(text(
"INSERT INTO docker_events (id, host_id, container_name, event, detail, at) "
"VALUES (:id, :h, :n, 'die', 'exit 1', :at)"),
{"id": str(uuid.uuid4()), "h": hid, "n": removed, "at": now - timedelta(minutes=12)})
await s.execute(text(
"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)})
for i, (stream, line) in enumerate((
("stdout", "starting up"), ("stdout", "connected to db"),
("stderr", "ERROR: 100% of disk_used"), ("stdout", "retrying"),
@@ -194,6 +203,19 @@ def test_docker_and_agent_tools_over_seeded_rows(app):
with pytest.raises(ValueError, match="context needs `container`"):
await dtools.search_container_logs(text="x", context=2)
# ── a container Docker removed is still reachable by name ─────────
gone = await dtools.get_container(removed)
assert gone["removed"] is True and gone["host_name"] == host_name
assert [e["event"] for e in gone["events"]] == ["die"] and "no longer exists" in gone["note"]
assert (await dtools.get_container(web))["removed"] is False
lines = await dtools.search_container_logs(container=removed, range="1h")
assert lines["container_removed"] is True
assert [m["line"] for m in lines["matches"]] == ["FATAL: cannot open database"]
# A unique part of its name finds it through its events.
assert (await dtools.get_container(f"2.gone{tag}"))["container_name"] == removed
with pytest.raises(ValueError, match="may have been removed"):
await dtools.get_container(f"nothing-{tag}")
# ── swarm, disk ────────────────────────────────────────────────────
sw = await dtools.list_swarm_services(limit=500)
s = next(x for x in sw["services"] if x["service_name"] == svc)