from typing import Any
from ..models import CollectorError, InstanceDescriptor, JobDescriptor
from ..normalizer import observation_from_signals
from .base import CollectorContext, CommandRunner, SubprocessCommandRunner, result
class DockerCollector:
"""Read-only Docker container status collector."""
name = "docker"
version = "0.2"
def __init__(self, runner: CommandRunner | None = None) -> None:
self.runner = runner or SubprocessCommandRunner()
def collect(self, context: CollectorContext): # type: ignore[no-untyped-def]
observations = []
errors: list[CollectorError] = []
for definition in context.config.definitions:
container = str(definition["container_name"])
job = JobDescriptor(str(definition["job_key"]), str(definition.get("display_name", container)), "docker",
str(definition.get("job_type", "worker")), stale_after_seconds=definition.get("stale_after_seconds"),
timeout_seconds=context.config.timeout_seconds)
worker_key = str(definition.get("worker_key", job.job_key))
instance = InstanceDescriptor(job, context.host, "docker", container, instance_name=container,
container_name=container, worker_key=worker_key, location_type="vps",
location_name="VPS Docker")
signals: dict[str, Any] = {"source": "docker", "container_name": container, "container_exists": False}
try:
code, stdout, stderr = self.runner.run(
["docker", "inspect", "--format", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{end}}", container],
context.config.timeout_seconds,
)
if code != 0:
errors.append(CollectorError("docker_error", stderr.strip() or f"docker exited {code}", instance.instance_key))
signals["unavailable"] = True
else:
state, health = (stdout.strip().split("|", 1) + [""])[:2]
signals.update({"container_exists": True, "container_state": state, "container_health": health or None,
"reported_state": "running" if state == "running" else "stopped"})
if state != "running" or health == "unhealthy":
signals["reported_health"] = "failed"
elif health == "starting":
signals["reported_health"] = "degraded"
else:
signals["reported_health"] = "healthy"
except (OSError, ValueError) as exc:
errors.append(CollectorError(type(exc).__name__, str(exc), instance.instance_key))
signals["unavailable"] = True
observations.append(observation_from_signals(job, instance, None, signals, context.now))
return result(self.name, self.version, context.now, observations, errors, {"read_only": True, "commands": ["docker inspect"]})