Home / Stories / Monitoring & dashboards
Story · Observability Live

From "nobody scrapes this" to a dashboard that actually has data.

A team ships a new service. It exposes Prometheus metrics, but nobody wrote the ServiceMonitor, so nothing collects them, and there is no dashboard. This is what the assistant does about it, step by step, and the two draft merge requests you get at the end.

Starts fromA workload with a metrics port and no ServiceMonitor or PodMonitor.
Looks atLive Services and monitors, the metrics endpoint itself, your metrics backend (Prometheus / Mimir), grafana.com.
You getA ServiceMonitor MR, then a dashboard ConfigMap MR whose every panel is proven to return data.
01 · Notice the gap

A service with a metrics port, and nothing scraping it.

Every day the monitoring scan lists the workloads in the cluster and joins them to their Services and to every existing ServiceMonitor and PodMonitor. A ServiceMonitor selects Services, not pods, so the join follows the real chain: monitor → Service → workload, honouring namespace selectors.

orders-api has a port called metrics on 9102. No monitor reaches it. Workloads scaled to zero on purpose are skipped, so a parked relic never gets a monitor.

02 · Check the endpoint really serves

It asks the endpoint before it writes a line of YAML.

A port name is a hint, not proof. Before proposing anything, the assistant fetches /metrics through a read-only probe and checks that it answers with Prometheus exposition format.

That same response tells it which metric families exist, which is what the dashboard will be built from later.

03 · Propose the ServiceMonitor

Labels copied from the live Service, not guessed.

The generated ServiceMonitor selects the Service by its actual labels, names the actual port, and carries the release label your Prometheus operator or Alloy instance uses to pick monitors up.

Where the Helm chart already ships a monitor behind a values flag, you get the values flip instead of a hand-written manifest, so the chart stays the owner.

04 · Merged. Data starts flowing.

Your collector does the scraping. The bot reads the result.

After you merge, Flux applies the monitor, your collector (Prometheus, or Grafana Alloy) starts scraping, and the series land in your metrics backend.

The assistant never writes to that pipeline. It only reads from the backend, per tenant, when it builds and checks the dashboard.

05 · Find or write the dashboard

Community dashboards first. Generated only when none fit.

For well-known exporters (node, Redis, PostgreSQL, Traefik…) it prefers the maintained grafana.com dashboard and adapts it to your datasource and namespace variables.

orders-api is your own code, so there is nothing to import. The assistant drafts a dashboard from the metric families it actually saw: request rate, latency histogram, errors, business counters, runtime.

06 · Prove every panel

Each query runs against your data before you see it.

Every panel expression is executed against the metrics backend. Panels that return nothing are dropped and counted. A dashboard with no grounded panel is never shipped quietly: it is held for review with the reason named.

  • rate() over a gauge is caught and escalated.
  • Every $variable must be declared.
  • Datasource and namespace are variables, never hard-coded.
07 · Ship it the GitOps way

A ConfigMap in git. Grafana's sidecar does the rest.

The dashboard is wrapped in a ConfigMap with the grafana_dashboard label, placed in the tenant's folder, and stamped with how many queries were grounded and when.

Merge it and Flux applies it. The Grafana sidecar picks it up. No click-ops, no dashboard that only exists in one Grafana's database.

08 · The result

A dashboard that maps one-to-one onto what the service exposes.

Every panel is one of the metric families the probe saw in step 2. Nothing invented, nothing empty.

Committed dashboards are audited for known defects and repaired by MR. Re-checking them on a schedule, so a panel that goes dark after a service change gets noticed too, is in development.

monitoring scan · shop namespace
  • checkout-web Service web → ServiceMonitor checkout
  • payments PodMonitor payments
  • redis chart metrics.serviceMonitor.enabled
  • orders-api port metrics:9102 · no monitor
  • legacy-report replicas 0 · skipped
gap: shop/orders-api exposes metrics that nothing collects
metrics_endpoint_serves · read-only probe
$ GET http://orders-api.shop:9102/metrics
200 OK  content-type: text/plain; version=0.0.4
# HELP http_requests_total Requests by route and code.
# TYPE http_requests_total counter
http_requests_total{route="/orders",code="200"} 184223
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.25"} 171004
# TYPE orders_created_total counter
# TYPE outbox_queue_depth gauge
# TYPE go_goroutines gauge
→ 5 metric families recorded for later
apps/shop/orders-api/servicemonitor_orders-api.yamlnew file
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: orders-api
+  namespace: shop
+  labels:
+    release: kube-prometheus-stack   # what your collector selects on
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: orders-api  # from the live Service
+  endpoints:
+    - port: metrics                     # named port, verified
+      interval: 30s
category:reliabilitysource:monitoringrisk:lowxrev::LGTM
orders-api:9102/metrics
CollectorPrometheus / Alloy
BackendMimir · tenant shop
Assistantread-only queries
series now present in backend
monitor applied
dashboard source resolution
  • redis-exporter grafana.com #763 · import + adapt
  • postgres-exporter grafana.com #9628 · import + adapt
  • traefik already committed · skip
  • orders-api in-house · no community match
