#5752 "Container CPU and memory history lives in the one metric store; docker_metrics and docker_metrics_hourly are retired". Container cpu_pct and mem_pct already went to plugin_metrics for alerting; now mem_usage_bytes does too, and every snapshot in an ingest (backlog included) is recorded at its own time. The container history chart, Busiest Containers, a service's history and the MCP container tools read plugin_metrics under resource "<host>/<container>", rolled up and pruned by the shared metric retention. docker_012_drop_metrics drops both tables. Their history is not copied (lesson #5221 "A data migration's cost is set by the live table"); the store already holds 7 days of container CPU and memory %. Docker retention now prunes only lifecycle events, and Settings -> Thresholds has one "Metric retention" card for every source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
# plugins/docker/retention.py
|
|
"""Bound Docker history growth: prune old lifecycle events and log lines.
|
|
|
|
Published as the "docker.run_retention" and "docker.prune_logs" capabilities
|
|
(see __init__.setup) so the core cleanup task can drive them WITHOUT importing
|
|
the docker models (same opportunistic-coupling pattern as
|
|
docker.persist_host_samples). Each runs inside the caller's open transaction;
|
|
never opens or commits its own.
|
|
|
|
Container CPU and memory are not here: they live in the one metric store
|
|
(plugin_metrics) and its rollup and retention (#5752).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
async def run_docker_retention(
|
|
session,
|
|
*,
|
|
events_days: int,
|
|
now: datetime | None = None,
|
|
) -> dict:
|
|
"""Prune docker_events older than the events window. Returns a counts
|
|
dict for logging. The container-log ring is prune_docker_logs, run in its
|
|
own transaction."""
|
|
from datetime import timezone
|
|
|
|
from sqlalchemy import delete
|
|
|
|
from .models import DockerEvent
|
|
|
|
if now is None:
|
|
now = datetime.now(timezone.utc)
|
|
res = await session.execute(
|
|
delete(DockerEvent).where(DockerEvent.at < now - timedelta(days=events_days))
|
|
)
|
|
return {"events_pruned": res.rowcount or 0}
|
|
|
|
|
|
async def prune_docker_logs(
|
|
session,
|
|
*,
|
|
retention_days: int,
|
|
max_bytes_per_container: int,
|
|
now: datetime | None = None,
|
|
) -> dict:
|
|
"""The docker_logs ring (m79): drop lines older than the age window, then
|
|
keep only the newest ~cap bytes per (host, container). Runs inside the
|
|
caller's open transaction, like run_docker_retention.
|
|
|
|
Only containers at or over the cap are ranked (#5449). Ranking every line
|
|
in the table each hour to find the few containers over it sorted the whole
|
|
table on slow storage.
|
|
"""
|
|
from datetime import timezone
|
|
|
|
from sqlalchemy import and_, delete, func, select
|
|
|
|
from .models import DockerLog
|
|
|
|
if now is None:
|
|
now = datetime.now(timezone.utc)
|
|
|
|
res = await session.execute(
|
|
delete(DockerLog).where(DockerLog.ts < now - timedelta(days=retention_days))
|
|
)
|
|
age_pruned = res.rowcount or 0
|
|
|
|
size = func.length(DockerLog.line)
|
|
over = (
|
|
select(DockerLog.host_id, DockerLog.container_name)
|
|
.group_by(DockerLog.host_id, DockerLog.container_name)
|
|
.having(func.sum(size) >= max_bytes_per_container)
|
|
.subquery()
|
|
)
|
|
# Per container, sum line bytes newest-first; delete a row once its
|
|
# strictly-newer siblings already fill the cap. The EXCLUSIVE prefix
|
|
# (running total minus this row) means the newest row always survives, so
|
|
# a single line larger than the cap is never wiped out.
|
|
running = func.sum(size).over(
|
|
partition_by=[DockerLog.host_id, DockerLog.container_name],
|
|
order_by=[DockerLog.ts.desc(), DockerLog.id.desc()],
|
|
)
|
|
ranked = (
|
|
select(DockerLog.id, (running - size).label("prefix_excl"))
|
|
.join(over, and_(DockerLog.host_id == over.c.host_id,
|
|
DockerLog.container_name == over.c.container_name))
|
|
.subquery()
|
|
)
|
|
res = await session.execute(
|
|
delete(DockerLog).where(DockerLog.id.in_(
|
|
select(ranked.c.id).where(ranked.c.prefix_excl >= max_bytes_per_container)))
|
|
)
|
|
return {"logs_age_pruned": age_pruned, "logs_size_pruned": res.rowcount or 0}
|