· · 12 min leestijd Intermediate

Grafana-dashboards die daadwerkelijk gebruikt worden

Bouw Grafana dashboards waar mensen echt naar kijken. Vermijd dashboard rot met praktische design principes, nuttige panels, en real-world voorbeelden.

Veertig schermen vol spaghetti-grafieken, een plant groeit over één heen, zij tekent er drie getallen en één lijn op

TL;DR Elk dashboard beantwoordt één vraag, en binnen vijf seconden weet ik of ik me druk moet maken. Vier soorten dashboards dekken alles, variabelen en links knopen ze aan elkaar, en de JSON staat in Git. M’n homelab draait er vijf, en wat een maand niet geopend wordt gooi ik weg.

Je hebt Grafana. Je hebt Prometheus-metrics. Je hebt logs in Loki en traces in Tempo. De data staat er allemaal.

Je hebt ook 47 dashboards die niemand opent.

Dat heb ik mezelf meer dan eens aangedaan. Er gaat iets stuk, ik klus een dashboard in elkaar om te zien wat er gebeurt, en daarna blijft het staan. Een jaar aan incidenten en een paar “even een panel erbij”-momenten later was m’n Grafana vooral archeologie. Niemand wist nog wat de helft van de panels betekende, ik ook niet. Het meeste kan weg. Maar voor je gaat weggooien helpt het om te weten wat de overlevers hebben wat de rest mist.

De dashboards die ik nog open hebben één ding gemeen. Ik open er een en weet binnen vijf seconden of ik me druk moet maken. Dat is de hele lat, en deze post is hoe ik daar kom, van het kleinste dashboard dat het bewaren waard is tot de vijf die ik op m’n homelab-cluster draai.

Ik kom hier niet voor niks steeds op terug. Een stapel metrics die je niet kunt lezen is een black box, alleen heb je deze zelf gebouwd. Observability hoort begrip op te leveren. Een dashboard dat niemand kan lezen levert niks op.

Waar dashboards misgaan

Eerst hoe het misgaat. Elk van deze heb ik zelf live gezet.

Te veel panels. Dertig panels, dertig metrics, nul antwoorden, want niemand leest dertig grafieken in één oogopslag. Ze vechten om aandacht en geen enkele wint.

Verkeerd abstractieniveau. CPU, geheugen en schijf per pod is wat je wilt als je tot je ellebogen in een probleem zit. Nutteloos als je alleen wilt weten of de service up is. Dat zijn andere vragen, en die willen andere dashboards.

Geen context. “500 requests/seconde.” Goed? Slecht? Normaal? Zonder iets om mee te vergelijken is dat getal versiering.

Gebouwd en vergeten. Het incident-dashboard van twee jaar terug, dat nog steeds services bevraagt die bij een refactor hernoemd zijn zonder dat iemand het dashboard erbij haalde. Vlakke lijnen, en iedereen kijkt er inmiddels overheen.

Eén dashboard, één vraag

De regel die het meeste hiervan oplost is saai. Elk dashboard beantwoordt precies één vraag.

VraagDashboard
“Is het systeem gezond?”Service overzicht
“Waarom is deze service traag?”Service deep dive
“Wat gebeurde er om 3 uur ’s nachts?”Incident onderzoek
“Halen we onze SLOs?”SLO dashboard

Houd die uit elkaar. Zodra je een debug-panel op het health-check-dashboard schroeft, ben je onderweg terug naar 47.

Het simpelste nuttige dashboard is “is alles OK?”. Een handvol stat panels, groot en met kleur, rood als er iets mis is. Bouw je alleen dat ene, dan zit je al voor op de meeste teams waar ik heb gewerkt.

Doorklikken in plaats van alles erop proppen

Werkt het één-blik-dashboard, dan is de volgende vraag “OK, welke service?”. Beantwoord die niet op hetzelfde scherm. Link naar een dieper dashboard. Het model dat ik gebruik:

Level 1: Is alles OK? (groen/geel/rood)
    ↓
Level 2: Welke service heeft problemen?
    ↓
Level 3: Welke specifieke metric is fout?
    ↓
Level 4: Ruwe data voor debugging

Elk level is z’n eigen dashboard of sectie, met links aan elkaar geknoopt. Je stopt bij het level dat je vraag beantwoordt. De meeste dagen is dat level 1.

Geef elk getal iets om mee te vergelijken

Een kaal getal zegt bijna niks. Dezelfde waarde is prima op een dinsdagmiddag en een incident midden in de nacht. Dus ik voeg context toe waar het kan.

  • Thresholds. Het panel verandert van kleur als een metric een SLO-grens kruist, zodat je niet zelf hoeft te rekenen.
  • Vergelijkingen. Vandaag tegenover vorige week, deze deploy tegenover de vorige.
  • Annotaties. Deployments, incidenten en configwijzigingen direct op de grafiek.