grounding against Mimir · tenant shop
  • sum by (route) (rate(http_requests_total{namespace="$namespace"}[5m])) 12 series
  • histogram_quantile(0.95, …duration_seconds_bucket…) 4 series
  • 5xx / total ratio 1 series
  • increase(orders_created_total[1h]) 1 series
  • max(outbox_queue_depth{namespace="$namespace"}) gauge · type checked against metadata
  • orders_refunded_total 0 series · panel dropped
grounded 6/7 panels · 1 dropped · variables $datasource $namespace declared
dashboards/shop/orders-api.yamlnew file
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: grafana-dashboard-orders-api
+  namespace: monitoring
+  labels:
+    grafana_dashboard: "1"          # sidecar picks it up
+  annotations:
+    grafana_folder: Shop
+    gitops-assistant/grounded-queries: "6/7"
+    gitops-assistant/grounded-at: "2026-09-18T04:12:09Z"
+data:
+  orders-api.json: |
+    { "title": "orders-api", "templating": { … }, "panels": [ … ] }
gitmerged
Fluxapplies CM
Grafanasidecar loads
Grafana · Shop / orders-apilast 6h
datasource mimir-shopnamespace shop
Requests / s
412
p95 latency
184 ms
5xx ratio
0.21%
Orders / h
1,930
Request rate by route
Latency p50 / p95 / p99
Outbox queue depth
Goroutines
Illustrative mock. Every panel maps to a family seen by the probe in step 02.

The merge request you get.

Two small MRs, in order: the ServiceMonitor first, then the dashboard once there is data to ground it on. This is the second one.

Draft

Add Grafana dashboard for shop/orders-api

from gitops-ai/dashboard-orders-api-3f1c into main · 1 file · +214
category:dashboardsource:dashboardrisk:lowxrev::LGTM
OverviewChangesValidation
Summary

orders-api in namespace shop has been scraped since the ServiceMonitor in !212 merged, but has no dashboard. No grafana.com dashboard matches an in-house service, so this one is generated from the five metric families the service exposes.

Panels
PanelSource familyGrounded
Requests / s, by routehttp_requests_total✓ 12 series
Latency p50 / p95 / p99http_request_duration_seconds✓ 4 series
5xx ratiohttp_requests_total✓ 1 series
Orders created / horders_created_total✓ 1 series
Outbox queue depthoutbox_queue_depth (gauge)✓ 1 series
Goroutinesgo_goroutines✓ 1 series

Dropped: "Refunds / h" (orders_refunded_total returned no series in the last 7 days).

+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: grafana-dashboard-orders-api
+  namespace: monitoring
+  labels:
+    grafana_dashboard: "1"
+  annotations:
+    grafana_folder: Shop
+    gitops-assistant/grounded-queries: "6/7"
+    gitops-assistant/grounded-at: "2026-09-18T04:12:09Z"
+    gitops-assistant/grounded-panels-dropped: "1"
+data:
+  orders-api.json: |
+    {
+      "title": "orders-api",
+      "templating": { "list": [
+        { "name": "datasource", "type": "datasource", "query": "prometheus",
+          "current": { "text": "mimir-shop", "value": "mimir-shop" } },
+        { "name": "namespace", "type": "query", "datasource": "${datasource}",
+          "current": { "text": "shop", "value": "shop" } } ] },
+      "panels": [ … 6 panels, all bound to ${datasource} … ]
+    }
Checks run before this MR was opened
  • 7 panel queries executed against mimir-shop; 6 returned data, 1 panel dropped.
  • No counter function over a gauge: metric types checked against the backend's metadata (outbox_queue_depth is a gauge and is graphed as one).
  • All template variables declared; no hard-coded datasource UID in any panel.
  • ConfigMap placed under the tenant folder and label your Grafana sidecar watches.
After merge, expect
  • Flux applies the ConfigMap in the next reconcile of the monitoring kustomization.
  • The dashboard appears in Grafana under Shop within a minute (sidecar reload).
  • All six panels show data for the last 6 hours on first open.
R
independent reviewer second opinion, no pipeline context

Verified: the ServiceMonitor from !212 is Ready and the series exist in the tenant. Queries and folder placement match the committed conventions. LGTM.

What keeps it honest.

The model drafts. Deterministic checks decide whether the draft is allowed out.

Endpoint probed before proposingNo monitor for a port that does not actually serve Prometheus format.
Every panel groundedQueries run against your backend; empty panels are dropped and a zero-data dashboard is held for review, never shipped quietly.
Metric types respectedA counter function over a gauge is caught using the backend's own metadata, and escalated rather than silently rewritten.
Portable by constructionDatasource and namespace are variables, so one dashboard works across clusters and tenants.
Chart owns chart outputIf the dashboard lives inside a Helm chart, the fix goes to the chart's values, not to rendered output.
Audited after shippingCommitted dashboards are checked for known defects (broken variables, hard-coded datasources) and repaired by MR.

Where this stands.

Live
  • Missing ServiceMonitor / PodMonitor detection and MRs
  • grafana.com import with datasource adaptation
  • Generated dashboards with per-query grounding
  • Multi-tenant folders and sidecar labels
In development
  • Scheduled re-grounding of committed dashboards as exporters change
  • Checking datasource variables against Grafana's own API
Roadmap
  • Alert rules generated alongside the dashboard, executed against your backend first
← Previous storyRight-sizing from real usage