· · 11 min leestijd Advanced

Alerting dat werkt: van alert fatigue naar actionable notificaties

Bouw een alerting strategie die echt werkt. Verminder alert fatigue, creëer actionable notificaties, en stop met wakker worden voor non-issues.

Een muur met honderd alarmlampen, een zombie in pyjama die ze negeert, en één lamp aan een bel

TL;DR De meeste default alertregels pagen op een getal zonder gebruiker erachter, en na een week lees je geen page meer. Ik alert op error rate, P99-latency, een disk die binnen 24 uur vol zit en een certificaat dat binnen zeven dagen verloopt, een stuk of tien regels bij elkaar, elk met een runbook. Alertmanager stuurt critical naar de pager, warning overdag naar Slack en info nergens heen. Alles wat geen mens nodig heeft gaat weg.

Een tijdlang was m’n alerting prima. Een handvol regels, een page was zeldzaam, en kwam er een binnen, dan betekende die iets. Toen groeide het cluster, schroefde ik de defaults van de Prometheus Operator erop, en werd “prima” ruis zonder dat ik het doorhad.

De page die het deed kwam om 3 uur ’s nachts. “High CPU usage on node-worker-3.” Ik keek naar de grafiek, zag dat de node al tien minuten op 75% stond, en ging weer slapen. Volgende nacht dezelfde alert. Een week later maakte ik ze niet eens meer open.

Dat is alert fatigue, en het is de gevaarlijke soort kapot, omdat het eruitziet alsof het werkt. De pages blijven komen en jij blijft ze negeren. Als alles paget, belandt een echt incident op dezelfde stapel, en de eerste die het merkt is een gebruiker.

Dus welke alerts verdienen het om iemand wakker te maken? Dat is de enige vraag in dit stuk, en met tooling heeft het antwoord weinig te maken. Ik heb aan allebei de kanten gezeten, verzuipen in pages, en systemen waar een page zo zeldzaam is dat ik hem vertrouw op het moment dat hij afgaat. Het verschil zit in hoe je bepaalt wat een mens mag onderbreken.

Het is ook de waarde waar ik op deze blog steeds op uitkom, begrip boven vertrouwen. Een alert waarvan je niet kunt uitleggen wat hij betekent is een black box aan je pager. Je kunt er niet over redeneren, dus leer je hem te negeren.

Waarom default alerts het erger maken

De meeste monitoring-setups komen met alertregels al ingeladen. De Prometheus Operator levert er honderden mee, en ze vuren op alles:

  • CPU > 80%
  • Memory > 80%
  • Disk > 80%
  • Pod restarts > 0
  • Elke 5xx error

Ik snap waarom ze meegeleverd worden. Er gaan alsnog vier dingen mis:

  1. Geen context. Is 80% CPU erg? Hangt van de workload af. Een batchjob die een core volpompt doet gewoon z’n werk.
  2. Geen impact. Merkt een gebruiker hier nu iets van? De alert weet het niet, en jij half slapend ook niet.
  3. Geen actie. Wat moet ik hieraan doen? Meestal niks.
  4. Te gevoelig. Een korte piek vuurt een alert die zichzelf oplost voor je je telefoon ontgrendeld hebt.

Je houdt pages over die geen mens nodig hebben, en mensen die geleerd hebben pages niet te vertrouwen. Die twee versterken elkaar.

Hoe ik bepaal wat me mag pagen

Een betere tool had dit niet opgelost. Wat het wél oploste waren een paar regels over wat een alert mag zijn.

1. Alert op symptomen, niet op oorzaken

Slecht: “Pod restarted” Goed: “Service error rate > 1%”

Niemand die je service gebruikt maalt erom of een pod herstart is. Ze willen weten of het ding werkt. Dus alert op wat de gebruiker merkt, en ga pas achter de oorzaak aan als je voor een echte reden wakker gemaakt bent. Een pod die crash-loopt terwijl de service gezond blijft, omdat de andere pods het werk overnemen, is precies de self-healing waar ik het cluster voor heb ingericht. Daar wil ik geen page voor.

2. Alert alleen op wat een hand nodig heeft

