Een tijdlang werkte mijn alerting prima. Een handvol regels, pages waren zeldzaam, en als er een binnenkwam betekende het iets. Toen groeide de cluster, schroefde ik de Prometheus Operator defaults erop, en veranderde “prima” stilletjes in ruis.

Het kantelpunt was een page om 3 uur ’s nachts. Mijn telefoon trilde, ik checkte slaperig: “High CPU usage on node-worker-3.” Ik keek naar de grafiek, zag dat hij al tien minuten op 75% stond, en ging weer slapen. Volgende nacht, dezelfde alert. Een week later checkte ik niet eens meer.

Dat is alert fatigue, en het is het gevaarlijke soort kapot, want het ziet eruit alsof het werkt. De pages blijven komen, de dashboards blijven knipperen, en jij blijft ze negeren. Als alles paged, verdrinken echte incidenten in de ruis en merkt niemand het tot een gebruiker het doet.

Dus welke alerts verdienen het echt om iemand om 3 uur ’s nachts wakker te bellen? Dat is de enige vraag die hier de moeite waard is, en hij heeft weinig met de tooling te maken. Ik heb systemen aan beide kanten gedraaid: verdrinken in pages, en systemen waar een page zeldzaam genoeg is dat ik hem op het moment dat hij vuurt meteen vertrouw. Het gat daartussen is hoe je bepaalt wat iemand mag onderbreken.

Dit haakt direct in op een van de waarden waar ik op deze blog steeds op terugkom: begrip boven vertrouwen. Een alert waarvan je de betekenis niet snapt is een black box die op je pager geschroefd zit. Je kunt er niet over redeneren, dus ga je hem negeren.

Waarom default alerts het erger maken

De meeste monitoring setups arriveren met alerting regels al ingeladen. De Prometheus Operator shipt er honderden. Ze vuren op alles:

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

Goede bedoelingen, verschrikkelijk resultaat. Vier dingen gaan mis:

  1. Geen context. Is 80% CPU slecht? Hangt volledig af van de workload. Een batch job die een core volledig benut doet gewoon zijn werk.
  2. Geen impact. Raakt dit nu een gebruiker? De alert heeft geen idee, en jij om 3 uur ’s nachts ook niet.
  3. Geen actie. Wat word ik geacht hieraan te doen? Meestal niets.
  4. Te gevoelig. Een korte spike vuurt een alert die zichzelf oplost voor je je telefoon hebt ontgrendeld.

Je houdt pages over die geen mens nodig hebben, en mensen die hebben geleerd pages niet meer te vertrouwen. Beide helften zijn slecht, en ze voeden elkaar.

Hoe ik bepaal wat me mag pagen

De fix is geen betere tool. Het zijn een paar regels over wat een alert mag zijn.

1. Alert op symptomen, niet oorzaken

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

Niemand die je service gebruikt geeft erom of een pod herstartte. Ze geven erom of het ding werkt. Dus alert op wat de gebruiker ervaart, en ga de oorzaak pas najagen als je voor een echte reden bent gewekt. Een pod die crash-loopt terwijl de service gezond blijft (omdat de andere het werk overnemen) is precies het soort self-healing dat ik wil, en precies het soort waarvoor ik geen page wil.

2. Alert alleen op wat een hand nodig heeft

Als niemand iets hoeft te doen, heeft het niks te zoeken op iemands pager. 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 ik deze alert krijg, wat doe ik dan echt?”

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

3. Elke alert heeft een runbook nodig

Als het ding vuurt, wat doe je eraan? Als je dat niet vooraf kunt opschrijven, is de alert niet af. Dit is het low-friction principe toegepast op 3 uur ’s nachts: half-wakkere toekomstige-ik zou niet moeten hoeven reverse-engineeren wat vroegere-ik 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

Op het moment dat een critical alert vuurt die geen directe actie nodig had, heb je jezelf aangeleerd dat critical niet echt critical betekent. Doe dat een paar keer en de hele schaal is waardeloos. Bewaak hem.

Koppel alerts aan SLO’s, niet aan losse getallen

De alerts die ik het meest vertrouw mappen direct op een Service Level Objective. De SLO codeert al “hoeveel slechtheid acceptabel is”, dus de alert erft die betekenis in plaats van naar een threshold 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: je verbrandt het error budget nu, maak iemand wakker.
  • Slow burn: je drijft over uren naar een SLO breach, pak het op tijdens werktijd.

Een paar alerts die ik echt draai

Hier het concrete spul. Geen uitputtende catalogus, gewoon de categorieën die een plek op mijn pager hebben verdiend.

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 waarschuwing dat een disk morgen vol is dan een critical page om middernacht als 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 hier is iets wat een default config graag voor je opzet, 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 zorgen dat de page op de juiste plek op het juiste moment landt. Critical gaat naar de pager, warning naar Slack maar alleen tijdens werkuren, info nergens heen behalve een log.

# 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 gereedschappen om de ruis laag te houden: stilleg wat je al weet, en onderdruk alerts die slechts symptomen 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 de ene alert overduidelijk een stapel andere impliceert, onderdruk die stapel. Als de hele cluster down is, heb ik geen veertig losse pages nodig die me elk vertellen dat ook die service erop down is.

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 nog steeds zien. De alerts die je niet wakker maken leven op een dashboard, en daar gaan de info-alerts en de net opgeloste-maar-recente alerts heen om gereviewd te worden in plaats van genegeerd.

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

Maak een alerts overzichtspanel die toont:

  • Momenteel vurende alerts
  • Recente alert geschiedenis
  • Alert trends over tijd

Link van alerts naar relevante dashboards:

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 expliciet te maken.

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?

Als dezelfde alert blijft vuren en het antwoord steeds “niets doen” is, dan is dat geen robuust systeem dat je over een terugkerend probleem vertelt. Dat is een kapotte alert. Fix hem of verwijder hem.

Hoe mijn setup er eigenlijk uitziet

# 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.

Rond de tien regels, en elke verdient zijn plek:

  • SLO-based, zodat elke alert gekoppeld is aan iets wat een gebruiker echt zou voelen.
  • Minimaal, want een regel die ik niet kan verantwoorden gooi ik weg.
  • Duidelijke severity, waar critical echt betekent kom je bed uit en warning echt betekent het kan tot morgen wachten.
  • Runbook op alles, zodat half-slapende ik een volgende stap heeft in plaats van een raadsel.

Eruit graven als je al bedolven bent

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

  1. Inventariseer wat je hebt. Hoeveel alerts vuurden afgelopen maand, en hoeveel daarvan leidden ertoe dat iemand iets deed?
  2. Verwijder de dode. Als een regel in 90 dagen niet vuurde, heb je hem waarschijnlijk niet nodig, en als hij wel vuurde maar niemand handelde, dan zeker niet.
  3. Verhoog de thresholds die altijd auto-resolven. Als een alert zichzelf opheft voor je reageert, stond hij te strak.
  4. Voeg een for duration toe zodat korte spikes je niet kunnen pagen.
  5. Merge de bijna-duplicaten. Eén “high error rate” verslaat veertig per-endpoint varianten van hetzelfde.

De lat is simpel: elke page moet een mens nodig hebben. Alles wat dat niet doet, haal het van de pager.

Waarom ik me hier druk om maak

Een lawaaierige alerting setup ziet er gezond uit en laat ondertussen je betrouwbaarheid wegrotten. De engineer die on-call is leert dat de pages grotendeels rommel zijn, gaat ze scannen, en scant op een nacht langs degene die ertoe deed. Dat is de failure mode, en het is een mensenprobleem vermomd als een toolingprobleem.

Dus mik ik op alerting die respectvol is met iemands aandacht, accuraat over echte impact, en klein genoeg in volume dat elke page nog gelezen wordt. Dat is het hele spel.

Je observability stack met Loki en Tempo produceert een brandslang aan data. Alerting is de kraan die een sliver van die brandslang omzet in menselijke aandacht. Open hem te ver en je zit weer om 3 uur ’s nachts je eigen telefoon te negeren.


De beste alert is degene die je nooit hoeft te sturen, omdat het systeem zichzelf healde. De op een na beste vertelt je precies wat er mis is en precies wat je eraan moet doen.