"swarm services short on swarm is 6." becomes "6 swarm services are short of their replicas.", with a Which line naming each service and how many of its tasks run. The swarm figures get labels, and the counting figures (swarm, mounts, drives, updates) read as sentences in the email, its subject and the Alerts page. A plugin names what its count counts through core.alerts.names_behind; the Docker plugin does for the three swarm figures, from the stored swarm. Webhooks get alert.which too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
4.6 KiB
Python
110 lines
4.6 KiB
Python
# plugins/docker/__init__.py
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from quart import Quart
|
|
|
|
_app: "Quart | None" = None
|
|
|
|
|
|
def setup(app: "Quart") -> None:
|
|
global _app
|
|
_app = app
|
|
from .models import DockerContainer # noqa: F401 — register with Base
|
|
|
|
# Publish the per-host persist hook so the host agent can hand off the
|
|
# `docker` array from each sample without importing our models (opportunistic
|
|
# synergy — host_agent gates on has_capability and no-ops if we're disabled).
|
|
# required_role=viewer: this is a trusted server-side data-plane write driven
|
|
# by the authenticated agent ingest, not a user-facing privileged action.
|
|
from steward.core.capabilities import register_capability
|
|
from steward.models.users import UserRole
|
|
from .ingest import persist_host_docker
|
|
from .retention import prune_docker_logs, run_docker_retention
|
|
register_capability(
|
|
"docker.persist_host_samples", persist_host_docker,
|
|
label="Persist host Docker samples",
|
|
description="Store per-host container state + metrics pushed by the host agent.",
|
|
required_role=UserRole.viewer,
|
|
)
|
|
# Prune lifecycle events, driven by the core cleanup task without it
|
|
# importing our models. Same trusted server-side data-plane role as above.
|
|
# Container CPU and memory live in the one metric store and its retention.
|
|
register_capability(
|
|
"docker.run_retention", run_docker_retention,
|
|
label="Run Docker retention",
|
|
description="Prune Docker lifecycle events past their window.",
|
|
required_role=UserRole.viewer,
|
|
)
|
|
register_capability(
|
|
"docker.prune_logs", prune_docker_logs,
|
|
label="Prune Docker logs",
|
|
description="Keep each container's logs within the age window and byte cap.",
|
|
required_role=UserRole.viewer,
|
|
)
|
|
# Each container's CPU (100% is one core) and memory share learn their
|
|
# normal (#5742), each with its floor from Settings → Thresholds. Memory
|
|
# is its processes' own, file cache left out (#6030).
|
|
from steward.core.norms import floor_setting, learn_normal
|
|
learn_normal("docker", "cpu_pct", floor_setting(app, "container_cpu_pct"))
|
|
learn_normal("docker", "mem_used_pct", floor_setting(app, "container_mem_pct"))
|
|
# A host's agent carries its containers' figures: they go silent with it (#5721).
|
|
from steward.core.alerts import agent_fed
|
|
agent_fed("docker", lambda resource: resource.split("/", 1)[0])
|
|
# An alert on a swarm figure names the services or nodes it counts (#6400).
|
|
from steward.core.alerts import names_behind
|
|
from .swarm_health import NAMERS
|
|
for metric, names in NAMERS.items():
|
|
names_behind("docker", metric, names)
|
|
# Deploys and crash-loops go on the timeline.
|
|
from .timeline import register as register_timeline
|
|
register_timeline()
|
|
|
|
|
|
def get_scheduled_tasks() -> list:
|
|
# Collection is now agent-driven (pushed to the host_agent ingest); the
|
|
# central socket scrape was removed. No periodic task to register.
|
|
return []
|
|
|
|
|
|
def get_blueprint():
|
|
from .routes import docker_bp
|
|
return docker_bp
|
|
|
|
|
|
def register_mcp_tools(register) -> None:
|
|
from .mcp_tools import register_tools
|
|
register_tools(register)
|
|
|
|
|
|
async def mcp_overview(db) -> list[dict]:
|
|
from .mcp_tools import mcp_overview as findings
|
|
return await findings(db)
|
|
|
|
|
|
async def mcp_events(db, host_id: str, host_name: str, since, until) -> list[dict]:
|
|
from .mcp_tools import mcp_events as events
|
|
return await events(db, host_id, host_name, since, until)
|
|
|
|
|
|
def get_nav() -> list[dict]:
|
|
# Surfaces in the sidebar's Infrastructure group (fleet-wide container view).
|
|
return [{"label": "Docker", "href": "/plugins/docker/"}]
|
|
|
|
|
|
def get_alert_metrics() -> dict[str, list[str]]:
|
|
"""What ingest.py records per container (resource "<host>/<container>"):
|
|
restart_count is cumulative (alert on crash-looping); is_healthy is 1.0 /
|
|
0.0 from the container's HEALTHCHECK, only for containers that define one.
|
|
net_rx_bps / net_tx_bps are the bytes a second it receives and sends
|
|
(#6115): "a container flooding the uplink" is net_tx_bps over a level.
|
|
Per host (resource "<host>"), what Docker keeps on its disk (#5304).
|
|
Swarm-wide (resource "swarm"), from any manager's report: services short
|
|
of their replicas, nodes down and managers unreachable, drained nodes
|
|
aside (#5788, swarm_health.py)."""
|
|
from .disk_view import DISK_METRICS
|
|
from .swarm_health import SWARM_METRICS
|
|
return {"docker": ["cpu_pct", "mem_used_pct", "restart_count", "is_healthy", "net_rx_bps", "net_tx_bps",
|
|
*DISK_METRICS, *SWARM_METRICS]}
|