feat(timeline): maintenance, reboots, deploys, crash-loops and reviewed anomalies on the timeline (#6349)
The host agent writes each maintenance job once it ends, and each stretch a host was away (a reboot, a crash, lost contact, the agent restarting), kept up to date until it is back. Docker judges each snapshot: a unit on a new image, or a container recreated, is a deploy; deaths past the Thresholds crash-loop count are one crash-loop entry that later deaths extend; single restarts are recorded but hidden. An anomaly has one entry whose type follows its review: only those judged real are shown by default. A source whose record changes keeps one entry for it (record_or_update). Plugins register the records their entries point to, so an aged-out job or downtime reads "Details no longer kept". The first fill runs once per source (timeline.filled), the host agent's jobs and downtime included. get_anomaly's "what else was going on" reads the timeline for the host or resource instead of querying alerts, runs, maintenance, downtime and container deaths itself; Docker's own source keeps only starts, stops and health changes. A new kind, Observed, is for what Steward saw happen with no rule or person behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
|
||||
from . import maintenance
|
||||
from . import maintenance, timeline
|
||||
|
||||
WEEKDAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||
# How long a host may take to be back in service after its job finished.
|
||||
@@ -241,6 +241,8 @@ async def advance(session, run, plan, now: datetime) -> str:
|
||||
if move == "fail":
|
||||
if job is not None and job.state == "done":
|
||||
maintenance._finish(job, "failed", now, why) # the host never came back into service
|
||||
await timeline.write(session, f"maintenance job {job.id}",
|
||||
timeline.job_entry(session, job, host.name if host else None))
|
||||
name = host.name if host else host_id
|
||||
_end(run, "failed", now, f"{name}: {why}")
|
||||
elif move == "stop":
|
||||
@@ -325,5 +327,7 @@ async def failed_hosts(session, now: datetime) -> dict[str, float]:
|
||||
HostAgentRegistration.host_id == host.id))).scalar_one_or_none()
|
||||
if await _back(session, host, job, reg, now) is None:
|
||||
job.came_back_at = now
|
||||
await timeline.write(session, f"maintenance job {job.id}",
|
||||
timeline.job_entry(session, job, host.name))
|
||||
out[name] = 1.0 if maintenance.still_failed(job) else 0.0
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user