Monitoring

Monitoring #

Log memberitahu apa yang sudah terjadi. Monitoring memberitahu apa yang sedang terjadi — dan memperingatkan sebelum masalah menjadi insiden. Mengotomasi setup monitoring dengan Ansible memastikan setiap server langsung termonitor sejak pertama kali di-provision, dengan konfigurasi yang konsisten. Artikel ini membahas cara men-deploy stack monitoring Prometheus + Grafana menggunakan Ansible, dari instalasi agen sampai alert rules yang bisa di-review di pull request.

Perbedaan penting dengan /observability/logging/: logging adalah catatan event-level yang immutable, monitoring adalah stream metrik numerik yang di-aggregate. Keduanya saling melengkapi — log menjawab “kenapa CPU naik?”, monitoring menjawab “CPU naik di host mana?”. Artikel ini fokus ke monitoring; untuk log lihat artikel logging, dan untuk cara mengumpulkannya dari banyak service lihat /observability/metric-collection/.

Anatomi Stack Prometheus #

Prometheus bukan sekadar database time-series — dia adalah ekosistem lengkap dengan komponen yang punya peran spesifik. Memahami arsitektur ini membantu kita melakukan debug masalah dan merancang high availability:

flowchart LR
    A["Node Exporter<br/>:9100"] -->|"scrape HTTP"| P["Prometheus<br/>:9090"]
    B["App Exporter<br/>:9090+"] -->|"scrape HTTP"| P
    C["Pushgateway<br/>:9091"] -->|"push"| P
    P -->|"query PromQL"| AM["Alertmanager<br/>:9093"]
    P -->|"remote_write"| R["Remote Storage<br/>Thanos / Cortex"]
    P -->|"datasource"| G["Grafana<br/>:3000"]
    AM -->|"route"| SL["Slack / PagerDuty / Email"]
    G -->|"dashboard"| U["SRE / Developer"]

    style A stroke:#b45309,stroke-width:2px
    style B stroke:#b45309,stroke-width:2px
    style C stroke:#b45309,stroke-width:2px
    style P stroke:#1d4ed8,stroke-width:2px
    style AM stroke:#be185d,stroke-width:2px
    style G stroke:#15803d,stroke-width:2px
    style U stroke:#7e22ce,stroke-width:2px

Arah aliran data penting: Prometheus menarik (pull/scrape) metrik dari target, bukan target mengirim (push) ke Prometheus. Pengecualian: Pushgateway dipakai untuk batch job yang berjalan pendek dan tidak bisa di-scrape.

Model pull Prometheus memiliki konsekuensi operasional yang perlu kita pahami: jika target di belakang firewall tidak reachable dari Prometheus, metric-nya tidak akan masuk. Untuk target di VPC lain atau di belakang NAT, gunakan federation atau relabel external_labels saat scrape. Untuk microservice di Kubernetes, service discovery lewat annotation prometheus.io/scrape lebih scalable dari static config.

Empat Tipe Metrik Prometheus #

Sebelum menulis alert rules atau dashboard, pahami dulu empat tipe metrik yang Prometheus bisa simpan. Pemahaman ini menentukan fungsi dan storage:

Tipe Karakteristik Contoh Use Case Fungsi di PromQL
Counter Hanya naik, reset ke 0 saat restart HTTP request total, bytes sent rate(), increase()
Gauge Nilai arbitrary, bisa naik/turun CPU usage, memory used, queue depth langsung dipakai, atau avg_over_time()
Histogram Distribusi nilai dalam bucket Request latency, response size histogram_quantile()
Summary Mirip histogram, quantile dihitung client SLA latency tracking langsung quantile

Untuk 90% use case, Counter dan Gauge cukup. Gunakan histogram saat kita membutuhkan p99 latency atau distribusi. Summary jarang digunakan karena quantile dihitung di client — kita kehilangan fleksibilitas agregasi cross-instance.


Instalasi Node Exporter #

Node Exporter adalah agen yang mengekspos metrik sistem (CPU, memory, disk, network) dalam format Prometheus. Wajib di-install di semua server production, idealnya sebagai bagian dari common role sehingga setiap server baru otomatis termonitor:

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

- name: Download Node Exporter
  get_url:
    url: >
      https://github.com/prometheus/node_exporter/releases/download/
      v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz      
    dest: /tmp/node_exporter.tar.gz
    checksum: "sha256:{{ node_exporter_checksum }}"

- name: Extract Node Exporter
  unarchive:
    src: /tmp/node_exporter.tar.gz
    dest: /tmp/
    remote_src: true