Hoeft niemand iets te doen, dan heeft het op niemands pager iets te zoeken. Informatief spul hoort op een dashboard of in een dagelijkse review. Niet ’s nachts op je telefoon.

De test die ik gebruik is één vraag. “Als deze alert binnenkomt, wat doe ik dan?”

  • Antwoord is “onderzoeken en waarschijnlijk niks” → geen alert
  • Antwoord is “deze specifieke stap” → prima, dat is een alert

3. Elke alert heeft een runbook

Als het ding afgaat, wat doe je dan? Kun je dat niet vooraf opschrijven, dan is de alert niet af. Dit is het low-friction-principe voor de versie van mij die half slaapt. Die hoeft niet uit te puzzelen wat de versie van overdag bedoelde.

annotations:
  runbook_url: https://wiki.example.com/runbooks/high-error-rate
  summary: "Error rate overschrijdt SLO"
  description: "Service {{ $labels.service }} error rate is {{ $value }}%"

4. Severity moet iets betekenen

SeverityResponseVoorbeeld
criticalMaak iemand NU wakkerService down, data verlies dreigt
warningHandel af tijdens kantoorurenDisk vult, certificaat verloopt
infoReview in dagelijkse standupOngewoon patroon, niet urgent

De eerste keer dat een critical afgaat waar niemand z’n bed voor uit hoefde, heb je jezelf geleerd dat critical niet critical betekent. Een paar van die en de hele schaal is waardeloos. Dat label bewaak ik dus.

Koppel alerts aan een SLO in plaats van aan een drempel

De alerts die ik het meest vertrouw liggen op een Service Level Objective. De SLO zegt al hoeveel ellende acceptabel is, dus de alert erft die betekenis in plaats van naar een drempel te gokken.

# SLO: 99.9% beschikbaarheid
# Error budget: 0.1% = 43 minuten/maand

groups:
  - name: slo-alerts
    rules:
      # Fast burn: verbruikt error budget snel
      - alert: HighErrorRateFastBurn
        expr: |
          (
            sum(rate(http_requests_total{status=~"5.."}[5m])) /
            sum(rate(http_requests_total[5m]))
          ) > 0.01
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Error rate boven 1% (fast burn)"
          description: "Met dit tempo is maandelijks error budget op in {{ $value | humanizeDuration }}"

      # Slow burn: verbruikt error budget gestaag
      - alert: HighErrorRateSlowBurn
        expr: |
          (
            sum(rate(http_requests_total{status=~"5.."}[1h])) /
            sum(rate(http_requests_total[1h]))
          ) > 0.001
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Error rate verhoogd (slow burn)"

De twee burn rates doen verschillend werk:

  • Fast burn betekent dat je het error budget nu aan het opstoken bent. Maak iemand wakker.
  • Slow burn betekent dat je over een paar uur richting een SLO -breach drijft. Pak het overdag op.

De alerts die ik draai

Hier het concrete spul. Niet de hele catalogus, alleen de categorieën die een plek op m’n pager verdiend hebben.

Beschikbaarheid

# Service is down
- alert: ServiceDown
  expr: up{job="my-service"} == 0
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "Service {{ $labels.job }} is down"
    runbook_url: https://wiki/runbooks/service-down

# Hoge error rate (user impact)
- alert: HighErrorRate
  expr: |
    sum by (service) (rate(http_requests_total{status=~"5.."}[5m])) /
    sum by (service) (rate(http_requests_total[5m])) > 0.05
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "{{ $labels.service }} error rate boven 5%"

Latency

# P99 latency te hoog
- alert: HighLatency
  expr: |
    histogram_quantile(0.99,
      sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))
    ) > 2
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "{{ $labels.service }} P99 latency boven 2s"

Capaciteit

Deze twee zijn voorspellend, en dat is het punt. Ik krijg liever overdag een warning dat een disk morgen vol zit dan om middernacht een critical omdat hij dat al is.

# Disk vult (voorspellend)
- alert: DiskFillingUp
  expr: |
    predict_linear(node_filesystem_avail_bytes{fstype!="tmpfs"}[6h], 24*60*60) < 0
  for: 1h
  labels:
    severity: warning
  annotations:
    summary: "Disk {{ $labels.mountpoint }} vol binnen 24u"

