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.

Ik heb dit mezelf meer dan eens aangedaan. Er gaat iets stuk om 2 uur ’s nachts, ik klus snel een dashboard in elkaar om te zien wat er aan de hand is, en daarna blijft het voor eeuwig staan. Vermenigvuldig dat met een jaar aan incidenten en een paar “ik voeg er even een panel voor toe”-momenten, en je houdt een Grafana over die vooral archeologie is. Niemand weet meer wat de helft van de panels betekent. De eerlijke keuze is het meeste weggooien, maar eerst helpt het om te snappen wat de overlevers de moeite waard maakt.

De dashboards die ik echt gebruik hebben één ding gemeen: ik kan er één openen en binnen vijf seconden weten of ik me zorgen moet maken. Dat is de hele lat. Alles hieronder is hoe ik daar kom, beginnend bij het simpelste dashboard dat zijn plek verdient en oplopend naar de volledige setup die ik op mijn homelab cluster draai.

Dit raakt iets waar ik op deze blog steeds op terugkom. Een stapel metrics die je niet kunt lezen is gewoon weer een black box, alleen heb je deze zelf gebouwd. Het punt van observability is begrip, en een dashboard dat niemand kan ontcijferen faalt precies daarin.

Waar dashboards misgaan

Eerst de faalmodi, dan de goede patronen. Ik heb ze allemaal zelf live gezet.

Te veel panels. Dertig panels, dertig metrics, en op de een of andere manier nul antwoorden, want geen mens leest dertig grafieken in één oogopslag. De panels vechten om aandacht en winnen niets.

Verkeerd abstractieniveau. CPU, memory en disk voor elke individuele pod is geweldig als je tot je ellebogen in een probleem zit en nutteloos als je gewoon wilt weten of de service up is. Dat zijn verschillende vragen en die willen verschillende dashboards.

Geen context. “500 requests/seconde.” Goed? Slecht? Normaal? Een getal zonder iets om mee te vergelijken is decoratie.

Gebouwd en vergeten. Het incident-dashboard van twee jaar geleden, dat nog steeds services bevraagt die zijn hernoemd in een refactor waar het niets van wist. Het toont vlakke lijnen en iedereen heeft geleerd het te negeren.

Begin simpel: éé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 de doelen gescheiden. Op het moment dat je een debugging panel aan je health-check dashboard vastschroeft, ben je begonnen aan de afdaling terug naar 47 dashboards die niemand leest.

Het simpelste nuttige dashboard is de “is alles OK?"-versie. Een handvol stat panels, groot en kleurgecodeerd, die rood worden als er echt iets mis is. Als dat het enige dashboard is dat je ooit bouwt, ben je al verder dan de meeste teams.

Laat mensen inzoomen in plaats van alles te proppen

Zodra het één-blik dashboard werkt, is de logische volgende vraag “OK, maar welke service?” Beantwoord die niet op hetzelfde scherm. Link door naar een dieper dashboard. Het mentale 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 woont in zijn eigen dashboard of sectie, aan elkaar gedraad met links. Je stopt bij het level dat je vraag beantwoordt. Meestal is dat level 1.

Geef elk getal iets om mee te vergelijken

Een ruw getal zegt je bijna niets. Dezelfde waarde kan prima zijn op een dinsdagmiddag en een vijf-alarm brand om 3 uur ’s nachts. Dus ik voeg context toe waar ik kan:

  • Thresholds: het panel verandert van kleur wanneer een metric een SLO grens kruist, zodat rood gewoon rood betekent zonder dat je hoeft te rekenen
  • Vergelijkingen: vandaag tegenover vorige week, deze deploy tegenover de vorige
  • Annotaties: deployments, incidenten en config changes direct op de grafiek getekend

Dat laatste heeft me meer debug-tijd bespaard dan wat dan ook. Als latency piekt en er precies onder de piek een deployment-marker staat, heb je je dader gevonden voordat je koffie op is.

Maak het leesbaar in vijf seconden

