feat(docker): per-container enrichment — health, restarts, exit code, I/O, grouping
First slice of milestone 77 (Docker monitoring depth). Surfaces real per-container
stats beyond basic state, all read-only on the existing push model.
- agent (→1.4.0): collect_docker now inspects each container (health, restart
count, exit code, OOM) and reads net + block I/O from the stats payload; pulls
compose project + swarm service/task/node from container labels. Per-container
inspect+stats calls run over a small bounded ThreadPool so the ~1s-per-stats
blocking doesn't stretch the sample on a busy host.
- schema (docker_003): additive columns on docker_containers — health, exit_code,
oom_killed, compose_project, service_name, task_id, node_id, and BigInteger
net/blk byte counters.
- ingest: persists the enrichment + restart_count (.get keeps older agents working).
- ui: Docker page rows now show health badge, uptime ("up 3d 4h"), restart count,
exit code (+OOM) for stopped containers, and compose/service grouping label.
- tests: agent helpers (grouping, inspect fields, net/IO sum) + collect_docker
assembly incl. inspect; integration asserts enrichment round-trips.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# plugins/docker/routes.py
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from quart import Blueprint, current_app, render_template, request
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -13,6 +15,25 @@ from .models import DockerContainer, DockerMetric
|
||||
docker_bp = Blueprint("docker", __name__, template_folder="templates")
|
||||
|
||||
|
||||
def _human_uptime(started_at: datetime | None) -> str | None:
|
||||
"""Compact 'how long running' string (e.g. '3d 4h', '5h 12m', '8m')."""
|
||||
if started_at is None:
|
||||
return None
|
||||
if started_at.tzinfo is None:
|
||||
started_at = started_at.replace(tzinfo=timezone.utc)
|
||||
secs = int((datetime.now(timezone.utc) - started_at).total_seconds())
|
||||
if secs < 0:
|
||||
return None
|
||||
d, rem = divmod(secs, 86400)
|
||||
h, rem = divmod(rem, 3600)
|
||||
m, _ = divmod(rem, 60)
|
||||
if d:
|
||||
return f"{d}d {h}h"
|
||||
if h:
|
||||
return f"{h}h {m}m"
|
||||
return f"{m}m"
|
||||
|
||||
|
||||
def _sparkline(values: list[float], width: int = 80, height: int = 20) -> str:
|
||||
if len(values) < 2:
|
||||
return f'<svg width="{width}" height="{height}"></svg>'
|
||||
@@ -119,6 +140,7 @@ async def rows():
|
||||
g["containers"].append({
|
||||
"container": c,
|
||||
"ports": json.loads(c.ports_json) if c.ports_json else [],
|
||||
"uptime": _human_uptime(c.started_at) if c.status == "running" else None,
|
||||
"sparkline_cpu": _sparkline(cpu_hist),
|
||||
"sparkline_mem": _sparkline(mem_hist),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user