Die annotaties hebben me meer debugtijd bespaard dan al het andere op dat lijstje. Latency piekt, er staat een deployment-marker precies onder de piek, klaar.

Leesbaar in vijf seconden

Kan ik een dashboard niet in vijf seconden lezen, dan is het te complex en open ik het niet meer, en dan is het hele punt weg. Dus ik houd het bij drie soorten panels.

  • Stat panels voor de huidige staat, grote getallen, met kleur
  • Time series voor trends, zodat je ziet wanneer iets begon
  • Heatmaps voor verdelingen zoals latency-percentielen

En ik sla de dingen over die informatief lijken en het niet zijn. Tabellen met 50 rijen, grafieken met 20 lijnen door elkaar, pie charts. Vooral pie charts.

De dashboards die je nodig hebt

Met die regels op tafel, dit zijn de dashboards die een vaste plek krijgen. Vier soorten. Een vijfde soort heb ik nooit nodig gehad, hooguit meer exemplaren van deze vier.

Service-overzicht

Het “is alles OK?"-dashboard, één per service of servicegroep. Dit is degene die je het vaakst opent.

# Panels (boven naar onder, links naar rechts)

Rij 1: Status (stat panels)
  - Beschikbaarheid (% uptime laatste 24u)
  - Error Rate (huidig, kleurgecodeerd)
  - P99 Latency (huidig, kleurgecodeerd)
  - Actieve Instances (aantal)

Rij 2: Trends (time series)
  - Request Rate (gestapeld per status code)
  - Latency (P50, P95, P99 lijnen)
  - Error Rate (percentage over tijd)

Rij 3: Resources (gauges)
  - CPU Gebruik (% van limit)
  - Memory Gebruik (% van limit)
  - Pod Count (huidig vs gewenst)

De queries erachter:

# Beschikbaarheid (percentage succesvolle requests)
sum(rate(http_requests_total{status!~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) * 100

# Error rate
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) * 100

# P99 latency
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)

SLO-dashboard

Deze volgt de Service Level Objectives en vertelt je of je je error budget sneller opmaakt dan je je kunt veroorloven. Het is de ruggengraat van alerting die wat betekent, want je wilt iemand pagen als het budget opbrandt, en niet voor één request dat hapert.

Rij 1: SLO Status (stat panels)
  - Beschikbaarheid SLO (99.9% target)
  - Latency SLO (P99 < 500ms target)
  - Error Budget Resterend (%)

Rij 2: Burn Rate (time series)
  - Error budget consumptie snelheid
  - Voorspelde budget uitputting

Rij 3: SLI Breakdown (time series)
  - Beschikbaarheid over tijd
  - Latency percentielen over tijd

De error-budget-query:

# Error budget resterend (voor 99.9% SLO over 30 dagen)
1 - (
  (1 - (sum(rate(http_requests_total{status!~"5.."}[30d])) /
        sum(rate(http_requests_total[30d])))) /
  0.001  # 0.1% error budget
)

Kubernetes-cluster-dashboard

Een vogelvlucht over de gezondheid van het cluster. Weersta de neiging om hier per-pod-detail in te stoppen. Dat hoort in het debug-dashboard, en die twee mengen is precies hoe je weer op dertig panels uitkomt.

Rij 1: Cluster Status
  - Nodes Ready / Totaal
  - Pods Running / Scheduled
  - PVCs Bound / Totaal

Rij 2: Resource Gebruik
  - CPU: Gebruikt / Requested / Allocatable
  - Memory: Gebruikt / Requested / Allocatable

Rij 3: Workload Health
  - Deployments Available / Totaal
  - StatefulSets Ready / Totaal
  - DaemonSets Ready / Totaal

Rij 4: Recente Events (tabel)
  - Warning events van afgelopen uur

Debug-dashboard

De enige plek waar complexiteit mag. Bij een incident wil je detail, variabelen, en kunnen filteren op namespace en pod. De vijfsecondenregel geldt niet als je al tot je nek in een probleem zit.

Rij 1: Service Selectie (variabelen)
  - Namespace dropdown
  - Service dropdown
  - Pod dropdown

Rij 2: Request Flow
  - Inkomende requests per endpoint
  - Uitgaande requests per bestemming
  - Error breakdown per type

Rij 3: Resource Details
  - CPU per container (time series)
  - Memory per container (time series)
  - Network I/O (time series)

Rij 4: Logs Integratie
  - Link naar Loki met pre-gefilterde query
  - Recente error log count

Panels om te stelen

De losse panels waar ik steeds weer naar grijp. Kopiëren, op je eigen metrics richten, klaar.

Request rate met statuscodes

sum by (status_code) (
  rate(http_requests_total{service="$service"}[5m])
)

