feat(docker): Docker's own status reaches Steward — an empty answer clears a host, a lost one greys it, a demoted manager's swarm rows go (#5152)
The agent (1.9.0) said nothing when Docker could not be asked and nothing when Docker had no containers, so Steward could tell neither apart from the other, and kept a host's last containers on screen as running either way. - Agent: collect_docker returns (status, detail, containers). "ok" sends docker: [] when there are none; "unavailable" (no socket, nothing listening) and "error" (permission, timeout, bad answer) say why. collect_swarm says whether /info answered, and a node that is not a manager sends swarm_manager: false. A failed /info says nothing. - Ingest: an empty docker list is a snapshot; the newest sample's status goes to the Docker plugin with the rest. - docker_host_status (docker_011): per host, status, detail, since and last report. A host that never had Docker gets no row. - An "ok" empty snapshot reaps the host's containers; an error keeps them. swarm_manager: false drops the host's swarm services and nodes. - Docker page: a "Docker not answering" card names each such host, why, and since when; its containers read as stale. Duplicate header comment in rows.html removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+57
-31
@@ -20,7 +20,7 @@ from collections import deque
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
AGENT_VERSION = "1.8.0"
|
||||
AGENT_VERSION = "1.9.0"
|
||||
|
||||
# Default path to the local Docker Engine socket. Overridable via the
|
||||
# `docker_socket` config key; collection is silently skipped if it's absent or
|
||||
@@ -645,19 +645,37 @@ def _collect_one_container(socket_path: str, c: dict) -> dict:
|
||||
return record
|
||||
|
||||
|
||||
def collect_docker(socket_path: str) -> list:
|
||||
"""Per-container state from the local Docker socket, or [] if unavailable.
|
||||
def _docker_failure(socket_path: str, exc: Exception) -> tuple:
|
||||
"""(status, detail) for a failed Docker call: "unavailable" when there is no
|
||||
Docker here to ask, "error" when there is one and the call went wrong."""
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
return "unavailable", "no Docker socket at " + socket_path
|
||||
if isinstance(exc, ConnectionRefusedError):
|
||||
return "unavailable", "Docker is not accepting connections on " + socket_path
|
||||
if isinstance(exc, PermissionError):
|
||||
return "error", "permission denied on " + socket_path
|
||||
if isinstance(exc, socket.timeout):
|
||||
return "error", "Docker did not answer within %.0f s" % DOCKER_API_TIMEOUT
|
||||
return "error", (str(exc) or type(exc).__name__)[:200]
|
||||
|
||||
Absent socket / permission denied / non-Docker endpoint all yield [] — the
|
||||
agent silently reports no containers rather than erroring, so a Docker-less
|
||||
host needs no configuration.
|
||||
|
||||
def collect_docker(socket_path: str) -> tuple:
|
||||
"""(status, detail, containers) from the local Docker socket.
|
||||
|
||||
status is "ok" when Docker answered (containers may then be [] on a host
|
||||
with none, which tells Steward to clear its list), "unavailable" when there
|
||||
is no Docker here (no socket, or nothing listening), and "error" when Docker
|
||||
is here but the listing failed. detail says why, for the last two. A
|
||||
Docker-less host still needs no configuration: it reports "unavailable".
|
||||
"""
|
||||
try:
|
||||
containers = _docker_request(socket_path, "/containers/json?all=true")
|
||||
except (OSError, ValueError):
|
||||
return []
|
||||
if not isinstance(containers, list) or not containers:
|
||||
return []
|
||||
except (OSError, ValueError) as exc:
|
||||
return _docker_failure(socket_path, exc) + ([],)
|
||||
if not isinstance(containers, list):
|
||||
return "error", "unexpected answer from /containers/json", []
|
||||
if not containers:
|
||||
return "ok", None, []
|
||||
|
||||
# Each container needs an inspect (+ a stats read if running), and the stats
|
||||
# call blocks ~1s while the daemon computes the cpu delta. Done serially that
|
||||
@@ -665,7 +683,7 @@ def collect_docker(socket_path: str) -> list:
|
||||
# over a small bounded thread pool (I/O-bound → threads are enough).
|
||||
workers = min(8, len(containers))
|
||||
with ThreadPoolExecutor(max_workers=workers) as ex:
|
||||
return list(ex.map(lambda c: _collect_one_container(socket_path, c), containers))
|
||||
return "ok", None, list(ex.map(lambda c: _collect_one_container(socket_path, c), containers))
|
||||
|
||||
|
||||
# ─── container logs (m79) ─────────────────────────────────────────────────────
|
||||
@@ -978,26 +996,28 @@ def _swarm_nodes(nodes: list) -> list:
|
||||
return out
|
||||
|
||||
|
||||
def collect_swarm(socket_path: str):
|
||||
"""Swarm topology from a manager node, or None on workers / non-swarm hosts.
|
||||
def collect_swarm(socket_path: str) -> tuple:
|
||||
"""(is_manager, topology) from the local Docker socket.
|
||||
|
||||
Self-detects manager via `/info` (one cheap call); only then queries the
|
||||
manager-only endpoints. Any transport failure degrades to None, same silent
|
||||
contract as collect_docker — a non-swarm host adds nothing to the payload.
|
||||
manager-only endpoints. is_manager is None when /info itself failed, so a
|
||||
transient error is never mistaken for a node that stopped being a manager;
|
||||
False tells Steward to drop the swarm rows this host reported as one.
|
||||
topology is None unless this is a manager and every call answered.
|
||||
"""
|
||||
try:
|
||||
info = _docker_request(socket_path, "/info")
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return None, None
|
||||
if not _swarm_is_manager(info):
|
||||
return None
|
||||
return False, None
|
||||
try:
|
||||
services = _docker_request(socket_path, "/services")
|
||||
tasks = _docker_request(socket_path, "/tasks")
|
||||
nodes = _docker_request(socket_path, "/nodes")
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return {
|
||||
return True, None
|
||||
return True, {
|
||||
"services": _swarm_services(
|
||||
services if isinstance(services, list) else [],
|
||||
tasks if isinstance(tasks, list) else []),
|
||||
@@ -1161,26 +1181,32 @@ def build_sample(mounts: list[str], state: dict,
|
||||
]
|
||||
state["net"], state["disk"], state["mono"] = net_raw, disk_raw, now_mono
|
||||
|
||||
# Docker's status rides every sample, so Steward can tell "no containers"
|
||||
# (status ok, docker: []) from "couldn't ask" (unavailable / error).
|
||||
try:
|
||||
docker = collect_docker(docker_socket)
|
||||
except Exception:
|
||||
docker = []
|
||||
if docker:
|
||||
docker_status, docker_detail, docker = collect_docker(docker_socket)
|
||||
except Exception as exc:
|
||||
docker_status, docker_detail, docker = "error", (str(exc) or type(exc).__name__)[:200], []
|
||||
sample["docker_status"] = docker_status
|
||||
if docker_detail:
|
||||
sample["docker_detail"] = docker_detail
|
||||
if docker_status == "ok":
|
||||
sample["docker"] = docker
|
||||
|
||||
# Swarm topology is only emitted by managers (collect_swarm returns None
|
||||
# everywhere else), so the key is absent on workers / non-swarm hosts.
|
||||
# Swarm topology is only emitted by managers. A node whose /info says it
|
||||
# is not a manager says so, so Steward can expire what it once reported.
|
||||
try:
|
||||
swarm = collect_swarm(docker_socket)
|
||||
is_manager, swarm = collect_swarm(docker_socket)
|
||||
except Exception:
|
||||
swarm = None
|
||||
is_manager, swarm = None, None
|
||||
if swarm is not None:
|
||||
sample["swarm"] = swarm
|
||||
elif is_manager is False:
|
||||
sample["swarm_manager"] = False
|
||||
|
||||
# Image/disk usage — host-level docker fact (one /system/df call), omitted on
|
||||
# Docker-less hosts. Only meaningful when there's docker here at all, so skip
|
||||
# the call entirely when collect_docker found nothing.
|
||||
if docker:
|
||||
# Image/disk usage — host-level docker fact (one /system/df call), only
|
||||
# when Docker answered.
|
||||
if docker_status == "ok":
|
||||
try:
|
||||
disk = collect_disk_usage(docker_socket)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user