"""Generic metric access: anything recorded in Steward's metric store.""" from __future__ import annotations from steward.core import metric_series from steward.models.users import UserRole from ._shape import age_s, bounded, clamp_limit, db_session, iso, lookback, rnd, summarize async def query_metrics(source_module: str = "", resource: str = "", metric: str = "", range: str = "24h", limit: int = 200) -> dict: """Read Steward's metric store, narrowing one argument at a time. Every plugin and monitor type records into one store keyed by (source_module, resource, metric): host_agent, docker, traefik, unifi, snmp, the monitor types (icmp/tcp/dns/http) and Steward's own self-monitor. - No arguments: the source modules that have data. - source_module only: its resources (hosts, routers, containers, devices). A host agent's sub-resources are named ":", e.g. "nas:/". - source_module + resource: the latest value of each metric it reports. - all three: that metric over `range` ("1h", "6h", "24h" default, "7d", "30d") as min/avg/max/p95/last plus a ≤60-point [time, value] series. limit bounds the resource list (default 200, max 1000). """ limit = clamp_limit(limit, 200, 1000) async with db_session() as db: if not source_module: return {"source_modules": await metric_series.sources(db), "next": "Pass one as `source_module` to list its resources."} if not resource: names = await metric_series.resources(db, source_module) if not names: known = await metric_series.sources(db) raise ValueError(f"no metrics recorded for source_module {source_module!r}. " f"Sources with data: {', '.join(known) or 'none'}.") return {"source_module": source_module, **bounded(names, limit, key="resources", more="Pass a larger `limit` (max 1000)."), "next": "Pass one as `resource` to see its metrics."} if not metric: latest = await metric_series.latest_values(db, source_module, resource) if not latest: raise ValueError(f"no metrics for resource {resource!r} in {source_module!r}; " "call query_metrics with only source_module to list its resources.") return { "source_module": source_module, "resource": resource, "metrics": [ {"metric": s.metric_name, "last": rnd(s.value), "last_at": iso(s.recorded_at), "age_s": age_s(s.recorded_at)} for s in latest ], "next": "Pass one as `metric` for its history.", } since, key = lookback(range) pts = await metric_series.points(db, source_module, resource, metric, since) if not pts: names = [s.metric_name for s in await metric_series.latest_values(db, source_module, resource)] if metric not in names: raise ValueError(f"{resource!r} in {source_module!r} has no metric {metric!r}. " f"It reports: {', '.join(names) or 'nothing'}.") return {"source_module": source_module, "resource": resource, "metric": metric, "range": key, **summarize(pts)} def register(register_tool) -> None: register_tool(query_metrics, role=UserRole.viewer, read_only=True)