Stapel de series en kleur ze per status. 2xx groen, 3xx blauw, 4xx geel, 5xx rood. Wordt de grafiek rood, dan heb je de legenda niet nodig.

Latency-heatmap

sum(rate(http_request_duration_seconds_bucket{service="$service"}[5m])) by (le)

Render het als heatmap. Je krijgt de hele latency-verdeling over tijd, en de hotspots springen eruit waar een gemiddelde ze had weggepoetst. Een p99-piek die een lijngrafiek gladstrijkt is hier een felle band.

Resourcegebruik tegenover limits

# CPU gebruik percentage van limit
sum(rate(container_cpu_usage_seconds_total{pod=~"$pod"}[5m])) /
sum(kube_pod_container_resource_limits{resource="cpu", pod=~"$pod"}) * 100

Een gauge met thresholds op 0-70 groen, 70-90 geel, 90-100 rood. Gebruik als percentage van de limit in plaats van kale cores, zodat hetzelfde panel voor elke service werkt zonder bijstellen.

Pod-restart-indicator

sum(increase(kube_pod_container_status_restarts_total{namespace="$namespace"}[1h])) by (pod)

Een stat panel dat alleen verschijnt als de teller boven nul staat, direct gelinkt aan de pod-logs. Een pod die stilletjes om de paar minuten herstart is precies het soort langzame lek dat ik wil zien voor het een storing wordt.

Variabelen, één dashboard voor elke service

Variabelen zijn wat je ervan weerhoudt hetzelfde dashboard vijftig keer te klonen. Eén keer definiëren en het dashboard richt zichzelf via een dropdown. Deze drie dekken het meeste van wat ik nodig heb:

# Namespace variabele
name: namespace
query: label_values(kube_pod_info, namespace)
multi: true
include_all: true

# Service variabele (gefilterd op namespace)
name: service
query: label_values(kube_pod_info{namespace=~"$namespace"}, pod)
multi: false

# Tijd vergelijking
name: comparison
options:
  - 1h geleden
  - 1d geleden
  - 1w geleden

Daarna verwijs je ernaar in de queries:

# Huidige waarde
sum(rate(http_requests_total{namespace="$namespace"}[5m]))

# Vergelijkingswaarde (offset met geselecteerde tijd)
sum(rate(http_requests_total{namespace="$namespace"}[5m] offset $comparison))

Die offset $comparison-truc geeft je bijna gratis een “tegenover vorige week”-overlay. Dat is de contextregel van hierboven, in één query.

Annotaties, zodat je niet hoeft te gokken wat er veranderde

Annotaties zetten events direct op de grafieken. Twee bronnen zijn het inrichten waard:

# Deployment annotaties
name: Deployments
datasource: Prometheus
query: changes(kube_deployment_status_observed_generation{deployment="$service"}[5m]) > 0
tags: deployment

# Alert annotaties
name: Alerts
datasource: Alertmanager
filter: service="$service"
tags: alert

Nu beantwoordt een latency-piek met een deployment-marker eronder de vraag “wat is er veranderd?” voor je een terminal hebt geopend. De helft van m’n incidenten blijkt “we hebben iets gedeployed”, en de marker zegt me dat meteen.

Dashboards aan elkaar knopen

Links tussen dashboards zijn wat de levels laat werken. Het één-blik-dashboard geeft je door aan het detail-dashboard, en je zoekt nooit door folders.

Op een service-panel, link direct naar het detail-dashboard van die service:

URL: /d/service-detail?var-service=${__field.labels.service}
Title: Bekijk ${__field.labels.service} details

Op het cluster-overzicht, link omlaag naar de namespace-views:

URL: /d/namespace-view?var-namespace=${namespace}
Title: Drill down naar namespace

En link naar Loki , zodat je van een metric naar de bijbehorende logs gaat met één klik:

URL: /explore?left=["now-1h","now","Loki",{"expr":"{namespace=\"${namespace}\",pod=~\"${pod}\"}"}]
Title: Bekijk logs in Explore

Georganiseerd houden

Voorbij een handvol dashboards is structuur niet langer optioneel. Een beetje discipline hier is het verschil tussen het juiste dashboard in twee seconden vinden en het opgeven en Prometheus met de hand bevragen.

Folderstructuur

├── Overzicht
│   ├── Platform Health
│   └── SLO Samenvatting
├── Services
│   ├── API Gateway
│   ├── Auth Service
│   └── ...
├── Infrastructuur
│   ├── Kubernetes Cluster
│   ├── Nodes
│   └── Storage
├── Debugging
│   ├── Service Debug
│   └── Incident Onderzoek
└── Archief
    └── (oude dashboards, verborgen)

Naamconventie

[Categorie] - [Onderwerp] - [Type]

Voorbeelden:
- Services - API Gateway - Overzicht
- Services - API Gateway - Debug
- Infrastructuur - Kubernetes - Nodes
- SLO - Platform - 30d

