Files
manage/openspec/specs/prometheus-charting/spec.md
T
Developer 3e77075171 spec(prometheus-direct-charting): sync into canonical prometheus-charting domain
New canonical domain openspec/specs/prometheus-charting/spec.md with all
27 requirements (SC-101..127) as the durable post-change contract. Change-side
delta specs/prometheus-charting/spec.md + sync-report.md. web-ui canonical
untouched (different concern).
2026-07-08 22:57:29 +00:00

129 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prometheus Charting
> Domain: `prometheus-charting` · **Canonical specification.** Synced from change `prometheus-direct-charting`.
>
> This is the merged end-state of cutting Grafana out of the metric-visualization path and making
> Prometheus the direct data source. It captures the durable, post-change contracts for in-app
> Prometheus-backed metric rendering, not the per-slice delivery strategy (which remains on record
> in the change's `spec.md` / `tasks.md` under `openspec/changes/prometheus-direct-charting/`).
## Purpose
Define WHAT must be true of Manage's metric-visualization surface after the change: Prometheus is
queried **directly** (no Grafana middleman) to back a multi-series line **chart**, a scalar
**gauge**, and a windowed **mean**, alongside the existing instant-query numeric **metric** widget.
In-app charting via `recharts` is the sanctioned renderer; the `grafana` service type, its widgets,
status endpoint, and UI surfaces are fully removed; orphaned Grafana widget instances degrade
gracefully; and operators migrate by recreating services/widgets (no silent data migration). This
spec is acceptance-focused and verifiable; it deliberately does not prescribe implementation.
## Requirements
### Requirement: SC-101 — Prometheus range query returns the existing series shape
When a `prometheus` widget of kind `chart` is fetched, the backend MUST query `{base_url}/api/v1/query_range` with `query`, `start`, `end`, and `step` derived from the widget config, and return a payload of shape `{ "series": [{ "label": str, "points": [{ "t": int, "v": float|null }] }] }` — the exact shape the frontend chart renderer already consumes.
### Requirement: SC-102 — Series label normalization is shared and Prometheus-native
The metric-label → readable-label normalization MUST live in a single shared helper (not duplicated in a Grafana path) and MUST produce meaningful labels for Prometheus matrix results, including deduplicating repeated labels via a `label (n)` suffix.
### Requirement: SC-103 — Range query errors degrade gracefully
A Prometheus timeout, connection error, or non-2xx response MUST cause the widget data fetch to return `{ "error": str }` (not raise), so the frontend renders the standard per-widget error state and the rest of the dashboard remains functional.
### Requirement: SC-104 — Step is derived from the window preset
Given a window preset (1h / 6h / 24h / 7d), the backend MUST derive a `step` that yields a reasonable number of points (target ~100300 points). Users do not configure `step` directly.
### Requirement: SC-105 — Chart widget moves from grafana to prometheus
A widget kind named `chart` MUST be bound to the `prometheus` service type in both the backend registry and the frontend `SERVICE_REGISTRY`. The `grafana` service type MUST NOT offer a `chart` kind.
### Requirement: SC-106 — Chart renderer is reused unchanged
The recharts rendering (line chart, multi-series, axes, tooltip, `mergeSeries`, color tokens) MUST be preserved in the rebranded `PrometheusChartWidget`. The rename is structural; the rendering code is not rewritten.
### Requirement: SC-107 — Chart supports multiple series
The `chart` widget MUST render all series returned by the range query, each as its own line with a distinct color. There is no single-series restriction on `chart`.
### Requirement: SC-108 — Chart window is a preset
The `chart` widget config MUST expose the time window as a preset selector (`1h`, `6h`, `24h`, `7d`), not raw `from`/`to`/`step` fields. The preset is stored in widget config and resolved to `start`/`end` server-side.
### Requirement: SC-109 — Gauge renders an instant scalar
A widget kind named `gauge` MUST be bound to the `prometheus` service. Its data fetch MUST run an instant PromQL query (`/api/v1/query`) and return the scalar result for rendering as a gauge.
### Requirement: SC-110 — Gauge supports configurable threshold bands
The `gauge` widget config MUST accept optional threshold values (e.g. `warn_at`, `crit_at`) and the renderer MUST display green / amber / red bands accordingly. When thresholds are omitted, the gauge renders with a single neutral color and no bands.
### Requirement: SC-111 — Gauge is scalar-only
The `gauge` widget MUST render exactly one scalar value. If the instant query returns multiple series, the adapter MUST return `{ "error": str }` (not silently pick one), directing the user to refine the PromQL.
### Requirement: SC-112 — Mean computes client-side over a window
A widget kind named `mean` MUST be bound to the `prometheus` service. Its data fetch MUST run a range query over the configured window preset and return the arithmetic mean of all non-null point values as a single scalar.
### Requirement: SC-113 — Mean uses plain PromQL + window preset
The `mean` widget config MUST accept a plain PromQL expression (no requirement to wrap in `avg_over_time`) plus a window preset. Users do not write range-vector functions.
### Requirement: SC-114 — Mean is scalar-only
The `mean` widget MUST render exactly one scalar value. If the range query returns multiple series, the adapter MUST return `{ "error": str }` (not silently aggregate across series).
### Requirement: SC-115 — No grafana references in backend source
After the change, `grep -ri grafana backend/src --include='*.py'` MUST return no matches (excluding comments/changelog that are explicitly about the removal, if any are retained — but ideally zero).
### Requirement: SC-116 — No grafana references in frontend source
After the change, `grep -ri grafana frontend/src` MUST return no matches, **excluding** (a) test fixtures where "Grafana" appears as a user-authored dashboard *shortcut label* unrelated to the grafana service type (e.g. `Dashboard.test.tsx`), and (b) `LinksTab.tsx` / `service-tabs/index.ts` lines that are themselves being deleted as part of SC-118. (Source finding: `ObservabilityPage.tsx` was refactored into `service-tabs/`.)
### Requirement: SC-117 — Grafana service type is gone from registries
Neither the backend `SERVICE_DEFINITIONS` / `SERVICE_ADAPTERS` nor the frontend `SERVICE_REGISTRY` / `BUILTIN_WIDGETS` MUST contain a `grafana` entry. The `integrations/grafana.py` file MUST be deleted.
### Requirement: SC-118 — Grafana status checks and UI sections are removed
The `get_grafana_status` endpoint and its frontend hook (`useGrafanaStatus`) MUST be removed. The UI surface previously in `ObservabilityPage.tsx` has been refactored into a per-service-type `service-tabs/` architecture; the Grafana removal targets are therefore `service-tabs/LinksTab.tsx` + its test, the `grafana` case in `service-tabs/index.ts`, the grafana entry in `integrations/navEntries.ts`, the `grafana` member of `Dashboard.tsx`'s `OBSERVABILITY_TYPES` set, and any Grafana empty-state copy in `ServicesPage.tsx`. The literal `ObservabilityPage.tsx` no longer exists; SC-118's *intent* (no Grafana UI surface) is what is verified.
### Requirement: SC-119 — Grafana widget instances degrade gracefully
An existing persisted widget row referencing a `grafana` service MUST NOT crash the dashboard. It resolves to the existing "unknown widget" error state and surfaces a clear message; the operator can then delete it.
### Requirement: SC-120 — Grafana tests are removed
All Grafana-specific tests (backend and frontend) MUST be deleted; no test references grafana.
### Requirement: SC-121 — config.yaml matches implementation
`openspec/config.yaml` MUST NOT contain the stale claims "Do NOT re-implement charting in-app" or "No recharts/d3 is in use." It MUST reflect that in-app charting via `recharts` is the sanctioned approach for Prometheus-backed series, and MUST NOT reference Grafana as a chart path.
### Requirement: SC-122 — CHANGELOG documents the migration
`CHANGELOG.md` MUST include an entry instructing operators to delete existing Grafana service instances and recreate them as Prometheus services, noting that configured `grafana/chart` widgets must be recreated as `prometheus/chart` widgets.
### Requirement: SC-123 — Backend tests pass
`pytest` run from `backend/` MUST pass, including new tests covering: Prom range query → `{series}` normalization, gauge scalar-only enforcement, mean client-side aggregation, and the shared label helper.
### Requirement: SC-124 — Frontend typechecks, builds, and lints
`npm run build` (which runs `tsc -b` + `vite build`) and `npm run lint` from `frontend/` MUST pass.
### Requirement: SC-125 — New widget kinds have tests
`PrometheusChartWidget`, `PrometheusGaugeWidget`, and `PrometheusMeanWidget` MUST each have a frontend test covering at least: loading state, error state, and a rendered data case.
### Requirement: SC-126 — No silent data migration
The change MUST NOT attempt to auto-migrate existing `grafana` service rows into `prometheus` rows (URLs differ; true migration is impossible). Migration is operator-driven per the CHANGELOG note.
### Requirement: SC-127 — Non-blocking on the service-storage-harness change
This change MUST NOT depend on the `service-storage-harness` change. It is independently buildable, testable, and deployable. (The reverse dependency holds: the qBit speed widget depends on this change's chart capability.)