- name: Instal binary Node Exporter
  copy:
    src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
    dest: /usr/local/bin/node_exporter
    owner: root
    group: root
    mode: '0755'
    remote_src: true

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

- name: Jalankan Node Exporter
  systemd:
    name: node_exporter
    state: started
    enabled: true
{# roles/node-exporter/templates/node_exporter.service.j2 #}
[Unit]
Description=Prometheus Node Exporter
Documentation=https://prometheus.io/docs/guides/node-exporter/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=node_exporter
Group=node_exporter
ExecStart=/usr/local/bin/node_exporter \
  --web.listen-address=:9100 \
  --web.listen-address=:9100 \
  --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/.+)($$|/) \
  --collector.netclass.ignored-devices=^(veth.*|docker.*|br-.*) \
  --collector.netdev.device-exclude=^(veth.*|docker.*|br-.*) \
  --no-collector.wifi
Restart=always
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Perhatikan flag --collector.netclass.ignored-devices=^(veth.*|docker.*|br-.*) — tanpa flag ini, metric node_network_* di server Docker akan di-bombardir metrik dari interface virtual container, menyulitkan monitoring interface fisik. Pilih collector yang kita butuhkan; default Node Exporter mengaktifkan semua collector, beberapa di antaranya noisy atau tidak relevan.


Konfigurasi Prometheus dengan Inventory-Driven Targets #

Prometheus scrape target harus bisa berubah seiring waktu — server baru ditambah, server lama di-decommission, service di-scale. Template Jinja2 yang membaca inventory otomatis menyesuaikan diri — tidak perlu diedit secara manual saat deployment:

# roles/prometheus/tasks/main.yml
---
- name: Deploy konfigurasi Prometheus
  template:
    src: prometheus.yml.j2
    dest: /etc/prometheus/prometheus.yml
    owner: prometheus
    group: prometheus
    mode: '0644'
    validate: promtool check config %s
  notify: Reload Prometheus
{# roles/prometheus/templates/prometheus.yml.j2 #}
global:
  scrape_interval: {{ prometheus_scrape_interval | default('15s') }}
  evaluation_interval: {{ prometheus_evaluation_interval | default('15s') }}
  external_labels:
    cluster: {{ cluster_name }}
    environment: {{ env }}
  query_log_file: /var/log/prometheus/queries.log

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093

rule_files:
  - /etc/prometheus/rules/*.yml

scrape_configs:
  # Prometheus scraping itself
  - job_name: prometheus
    static_configs:
      - targets: ['localhost:9090']

  # Node Exporter dari semua server di inventory
  - job_name: node_exporter
    static_configs:
{% for host in groups['all'] %}
      - targets: ['{{ hostvars[host]['ansible_default_ipv4']['address'] }}:9100']
        labels:
          hostname: {{ host }}
          environment: {{ env }}
          os: {{ hostvars[host].get('ansible_os_family', 'unknown') | lower }}
{% endfor %}
    relabel_configs:
      - source_labels: [__address__]
        target_label: __address__
        replacement: '${1}:9100'

  # Application metrics jika tersedia
  - job_name: {{ app_name }}
    static_configs:
{% for host in groups['appservers'] %}
      - targets: ['{{ hostvars[host]['ansible_default_ipv4']['address'] }}:{{ app_metrics_port | default(9090) }}']
        labels:
          hostname: {{ host }}
          tier: application
{% endfor %}
    metrics_path: /metrics

Parameter validate: promtool check config %s wajib — ini menjalankan promtool di remote host untuk validasi syntax dan struktur config sebelum file di-overwrite. Tanpa validasi, konfigurasi salah akan membuat Prometheus gagal reload dan kita akan kehilangan monitoring tanpa mengetahui penyebabnya.

Pair Anti-Pattern: Hardcoded Target vs Inventory-Driven #

{# ANTI-PATTERN: target di-hardcode di file konfigurasi, harus diedit secara manual saat server ditambahkan #}
{# roles/prometheus/templates/prometheus.yml.j2 #}
scrape_configs:
  - job_name: node_exporter
    static_configs:
      - targets:
          - '10.0.1.10:9100'
          - '10.0.1.11:9100'
          - '10.0.1.12:9100'
        labels:
          environment: production
# Masalah: setiap server baru harus dimasukkan ke file ini secara manual, lalu di-commit,
# lalu jalankan playbook. Saat auto-scaling (5 server sekaligus),
# target akan tertinggal dan server baru tidak termonitor.

{# BENAR: baca dari inventory Ansible secara dinamis #}
{# roles/prometheus/templates/prometheus.yml.j2 #}
scrape_configs:
  - job_name: node_exporter
    static_configs:
{% for host in groups['all'] %}
      - targets: ['{{ hostvars[host]['ansible_default_ipv4']['address'] }}:9100']
        labels:
          hostname: {{ host }}
          environment: {{ env }}
{% endfor %}
# Hasil: jalankan playbook di server baru → otomatis masuk ke target list.
# Saat auto-scaling, jalankan ulang playbook setelah instance ready.
# Tidak perlu edit file atau commit.

Alert Rules sebagai Kode #

Alert rules mendefinisikan kondisi yang memicu notifikasi. Menyimpannya sebagai file yang di-manage Ansible memastikan alert yang sama berlaku di semua environment dan bisa di-review lewat pull request sebelum di-deploy ke production:

# roles/prometheus/tasks/alert-rules.yml
---
- name: Buat direktori rules
  file:
    path: /etc/prometheus/rules
    state: directory
    owner: prometheus
    mode: '0755'

- name: Deploy alert rules
  template:
    src: "{{ item }}"
    dest: "/etc/prometheus/rules/{{ item | basename | replace('.j2', '') }}"
    owner: prometheus
    mode: '0644'
  with_fileglob:
    - "templates/rules/*.j2"
  notify: Reload Prometheus

- name: Validasi syntax rules
  command: promtool check rules /etc/prometheus/rules/*.yml
  register: rules_check
  changed_when: false
  failed_when: rules_check.rc != 0
# roles/prometheus/templates/rules/system.yml.j2
---
groups:
  - name: system
    rules:
      - alert: HighCPUUsage
        expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > {{ alert_cpu_threshold | default(85) }}
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU usage tinggi di {{ '{{' }} $labels.instance {{ '}}' }}"
          description: "CPU usage {{ '{{' }} $value | humanizePercentage {{ '}}' }} selama 5 menit terakhir"
          runbook_url: "https://runbooks.example.com/cpu-high"

      - alert: LowDiskSpace
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) < {{ alert_disk_threshold | default(0.15) }}
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disk hampir penuh di {{ '{{' }} $labels.instance {{ '}}' }}"
          description: "Disk tersisa {{ '{{' }} $value | humanizePercentage {{ '}}' }} (mountpoint /)"

      - alert: HostDown
        expr: up == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Host tidak responsif: {{ '{{' }} $labels.instance {{ '}}' }}"
          description: "Prometheus tidak bisa scrape {{ '{{' }} $labels.instance {{ '}}' }} selama 2 menit"

      - alert: HighMemoryUsage
        expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > {{ alert_memory_threshold | default(0.90) }}
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Memory usage tinggi di {{ '{{' }} $labels.instance {{ '}}' }}"
          description: "Memory usage {{ '{{' }} $value | humanizePercentage {{ '}}' }} (threshold 90%)"

Empat alert di atas adalah baseline yang harus ada di semua environment. Tambahkan alert spesifik aplikasi sesuai kebutuhan. Penting: setiap alert yang kita buat harus memiliki runbook URL di annotation — saat alert bunyi jam 3 pagi, tim on-call harus bisa langsung mendapatkan instruksi mitigasi tanpa harus scroll Slack history.

Severity dan Routing #

Tidak semua alert sama pentingnya. Pakai label severity untuk routing di Alertmanager — critical ke PagerDuty, warning ke channel Slack, info ke email mingguan:

# roles/alertmanager/tasks/main.yml — routing config
- name: Deploy konfigurasi Alertmanager
  template:
    src: alertmanager.yml.j2
    dest: /etc/alertmanager/alertmanager.yml
    validate: amtool check-config %s
  notify: Restart Alertmanager
# roles/alertmanager/templates/alertmanager.yml.j2
route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'default'
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
      group_wait: 10s
      repeat_interval: 1h
    - match:
        severity: warning
      receiver: 'slack-warn'
      group_wait: 1m
      repeat_interval: 4h
    - match_re:
        severity: ^(info|debug)$
      receiver: 'null'

receivers:
  - name: 'default'
    slack_configs:
      - channel: '#alerts-general'
        send_resolved: true

  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: "{{ vault_pagerduty_service_key }}"
        send_resolved: true
        description: '{{ .CommonAnnotations.summary }}'

  - name: 'slack-warn'
    slack_configs:
      - channel: '#alerts-warning'
        send_resolved: true
        title_link: 'https://grafana.example.com/alerting/grafana/{{ .GroupLabels.alertname }}'

  - name: 'null'
Hindari alert spam. Alert yang firing setiap 5 menit membuat orang mengabaikan SEMUA alert, termasuk yang penting. Aturan praktis: setiap alert harus actionable (ada yang bisa dilakukan), rare (tidak setiap hari firing), dan specific (jelas apa yang salah). Jika alert kita mengalami firing setiap jam dan tidak ada tindakan yang diambil, threshold-nya salah atau alert-nya tidak relevan — disable atau naikkan threshold.

Setup Grafana dengan Provisioning Otomatis #

Grafana yang di-setup manual lewat UI click-click akan mengalami drift dalam hitungan minggu: orang A menambah datasource, orang B mengubah dashboard, dan tidak ada yang mengingat konfigurasi yang sebenarnya. Provisioning via file YAML memastikan datasource dan dashboard selalu sama di semua environment:

# roles/grafana/tasks/main.yml
---
- name: Install Grafana
  apt:
    name: "grafana={{ grafana_version }}"
    state: present

- name: Deploy konfigurasi Grafana
  template:
    src: grafana.ini.j2
    dest: /etc/grafana/grafana.ini
    owner: root
    group: grafana
    mode: '0640'
  notify: Restart Grafana

- name: Provisioning datasource Prometheus
  template:
    src: datasource-prometheus.yml.j2
    dest: /etc/grafana/provisioning/datasources/prometheus.yml
    owner: root
    group: grafana
    mode: '0640'
  notify: Restart Grafana

- name: Provisioning dashboard otomatis
  copy:
    src: "files/dashboards/{{ item }}"
    dest: "/etc/grafana/provisioning/dashboards/{{ item }}"
    owner: root
    group: grafana
    mode: '0640'
  with_fileglob:
    - "*.json"
  notify: Restart Grafana
{# templates/datasource-prometheus.yml.j2 #}
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://{{ prometheus_host }}:9090
    isDefault: true
    editable: false
    jsonData:
      timeInterval: 15s
      httpMethod: POST
      manageAlerts: true
      prometheusType: Prometheus
# templates/dashboards/dashboards.yml — provider config
apiVersion: 1

providers:
  - name: 'default'
    orgId: 1
    folder: 'Production'
    type: file
    disableDeletion: true
    updateIntervalSeconds: 30
    allowUiUpdates: false
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: true

disableDeletion: true dan allowUiUpdates: false mencegah perubahan lewat UI — semua perubahan harus lewat Ansible. Untuk dev/staging environment, set allowUiUpdates: true supaya developer bisa bereksperimen.


State Monitoring Setup yang Sehat #

Monitoring yang sudah deployed harus terus dipantau kondisinya sendiri. Jika monitoring mengalami down saat insiden terjadi, kita menjadi buta. Pakai meta-monitoring untuk memastikan monitoring-nya sendiri hidup:

stateDiagram-v2
    [*] --> Setup: "Ansible deploy"
    Setup --> Scrape: "Prometheus scrape targets"
    Scrape --> Healthy: "semua target up"
    Scrape --> Degraded: "1-2 target down"
    Scrape --> Broken: ">50% target down"
    Healthy --> Scrape: "scrape interval"
    Degraded --> Alerting: "Alertmanager notify"
    Alerting --> Scrape: "target recovered"
    Broken --> [*]: "monitoring blind"
    Healthy --> [*]: "planned maintenance"

Alert untuk monitoring-down itu sendiri:

# roles/prometheus/templates/rules/monitoring-self.yml.j2
---
groups:
  - name: monitoring-health
    rules:
      - alert: PrometheusTargetDown
        expr: up == 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Prometheus tidak bisa scrape {{ '{{' }} $labels.instance {{ '}}' }}"
          description: "Job {{ '{{' }} $labels.job {{ '}}' }} target {{ '{{' }} $labels.instance {{ '}}' }} down 5 menit"

      - alert: PrometheusDown
        expr: absent(up{job="prometheus"})
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Prometheus sendiri tidak termonitor"
          description: "Metric `up` Prometheus tidak ada — kemungkinan Prometheus crash"

      - alert: AlertmanagerDown
        expr: absent(alertmanager_alerts)
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Alertmanager tidak bisa dihubungi Prometheus"

      - alert: TooManyTargetsDown
        expr: 100 * (sum by(job) (up == 0) / count by(job) (up)) > 50
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: ">50% target {{ '{{' }} $labels.job {{ '}}' }} down"
          description: "Monitoring blind untuk job ini"
Alert PrometheusDown adalah critical tanpa route ke receiver tertentu — kita bisa melakukannya dengan receiver: pagerduty di Alertmanager. Namun hati-hati: jika Prometheus mati, Prometheus tidak bisa mengirim alert. Solusinya: jalankan Alertmanager secara terpisah di host yang berbeda, dan monitor Alertmanager dengan health check dari sistem eksternal (bisa juga ping dari host yang berbeda, atau pakai uptime monitoring SaaS).

Relabel Config: Pembersihan Label Otomatis #

Metric Prometheus datang dengan banyak label default yang tidak selalu kita butuhkan. relabel_configs membersihkan dan menstandarkan label sebelum disimpan ke TSDB. Contoh paling berguna: drop label yang tidak perlu dan tambahkan environment:

# Tambahkan di scrape_configs job tertentu
scrape_configs:
  - job_name: node_exporter
    static_configs:
      - targets: ['host1:9100', 'host2:9100']
    relabel_configs:
      # Drop instance label asli, ganti dengan hostname
      - source_labels: [__address__]
        regex: '([^:]+)(:\d+)?'
        target_label: hostname
        replacement: '${1}'

      # Drop label kosong
      - action: labeldrop
        regex: '__meta_kubernetes_pod_label_(?!app|name).*'

      # Hanya scrape target yang punya label env
      - source_labels: [__meta_ec2_tag_Environment]
        regex: 'production'
        action: keep

action: keep sangat berguna saat scrape target campuran dan kita hanya menginginkan sebagian. Contoh: di shared cluster dengan staging dan production, keep hanya yang punya tag Environment=production.


Decision Tree: Prometheus atau Alternatifnya? #

Pemilihan stack monitoring menentukan banyak hal downstream. Decision tree ini untuk orientasi awal:

flowchart TD
    A["Butuh monitoring<br/>untuk?"] -->|"Kubernetes-native"| K["Prometheus +<br/>Grafana + Loki"]
    A -->|"VM / bare metal klasik"| B{"Volume metrik<br/>per detik?"}

    B -->|"< 1M"| E["Prometheus<br/>self-hosted"]
    B -->|"1-10M"| F["Prometheus +<br/>Thanos / Mimir"]
    B -->|"> 10M"| G["Hosted solution<br/>Datadog / Grafana Cloud"]

    A -->|"Cloud-native AWS"| H["CloudWatch +<br/>managed Prometheus"]
    A -->|"Compliance ketat"| I["Vendor dengan<br/>SLA & support"]

    K --> J["scrape config<br/>via annotations"]
    E --> L["static atau file_sd"]
    F --> M["object storage<br/>S3 / GCS"]

    style A stroke:#b45309,stroke-width:2px
    style E stroke:#15803d,stroke-width:2px
    style F stroke:#15803d,stroke-width:2px
    style G stroke:#be185d,stroke-width:2px
Kriteria Prometheus Self-Hosted Hosted (Datadog/Cloud)
Biaya per host/bulan Infrastruktur saja (~$5-20) $15-30 per host
Custom metric Gratis, unlimited Berbayar per metric
Konfigurasi YAML di Git (as code) UI / API, drift risk
Operasi Tim perlu expertise Prometheus Vendor handle scaling
Lock-in Rendah (format terbuka) Tinggi
Best for Tim DevOps/SRE yang sudah pakai K8s Tim kecil tanpa dedicated ops

Pair Anti-Pattern: Reload Tanpa Validasi vs Dengan Validasi #

# ANTI-PATTERN: copy file config langsung tanpa validasi
- name: Deploy konfigurasi Prometheus
  copy:
    src: prometheus.yml
    dest: /etc/prometheus/prometheus.yml
  notify: Reload Prometheus
# Masalah: jika terdapat typo atau kesalahan struktur, Prometheus akan gagal melakukan reload.
# Monitoring down tanpa bunyi alert — karena monitoring-nya yang down.
# Kita baru menyadarinya saat ada yang melaporkan "dashboard Grafana kosong" beberapa waktu kemudian.

# BENAR: validasi syntax sebelum file di-overwrite
- name: Deploy konfigurasi Prometheus
  template:
    src: prometheus.yml.j2
    dest: /etc/prometheus/prometheus.yml
    validate: promtool check config %s   # ✓ promtool jalankan di remote, return 0 jika OK
  notify: Reload Prometheus
# Hasil: kalau config salah, task fail sebelum file ditulis. Prometheus
# tetap pakai config lama yang valid. Kita mendapatkan pesan error yang jelas
# di Ansible output: baris mana, field mana yang salah.

promtool adalah CLI resmi Prometheus untuk validasi config. Install sebagai dependency role:

# roles/prometheus/tasks/dependencies.yml
- name: Download promtool
  get_url:
    url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz"
    dest: /tmp/prometheus.tar.gz

- name: Install promtool binary
  unarchive:
    src: /tmp/prometheus.tar.gz
    dest: /usr/local/bin/
    include: ['promtool']
    extra_opts: ['--strip-components=1']
    remote_src: true
    creates: /usr/local/bin/promtool

Pair Anti-Pattern: Threshold Sama untuk Semua Host vs Threshold Berbasis Role #

# ANTI-PATTERN: threshold tunggal untuk semua server
- alert: HighCPUUsage
  expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
  for: 5m
# Masalah: database server yang memang CPU-intensive untuk query legitimate
# akan memicu alert setiap 5 menit. Alert menjadi noise dan cenderung diabaikan.

# BENAR: threshold berbeda per role
- alert: HighCPUUsageDatabase
  expr: |
    100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90    
  for: 10m
  labels:
    role: database
  annotations:
    summary: "DB server CPU tinggi: {{ '{{' }} $labels.instance {{ '}}' }}"

- alert: HighCPUUsageApp
  expr: |
    100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 75    
  for: 5m
  labels:
    role: application
  annotations:
    summary: "App server CPU tinggi: {{ '{{' }} $labels.instance {{ '}}' }}"
# Hasil: alert specific ke role, threshold realistis untuk workload
# masing-masing. Alert untuk DB firing di 90% (sangat tinggi), untuk
# app di 75% (lebih ketat karena app biasanya tidak CPU-bound).

Pendekatan yang lebih scalable: pisahkan alert definition ke file terpisah per role (rules/database.yml, rules/application.yml, rules/worker.yml) dan load via rule_files glob di config utama. Threshold bisa dibedakan, dan saat add service baru cukup tambah file.


Menghubungkan Monitoring dengan Logging #

Monitoring memberitahu “CPU tinggi di host X”. Logging memberitahu “kenapa CPU tinggi di host X”. Keduanya paling powerful saat digabung di dashboard yang sama. /observability/logging/ sudah menjelaskan setup Loki; di sini kita akan melihat cara menyatukan label:

# roles/prometheus/tasks/integration.yml
---
- name: Pastikan label Prometheus konsisten dengan Loki
  lineinfile:
    path: /etc/prometheus/prometheus.yml
    regexp: '^    environment:'
    line: "    environment: {{ env }}"
    insertbefore: '^scrape_configs:'
  notify: Reload Prometheus

Grafana bisa menampilkan panel log (Loki) dan panel metric (Prometheus) dalam satu dashboard. Cukup klik titik anomali pada grafik CPU untuk langsung melihat log error di host yang sama dengan filter otomatis.


Ringkasan #

  • Node Exporter di setiap managed node mengekspos metrik sistem — instal ini di semua server sebagai bagian dari role common, jangan sebagai langkah terpisah.
  • Template konfigurasi Prometheus menggunakan hostvars dan groups untuk otomatis menambahkan semua server di inventory sebagai scrape target — tidak perlu diedit secara manual saat server baru ditambahkan.
  • validate: promtool check config %s untuk memvalidasi konfigurasi Prometheus sebelum disimpan — konfigurasi yang salah bisa membuat Prometheus gagal reload tanpa bunyi alert.
  • Alert rules sebagai file template yang di-manage Ansible adalah monitoring as code — review di Git, konsisten di semua environment, dan bisa di-rollback via git revert.
  • Grafana provisioning via file YAML memungkinkan datasource dan dashboard dikonfigurasi otomatis saat Grafana di-restart — tidak perlu klik manual di UI yang rentan drift.
  • Pin versi Prometheus, Node Exporter, dan Grafana di defaults/main.yml — update minor yang tidak terkontrol bisa membreak dashboard atau mengubah format metrik.
  • Alert harus punya runbook_url di annotation — saat alert bunyi, orang on-call butuh link langsung ke instruksi mitigasi, bukan harus scroll Slack history.
  • Monitor monitoring itu sendiri: alert PrometheusDown, AlertmanagerDown, dan TooManyTargetsDown memastikan kita mengetahui saat terjadi blind spot.

← Sebelumnya: Logging   Berikutnya: Alerting →

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