7efc06a629
Move to openspec/changes/archive/2026-07-08-prometheus-direct-charting/ (git mv, history preserved). 9 artifacts: proposal/spec/design/tasks/ apply-progress/verify-report/sync-report/archive-report + delta spec. Canonical openspec/specs/prometheus-charting/ remains.
153 lines
9.1 KiB
Markdown
153 lines
9.1 KiB
Markdown
# SDD Spec: Prometheus Direct Charting (drop Grafana middleman)
|
||
|
||
**Change:** `prometheus-direct-charting`
|
||
**Phase:** spec
|
||
**Date:** 2026-07-08
|
||
|
||
This spec defines the acceptance requirements for the change. Each requirement is testable. Requirements derived from `proposal.md` §6 (success criteria) and the resolved §8 question round.
|
||
|
||
## Requirement categories
|
||
|
||
1. Direct Prometheus range query path
|
||
2. Prometheus chart widget (rebrand + rebind)
|
||
3. Prometheus gauge widget
|
||
4. Prometheus mean widget
|
||
5. Grafana removal
|
||
6. Configuration documentation accuracy
|
||
7. Test and build greenness
|
||
8. Migration guidance
|
||
|
||
---
|
||
|
||
## 1. Direct Prometheus range query path
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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 ~100–300 points). Users do not configure `step` directly.
|
||
|
||
## 2. Prometheus chart widget (rebrand + rebind)
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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`.
|
||
|
||
### 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.
|
||
|
||
## 3. Prometheus gauge widget
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
## 4. Prometheus mean widget
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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).
|
||
|
||
## 5. Grafana removal
|
||
|
||
### 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).
|
||
|
||
### 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/`.)
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
### SC-120 — Grafana tests are removed
|
||
|
||
All Grafana-specific tests (backend and frontend) MUST be deleted; no test references grafana.
|
||
|
||
## 6. Configuration documentation accuracy
|
||
|
||
### 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.
|
||
|
||
### 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.
|
||
|
||
## 7. Test and build greenness
|
||
|
||
### 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.
|
||
|
||
### SC-124 — Frontend typechecks, builds, and lints
|
||
|
||
`npm run build` (which runs `tsc -b` + `vite build`) and `npm run lint` from `frontend/` MUST pass.
|
||
|
||
### 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.
|
||
|
||
## 8. Migration guidance
|
||
|
||
### 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.
|
||
|
||
### 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.)
|
||
|
||
---
|
||
|
||
## Notes for downstream phases
|
||
|
||
- **Design (next phase)** should specify: the exact `step`-derivation function for window presets (SC-104), the shared normalization helper's location and signature (SC-102), and whether the gauge renderer uses recharts `RadialBarChart` or a contained SVG (SC-110).
|
||
- **Tasks** should slice into chained PRs ≤400 lines per `config.yaml` rules: e.g. (1) Prom range path + chart rebrand, (2) gauge + mean, (3) Grafana removal + config rewrite + CHANGELOG.
|
||
- The **review-budget guard** applies: if total changed lines exceed ~400, the chained-PR strategy from the `tasks` phase is mandatory.
|