feat(prometheus-direct-charting): slice 2 — gauge + mean widgets
Add gauge widget (recharts RadialBarChart with configurable threshold bands, scalar-only per SC-111) and mean widget (client-side average over range-query window, scalar-only per SC-114). Extract shared _instant_query helper from the metric path; _fetch_gauge and _fetch_mean dispatch in PrometheusWidgetSource.fetch(). Both new widget kinds declared in integrations/prometheus.py and frontend registry. Backend: 305 pytest pass, ruff clean. Frontend: 136 vitest pass, build+lint green.
This commit is contained in:
@@ -225,28 +225,13 @@ class PrometheusWidgetSource:
|
||||
timeout = int(service.config.get("timeout_seconds") or 10)
|
||||
if widget_kind == "chart":
|
||||
return await self._fetch_chart(base_url, timeout, config)
|
||||
if widget_kind == "gauge":
|
||||
return await self._fetch_gauge(base_url, timeout, config)
|
||||
if widget_kind == "mean":
|
||||
return await self._fetch_mean(base_url, timeout, config)
|
||||
# Default: instant-query metric path (unchanged).
|
||||
promql = config.get("promql")
|
||||
if not promql:
|
||||
return {"error": "promql is required"}
|
||||
url = f"{base_url}/api/v1/query"
|
||||
response = await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
requests.get,
|
||||
url,
|
||||
params={"query": promql},
|
||||
timeout=timeout,
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
return {"result": payload.get("data", {})}
|
||||
except asyncio.TimeoutError:
|
||||
return {"error": "Widget data fetch timed out"}
|
||||
except requests.RequestException as exc:
|
||||
logger.exception("prometheus adapter failed")
|
||||
return {"error": f"Prometheus query failed: {exc}"}
|
||||
raw = await self._instant_query(base_url, timeout, config.get("promql", ""))
|
||||
return raw
|
||||
except Exception as exc:
|
||||
logger.exception("prometheus adapter failed")
|
||||
return {"error": f"Prometheus query failed: {exc}"}
|
||||
@@ -281,6 +266,34 @@ class PrometheusWidgetSource:
|
||||
result = payload.get("data", {}).get("result", [])
|
||||
return {"matrix": result}
|
||||
|
||||
async def _instant_query(self, base_url: str, timeout: int, promql: str) -> dict[str, Any]:
|
||||
"""Run a Prometheus ``/api/v1/query`` instant query.
|
||||
|
||||
Shared by the ``metric`` and ``gauge`` widget kinds. Returns
|
||||
``{"result": data}`` on success or ``{"error": str}`` (never raises,
|
||||
per SC-103).
|
||||
"""
|
||||
if not promql:
|
||||
return {"error": "promql is required"}
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
asyncio.to_thread(
|
||||
requests.get,
|
||||
f"{base_url}/api/v1/query",
|
||||
params={"query": promql},
|
||||
timeout=timeout,
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
except asyncio.TimeoutError:
|
||||
return {"error": "Prometheus query timed out"}
|
||||
except requests.RequestException as exc:
|
||||
logger.exception("prometheus instant query failed")
|
||||
return {"error": f"Prometheus query failed: {exc}"}
|
||||
return {"result": payload.get("data", {})}
|
||||
|
||||
async def _fetch_chart(self, base_url: str, timeout: int, config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Range query → ``{series}`` for the chart widget (SC-101..SC-104)."""
|
||||
promql = config.get("promql")
|
||||
@@ -292,6 +305,63 @@ class PrometheusWidgetSource:
|
||||
return raw
|
||||
return {"series": normalize_prometheus_matrix(raw["matrix"])}
|
||||
|
||||
async def _fetch_gauge(self, base_url: str, timeout: int, config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Instant query → scalar for the gauge widget (SC-109, SC-110, SC-111).
|
||||
|
||||
Scalar-only: a multi-series query returns an error (SC-111). Threshold
|
||||
config (``warn_at``/``crit_at``/``min``/``max``/``unit``) is passed
|
||||
through for the frontend renderer.
|
||||
"""
|
||||
raw = await self._instant_query(base_url, timeout, config.get("promql") or "")
|
||||
if "error" in raw:
|
||||
return raw
|
||||
result = raw["result"].get("result", [])
|
||||
if len(result) != 1:
|
||||
return {"error": "Gauge requires a single-series query; refine your PromQL"}
|
||||
try:
|
||||
value = float(result[0]["value"][1])
|
||||
except (KeyError, IndexError, ValueError, TypeError):
|
||||
return {"error": "Gauge query returned no scalar value"}
|
||||
return {
|
||||
"value": value,
|
||||
"warn_at": config.get("warn_at"),
|
||||
"crit_at": config.get("crit_at"),
|
||||
"min": config.get("min"),
|
||||
"max": config.get("max"),
|
||||
"unit": config.get("unit"),
|
||||
}
|
||||
|
||||
async def _fetch_mean(self, base_url: str, timeout: int, config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Range query → client-side mean for the mean widget (SC-112..SC-114).
|
||||
|
||||
Runs ``query_range`` over the configured window preset, averages all
|
||||
non-null numeric samples of the single series, and returns a scalar.
|
||||
Scalar-only: a multi-series query returns an error (SC-114).
|
||||
"""
|
||||
promql = config.get("promql")
|
||||
if not promql:
|
||||
return {"error": "promql is required"}
|
||||
window = WINDOW_PRESETS.get(config.get("window", "1h"), WINDOW_PRESETS["1h"])
|
||||
raw = await self._range_query(base_url, timeout, promql, window)
|
||||
if "error" in raw:
|
||||
return raw
|
||||
result = raw["matrix"]
|
||||
if len(result) != 1:
|
||||
return {"error": "Mean requires a single-series query; refine your PromQL"}
|
||||
points = result[0].get("values") or []
|
||||
nums: list[float] = []
|
||||
for _, v in points:
|
||||
if v in (None, "NaN", "+Inf", "-Inf"):
|
||||
continue
|
||||
try:
|
||||
nums.append(float(v))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not nums:
|
||||
return {"error": "Mean query returned no numeric samples in the window"}
|
||||
mean = sum(nums) / len(nums)
|
||||
return {"value": mean, "unit": config.get("unit")}
|
||||
|
||||
|
||||
class AlertmanagerWidgetSource:
|
||||
"""Fetch firing alerts from an Alertmanager service and summarize them."""
|
||||
|
||||
Reference in New Issue
Block a user