# Certificaat verloopt
- alert: CertificateExpiringSoon
  expr: |
    certmanager_certificate_expiration_timestamp_seconds - time() < 7*24*60*60
  for: 1h
  labels:
    severity: warning
  annotations:
    summary: "Certificaat {{ $labels.name }} verloopt binnen 7 dagen"

Waar ik bewust niet op alert

Deze lijst telt net zo zwaar als die hierboven. Elke regel erin zet een default config graag voor je klaar, en stuk voor stuk is het ruis.

# NIET: Generiek resource gebruik
- alert: HighCPU
  expr: node_cpu_seconds_total > 0.8  # Slecht: geen context

# NIET: Verwacht gedrag
- alert: PodRestart
  expr: increase(kube_pod_container_status_restarts_total[1h]) > 0  # Slecht: restarts zijn normaal

# NIET: Dingen die auto-resolven
- alert: PodPending
  expr: kube_pod_status_phase{phase="Pending"} == 1
  for: 1m  # Slecht: te kort, scheduler handelt het af

Alles routeren via Alertmanager

Bepalen wat een page waard is, is de helft van het werk. De andere helft is de page op de juiste plek op het juiste moment laten landen. Critical gaat naar de pager, warning naar Slack en alleen tijdens werkuren, info naar een log en verder nergens heen.

# alertmanager.yaml
global:
  resolve_timeout: 5m

route:
  receiver: 'default'
  group_by: ['alertname', 'severity']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
    # Critical: page direct
    - match:
        severity: critical
      receiver: 'pagerduty'
      continue: true

    # Warning: Slack tijdens werkuren
    - match:
        severity: warning
      receiver: 'slack-ops'
      active_time_intervals:
        - work_hours

    # Info: alleen loggen
    - match:
        severity: info
      receiver: 'null'

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: '<key>'
        severity: '{{ .CommonLabels.severity }}'

  - name: 'slack-ops'
    slack_configs:
      - api_url: '<webhook>'
        channel: '#ops-alerts'
        title: '{{ .CommonAnnotations.summary }}'
        text: '{{ .CommonAnnotations.description }}'

  - name: 'null'

time_intervals:
  - name: work_hours
    time_intervals:
      - weekdays: ['monday:friday']
        times:
          - start_time: '09:00'
            end_time: '18:00'

Silencing en inhibition

Nog twee manieren om het volume laag te houden. Leg stil wat je al weet, en onderdruk de alerts die alleen een gevolg zijn van een grotere.

Silences voor onderhoud

# Maak silence tijdens onderhoud
amtool silence add alertname=~".+" --duration=2h --comment="Gepland onderhoud"

# Silence specifieke service
amtool silence add service="api" --duration=1h --comment="Deployen v2.3.0"

Inhibition rules

Als één alert een stapel andere impliceert, onderdruk dan de stapel. Ligt het hele cluster plat, dan hoef ik geen veertig losse pages die me elk vertellen dat ook die service erop plat ligt.

inhibit_rules:
  # Als cluster down is, alert niet op individuele services
  - source_match:
      alertname: 'ClusterDown'
    target_match_re:
      alertname: '.+'
    equal: ['cluster']

  # Als node down is, alert niet op pods op die node
  - source_match:
      alertname: 'NodeDown'
    target_match_re:
      alertname: 'Pod.+'
    equal: ['node']

Alert state zichtbaar maken in Grafana

Niet elke alert hoort te pagen, maar je wilt ze wel zien. De alerts die je niet wakker maken leven op een dashboard. Daar gaan info en de net opgeloste alerts heen om gereviewd te worden in plaats van genegeerd.

# Prometheus datasource query
ALERTS{alertstate="firing"}

Maak een overzichtspanel dat toont:

  • Alerts die nu afgaan
  • Recente alertgeschiedenis
  • Alerttrends over tijd

Link van een alert naar het bijbehorende dashboard:

annotations:
  dashboard_url: https://grafana/d/abc123?var-service={{ $labels.service }}

De menselijke kant van on-call

