b7e5ca3cbc
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).
12 KiB
12 KiB
SDD Proposal: Prometheus Direct Charting (drop Grafana middleman)
Change: prometheus-direct-charting
Phase: proposal
Date: 2026-07-08
1. Problem / Why Now
Manage already does in-app charting — but indirectly. Today the only chart path is:
GrafanaChartWidget (recharts) ← GrafanaWidgetSource._fetch_chart
← POST {grafana}/api/ds/query (datasource.type = "prometheus", hardcoded)
← Grafana proxies to Prometheus
Two problems with this:
- Grafana is a pure middleman. The backend already hardcodes
datasource.type: "prometheus"in every chart query. Grafana adds a hop, an API key, a different response shape (/api/ds/queryframes), and a normalization layer — all to reach a Prometheus instance Manage could query directly via/api/v1/query_range. The existingPrometheusWidgetSourcealready proves Manage can talk to Prom straight; it just only does instant queries today. - The project's documented rules are stale.
openspec/config.yamlsays "Do NOT re-implement charting in-app" and "No recharts/d3 is in use." Both are already false in code:recharts ^3.9.2is declared and imported byGrafanaChartWidget. The config documents a reality the code left behind.
Meanwhile the operator wants two more metric visualizations Manage cannot currently render: a gauge and a single value / mean-over-time widget. Both belong naturally on the prometheus service, which today only exposes an instant-query numeric widget.
The clean answer is to stop routing charts through Grafana: query Prometheus directly, reuse the rendering infrastructure already written, add the two new modes, and remove the now-redundant Grafana surface.
2. Target Users and Situations
- Primary users: Homelab operators who want metric visualizations on the Manage dashboard without bouncing to Grafana for a quick glance.
- Workflow moments:
- Glance at the dashboard: see a trend line, a gauge, or a mean value for a key PromQL query.
- Build a custom at-a-glance panel from any PromQL expression Manage can already evaluate.
- Decommission the Grafana hop for charts (one fewer external dependency in the chart path, one fewer API key to rotate).
- Urgency: Medium. Charts already work via Grafana today; this is a simplification plus two new widget modes, not an outage fix.
3. Product Outcome
After this change, an authenticated user can:
- Place a Prometheus Chart widget (line chart, multi-series) backed by a direct
/api/v1/query_rangecall — same look as today's Grafana chart, no Grafana required. - Place a Prometheus Gauge widget rendering an instant PromQL scalar as a gauge.
- Place a Prometheus Mean widget rendering a single value aggregated over a time window (e.g.
avg_over_time(...), or a query_range aggregate). - Manage the dashboard without any Grafana service configured: the Grafana service type, link widget, chart widget, and status checks are removed.
4. Scope Boundaries and Non-Goals
In scope
- Direct Prometheus range query path — backend hits
/api/v1/query_rangeand returns the existing{series:[{label,points}]}shape so the frontend renderer is reused unchanged. - Shared series normalization — extract the metric-label → readable-label logic currently inside
GrafanaWidgetSource._fetch_chartinto a reusable helper used by both the chart path and (where relevant) the new modes. - Rebrand + rebind —
GrafanaChartWidget→PrometheusChartWidget, moved from thegrafanaservice to theprometheusservice as thechartwidget kind. - New
prometheuswidget kinds:gaugeandmean(semantics confirmed in the question round). - Grafana removal — delete
integrations/grafana.py,GrafanaWidgetSource,GrafanaLinkWidget,get_grafana_status, the ObservabilityPage Grafana section, both registries'grafanaentries, nav entries, and their tests. config.yamlrewrite — replace the stale thin-dashboard / no-recharts wording with the reality: in-app charting viarechartsis the sanctioned approach for Prometheus-backed series; Grafana is no longer referenced.- Migration note — CHANGELOG entry: existing Grafana service instances must be deleted and recreated as Prometheus services (true data migration is impossible; different URLs).
Non-goals (explicitly out of scope)
- A general chart-widget framework with pluggable data-source middlewares. Chart rendering is reused; data sources are not abstracted into a swappable adapter registry. Two concrete sources (Prometheus range query, and later the service-storage harness) are wired directly where needed.
- Grafana datasource proxy for non-Prometheus sources (Loki, InfluxDB, Postgres). If a real non-Prom need appears later, it is a separate change.
- Embedding Grafana panels as images/iframes. Grafana is removed, not embedded.
- Editing PromQL in a rich editor (autocomplete, metric explorer). Plain text input only, matching today's
prometheus_metricwidget. - qBittorrent widgets and the service-storage harness. Those are a separate change (
service-storage-harness); only the qBit speed widget depends on this change's chart capability. - Touching
prometheus_metric(instant numeric widget). It stays as-is;meanandchartare sibling kinds, not modes bolted onto it. - Re-indexing or migrating existing widget instance rows automatically. Existing
grafanachartwidgets are orphaned by the removal and must be recreated asprometheuschartwidgets by the operator (documented in CHANGELOG).
5. High-Level Approach
5.1 Backend
- Prometheus range query — extend
PrometheusWidgetSource(or add a sibling code path) to handlewidget_kind == "chart":GET {base_url}/api/v1/query_range?query=...&start=...&end=...&step=...- Parse Prom
{value:[ts, val]}matrix into the existing{series:[{label, points}]}shape.
- Shared normalization — move the "metric labels → readable label" logic out of
GrafanaWidgetSource._fetch_chartintowidgets/series.py(or similar), so the Prom path and any future consumer reuse it. - New kinds wiring —
gaugeandmeanresolve inPrometheusWidgetSource.fetch:gauge: instant query (/api/v1/query), return{value, threshold?, ...}for a gauge renderer.mean: range query aggregated to a single value (either PromQLavg_over_timevia instant query, or client-side mean over a query_range window). Semantics decided in the question round.
- Integration update —
integrations/prometheus.pydeclares the three widget kinds (metric,chart,gauge,mean) with config schemas (promql, plus range params forchart/mean). - Grafana removal — delete
integrations/grafana.py, dropgrafanafromSERVICE_ADAPTERS,SERVICE_DEFINITIONS, remove_fetch_chartand the Grafana link logic. Removeget_grafana_statusfromrouters/monitoring.pyand the Grafana branch fromObservabilityPage. config.yamlrewrite — replace the stale charting rules with accurate wording.
5.2 Frontend
- Rebrand — rename
GrafanaChartWidget.tsx→PrometheusChartWidget.tsx; the recharts rendering (LineChart/Line/XAxis/YAxis/Tooltip/mergeSeries/CHART_COLORS) stays essentially unchanged. - Registry — in
integrations/registry.ts, movechartto theprometheusbinding and addgauge+meanbindings; delete the entiregrafanabinding. - New components:
PrometheusGaugeWidget.tsx— recharts<RadialBarChart>or a small SVG gauge; instant value.PrometheusMeanWidget.tsx— single-value display (reusesMetricCard-style rendering) of the windowed mean.
- Nav + ObservabilityPage — remove Grafana nav entries and the Grafana status card.
- Types —
frontend/src/types/index.tsdrops Grafana status types; no new endpoint types (data still flows throughuseWidgetData).
5.3 Type contracts
- Backend: update
integrations/prometheus.pywidget-kind config models; remove Grafana models. - Frontend: remove
GrafanaStatustype; widget payloads stay{series}/{value}shaped.
6. Success Criteria / Acceptance Criteria
- A user can configure a Prometheus service and place
chart,gauge,mean, andmetricwidgets without any Grafana service present. - The
chartwidget renders multi-series line charts from/api/v1/query_rangewith the same look as the prior Grafana-backed chart. - The
gaugewidget renders an instant PromQL scalar as a gauge. - The
meanwidget renders a single value aggregated over the configured window. - No
grafanareferences remain inbackend/srcorfrontend/src(grep clean). openspec/config.yamlno longer claims "no recharts" or "do not chart in-app"; its wording matches the implementation.- Existing
pytest,npm run build, andnpm run lintstay green; Grafana tests are removed, Prom chart/gauge/mean tests are added. - CHANGELOG documents the migration (delete Grafana services, recreate as Prometheus).
7. Risks and Mitigations
| Risk | Mitigation |
|---|---|
Response-shape regression. Prom /api/v1/query_range matrix differs from Grafana /api/ds/query frames. |
Extract normalization into a shared helper; add a backend test that feeds a sample Prom range response and asserts the {series} shape the frontend already consumes. |
Orphaned configured widgets. Existing grafana/chart widget rows break at render. |
Acceptable + documented in CHANGELOG; the widget resolves to "unknown widget" gracefully (existing WidgetInstance error path). No silent data corruption. |
| Gauge rendering complexity. recharts gauges can be fiddly. | Constrain gauge to a single scalar + optional thresholds; if recharts gauge proves heavy, fall back to a ~50-line SVG gauge (contained, no new dep). |
| Scope creep into a generic chart framework. Tempting to abstract data sources. | Non-goal enforced: two direct wirings, no adapter registry. |
| Stale docs/tests lingering. | "No grafana references" acceptance criterion (grep) catches leftovers. |
| Review budget (>400 lines). | Slice into chained PRs (e.g. Slice 1: Prom range query + rebrand chart; Slice 2: gauge + mean; Slice 3: Grafana removal + config rewrite). Each slice leaves build/lint/test green. |
| DataGrid migration (config rule callout). | Not applicable — no DataGrid work here. The key technical risk is the response-shape regression above. |
8. Open Questions (for the proposal question round)
- Q1 —
meansemantics. Default: client-side mean over aquery_rangewindow (e.g. last 1h, step 30s → average all returned values). Alternative: require the user to writeavg_over_time(...)in the PromQL and just run an instant query. Which UX do you want? - Q2 —
gaugethresholds. Default: green/amber/red bands at user-configured thresholds (e.g. 70%/90%). Alternative: single color, no bands (simplest). - Q3 — Chart time window config. Default: expose
from/to/step(or a simpler "window" preset like 1h/6h/24h). Confirm the granularity users can configure. - Q4 — Multi-series on
mean/gauge. Default: single series only (one scalar). Confirm we do not try to render multi-series gauges.
9. Future Phases
- Service-storage harness integration — the qBittorrent speed widget (separate
service-storage-harnesschange) reuses this change's chart rendering with an in-service data source wired directly. - Rich PromQL editing — metric explorer / autocomplete.
- Non-Prom datasources — only if a concrete need (Loki logs in-app) appears.
- Threshold-based alerting from chart widgets — surface bands/lines from Alertmanager rules.