Sterretjes en het home-dashboard

Zet het “is alles OK?"-dashboard als Grafana-home, zodat het het eerste is wat je na het inloggen ziet, en geef de paar die je dagelijks opent een ster. Hoe sneller de belangrijke binnen handbereik zijn, hoe minder je terugvalt op oude gewoontes.

Dashboards als code

Dashboards in de UI in elkaar klikken gaat prima tot iemand een panel verprutst, of de Grafana-pod opnieuw gescheduled wordt en je ontdekt dat het dashboard alleen in z’n geheugen leefde. De oplossing is dezelfde die ik op alles toepas. In Git zetten en ArgoCD het laten deployen. Een dashboard dat ik niet vanuit source kan herbouwen is niet van mij.

Grafana-dashboard-ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: service-overview-dashboard
  labels:
    grafana_dashboard: "1"
data:
  service-overview.json: |
    {
      "title": "Service Overview",
      "uid": "service-overview",
      "panels": [
        ...
      ]
    }

Zet een ConfigMap neer met het label grafana_dashboard: "1" en de sidecar pikt hem op. De dashboard-JSON staat nu naast de service die hij bewaakt, en gaat mee in dezelfde merge request die de service uitrolt.

Grafonnet (Jsonnet-library)

Dashboard-JSON met de hand bewerken gaat snel vervelen. Grafonnet genereert het uit code, zodat een gedeeld panel een functie is die je aanroept in plaats van JSON die je kopieert en dan vergeet bij te houden:

local grafana = import 'grafonnet/grafana.libsonnet';
local dashboard = grafana.dashboard;
local prometheus = grafana.prometheus;

dashboard.new(
  'Service Overview',
  uid='service-overview',
  tags=['service', 'overview'],
)
.addPanel(
  grafana.statPanel.new(
    'Error Rate',
    datasource='Prometheus',
  ).addTarget(
    prometheus.target(
      'sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100',
    )
  ),
  gridPos={ x: 0, y: 0, w: 6, h: 4 },
)

Wat ik thuis draai

Hier het geheel in de praktijk. Op m’n homelab-cluster houd ik precies vijf dashboards:

  1. Home. Alles OK? Eén blik.
  2. SLOs. Haal ik m’n doelen, en hoe staat het error budget?
  3. Cluster. Kubernetes-gezondheid van bovenaf.
  4. Service Debug. Die met de variabelen, voor als er iets mis is.
  5. Incident. De rommelige, die metrics, logs en traces bij elkaar trekt.

Dat is alles. Een zesde houd ik bewust tegen. Elk van deze wordt wekelijks of vaker geopend, en dat is de enige test die ik hanteer. Een dashboard dat een maand niet geopend wordt gaat weg, en tot nu toe heb ik er geen één gemist.

Zet dat naast de 47 van bovenaan deze post. Ik verzamel niet minder data, m’n observability-stack zuigt ruim genoeg op. Ik lees vijf dashboards die ik vertrouw in plaats van er vijftig te negeren.

Dashboard-rot tegengaan

Vijf blijft vijf omdat ik blijf snoeien. Laat je het liggen, dan drijft elke Grafana terug naar archeologie. Een beetje proces houdt het op vijf.

Review op een vast moment

Eén keer per maand loop ik alles door en stel drie vragen. Welke dashboards zijn geopend? Wijzen de queries nog naar services die bestaan? Kloppen de thresholds nog met hoe het systeem zich gedraagt? Metrics worden hernoemd, SLO’s worden aangescherpt, en een threshold die een jaar geleden klopte kan stilletjes ophouden iets te betekenen.

Opruimen wat niemand gebruikt

30 dagen niet geopend, dan gaat het naar de folder Archief. Zestig dagen in Archief, dan is het weg. Git heeft de geschiedenis als ik ernaast zit.

Schrijf op waar elk dashboard voor is

Elk dashboard krijgt een korte beschrijving, zodat de volgende weet waarom het bestaat. De volgende ben ik meestal zelf, een half jaar later, alles vergeten.

Doel: Snelle health check voor productie services
Doelgroep: On-call engineers
Wanneer te gebruiken: Dagelijkse check, incident first response
Links naar: Service Debug dashboard voor details

Waarom ik de moeite neem

Een dashboard hoort je iets waars over je systeem te vertellen, snel genoeg dat je ernaar handelt. Een slecht dashboard is ruis, en ruis leert mensen de monitoring helemaal te negeren. Dat is erger dan geen monitoring, want je denkt dat je gedekt bent.

Metrics verzamelen is makkelijk, en Grafana maakt grafieken tekenen makkelijk, dus de data was nooit de bottleneck. Het lezen is dat. De vijf op m’n homelab staan in Git, elk wordt wekelijks geopend, en stopt dat, dan gaat-ie weg.