feat(docker): overview findings say when the problem began

#5013 "Overview findings for containers and Swarm services carry since=null"

- Container problems are dated from their own evidence: a crash or restart
  loop from its first death in the hour, an OOM kill or error exit from the
  death that ended it, a failing healthcheck from its last turn to unhealthy.
  list_containers and get_container carry problem_since / problem_age_s.
- Swarm services carry trouble_since: a crash loop's first failure in the
  hour, a short service's oldest troubled task in each failing slot, a
  recovered service's first failure in 24h. list_swarm_services shows it.
- get_overview findings from the docker plugin now have since and age_s,
  so they rank by how long they have stood.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 10:24:10 -04:00
co-authored by Claude Opus 5.5
parent bac2859455
commit e9341c5639
6 changed files with 209 additions and 15 deletions
+32
View File
@@ -137,3 +137,35 @@ async def test_prune_docker_refuses_an_unknown_target_before_reading_anything(mo
with pytest.raises(ValueError, match="'volumes' is not one of containers, images, system"):
await t.prune_docker("nas", "volumes")
assert inspect.signature(t.prune_docker).parameters["check"].default is True
def _death(event, minutes_ago):
return NS(event=event, at=NOW - timedelta(minutes=minutes_ago))
def test_problem_since_dates_each_problem_from_its_own_evidence():
hour_ago = NOW - timedelta(hours=1)
deaths = [_death("die", 5), _death("die", 20), _death("die", 50), _death("die", 300)]
# A crash loop began with its first death inside the hour, not the older one.
assert t.problem_since(["crash_looping"], deaths, hour_ago) == NOW - timedelta(minutes=50)
# An error exit dates from the death that ended it.
assert t.problem_since(["exited_nonzero"], deaths, hour_ago) == NOW - timedelta(minutes=5)
oom = [_death("die", 2), _death("oom", 3)]
assert t.problem_since(["oom_killed", "exited_nonzero"], oom, hour_ago) == NOW - timedelta(minutes=3)
turned = NOW - timedelta(hours=2)
assert t.problem_since(["unhealthy"], [], hour_ago, turned) == turned
# No retained evidence: unknown, not now.
assert t.problem_since(["unhealthy"], [], hour_ago) is None
assert t.problem_since([], deaths, hour_ago) is None
def test_findings_carry_when_the_problem_began():
began = NOW - timedelta(minutes=40)
row = t.container_row(_c(name="db", status="exited", exit_code=1), {}, ["exited_nonzero"],
NOW, since=began)
assert row["problem_since"] == "2026-10-06T11:20:00Z" and row["problem_age_s"] == 2400
(f,) = t.container_findings([row])
assert f["since"] == "2026-10-06T11:20:00Z" and f["age_s"] is not None
svc = {"name": "db", "state": "down", "running": 0, "desired": 1, "trouble_since": began}
(g,) = t.service_findings([svc])
assert g["since"] == "2026-10-06T11:20:00Z"