Alle config ter wereld helpt niet als de rotatie eromheen een puinhoop is. Een paar dingen die ik de moeite waard vind om op te schrijven.

Rotatiestructuur

Primary    → Eerste responder, handelt alle pages af
Secondary  → Backup als primary niet reageert in 10 min
Escalation → Management, voor grote incidenten

Response time SLA’s

SeverityAcknowledgeStart Werk
Critical5 minuten15 minuten
Warning4 uur8 uur
InfoVolgende standupWanneer het uitkomt

Post-incident review

Elke critical alert is achteraf een korte blik waard, ook als de fix makkelijk was:

  1. Was de alert noodzakelijk?
  2. Gaf hij genoeg context?
  3. Was de runbook nuttig?
  4. Kon het onderliggende ding voorkomen worden?

Blijft dezelfde alert afgaan en is het antwoord steeds “niks doen”, dan is de alert kapot. Fix hem of gooi hem weg.

Hoe m’n setup eruitziet

# Alleen core SLO alerts
groups:
  - name: slo
    rules:
      # Beschikbaarheid: error rate
      - alert: HighErrorRate
        expr: error_rate > 0.01
        for: 5m
        severity: critical

      # Latency: P99
      - alert: HighLatency
        expr: p99_latency > 2
        for: 10m
        severity: warning

      # Capaciteit: voorspellend
      - alert: DiskFillingUp
        expr: disk_will_fill_24h
        severity: warning

      # Security: cert expiry
      - alert: CertExpiring
        expr: cert_expires_7d
        severity: warning

# Dat is het. ~10 alert regels totaal.

Een stuk of tien regels, en elke ervan verdient z’n plek:

  • SLO-based, zodat elke alert gekoppeld is aan iets wat een gebruiker zou merken.
  • Minimaal, want een regel die ik niet kan verantwoorden gooi ik weg.
  • Duidelijke severity. Critical betekent je bed uit, warning betekent het wacht tot morgen.
  • Runbook op alles, zodat de half slapende ik een volgende stap heeft die vooraf is opgeschreven.

Eruit graven als je al bedolven bent

Lees je dit vanuit een berg lawaaierige alerts, dan fix je het niet door er meer bij te zetten. Je fixt het door te verwijderen. Ongeveer de volgorde die ik zou aanhouden:

  1. Inventariseer wat je hebt. Hoeveel alerts gingen er afgelopen maand af, en bij hoeveel daarvan heeft iemand iets gedaan?
  2. Verwijder de dode. Is een regel in 90 dagen niet afgegaan, dan heb je hem waarschijnlijk niet nodig. Is hij wel afgegaan maar deed niemand iets, dan zeker niet.
  3. Verhoog de drempels die altijd vanzelf oplossen. Heft een alert zichzelf op voor je gereageerd hebt, dan stond hij te strak.
  4. Zet er een for-duur op zodat korte pieken je niet kunnen pagen.
  5. Voeg de bijna-duplicaten samen. Eén “high error rate” is beter dan veertig varianten per endpoint van hetzelfde.

Elke page heeft een mens nodig. Zo niet, dan van de pager af.

Waarom ik me hier druk om maak

Een lawaaierige alerting-setup ziet er gezond uit terwijl hij je betrouwbaarheid wegvreet. De engineer die on-call staat leert dat de pages meestal rommel zijn, gaat ze scannen, en scant op een nacht langs degene die ertoe deed. Dat is waar ik tegen ontwerp, en geen tool lost het op, want de tool deed precies wat hem gezegd was.

Dus houd ik het volume laag genoeg dat elke page nog gelezen wordt, en page ik alleen op dingen die een gebruiker zou merken.

De observability stack met Loki en Tempo produceert veel meer data dan een mens kan bijhouden. Alerting bepaalt welk stukje daarvan iemand mag onderbreken. Laat je te veel door, dan zit je weer waar ik begon, om 3 uur ’s nachts je eigen telefoon te negeren.

De CPU-regel die me wakker maakte ging als eerste weg. Wat over is zijn de tien regels hierboven, en elke ervan heeft een runbook dat ik geschreven heb toen ik wakker was.