feat(traffic): a host's top senders, on its page and as get_host_senders (#6115)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m29s
CI / integration (push) Successful in 1m39s
CI / publish (push) Successful in 1m4s

The host page gains a "Who's sending" panel, and the MCP a tool, that
put the host's outbound (or inbound) traffic down to its containers and
NFS mounts: read in shared time buckets, so the host's busiest bucket
says who was sending in it. A swarm service's tasks add up under its
name; what none of them accounts for is the host itself.

Also fixes CI 9488: per-container net_rx_bps / net_tx_bps join the
docker alert catalog, so a rule can catch a container flooding the
uplink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 14:38:28 -04:00
co-authored by Claude Opus 5.5
parent 613ed4e78b
commit 26bc57ce04
11 changed files with 319 additions and 7 deletions
+17
View File
@@ -957,6 +957,23 @@ async def host_detail_charts(host_id: str):
return await render_template("_host_charts.html", series=series, range_key=range_key)
@host_agent_bp.get("/<host_id>/senders")
@require_role(UserRole.viewer)
async def host_detail_senders(host_id: str):
"""Who sent the host's traffic over the range (#6115), lazy-loaded below
the charts and refreshed with them."""
from .senders import senders
since, range_key = parse_range(request.args.get("range"))
async with current_app.db_sessionmaker() as session:
host = (await session.execute(
select(Host).where(Host.id == host_id))).scalar_one_or_none()
if host is None:
return "", 404
raw_days = int(await get_setting(session, "metrics.retention.raw_days") or 7)
got = await senders(session, host.name, since, raw_days=raw_days)
return await render_template("_host_senders.html", got=got, range_key=range_key)
def _new_token_pair() -> tuple[str, str]:
raw = secrets.token_urlsafe(32)
return raw, _hash_token(raw)