feat(mcp): get_host groups a host's parts by kind and says what it cut
#5012 "get_host cuts `parts` without saying so" - The agent's sub-resources come back as mounts (fullest first), interfaces and disk_io (busiest first) and temperatures (hottest first), each bounded on its own with total, truncated and a `more` that names parts_limit and query_metrics. - Per-core CPU is a summary (count, min/avg/max, three busiest), so 38 cores no longer push every interface out of the reply. - New argument parts_limit (default 20, max 200). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,63 @@ def _agent_glance(a: dict | None) -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
_PART_GROUPS = ( # (prefix, group, the field that names one part)
|
||||
("net:", "interfaces", "interface"),
|
||||
("diskio:", "disk_io", "device"),
|
||||
("temp:", "temperatures", "sensor"),
|
||||
)
|
||||
# Sort each group by what makes a part worth a look first.
|
||||
_PART_ORDER = {
|
||||
"mounts": lambda r: -(r.get("disk_used_pct") or 0),
|
||||
"interfaces": lambda r: -((r.get("net_rx_bps") or 0) + (r.get("net_tx_bps") or 0)),
|
||||
"disk_io": lambda r: -((r.get("disk_read_bps") or 0) + (r.get("disk_write_bps") or 0)),
|
||||
"temperatures": lambda r: -(r.get("temp_c") or 0),
|
||||
"other": lambda r: r["part"],
|
||||
}
|
||||
|
||||
|
||||
def agent_parts(parts: dict[str, dict], host_name: str, limit: int) -> dict:
|
||||
"""A host's sub-resources grouped by kind, each group bounded on its own.
|
||||
|
||||
`parts` maps the part name (the resource after "<host>:") to its latest
|
||||
metrics. Per-core CPU is summarised, not listed: the host's cpu_pct
|
||||
already says how busy it is, and 30+ cores would crowd out the rest.
|
||||
"""
|
||||
groups: dict[str, list] = {"mounts": [], "interfaces": [], "disk_io": [],
|
||||
"temperatures": [], "other": []}
|
||||
cores = []
|
||||
for name, metrics in parts.items():
|
||||
if name.startswith("core") and name[4:].isdigit():
|
||||
if metrics.get("cpu_pct") is not None:
|
||||
cores.append((int(name[4:]), metrics["cpu_pct"]))
|
||||
continue
|
||||
for prefix, group, field in _PART_GROUPS:
|
||||
if name.startswith(prefix):
|
||||
groups[group].append({field: name[len(prefix):], "part": name, **metrics})
|
||||
break
|
||||
else:
|
||||
if any(m.startswith("disk_") for m in metrics):
|
||||
groups["mounts"].append({"mount": name, "part": name, **metrics})
|
||||
else:
|
||||
groups["other"].append({"part": name, **metrics})
|
||||
more = (f"Pass a larger `parts_limit` (max 200), or read one part with query_metrics("
|
||||
f'source_module="host_agent", resource="{host_name}:<part>").')
|
||||
out: dict = {}
|
||||
for group, rows in groups.items():
|
||||
if rows or group != "other":
|
||||
rows.sort(key=_PART_ORDER[group])
|
||||
out[group] = bounded(rows, limit, key="items", more=more)
|
||||
if cores:
|
||||
pcts = [pct for _, pct in cores]
|
||||
busiest = sorted(cores, key=lambda c: (-c[1], c[0]))[:3]
|
||||
out["cpu_cores"] = {
|
||||
"count": len(cores), "min_pct": rnd(min(pcts)),
|
||||
"avg_pct": rnd(sum(pcts) / len(pcts)), "max_pct": rnd(max(pcts)),
|
||||
"busiest": [{"core": i, "cpu_pct": rnd(pct)} for i, pct in busiest],
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
async def list_hosts(limit: int = 100) -> dict:
|
||||
"""Every registered host with its health at a glance.
|
||||
|
||||
@@ -73,17 +130,27 @@ async def list_hosts(limit: int = 100) -> dict:
|
||||
return bounded(rows, limit, key="hosts")
|
||||
|
||||
|
||||
async def get_host(host: str) -> dict:
|
||||
async def get_host(host: str, parts_limit: int = 20) -> dict:
|
||||
"""One host in depth: its monitors, agent snapshot, active alerts and Ansible link.
|
||||
|
||||
host: the host's name (case-insensitive) or id, as list_hosts shows.
|
||||
parts_limit: how many of each kind of sub-resource to list (default 20,
|
||||
max 200).
|
||||
|
||||
Returns the host's monitors (latest result, error, uptime), the agent's
|
||||
latest values — host-level metrics plus each sub-resource such as a mount
|
||||
or interface under `agent.parts` — alerts currently pending, firing or
|
||||
acknowledged on this host's resources, and the Ansible inventory target
|
||||
linked to it with its groups (`ansible_target`: null when unlinked).
|
||||
latest values, alerts currently pending, firing or acknowledged on this
|
||||
host's resources, and the Ansible inventory target linked to it with its
|
||||
groups (`ansible_target`: null when unlinked).
|
||||
|
||||
The agent's values are `agent.metrics` (host-level) and its sub-resources
|
||||
grouped by kind: `mounts` (fullest first), `interfaces` and `disk_io`
|
||||
(busiest first), `temperatures` (hottest first), each with `items`,
|
||||
`total` and `truncated`; `cpu_cores` summarises per-core CPU (count,
|
||||
min/avg/max, the three busiest). One part's history is query_metrics with
|
||||
source_module="host_agent" and resource="<host>:<part>", where `part` is
|
||||
the item's `part`.
|
||||
"""
|
||||
parts_limit = clamp_limit(parts_limit, 20, 200)
|
||||
async with db_session() as db:
|
||||
h = pick(host, await _all_hosts(db), kind="host", list_tool="list_hosts")
|
||||
monitors = await host_svc.host_monitors(db, h.id)
|
||||
@@ -110,8 +177,7 @@ async def get_host(host: str) -> dict:
|
||||
agent = {
|
||||
**(_agent_glance(glance) or {}),
|
||||
"metrics": host_level,
|
||||
"parts": dict(list(sorted(parts.items()))[:40]),
|
||||
"parts_total": len(parts),
|
||||
**agent_parts(parts, h.name, parts_limit),
|
||||
}
|
||||
|
||||
prefix = h.name + ":"
|
||||
|
||||
Reference in New Issue
Block a user