Monitoring-stack (Prometheus, Alertmanager, Grafana)
Dit document legt uit hoe onze self-hosted monitoring in elkaar zit, waarom we voor bepaalde patronen hebben gekozen en waar de subtiliteiten zitten. Voor concrete feiten (URLs, alert-thresholds, secret-keys), zie de reference. Voor stap-voor-stap uitrollen op het cluster, zie de rollout-how-to.
Wat zit er in de stack
Drie componenten draaien in de monitoring-namespace op een vaste worker-node (tapster.nl/node-name: jive):
- Prometheus scrape’t metrics uit de
tapster- entapster-development-namespaces, pluskube-state-metricsen de cAdvisor-endpoints op de nodes. De evaluatie-loop draait elke 15 seconden en gaat door allerule_filesheen om alerts en recording-rules af te leiden. - Alertmanager ontvangt firing alerts van Prometheus, dedupliceert ze, past inhibit-rules toe en routeert naar receivers (e-mail via Flowmailer, webhook voor de Watchdog).
- Grafana leest data uit Prometheus én Alertmanager als datasources en serveert dashboards en de live alert-overview.
De drie communiceren cluster-intern (alertmanager.monitoring.svc.cluster.local:9093, http://prometheus:9090), dus daar is geen auth nodig. Voor extern verkeer staat er een Traefik IngressRoute per component op *.tapster.nl, met basic-auth voor Prometheus en Alertmanager en Grafana’s eigen login.
Waarom self-hosted en niet Grafana Cloud
Een managed service zou ons de bedrijfsvoering uit handen nemen maar zou ook betekenen dat klant-metrics (pod-labels, request-counts per tenant) een externe SaaS in gaan. Voor de schaal die we nu hebben (één cluster, beperkt aantal services) is self-hosted goedkoper en levert het ons ook de vrijheid om scrape-targets en rule-bestanden in dezelfde git-repo te beheren als de code waar ze over rapporteren. Mocht de operationele last toenemen, dan is migreren naar een managed setup later een open optie. Geen vendor-lock-in nu nodig.
Hoe een alert van metric naar mailbox komt
- Een pod in
tapsterexposed metrics op een Prometheus-port (annotatieprometheus.io/scrape: "true"). - Prometheus scrape’t elke 15 seconden, evalueert de rules en markeert een alert als
firingzodra de expressie minimaalfor: <duur>waar blijft. - Prometheus pusht actieve alerts naar Alertmanager via cluster-interne service-DNS.
- Alertmanager groepeert (
group_by: [alertname, severity]), wacht 30 seconden om volgende alerts te bundelen, past inhibit-rules toe en stuurt het resultaat naar de eerste matchende route. - Voor
severity = criticalgaat het naarroy-email(Flowmailer SMTP). Vooralertname = Watchdoggaat het naarwatchdog-webhook(zie hieronder).
repeat_interval zorgt dat een alert die firing blijft niet elk evaluatie-tikje opnieuw mailt, maar pas na 1 uur (critical) of 4 uur (default).
Watchdog: wie alarmeert er als het alerting-pad zelf stuk is
Een klassieke faalmode: Prometheus is down, dus de alerts vuren niet, dus jij krijgt niks. Stilte als signaal interpreteren is niet betrouwbaar. We lossen dit op met een Dead Man’s Switch:
- De
Watchdog-rule heeft expressievector(1)en vuurt dus continu zolang Prometheus draait en evalueert. - Alertmanager heeft een aparte route die alléén op
alertname = "Watchdog"matcht en die boven de andere routes staat. Hij stuurt elke minuut een POST naar een externe healthcheck-service (healthchecks.io, BetterStack, etc.) via webhook. - Die externe service alarmeert ons als de heartbeat 3 minuten uitblijft, via een ander kanaal (push, sms) dan onze eigen mail. Daarmee zit het alerting-pad niet in zijn eigen failure-keten.
Waarom de Watchdog-route boven de e-mailroute staat: zonder die volgorde zou Watchdog matchen op de default-route en elke minuut een mail veroorzaken. Met continue: false (Alertmanager’s default) stopt de match bij de eerste route en vallen alerts niet door.
Inhibit-rules: waarom twee paden in plaats van één
Een critical voor een pod moet de warning voor dezelfde pod onderdrukken, anders zit je mailbox vol met beide. Alertmanager doet dat via equal:-labels: alerts die dezelfde waarde hebben voor de genoemde labels worden als gerelateerd beschouwd.
Het gevoelige punt: in Alertmanager geldt dat een ontbrekend label aan beide kanten als “gelijk” wordt gezien. Een naïeve rule met equal: ['alertname', 'app_name'] zou dus betekenen dat een MongoosePoolCritical (zonder app_name) een PodCrashLooping voor pod X (ook zonder die specifieke label) zou onderdrukken, uit verschillende namespaces door elkaar.
We splitsen daarom in twee rules:
- App-specifiek: matcht alleen als
app_name =~ ".+"(niet-leeg) aan beide kanten. Dan correleren we opalertnameénapp_name. - Cluster-wide: matcht alleen als
app_name = ""aan beide kanten. Dan correleren we alleen opalertname(impliciet, want dat is hoe Alertmanagerequal:zonder lijst behandelt is niet correct, dus we zetten geenequal:en laten label-set gelijkheid het werk doen).
Het effect: critical-onderdrukt-warning werkt nu correct binnen één app én voor cluster-wide alerts, zonder kruislings effect.
envsubst-rendering: waarom we templates renderen en niet env vars direct gebruiken
Alertmanager leest zijn config uit een YAML-bestand, niet uit env-vars. Maar SMTP-credentials en de Watchdog-webhook-URL horen niet in git. We lossen dat op met een init-container die envsubst draait over een template-config, geheime waarden uit Secrets injecteert via envFrom, en het resultaat naar een emptyDir schrijft die de hoofd-container leest.
Twee subtiliteiten:
- Whitelist verplicht.
envsubstzonder argumenten vervangt elke$VAR-patroon dat in zijn omgeving bekend is en eet los staande$-tekens op alsof het variabele-prefixen zijn. Een password met een letterlijke$erin (bijv.pa$$w0rd) zou stilletjes gecorrumpeerd raken. De whitelist (envsubst '$SMTP_FROM $SMTP_HOST ...') zegt expliciet welke variabelen vervangen mogen worden en laat de rest letterlijk staan. - Whitelist moet kloppen met de template. Een variabele die wel in de template staat maar niet in de whitelist, blijft als letterlijke
${X}-string in de output achter en faalt later bij YAML-parsing of URL-parsing. Een variabele die wel in de whitelist staat maar niet in de template, is harmless. Dus bij elke nieuwe placeholder: voeg hem aan de whitelist toe. Een regressie op precies dit punt (Watchdog-URL niet in whitelist) hebben we gehad in #2025: Alertmanager faalde metinvalid URL: unsupported scheme "".
Severity-conventie
Alle alert-rules gebruiken één van drie waarden voor het severity-label:
critical: gebruikers raken nu of binnen minuten problemen, of een data-integriteits-risico. Direct mail én Watchdog blijft draaien zodat we weten dat het bericht aankwam.warning: trend gaat de verkeerde kant op maar er is tijd om in te grijpen voor de impact merkbaar wordt.none: meta-signalen zoals Watchdog die niet als alert maar als heartbeat dienen. Niet via mail routeren.
Routing in Alertmanager kijkt naar dit label, dus consistent toekennen is belangrijker dan precies de juiste keuze in een grensgeval.
Wat niet in de stack zit (bewust)
- Log-aggregatie zoals Loki of ELK. Logs lezen we momenteel via
kubectl logsof pod-logging directly. Een aparte log-stack is een tweede stap als monitoring volwassen is. - Tracing-backend voor OpenTelemetry. We exporteren wel OTel-data uit
apps/apimaar er zit nu nog geen Jaeger of Tempo achter (open vraag voor later). - Multi-cluster. Alle scrape-config gaat uit van één cluster.