Files
manage/openspec/changes/prometheus-direct-charting/spec.md
T
Developer b7e5ca3cbc spec(prometheus-direct-charting): add proposal + spec
Drop Grafana as chart middleman; query Prometheus directly via
/api/v1/query_range. Rebrand GrafanaChartWidget -> PrometheusChartWidget,
add gauge + mean widget kinds, remove Grafana surface, rewrite stale
thin-dashboard rule in config.yaml. 27 acceptance requirements (SC-101..127).
2026-07-08 21:21:14 +00:00

8.2 KiB
Raw Blame History

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 ~100300 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.

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 are removed

The get_grafana_status endpoint and its frontend hook (useGrafanaStatus) MUST be removed. The ObservabilityPage MUST NOT render a Grafana status section.

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.