feat(docker): Busiest Containers shows bars of the last hours beside each figure, scaled to the row's peak with a floor (#5214)
The fill bars were a share of the host's capacity, a sliver on hosts with 80 cores. Each figure now sits beside 24 bars of the last 6 hours (1h/24h per widget), each row scaled to its own peak but never below 1 core or 1 GiB (configurable per widget), so a 0.05-core container reads flat and a bar's height means real load. One grouped date_bin query feeds them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -269,10 +269,16 @@ CPU_AVERAGE_MINUTES = 5
|
||||
async def widget_resources():
|
||||
"""HTMX dashboard widget: the containers using the most CPU, averaged over
|
||||
the last few minutes so the order holds still between polls, with their
|
||||
CPU in cores and memory in bytes."""
|
||||
limit = max(1, min(20, int(request.args.get("limit", 10) or 10)))
|
||||
CPU in cores and memory in bytes, each beside bars of the last few hours
|
||||
scaled to the row's own peak (never below a floor, #5214)."""
|
||||
limit = max(1, min(20, _arg_number("limit", 10, int)))
|
||||
hours = max(1, min(24, _arg_number("hours", 6, int)))
|
||||
cpu_floor = max(0.1, _arg_number("cpu_floor", 1.0, float))
|
||||
mem_floor = max(0.1, _arg_number("mem_floor", 1.0, float))
|
||||
widget_id = request.args.get("wid", "0")
|
||||
since = datetime.now(timezone.utc) - timedelta(minutes=CPU_AVERAGE_MINUTES)
|
||||
now = datetime.now(timezone.utc)
|
||||
since = now - timedelta(minutes=CPU_AVERAGE_MINUTES)
|
||||
slots, width = docker_svc.bar_slots(now, hours)
|
||||
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
# Dedup before the limit, else swarm tasks reported by both managers can
|
||||
@@ -284,28 +290,48 @@ async def widget_resources():
|
||||
.where(DockerMetric.scraped_at >= since)
|
||||
.group_by(DockerMetric.host_id, DockerMetric.container_name)
|
||||
)).all()}
|
||||
hosts = await docker_svc.host_map(db, {c.host_id for c in running})
|
||||
capacity = await docker_svc.host_capacity(db, {c.host_id for c in running})
|
||||
|
||||
def cpu_of(c):
|
||||
v = average.get((c.host_id, c.name))
|
||||
return c.cpu_pct if v is None else float(v)
|
||||
def cpu_of(c):
|
||||
v = average.get((c.host_id, c.name))
|
||||
return c.cpu_pct if v is None else float(v)
|
||||
|
||||
busiest = sorted(running, key=lambda c: (-(cpu_of(c) or 0), c.name))[:limit]
|
||||
history = await docker_svc.recent_usage(
|
||||
db, {(c.host_id, c.name) for c in busiest}, slots[0], width)
|
||||
hosts = await docker_svc.host_map(db, {c.host_id for c in busiest})
|
||||
capacity = await docker_svc.host_capacity(db, {c.host_id for c in busiest})
|
||||
|
||||
def bars(c, i, scale, floor):
|
||||
seen = history.get((c.host_id, c.name), {})
|
||||
values = [None if seen.get(t) is None or seen[t][i] is None else seen[t][i] * scale
|
||||
for t in slots]
|
||||
return docker_svc.scaled_bars(values, floor)
|
||||
|
||||
busiest = sorted(running, key=lambda c: (-(cpu_of(c) or 0), c.name))[:limit]
|
||||
rows_data = [{
|
||||
"c": c,
|
||||
"label": docker_svc.display_name(c),
|
||||
"host_name": hosts[c.host_id].name if c.host_id in hosts else c.host_id,
|
||||
"usage": docker_svc.usage(c, *capacity.get(c.host_id, (None, None)), cpu_pct=cpu_of(c)),
|
||||
"cpu_bars": bars(c, 0, 0.01, cpu_floor), # cpu_pct → cores
|
||||
"mem_bars": bars(c, 1, 1.0, mem_floor * 2**30), # bytes; floor in GiB
|
||||
} for c in busiest]
|
||||
return await render_template(
|
||||
"docker/widget_resources.html",
|
||||
rows=rows_data,
|
||||
minutes=CPU_AVERAGE_MINUTES,
|
||||
hours=hours, cpu_floor=cpu_floor, mem_floor=mem_floor * 2**30,
|
||||
widget_id=widget_id,
|
||||
)
|
||||
|
||||
|
||||
def _arg_number(key, default, kind):
|
||||
"""A numeric query argument, or `default` when missing or not a number."""
|
||||
try:
|
||||
return kind(request.args.get(key, default))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
@docker_bp.get("/host/<host_id>")
|
||||
@require_role(UserRole.viewer)
|
||||
async def host_panel(host_id: str):
|
||||
|
||||
Reference in New Issue
Block a user