Als ik een dashboard niet in vijf seconden kan lezen, is het te complex en ga ik het niet meer openen, wat het hele punt onderuit haalt. Houd een kleine woordenschat aan paneltypes aan:

  • Stat panels voor huidige staat (grote nummers, kleurgecodeerd)
  • Time series voor trends, zodat je ziet wanneer iets begon
  • Heatmaps voor distributies zoals latency percentielen

En vermijd de dingen die informatief lijken maar het niet zijn: tabellen met 50 rijen, grafieken met 20 overlappende lijnen, en pie charts. Vermijd altijd pie charts.

Laag 1: de dashboards die je echt nodig hebt

Met de principes op hun plek, hier de dashboards die een vaste plek verdienen. Meer dan een paar heb je niet nodig.

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)

Belangrijke queries:

# 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 Service Level Objectives en vertelt je of je je error budget sneller uitgeeft dan je je kunt veroorloven. Het is de ruggengraat van betekenisvolle alerting, want je wilt iemand pagen wanneer het budget brandt, niet elke keer dat een enkele request 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

Error budget berekening:

# 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 cluster health. Weersta de drang om per-pod detail hierin te stoppen. Dat hoort in het debugging dashboard, en het mengen van de twee is hoe je weer bij dertig panels eindigt.

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

Debugging dashboard

Dit is de enige plek waar complexiteit is toegestaan. Als je een incident onderzoekt wil je detail, variabelen, en de mogelijkheid om dingen te slicen per namespace en pod. De vijf-seconden regel 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

Laag 2: panels die de moeite waard zijn

Hier zijn de losse panels waar ik steeds weer naar grijp. Kopieer ze, richt ze op je eigen metrics, klaar.

Request rate met status codes

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

Stack de series en kleur ze per status: 2xx groen, 3xx blauw, 4xx geel, 5xx rood. Als de grafiek rood wordt, hoef je de legenda niet te lezen.

Latency heatmap

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

Render het als een heatmap. Je krijgt de volledige latency distributie over tijd, en de hot spots springen eruit waar gemiddelden ze verborgen zouden hebben. Een p99 spike die een lijngrafiek wegmiddelt verschijnt hier als een felle band.

Resource gebruik vs 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. Door gebruik als percentage van de limit te tonen in plaats van ruwe cores werkt hetzelfde panel voor elke service zonder hertunen.

Pod restart indicator

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

Een stat panel dat alleen verschijnt als de count boven nul is, gelinkt aan de pod logs. Een pod die stilletjes elke paar minuten herstart is precies het soort langzame bloeding dat je gesignaleerd wilt hebben voordat het een outage wordt.

Variabelen: één dashboard voor elke service

Variabelen zijn wat je tegenhoudt om hetzelfde dashboard vijftig keer te klonen. Definieer ze één keer en het dashboard richt zichzelf opnieuw met 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

Verwijs er vervolgens naar 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 “vs vorige week” overlay, wat het idee van context-boven-nummers omgezet in één query is.

Annotaties: correleren zonder gokken

Annotaties markeren events direct op je grafieken. Twee bronnen verdienen hun plek:

# 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 spike met een deployment-marker eronder de “wat is er veranderd?"-vraag voordat je überhaupt een terminal hebt geopend. De helft van mijn incidenten blijkt “we hebben iets gedeployed,” en de marker vertelt me dat meteen.

Dashboards aan elkaar knopen

Zo werkt progressieve onthulling in Grafana in de praktijk: links tussen dashboards, zodat de één-blik view je kan doorverwijzen naar de detail view zonder dat je door folders hoeft te zoeken.

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 namespace-specifieke views:

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

En link door naar Loki zodat springen van een metric naar de bijbehorende logs één klik is, geen context switch:

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

Het georganiseerd houden

Zodra je meer dan een handvol dashboards hebt, is structuur niet langer optioneel. Een beetje discipline hier is het verschil tussen het juiste dashboard in twee seconden vinden en het opgeven en direct Prometheus grepppen.

Folder structuur

├── 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

Starring en het home dashboard

Stel je “is alles OK?” dashboard in als Grafana home zodat het het eerste is dat je ziet als je inlogt, en star de paar die je dagelijks opent. Hoe sneller de belangrijke binnen handbereik zijn, hoe minder je terugvalt op slechte gewoontes.

Laag 3: dashboards als code

Dashboards in de UI in elkaar klikken is prima totdat iemand een panel verprutst, of je Grafana pod gereschedduled wordt en je ontdekt dat het dashboard alleen in zijn geheugen leefde. De oplossing is dezelfde die ik op alles toepas: zet het in Git en laat ArgoCD het deployen. Een dashboard dat je niet vanuit source kunt herbouwen is een dashboard dat je niet echt bezit.

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": [
        ...
      ]
    }

Plaats een ConfigMap met het grafana_dashboard: "1" label en de sidecar pikt het automatisch op. Je dashboard JSON woont nu naast de service die het monitort, gereviewed in dezelfde merge request die de service ships.

Grafonnet (Jsonnet library)

Met de hand dashboard JSON editen wordt snel vervelend. Grafonnet laat je het genereren vanuit code, zodat een gedeeld panel een functie wordt die je hergebruikt in plaats van gekopieerde JSON die je vergeet in sync 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 },
)

Het volledige plaatje: wat ik echt draai

Na al dat laag-voor-laag bouwen, hier het geheel in de praktijk. Voor mijn homelab cluster houd ik exact vijf dashboards:

  1. Home - alles OK? Beantwoord in één blik.
  2. SLOs - haal ik mijn objectives, en hoe staat het error budget?
  3. Cluster - Kubernetes health van bovenaf.
  4. Service Debug - de variabele-gedreven deep dive voor als er iets mis is.
  5. Incident - de rommelige die metrics, logs en traces bij elkaar trekt.

Dat is alles. Ik weersta actief de verleiding om een zesde te maken. Elk van deze wordt wekelijks of vaker geopend, wat de enige test is die ertoe doet. De dag dat een dashboard een maand niet geopend wordt, verwijder ik het, en tot nu toe heb ik er nooit één gemist.

Vergelijk dat met het 47-dashboard kerkhof van bovenaan deze post. Het verschil is niet dat ik minder data verzamel, mijn observability stack zuigt ruim genoeg op. Het verschil is dat vijf dashboards die ik vertrouw winnen van vijftig die ik mezelf heb aangeleerd te negeren.

Dashboard rot bestrijden

Vijf dashboards blijft alleen vijf omdat ik blijf snoeien. Met rust gelaten drijft elke Grafana terug richting archeologie. Een beetje proces houdt het eerlijk.

Review op schema

Eén keer per maand loop ik alles door en stel drie dingen vast: welke dashboards werden geopend, wijzen de queries nog naar services die bestaan, en kloppen de thresholds nog met hoe het systeem zich werkelijk gedraagt. Metrics worden hernoemd, SLOs worden aangescherpt, en een threshold die een jaar geleden klopte kan stilletjes ophouden iets te betekenen.

Sunset wat niemand gebruikt

De regel is simpel en zonder sentiment. Niet geopend in 30 dagen, het gaat naar de Archief folder. Zestig dagen in Archief, het is weg. Git heeft de geschiedenis als ik er ooit naast zit.

Schrijf op waar elk dashboard voor is

Elk dashboard krijgt een korte beschrijving zodat de volgende persoon (vaak de toekomstige ik, die alles vergeten is) weet waarom het bestaat:

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 is een communicatiemiddel. Een goede vertelt je iets waars over je systeem snel genoeg dat je ernaar handelt. Een slechte is ruis, en ruis leert mensen de monitoring helemaal te negeren, wat erger is dan helemaal geen monitoring omdat je denkt dat je gedekt bent.

Daar ligt de draad terug naar het begin. Metrics verzamelen is makkelijk en Grafana maakt grafieken tekenen makkelijk, dus de bottleneck was nooit de data. Het moeilijke deel is begrip, en een dashboard dat niemand kan lezen laat je precies zo blind als geen dashboard, alleen met meer kubussen op je scherm. Bouw de paar die hun plek verdienen, houd ze in Git, snoei de rest, en de volgende keer dat er iets stukgaat om 2 uur ’s nachts ben je blij dat je het antwoord in vijf seconden kunt lezen.