Files
manage/openspec/changes/grafana-metric-gateway/specs/prometheus-charting/spec.md
T
Developer cb8dd13514 spec(grafana-metric-gateway): sync — FIRST non-additive (MODIFIED) canonical
16 MODIFIED prometheus-charting requirements (transport: direct Prom -> Grafana
gateway; intent preserved where applicable), 3 ADDED (SC-128 gateway status,
SC-129 startup validation, SC-130 sanctioned transport), 11 PRESERVED, 0 REMOVED.
All 16 MODIFIED headers matched canonical exactly. Post-sync: 30 requirements.
web-ui + service-storage canonicals untouched.
2026-07-09 22:01:55 +00:00

107 lines
11 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 — Delta (`grafana-metric-gateway`)
> Change: `grafana-metric-gateway` · Domain: `prometheus-charting` · Phase: **spec** (authored during `sdd-sync`).
>
> **This is the project's first non-additive canonical sync.** The `prometheus-charting` canonical
> domain (SC-101 … SC-127, created by `prometheus-direct-charting`) described the *direct* Prometheus
> transport. This change switches the `prometheus` service to a **Grafana gateway** transport
> (`POST {grafana_url}/api/ds/query`), renames the chart/gauge/mean widgets to neutral `Metric*`
> names, and validates the gateway path. Durable *behavior* requirements (multi-series chart, gauge
> threshold bands, windowed mean) are **preserved**; only the *transport* and a few naming/status
> statements are modified. The mapping of every change requirement (GM-101 … GM-116) to a canonical
> SC-xxx is recorded in the flat `spec.md` §"Canonical delta intent".
>
> This delta therefore contains **both** a `## MODIFIED Requirements` block (full replacement bodies
> for each superseded SC-xxx — matched by exact requirement ID) and an `## ADDED Requirements` block
> (genuinely new requirements assigned fresh canonical IDs SC-128 … SC-130). PRESERVED requirements
> (SC-106, SC-108, SC-110, SC-113, SC-117 … SC-120, SC-123, SC-124, SC-127) appear in **neither**
> section — they are unchanged in the canonical spec.
## MODIFIED Requirements
> Each block below replaces the full canonical requirement of the same ID (matched by the exact
> `### Requirement: SC-xxx — <name>` header). The requirement *intent* is preserved where the change
> only swaps the mechanism (direct Prom → Grafana gateway); the body text is updated to the gateway
> transport. No requirement IDs are renamed (RENAMED is unsupported by the native delta helper).
### Requirement: SC-101 — Prometheus range query returns the existing series shape
The `prometheus` service type's config MUST point at a Grafana gateway — declaring `grafana_url` (http(s)-schema-enforced), `datasource_uid` (default `"prometheus"`), and `timeout_seconds` — with a required, Fernet-encrypted `grafana_api_key` secret. The former direct-Prometheus `base_url` config field and the old optional `api_key` secret MUST be gone. When a `prometheus` widget of kind `chart` is fetched, the backend MUST issue `POST {grafana_url}/api/ds/query` (carrying `Authorization: Bearer {grafana_api_key}` and a `queries[0]` entry keyed by `datasource_uid`), MUST NOT issue any direct `GET /api/v1/query_range` against a Prometheus URL, and MUST 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 module that holds **both** normalizers — the retained Prom-matrix normalizer (`normalize_prometheus_matrix`) and the (re-)added Grafana-frames normalizer (`normalize_grafana_frames`) — and MUST deduplicate repeated labels via a `label (n)` suffix using **one shared dedup rule** (not duplicated across the two normalizers). Both normalizers produce the same `{label, points}` series shape, and the frames normalizer MUST apply the label fallback chain (displayName → sorted Prometheus labels excluding `__`-prefixed keys → `"value"`).
### Requirement: SC-103 — Range query errors degrade gracefully
A Grafana gateway timeout, connection error, HTTP 401/403 (auth), datasource-not-found, 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. The error message MUST be specific enough to distinguish auth failure from unreachable-host from datasource-not-found where the Grafana response permits.
### Requirement: SC-104 — Step is derived from the window preset
Given a window preset (1h / 6h / 24h / 7d), the backend MUST reuse the existing `WINDOW_PRESETS` and `step_for_window` math to derive the gateway request's `intervalMs` (`step * 1000`), `maxDataPoints`, and `from`/`to` time bounds, landing the resulting point count in the same ~100300 band as the pre-change direct-Prom path. Users do not configure `from`/`to`/`step`/`intervalMs` directly.
### Requirement: SC-105 — Chart widget moves from grafana to prometheus
Widget kinds `chart`, `gauge`, `mean`, and `metric` MUST remain bound to the `prometheus` service type — the kind strings are persisted in widget instance rows and MUST NOT change. The `grafana` service type MUST NOT be reintroduced and MUST NOT offer any widget kind. The backend adapter (internal class `MetricSource`, registry key still `"prometheus"`) and the frontend `SERVICE_REGISTRY` bind these kinds to the neutral `Metric*` components for `chart`/`gauge`/`mean` (and `PrometheusMetricWidget` for `metric`); all four kinds source their data via the Grafana gateway.
### Requirement: SC-107 — Chart supports multiple series
The `chart` widget MUST render all series returned by the gateway range query, each as its own line with a distinct color (via `normalize_grafana_frames`). There is no single-series restriction on `chart`.
### 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 through the Grafana gateway (a gateway query with a minimal window / single data point) and return the scalar result for rendering as a gauge.
### Requirement: SC-111 — Gauge is scalar-only
The `gauge` widget MUST render exactly one scalar value. If the gateway 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 through the Grafana gateway over the configured window preset and return the arithmetic mean of all non-null point values of the single series as a single scalar.
### Requirement: SC-114 — Mean is scalar-only
The `mean` widget MUST render exactly one scalar value. If the gateway 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
Grafana is the sanctioned **transport** for the `prometheus` service, so backend source legitimately contains Grafana transport references (e.g. `grafana_url`, `grafana_api_key`, `normalize_grafana_frames`, `POST …/api/ds/query`). After the change, those references are transport-only: there MUST be no `grafana` **service type**, no `grafana` service definition, and no `grafana` entry in the backend `SERVICE_DEFINITIONS` / `SERVICE_ADAPTERS` registries. The `integrations/grafana.py` service-type module stays deleted.
### Requirement: SC-116 — No grafana references in frontend source
Grafana transport is allowed in the frontend — the `prometheus` service config form renders Grafana gateway fields (`grafana_url`, `datasource_uid`) from the backend-provided schema. No frontend code MUST reintroduce a `grafana` **service type**, a Grafana deep-link / dashboard widget, `LinksTab`, or any Grafana-status UI surface. (Test fixtures where "Grafana" appears as a user-authored dashboard *shortcut label* unrelated to a service type remain excluded.)
### Requirement: SC-121 — config.yaml matches implementation
`openspec/config.yaml` MUST reflect that metric queries for the `prometheus` service are routed through a Grafana gateway (`POST /api/ds/query`) and that in-app charting via `recharts` is the sanctioned renderer. It MUST NOT carry the stale claims "Do NOT re-implement charting in-app", "No recharts/d3 is in use", or "Prometheus is queried directly (no Grafana middleman)".
### Requirement: SC-122 — CHANGELOG documents the migration
`CHANGELOG.md` `[Unreleased]` MUST include a **BREAKING** entry stating that existing `prometheus` service instances must be reconfigured: replace the old `base_url` with `grafana_url`, add the `grafana_api_key` secret, and optionally set `datasource_uid`, because metric queries now route through Grafana's `POST /api/ds/query`.
### Requirement: SC-125 — New widget kinds have tests
The renamed widgets `MetricChartWidget`, `MetricGaugeWidget`, and `MetricMeanWidget` MUST each have a frontend test covering at least: loading state, error state, and a rendered data case. (The `metric`-kind widget `PrometheusMetricWidget` is not renamed but remains tested.)
### Requirement: SC-126 — No silent data migration
The change MUST NOT auto-migrate persisted `prometheus` service instances from the old `base_url` config shape to the new `grafana_url` gateway shape (URLs and credentials differ; a safe automated rewrite is not possible). Migration is operator-driven per the CHANGELOG note; old-shape instances are flagged on startup (see SC-129) and degrade at runtime to a gateway error until reconfigured.
## ADDED Requirements
> Genuinely new requirements with fresh canonical IDs continuing the SC- sequence. The `GM-` change
> prefixes are intentionally not reused in the canonical store.
### Requirement: SC-128 — get_prometheus_status validates the full gateway path
`get_prometheus_status` MUST validate the full metric path by running a trivial query (e.g. `up`) through the Grafana gateway (`POST {grafana_url}/api/ds/query`). Success MUST return `{ "up": true, "version": <str>, … }` — a sentinel such as `"ok"` is acceptable when Grafana's `/api/ds/query` response carries no Prometheus build-info. Failure MUST return `{ "up": false, "error": <specific str> }` distinguishing auth failure (HTTP 401/403), unreachable host, and datasource/gateway error where the response permits.
### Requirement: SC-129 — Startup logs old-shape prometheus config without crashing
When the backend starts and a persisted `prometheus` service instance carries the old `base_url` config key without `grafana_url`, startup MUST log a clear migration message naming the service and instructing reconfiguration, and MUST complete without crashing (it MUST NOT delete, disable, or otherwise mutate the instance). The instance's widget fetches degrade to a gateway error until the operator reconfigures it.
### Requirement: SC-130 — Grafana is the sanctioned transport for the prometheus service
The `prometheus` service type is the sole metric surface in Manage. Its metric queries (`chart`, `gauge`, `mean`, `metric`) are sanctioned to route through a Grafana gateway — `POST {grafana_url}/api/ds/query` with a `grafana_api_key` and a Prometheus `datasource_uid`. Grafana is the transport, not a first-class service type; a direct-Prometheus HTTP path is not supported.