Alerting

Alerting #

Monitoring tanpa alerting seperti dashboard yang tidak ada yang melihat — masalah bisa terjadi tanpa ada yang tahu. Alerting adalah lapisan di atas monitoring yang secara proaktif memberitahu tim saat kondisi abnormal terdeteksi. Tapi alerting yang buruk bisa sama berbahayanya dengan tidak ada alerting sama sekali: terlalu banyak notifikasi yang tidak actionable membuat tim mati rasa dan mengabaikan alert yang benar-benar penting. Artikel ini membahas cara mengotomasi setup Alertmanager dengan Ansible, merancang rute notifikasi yang terstruktur, dan membangun pola alert yang bisa ditindaklanjuti tanpa membanjiri tim on-call.

Arsitektur Alerting #

Alur alerting di stack Prometheus terbagi menjadi dua tahap yang jelas. Prometheus bertugas mendeteksi — mengevaluasi alert rules terhadap metrik yang sudah di-scrape dan mengubahnya menjadi status firing atau resolved. Alertmanager bertugas menyalurkan — menerima alert dari Prometheus, lalu memutuskan siapa yang harus tahu, di channel mana, seberapa sering, dan apa yang harus di-suppress. Pemisahan ini penting: jika kita menggabungkan keduanya, kita akan kehilangan semua fitur grouping, deduplication, inhibition, dan silencing yang membuat alerting praktis di skala produksi.

flowchart TD
    P[Prometheus] -->|evaluasi alert rules| P
    P -->|alert firing| AM[Alertmanager]

    AM --> G1{"Grouping<br/>by alertname+env+job"}
    G1 --> G2{"Dedup<br/>cluster-wide"}
    G2 --> R{"Routing tree<br/>matchers"}

    R -->|"severity=critical"| PD["PagerDuty<br/>on-call paging"]
    R -->|"severity=warning"| SL["Slack<br/>#alerts-warning"]
    R -->|"environment=production"| SP["Slack<br/>#alerts-production"]
    R -->|"job=db"| SD["Slack<br/>#team-database"]
    R -->|digest harian| EM["Email<br/>nightly digest"]

    AM -. "inhibit" .-> AM
    AM -. "silence" .-> AM

Tiga fitur inti Alertmanager yang membedakannya dari “kirim notifikasi mentah” adalah grouping (menggabungkan alert yang berkaitan menjadi satu notifikasi), inhibition (menekan alert turunan ketika alert induk sudah firing), and silencing (menyembunyikan alert untuk waktu tertentu, biasanya saat maintenance). Ketiganya akan dibahas mendalam di bagian selanjutnya.


Instalasi dan Konfigurasi Alertmanager #

Role Ansible untuk Alertmanager harus meng-handle tiga hal secara terpisah: download binary, deploy konfigurasi, dan setup systemd service. Pemisahan ini membuat setiap komponen bisa di-redeploy tanpa mengulang yang lain:

# roles/alertmanager/tasks/main.yml
---
- name: Buat user alertmanager
  user:
    name: alertmanager
    system: true
    shell: /usr/sbin/nologin
    home: /var/lib/alertmanager
    create_home: true

- name: Download Alertmanager
  get_url:
    url: >
      https://github.com/prometheus/alertmanager/releases/download/
      v{{ alertmanager_version }}/alertmanager-{{ alertmanager_version }}.linux-amd64.tar.gz      
    dest: /tmp/alertmanager.tar.gz
    checksum: "sha256:{{ alertmanager_checksum }}"

- name: Extract dan install binary
  unarchive:
    src: /tmp/alertmanager.tar.gz
    dest: /tmp/
    remote_src: true

- name: Salin binary ke /usr/local/bin
  copy:
    src: "/tmp/alertmanager-{{ alertmanager_version }}.linux-amd64/alertmanager"
    dest: /usr/local/bin/alertmanager
    owner: root
    group: root
    mode: '0755'
    remote_src: true

- name: Buat direktori konfigurasi dan data
  file:
    path: "{{ item }}"
    state: directory
    owner: alertmanager
    group: alertmanager
    mode: '0750'
  loop:
    - /etc/alertmanager
    - /etc/alertmanager/templates
    - /var/lib/alertmanager

- name: Deploy konfigurasi Alertmanager
  template:
    src: alertmanager.yml.j2
    dest: /etc/alertmanager/alertmanager.yml
    owner: alertmanager
    group: alertmanager
    mode: '0640'
    validate: "alertmanager --config.file=%s --storage.path=/tmp/amtest check-config"
  notify: Restart Alertmanager

- name: Deploy systemd unit file
  template:
    src: alertmanager.service.j2
    dest: /etc/systemd/system/alertmanager.service
  notify:
    - Reload systemd
    - Restart Alertmanager

- name: Jalankan Alertmanager
  systemd:
    name: alertmanager
    state: started
    enabled: true

Perhatikan penggunaan validate di task konfigurasi — Ansible akan menjalankan alertmanager --config.file=%s --storage.path=/tmp/amtest check-config terhadap file yang baru di-render sebelum memindahkannya ke tempat final. Jika sintaks YAML salah atau field tidak valid, Ansible akan gagal sebelum konfigurasi rusak menimpa yang lama. Pola ini wajib untuk semua konfigurasi service yang kritis.


Konfigurasi Routing dan Notifikasi #

Routing tree adalah otak dari Alertmanager. Ia menentukan kepada siapa dan di mana alert tertentu harus sampai. Konfigurasi di bawah ini menunjukkan pola bertingkat yang umum di perusahaan dengan multiple environment dan multiple team:

{# roles/alertmanager/templates/alertmanager.yml.j2 #}
global:
  resolve_timeout: 5m
  slack_api_url: "{{ vault_slack_webhook_url }}"
  pagerduty_url: "https://events.pagerduty.com/v2/enqueue"
  smtp_smarthost: "{{ smtp_host }}:{{ smtp_port }}"
  smtp_from: "alertmanager@{{ mail_domain }}"
  smtp_require_tls: true

templates:
  - /etc/alertmanager/templates/*.tmpl

route:
  # Default receiver jika tidak ada route yang cocok
  receiver: slack-warnings
  group_by: ['alertname', 'environment', 'job']
  group_wait: 30s           # Tunggu 30 detik sebelum kirim notifikasi pertama
  group_interval: 5m        # Kirim update setiap 5 menit jika ada alert baru dalam grup
  repeat_interval: 4h       # Ulangi notifikasi setiap 4 jam jika belum resolved
  routes:
    # Alert critical → PagerDuty (untuk on-call duty)
    - matchers:
        - severity = critical
      receiver: pagerduty-critical
      repeat_interval: 1h    # Ulangi lebih sering untuk critical
      continue: true         # Tetap evaluasi route di bawahnya juga

    # Alert untuk environment production → channel terpisah
    - matchers:
        - environment = production
        - severity =~ "warning|critical"
      receiver: slack-production
      continue: true         # Lanjutkan ke route berikutnya juga

    # Alert database → team database
    - matchers:
        - job =~ "postgresql|mysql|redis"
      receiver: slack-database-team
      continue: true

    # Nightly digest untuk alert informational
    - matchers:
        - severity = info
      receiver: email-digest
      repeat_interval: 24h
      active_time_intervals:
        - business-hours

receivers:
  - name: slack-warnings
    slack_configs:
      - channel: "#alerts-warning"
        send_resolved: true
        title: >
          {{ '{{' }} template "slack.title" . {{ '}}' }}
        text: >
          {{ '{{' }} template "slack.text" . {{ '}}' }}
        actions:
          - type: button
            text: "Acknowledge"
            url: "{{ '{{' }} .CommonAnnotations.ack_url {{ '}}' }}"
          - type: button
            text: "Runbook"
            url: "{{ '{{' }} .CommonAnnotations.runbook_url {{ '}}' }}"

  - name: slack-production
    slack_configs:
      - channel: "#alerts-production"
        send_resolved: true
        icon_emoji: ":fire:"
        mention_users:
          - "{{ vault_oncall_user_id }}"

  - name: pagerduty-critical
    pagerduty_configs:
      - routing_key: "{{ vault_pagerduty_routing_key }}"
        description: >
          {{ '{{' }} template "pagerduty.description" . {{ '}}' }}
        severity: critical
        details:
          environment: "{{ '{{' }} .CommonLabels.environment {{ '}}' }}"
          service: "{{ '{{' }} .CommonLabels.job {{ '}}' }}"
          runbook: "{{ '{{' }} .CommonAnnotations.runbook_url {{ '}}' }}"

  - name: slack-database-team
    slack_configs:
      - channel: "#team-database"
        send_resolved: true
        mention_users:
          - "{{ vault_db_oncall_user_id }}"

  - name: email-digest
    email_configs:
      - to: "{{ alerts_digest_recipients | join(',') }}"
        send_resolved: true
        headers:
          Subject: "[Daily Alert Digest] {{ '{{' }} .CommonLabels.environment {{ '}}' }}"

inhibit_rules:
  # Jika ada alert critical dari host yang sama, suppress alert warning-nya
  - source_matchers:
      - severity = critical
    target_matchers:
      - severity = warning
    equal: ['instance', 'job']

  # Jika ada alert "HostDown", suppress semua alert metrik dari host tersebut
  - source_matchers:
      - alertname = HostDown
    target_matchers:
      - severity =~ "warning|critical"
    equal: ['instance']

  # Jika ada alert "ClusterUnreachable", suppress semua alert per-node
  - source_matchers:
      - alertname = ClusterUnreachable
    target_matchers:
      - alertname =~ "Node.*|Host.*"
    equal: ['cluster']

time_intervals:
  - name: business-hours
    time_intervals:
      - weekdays: ['monday:friday']
        times:
          - start_time: '08:00'
            end_time: '18:00'

Parameter penting yang perlu kita pahami:

  • group_by — label-label yang digunakan untuk menggabungkan alert. Alert dengan alertname, environment, dan job yang sama akan dikirim sebagai satu notifikasi, bukan tiga notifikasi terpisah.
  • group_wait — jeda setelah alert pertama firing sebelum notifikasi pertama dikirim. Berguna untuk menunggu apakah lebih banyak alert akan muncul yang bisa digabung.
  • group_interval — seberapa sering notifikasi tambahan dikirim jika ada alert baru dalam grup yang sama.
  • repeat_interval — seberapa sering notifikasi diulang untuk alert yang masih firing. Set lebih pendek untuk critical (1 jam) dan lebih panjang untuk warning (4-24 jam).
  • continue: true — evaluasi route di bawahnya juga. Tanpa continue, route pertama yang cocok akan menghentikan evaluasi.
  • inhibit_rules — saat ada alert sumber yang firing, alert target akan di-suppress jika label equal cocok.

Sequence Diagram: Alur Alert dari Deteksi ke Notifikasi #

Untuk memahami bagaimana semua komponen bekerja sama, perhatikan sequence di bawah ini. Dimulai dari Prometheus yang mengevaluasi rule, hingga alert muncul di channel yang tepat:

sequenceDiagram
    participant App as Aplikasi
    participant Prom as Prometheus
    participant AM as Alertmanager
    participant Slack as Slack
    participant PD as PagerDuty

    App->>Prom: "Expose /metrics (scrape setiap 15s)"
    Prom->>Prom: "Evaluasi alert rules<br/>(setiap 15s)"
    Note over Prom: "Alert rule:<br/>expr: error_rate > 5%<br/>for: 5m"
    Prom->>Prom: "Status: PENDING (kurang dari 5m)"
    Prom->>Prom: "Status: FIRING (setelah 5m)"
    Prom->>AM: "POST /api/v1/alerts<br/>(firing alert)"
    AM->>AM: "Group by alertname+env+job"
    AM->>AM: Apply inhibition rules
    AM->>AM: Check active silences
    AM->>AM: Apply routing tree
    AM->>AM: "group_wait 30s (tunggu alert lain)"
    AM->>Slack: "POST webhook ke #alerts-warning"
    AM->>PD: POST event ke PagerDuty
    Slack-->>AM: 200 OK
    PD-->>AM: 202 Accepted
    Note over Prom,AM: "Repeat interval 4h<br/>selama alert masih firing"
    Prom->>AM: "Status update (masih firing)"
    AM->>Slack: "Kirim reminder (resolved=false)"

Perhatikan dua hal kritis: (1) for: 5m di alert rule membuat alert hanya firing setelah kondisi bertahan 5 menit — ini mencegah false alarm dari spike sesaat. (2) group_wait di Alertmanager menunggu 30 detik sebelum mengirim, memberi kesempatan alert lain yang berkaitan untuk digabung.


Template Notifikasi yang Informatif #

Alert yang hanya bilang “something went wrong” tidak berguna. Template yang baik menyertakan semua informasi yang dibutuhkan untuk memulai investigasi — host, deskripsi, waktu mulai, dan link ke runbook. Berikut template Slack yang memisahkan informasi ke field-field terstruktur:

{# /etc/alertmanager/templates/slack.tmpl #}
{{ '{{' }} define "slack.title" {{ '}}' }}
[{{ '{{' }} .Status | toUpper {{ '}}' }}{{ '{{' }} if eq .Status "firing" {{ '}}' }}:{{ '{{' }} .Alerts.Firing | len {{ '}}' }}{{ '{{' }} end {{ '}}' }}]
{{ '{{' }} .CommonLabels.alertname {{ '}}' }} — {{ '{{' }} .CommonLabels.environment {{ '}}' }}
{{ '{{' }} end {{ '}}' }}

{{ '{{' }} define "slack.text" {{ '}}' }}
{{ '{{' }} range .Alerts {{ '}}' }}
*Host:* `{{ '{{' }} .Labels.instance {{ '}}' }}`
*Service:* {{ '{{' }} .Labels.job {{ '}}' }}
*Severity:* {{ '{{' }} .Labels.severity | toUpper {{ '}}' }}
*Keterangan:* {{ '{{' }} .Annotations.description {{ '}}' }}
*Dimulai:* {{ '{{' }} .StartsAt.Format "2006-01-02 15:04:05 WIB" {{ '}}' }}
*Runbook:* <{{ '{{' }} .Annotations.runbook_url {{ '}}' }}|Buka Runbook>
{{ '{{' }} if .Annotations.dashboard_url {{ '}}' }}
*Dashboard:* <{{ '{{' }} .Annotations.dashboard_url {{ '}}' }}|Lihat Dashboard>
{{ '{{' }} end {{ '}}' }}
---
{{ '{{' }} end {{ '}}' }}
{{ '{{' }} end {{ '}}' }}

Template PagerDuty harus lebih ringkas karena tampilannya berbeda — fokus pada deskripsi singkat (satu kalimat), severity (paging policy tergantung dari sini), dan payload details yang muncul di incident page:

{# /etc/alertmanager/templates/pagerduty.tmpl #}
{{ '{{' }} define "pagerduty.description" {{ '}}' }}
[{{ '{{' }} .Status | toUpper {{ '}}' }}] {{ '{{' }} .CommonLabels.alertname {{ '}}' }} on {{ '{{' }} .CommonLabels.environment {{ '}}' }}: {{ '{{' }} .CommonAnnotations.summary {{ '}}' }}
{{ '{{' }} end {{ '}}' }}

{{ '{{' }} define "pagerduty.details" {{ '}}' }}
{
  "firing": {{ '{{' }} .Alerts.Firing | len {{ '}}' }},
  "resolved": {{ '{{' }} .Alerts.Resolved | len {{ '}}' }},
  "environment": "{{ '{{' }} .CommonLabels.environment {{ '}}' }}",
  "service": "{{ '{{' }} .CommonLabels.job {{ '}}' }}",
  "instance": "{{ '{{' }} (index .Alerts 0).Labels.instance {{ '}}' }}",
  "runbook_url": "{{ '{{' }} .CommonAnnotations.runbook_url {{ '}}' }}"
}
{{ '{{' }} end {{ '}}' }}
Pisahkan template per channel: Slack butuh Markdown dan interaktif (mention, button), PagerDuty butuh payload JSON yang bisa di-parse automation, email butuh plain text. Hindari satu template besar yang mencoba melayani semuanya — formatting akan saling bentrok.

Mengelola Silences dengan Ansible #

Saat ada maintenance terjadwal, kita tidak ingin alert dari node yang sedang di-restart membanjiri channel. Silence adalah cara resmi untuk memberitahu Alertmanager: “jangan kirim alert yang cocok dengan matcher ini selama periode tertentu”. Ansible bisa mengotomasi lifecycle silence — dibuat sebelum maintenance, di-expire setelahnya:

# playbooks/create-silence.yml
---
- name: Buat silence di Alertmanager selama maintenance
  hosts: localhost
  vars:
    alertmanager_url: "http://alertmanager.internal:9093"
    silence_duration_hours: 4
    silence_comment: "Maintenance terjadwal — {{ ansible_date_time.date }}"

  tasks:
    - name: Hitung waktu berakhir silence
      set_fact:
        silence_end: >-
          {{ (ansible_date_time.epoch | int + silence_duration_hours * 3600) | strftime('%Y-%m-%dT%H:%M:%S.000Z') }}          

    - name: Buat silence di Alertmanager
      uri:
        url: "{{ alertmanager_url }}/api/v2/silences"
        method: POST
        body_format: json
        body:
          matchers:
            - name: environment
              value: "{{ env }}"
              isRegex: false
            - name: job
              value: "{{ silence_job | default('.*') }}"
              isRegex: "{{ silence_job is regex('\\*') or silence_job is regex('\\.') }}"
          startsAt: "{{ ansible_date_time.iso8601 }}"
          endsAt: "{{ silence_end }}"
          comment: "{{ silence_comment }}"
          createdBy: "ansible-automation"
        status_code: 200
      register: silence_result
      no_log: true

    - name: Tampilkan ID silence yang dibuat
      debug:
        msg: "Silence dibuat dengan ID: {{ silence_result.json.silenceID }}"

    - name: Simpan ID silence untuk di-expire nanti
      copy:
        content: "{{ silence_result.json.silenceID }}"
        dest: "/tmp/silence-{{ env }}-{{ silence_job | default('all') }}.id"
        mode: '0600'
      delegate_to: localhost

Playbook untuk menghapus silence setelah maintenance selesai:

# playbooks/expire-silence.yml
---
- name: Expire silence setelah maintenance selesai
  hosts: localhost
  vars:
    alertmanager_url: "http://alertmanager.internal:9093"
    silence_id_file: "/tmp/silence-{{ env }}-{{ silence_job | default('all') }}.id"

  tasks:
    - name: Baca ID silence
      slurp:
        src: "{{ silence_id_file }}"
      register: silence_id_b64
      ignore_errors: true

    - name: Decode ID silence
      set_fact:
        silence_id: "{{ silence_id_b64.content | b64decode | trim }}"
      when: silence_id_b64 is succeeded

    - name: Expire silence via API
      uri:
        url: "{{ alertmanager_url }}/api/v2/silences/{{ silence_id }}"
        method: DELETE
        status_code: 200
      when: silence_id_b64 is succeeded
      ignore_errors: true

    - name: Hapus file ID silence
      file:
        path: "{{ silence_id_file }}"
        state: absent

Untuk integrasi dengan workflow maintenance, panggil playbook create-silence.yml sebelum maintenance dimulai, dan expire-silence.yml setelahnya selesai — baik secara manual atau sebagai bagian dari job Jenkins/GitHub Actions.


Decision Tree: Pilih Severity yang Tepat #

Severity yang salah adalah sumber utama alert fatigue. Aturan praktisnya: critical = butuh respons dalam 15 menit (paging), warning = butuh respons dalam jam kerja (ticket), info = tidak butuh respons langsung (hanya untuk konteks). Decision tree berikut membantu tim memutuskan severity saat membuat alert baru:

flowchart TD
    A["Buat alert baru"] --> B{"Apakah layanan<br/>tidak bisa digunakan<br/>oleh user?"}
    B --|"Ya"| C{"Apakah ini<br/>membutuhkan<br/>respon 24/7?"}
    C --|"Ya"| D["severity: critical<br/>route ke PagerDuty"]
    C --|"Tidak"| E["severity: warning<br/>route ke Slack #alerts"]

    B --|"Tidak"| F{"Apakah ini bisa<br/>menjadi masalah besar<br/>jika dibiarkan?"}
    F --|"Ya"| G{"Ada dampak<br/>performance atau<br/>risiko kehilangan data?"}
    G --|"Ya"| E
    G --|"Tidak"| H["severity: warning<br/>route ke Slack channel tim"]

    F --|"Tidak"| I{"Apakah ini informasi<br/>berguna untuk<br/>investigasi?"}
    I --|"Ya"| J["severity: info<br/>route ke email digest"]
    I --|"Tidak"| K["Hapus alert<br/>tidak perlu"]
Aturan yang sering dilanggar: alert “info” tetap akan dikirim ke channel jika ada route yang cocok. Jika kita membuat alert severity=info tetapi tidak memiliki route yang menerima severity=info, alert tersebut akan jatuh ke default receiver (slack-warnings) dan menjadi noise. Selalu buat receiver khusus untuk info jika kita memang ingin mengirimkan alert info.

Perbandingan Channel Notifikasi #

Tidak semua alert cocok dikirim ke channel yang sama. Tabel ini membandingkan tiga channel utama dan kapan masing-masing paling tepat:

Kanal Latensi Cocok untuk Kelebihan Kekurangan
PagerDuty Detik (paging 24/7) Insiden production yang butuh respons immediate Paging ke on-call, eskalasi otomatis, ack tracking, timeline insiden Mahal per user, harus disiplin pakai — kalau untuk warning akan jadi alarm fatigue parah
Slack Detik (mention) Warning, info, alert tim spesifik Real-time, mudah di-acknowledge via thread, integrasi dengan workflow tim Bisa tenggelam di channel ramai, tidak ada eskalasi jika tidak ada yang lihat
Email Menit sampai jam Digest harian, audit trail, alert non-kritis Tidak mengganggu on-call, bisa di-batch, archival natural Latensi tinggi, tidak real-time, butuh filter inbox yang disiplin

Rekomendasi praktis: PagerDuty hanya untuk severity=critical yang benar-benar paging, Slack untuk warning dan info real-time, email untuk nightly digest alert info yang perlu dilihat saat working hours. Mencampur ketiganya tanpa aturan hanya akan menghasilkan alert yang diabaikan di semua channel.


Pola Mengurangi Alert Fatigue #

Alert fatigue terjadi saat tim menerima terlalu banyak notifikasi yang tidak actionable. Beberapa pola yang terbukti mengurangi noise secara signifikan:

# Alert rules yang baik memiliki:
# 1. Threshold yang bermakna (bukan arbitrer)
# 2. Durasi 'for' yang cukup (menghindari flapping)
# 3. Severity yang tepat
# 4. Annotation yang actionable
# 5. Link ke runbook

# ANTI-PATTERN: alert yang terlalu sensitif
- alert: HighCPU
  expr: cpu_usage > 80
  # Tidak ada 'for' — alert setiap kali ada spike sesaat
  # Threshold 80% terlalu rendah — CPU sering melewati 80% saat burst normal
  # Tidak ada runbook — on-call tidak tahu harus apa

# BENAR: alert dengan durasi, threshold bermakna, dan runbook
- alert: SustainedHighCPU
  expr: >
    avg by(instance) (
      rate(node_cpu_seconds_total{mode!="idle"}[5m])
    ) * 100 > 90    
  for: 15m              # Harus bertahan 15 menit sebelum alert
  labels:
    severity: warning
  annotations:
    summary: "CPU usage tinggi berkelanjutan di {{ '{{' }} $labels.instance {{ '}}' }}"
    description: "CPU {{ '{{' }} $value | printf "%.1f" {{ '}}' }}% selama 15 menit terakhir"
    runbook_url: "https://wiki.company.com/runbooks/high-cpu"
# ANTI-PATTERN: alert yang hanya menyebut masalah tanpa konteks
- alert: DiskFull
  expr: node_filesystem_avail_bytes{mountpoint="/"} < 1000000000
  annotations:
    summary: "Disk hampir penuh"

# BENAR: alert yang menyebut impact dan langkah selanjutnya
- alert: DiskSpaceCritical
  expr: >
    (node_filesystem_avail_bytes{mountpoint="/"}
     / node_filesystem_size_bytes{mountpoint="/"}) < 0.05    
  for: 10m
  labels:
    severity: critical
  annotations:
    summary: "Disk kritis di {{ '{{' }} $labels.instance {{ '}}' }} (sisa {{ '{{' }} $value | humanizePercentage {{ '}}' }})"
    description: >
      Disk hanya tersisa {{ '{{' }} $value | humanizePercentage {{ '}}' }}.
      Service akan mulai gagal dalam 30 menit jika tidak ada tindakan.      
    runbook_url: "https://wiki.company.com/runbooks/disk-full"
    dashboard_url: "https://grafana.company.com/d/disk-usage?var-host={{ '{{' }} $labels.instance {{ '}}' }}"
# ANTI-PATTERN: alert yang duplikat (satu masalah memicu banyak alert)
# Disk usage + inode usage + write latency + service crash — semua firing bersamaan
# saat disk benar-benar penuh

# BENAR: gunakan inhibition untuk suppress alert turunan
# Di alertmanager.yml
inhibit_rules:
  - source_matchers:
      - alertname = DiskSpaceCritical
    target_matchers:
      - alertname =~ "ServiceCrashed|HighLatency|WriteTimeout"
    equal: ['instance']
# Saat DiskSpaceCritical firing, alert turunan dari instance yang sama di-suppress
# On-call hanya melihat SATU alert utama, bukan 5

Tiga prinsip utama dari contoh di atas: (1) threshold harus berdasarkan dampak, bukan angka bulat yang terlihat bagus. (2) setiap alert punya for: duration yang realistis — minimum 5 menit untuk warning, 2-3 menit untuk critical. (3) inhibit_rules membersihkan ledakan alert saat ada masalah induk.


High Availability Alertmanager #

Alertmanager mendukung clustering untuk high availability. Tanpa clustering, jika satu instance Alertmanager mati, alert yang firing tidak akan terkirim sampai instance kembali — ini bisa berakibat fatal. Konfigurasi cluster mode menggunakan gossip protocol:

{# Tambahkan di alertmanager.yml.j2 untuk mode cluster #}
{% if alertmanager_cluster_enabled | default(false) %}
cluster:
  listen-address: ""
  # Cluster peers di-generate dari inventory group alertmanager
  {% for host in groups['alertmanager'] %}
  - {{ hostvars[host]['ansible_default_ipv4']['address'] }}:9094
  {% endfor %}
{% endif %}

Untuk mengotomasi setup cluster Alertmanager dengan Ansible:

# roles/alertmanager/tasks/cluster.yml
---
- name: Buka port cluster Alertmanager
  ufw:
    rule: allow
    port: '9094'
    proto: tcp
  when: ansible_facts['os_family'] == 'Debian'

- name: Deploy konfigurasi dengan cluster enabled
  template:
    src: alertmanager.yml.j2
    dest: /etc/alertmanager/alertmanager.yml
    owner: alertmanager
    group: alertmanager
    mode: '0640'
  vars:
    alertmanager_cluster_enabled: true
  notify: Restart Alertmanager

Alertmanager cluster menggunakan gossip protocol untuk sinkronisasi state — saat satu node menerima silence, semua node akan mengetahuinya. Saat ada alert baru, salah satu node akan mengirim notifikasi (berdasarkan konsensus), sehingga tidak ada duplikat.


Testing Alert Rules dengan Prometheus #

Sebelum deploy ke production, alert rules harus ditest untuk memastikan ekspresi PromQL benar dan threshold masuk akal. promtool menyediakan dua command penting:

# Validasi sintaks dan struktur alert rules
promtool check rules /etc/prometheus/rules/*.yml

# Unit test alert rules dengan skenario PromQL
promtool test rules /etc/prometheus/tests/alerts.test.yml

File test alert rules dalam format YAML mendefinisikan skenario input dan output yang diharapkan:

# tests/alerts.test.yml
rule_files:
  - /etc/prometheus/rules/slo-recording.yml

evaluation_interval: 1m

tests:
  # Test 1: Alert tidak firing saat error rate di bawah threshold
  - interval: 1m
    input_series:
      - series: 'http_requests_total{job="api",status="200"}'
        values: '1000x10'
      - series: 'http_requests_total{job="api",status="500"}'
        values: '5x10'    # 0.5% error rate
    alert_rule_test:
      - eval_time: 10m
        alertname: HighErrorBudgetBurnRate
        exp_alerts: []   # Tidak boleh firing di error rate 0.5%

  # Test 2: Alert firing saat burn rate tinggi
  - interval: 1m
    input_series:
      - series: 'http_requests_total{job="api",status="200"}'
        values: '100x10'
      - series: 'http_requests_total{job="api",status="500"}'
        values: '900x10'  # 90% error rate — disaster
    alert_rule_test:
      - eval_time: 10m
        alertname: HighErrorBudgetBurnRate
        exp_alerts:
          - exp_labels:
              severity: critical
              job: api
            exp_annotations:
              summary: "Error budget terbakar cepat"
              runbook_url: "https://wiki.company.com/runbooks/error-budget-burn"

Jalankan test ini di CI pipeline setiap kali alert rules diubah:

# .github/workflows/alert-tests.yml (konsep)
- name: Test alert rules
  run: |
    docker run --rm -v $(pwd):/etc/prometheus \
      prom/prometheus:latest \
      promtool test rules /etc/prometheus/tests/alerts.test.yml    

Artikel SLO & SLA membahas lebih detail tentang alert berbasis error budget yang sudah kita lihat pada test cases di atas. Untuk konteks yang lebih luas tentang alasan alert tertentu dibuat, lihat juga Monitoring yang membahas alert rules sebagai kode, dan Incident Response yang membahas runbook yang harus ditautkan pada setiap alert.


Ringkasan #

  • Alertmanager menangani routing, grouping, deduplication, inhibition, dan silencing — pisahkan tanggung jawab ini dari Prometheus yang hanya bertugas deteksi.
  • group_wait, group_interval, dan repeat_interval adalah tiga knob utama yang menentukan kapan dan seberapa sering notifikasi dikirim — set sesuai severity dan urgensi.
  • inhibit_rules menekan alert turunan saat alert induk sudah firing — misalnya, suppress warning saat critical dari host yang sama sudah aktif, atau suppress semua alert service saat HostDown aktif.
  • Severity yang tepat adalah critical untuk paging 24/7, warning untuk ticket jam kerja, info untuk digest harian — pencampuran tanpa aturan hanya akan menghasilkan alert yang diabaikan di semua channel.
  • Template notifikasi yang baik menyertakan: host, deskripsi masalah, waktu mulai, link ke runbook, dan link ke dashboard — semua yang dibutuhkan untuk memulai investigasi tanpa harus buka tools lain.
  • Gunakan for: di alert rules — alert tanpa durasi minimum akan fire setiap spike sesaat dan menjadi noise. Minimum realistis: 2-3 menit untuk critical, 5-15 menit untuk warning.
  • Silences via Ansible memungkinkan maintenance terjadwal tanpa banjir notifikasi false alarm — buat silence sebelum mulai maintenance, expire setelah selesai.
  • High availability Alertmanager wajib di production — cluster mode dengan gossip protocol memastikan alert terkirim meski satu node mati.

← Sebelumnya: Monitoring   Berikutnya: Dashboard →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact