Best Practice

Best Practice #

Semua artikel di section ini membahas cara membangun komponen observability — logging, monitoring, alerting, tracing, health check, SLO/SLA, sampai incident response. Artikel ini membahas cara membuatnya bekerja dengan baik di dunia nyata. Observability yang efektif bukan sekadar kumpulan tool yang terinstal — ia adalah praktik yang membuat tim bisa memahami sistem, merespons insiden lebih cepat, dan membuat keputusan berbasis data. Berikut delapan prinsip yang membedakan setup observability yang mature dari yang sekedar “jalan”, plus daftar anti-pattern yang paling sering ditemukan dan checklist review yang bisa langsung kita pakai.

stateDiagram-v2
    [*] --> Reactive
    Reactive: "Alert dari komplain user"
    Proactive: "Alert muncul sebelum user terpengaruh"
    Predictive: "Alert berdasarkan tren dan anomali"
    Reactive --> Proactive: "Tambah monitoring + SLO"
    Proactive --> Predictive: "Tambah ML anomaly detection"
    Predictive --> [*]: "Continuous improvement"

Lifecycle di atas menunjukkan tiga level kematangan yang umum ditemui: reactive (baru tahu ada masalah saat user komplain), proactive (alert muncul sebelum user terpengaruh), dan predictive (bisa antisipasi masalah dari tren). Best practice yang dibahas di artikel ini membantu kita bergerak minimal ke level proaktif.

1. Observability Harus Otomatis, Bukan Opsional #

Server baru harus langsung termonitor, log-nya langsung terkirim ke backend terpusat, dan health check-nya langsung berfungsi — tanpa langkah manual tambahan. Jika observability adalah langkah “nanti kalau sempat” setelah provisioning, ia tidak akan pernah terpasang di semua server.

# ANTI-PATTERN: observability sebagai langkah manual opsional
# "Nanti kalau sempat kita pasang monitoring..."
# Akibatnya: server baru tidak pernah benar-benar dimonitor
#
# Prosedur manual setelah deploy:
#   1. SSH ke server
#   2. wget node_exporter.tar.gz
#   3. Extract dan setup systemd unit
#   4. Tambahkan target ke Prometheus
#   5. Restart Prometheus
#   → 5 langkah mudah yang pasti terlewat di 30% server

# BENAR: observability sebagai bagian dari provisioning server
# roles/common/tasks/main.yml — dijalankan di SETIAP server baru
- import_tasks: install.yml
- import_tasks: configure.yml
- import_tasks: node-exporter.yml      # Monitoring sistem — selalu
- import_tasks: filebeat.yml           # Logging terpusat — selalu
- import_tasks: health-check.yml       # Health endpoint — selalu
- import_tasks: promtail.yml           # Collector log — selalu

Pisahkan role common yang berisi setup observability dari role aplikasi. Setiap role aplikasi (misal web, db, cache) mengimpor role common di awal. Ini memastikan bahwa tidak ada server baru yang lolos dari observability — secara struktural, bukan secara kebijakan.

Definisikan role common sebagai dependency meta-role di meta/main.yml sehingga Ansible menolak menjalankan role aplikasi tanpa common lebih dulu. Ini mengubah “best practice” menjadi “hard requirement” yang tidak bisa di-bypass.

2. Tiga Pilar Observability Harus Terhubung #

Log, metrik, dan trace yang berdiri sendiri-sendiri hanya setengah berguna. Nilai sesungguhnya muncul saat ketiganya bisa saling dikaitkan. Alert Prometheus harus bisa diklik untuk membuka dashboard Grafana di waktu yang sama, dari situ klik ke trace di Tempo, dan dari trace yang lambat klik ke baris log yang relevan. Ini hanya mungkin jika ketiga pilar berbagi identifier yang sama.

flowchart LR
    subgraph Sumber["Sumber"]
        App["Aplikasi<br/>(instrumentasi OTel)"]
    end
    subgraph Tiga["Tiga Pilar"]
        M["METRIK<br/>Prometheus<br/>counter, gauge, histogram"]
        L["LOG<br/>Loki / ELK<br/>event terstruktur"]
        T["TRACE<br/>Tempo / Jaeger<br/>request span tree"]
    end
    subgraph Korelasi["Lapisan Korelasi"]
        Labels["Label bersama<br/>service, env, host, trace_id"]
    end
    subgraph Visual["Visualisasi"]
        G["Grafana<br/>dashboard + derived fields"]
    end
    App --> M
    App --> L
    App --> T
    M -. "pakai" .-> Labels
    L -. "pakai" .-> Labels
    T -. "pakai" .-> Labels
    Labels --> G
    G -- "klik alert" --> M
    G -- "klik trace_id" --> T
    G -- "klik log line" --> L

Tiga pilar di tengah (Metrics, Logs, Traces) berasal dari satu aplikasi yang sama, dan semuanya menggunakan label yang konsisten (service, environment, host, trace_id). Lapisan korelasi di bawahnya adalah perekatnya: identifier yang sama memungkinkan navigasi silang di Grafana melalui derived fields dan Explore. Tanpa konsistensi label, ketiga pilar tetap menjadi tiga silo yang tidak saling memahami.

# group_vars/all.yml — definisikan label standar SEKALI
observability_labels:
  environment: "{{ env }}"
  cluster: "{{ cluster_name }}"
  service: "{{ app_name | default('unknown') }}"
  host: "{{ inventory_hostname }}"
  region: "{{ aws_region | default('local') }}"

# Gunakan di SEMUA template: Prometheus rules, Filebeat config, OTel resource
# ANTI-PATTERN: label hardcode berbeda di setiap file
# file 1: env=production, hostname=app01
# file 2: environment=prod, host=app-01
# file 3: stage=prd, server=APP_01
# → query lintas sistem mustahil, agregasi rusak

# BENAR: satu sumber variabel Ansible, dipakai di semua template
labels:
  env: "{{ observability_labels.environment }}"
  instance: "{{ observability_labels.host }}"
  service: "{{ observability_labels.service }}"

Tambahkan trace_id ke setiap log entry — biasanya lewat OpenTelemetry SDK yang otomatis menyuntikkan trace context ke logger. Konfigurasikan Grafana derived fields untuk mengekstrak trace_id dari log dan menavigasi ke Tempo. Ini adalah investasi kecil yang membayar kembali berlipat ganda saat incident response.


3. Alert Harus Actionable dan Terstruktur #

Alert yang tidak jelas harus dilakukan apa tidak berguna — bahkan berbahaya karena menyebabkan alert fatigue. Setiap alert yang kita buat harus lulus lima pertanyaan: siapa yang menerima, apa yang harus dilakukan, sudah ada runbook-nya, severity-nya tepat, dan berapa kali false alarm dalam sebulan terakhir.

flowchart TD
    Start["Alert firing"] --> T1{"Ada owner<br/>dan runbook?"}
    T1 -- "Tidak" --> X1["Tolak:<br/>lengkapi dulu"]
    T1 -- "Ya" --> T2{"False alarm<br/>lebih dari 1x/minggu?"}
    T2 -- "Ya" --> X2["Investigasi:<br/>threshold atau inhibisi"]
    T2 -- "Tidak" --> T3{"Severity<br/>sesuai dampak?"}
    T3 -- "Tidak" --> X3["Sesuaikan:<br/>critical/warning/info"]
    T3 -- "Ya" --> OK["Alert valid:<br/>kirim ke tim"]

Decision tree di atas adalah filter minimum yang harus dilalui setiap alert baru. Jika salah satu cabang “Tidak” terpicu, alert belum siap ditambahkan ke rule yang aktif. Filter ini juga bisa di-validasi lewat promtool test rules atau GitHub Action yang menjalankan assertion terhadap metadata setiap alert rule.

# ANTI-PATTERN: alert tanpa konteks
- alert: HighMemory
  expr: node_memory_MemAvailable_bytes < 500000000
  annotations:
    summary: "Memory rendah"

# BENAR: alert dengan konteks yang actionable
- alert: CriticallyLowMemory
  expr: >
    (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) < 0.1    
  for: 5m
  labels:
    severity: critical
    team: platform
    slo: availability
  annotations:
    summary: "Memory kritis di {{ '{{' }} $labels.instance {{ '}}' }}"
    description: >
      Hanya {{ '{{' }} $value | humanizePercentage {{ '}}' }} memory tersisa
      sejak 5 menit terakhir. Proses dengan RSS tertinggi biasanya
      adalah penyebabnya — lihat runbook untuk langkah diagnosis.      
    runbook_url: "https://wiki.company.com/runbooks/low-memory"
    dashboard_url: "https://grafana.company.com/d/memory?var-host={{ '{{' }} $labels.instance {{ '}}' }}"

Perhatikan bedanya: alert pertama tidak memberi tahu receiver apa yang harus dilakukan. Alert kedua menentukan threshold berbasis rasio (lebih akurat dari nilai absolut), menambahkan label team (siapa yang harus dipanggil) dan slo (apa konteks bisnisnya), menyertakan URL runbook, dan menambahkan link dashboard untuk investigasi cepat. Receiver tidak perlu menebak — semua link yang dibutuhkan sudah ada di notifikasi.


4. Observability Infrastructure Juga Harus Dimonitor #

Ini sering terlupakan: siapa yang memonitor Prometheus? Siapa yang memonitor Alertmanager? Jika pipeline monitoring mati di tengah malam, kita tidak akan mengetahui ada masalah lain sampai user mengajukan keluhan. Ironi ini nyata: incident monitoring yang tidak termonitor adalah silent failure terdistribusi yang baru ketahuan saat semuanya sudah terlambat.

# Prometheus yang memonitor dirinya sendiri
- job_name: prometheus
  static_configs:
    - targets: ['localhost:9090']

# Alert jika Alertmanager tidak bisa dijangkau
- alert: AlertmanagerDown
  expr: up{job="alertmanager"} == 0
  for: 1m
  labels:
    severity: critical
    team: platform
  annotations:
    summary: "Alertmanager tidak berjalan — semua alert tidak akan terkirim!"
    runbook_url: "https://wiki.company.com/runbooks/alertmanager-down"

# Alert jika Prometheus gagal scrape target
- alert: PrometheusTargetMissing
  expr: up == 0
  for: 5m
  labels:
    severity: warning
    team: platform
  annotations:
    summary: "Target scrape tidak terjangkau: {{ '{{' }} $labels.job {{ '}}' }}/{{ '{{' }} $labels.instance {{ '}}' }}"

# Alert jika Prometheus kehabisan memori
- alert: PrometheusHighMemory
  expr: process_resident_memory_bytes{job="prometheus"} > 4 * 1024 * 1024 * 1024
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "Prometheus menggunakan >4GB RAM — kemungkinan cardinality tinggi"

Tiga alert di atas adalah minimum yang harus ada: monitor dirinya sendiri (self-monitoring), monitor dependency Alertmanager, dan monitor resource usage Prometheus itu sendiri. Alert ketiga khususnya sering jadi early warning untuk masalah cardinality (akan dibahas lebih detail di Prinsip 6).

Tambahkan juga alert untuk disk usage di storage backend (TSDB Prometheus, S3 Loki, dll) — full disk = data loss permanen. Set threshold di 70% agar ada waktu untuk rotasi retention atau ekspansi storage sebelum benar-benar penuh.


5. Sampling Trace yang Ekonomis #

Menyimpan 100% trace adalah kemewahan yang harus ditolak di awal. Storage trace adalah yang paling mahal dari ketiga pilar — satu span bisa puluhan kilobyte, satu request bisa ratusan span, dan request normal mendominasi populasi. Sampling yang tepat mempertahankan insight dari trace yang bermasalah sambil memotong biaya storage hingga 90%.

# ANTI-PATTERN: head-based sampling 100%
# OTel Collector config — menyimpan SEMUA trace
processors:
  batch:
    timeout: 5s
    send_batch_size: 1000
  # Tidak ada filter, semua trace lewat

# BENAR: tail-based sampling dengan prioritas pada trace bermasalah
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 200
    policies:
      # Policy 1: SELALU simpan trace error
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }

      # Policy 2: SELALU simpan trace lambat
      - name: slow-traces
        type: latency
        latency: { threshold_ms: 1000 }

      # Policy 3: Simpan 5% trace normal secara acak
      - name: baseline
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }

Konfigurasi di atas menyimpan 100% trace error, 100% trace yang lebih lambat dari 1 detik, dan hanya 5% trace normal. Hasilnya: storage turun 90% tetapi kita tidak pernah kehilangan trace yang sebenarnya kita butuhkan. Aturan “trace error dan trace lambat selalu disimpan” adalah konstanta yang tidak bisa ditawar.

Tail-based sampling butuh decision_wait yang cukup besar (5-10 detik) untuk memberi waktu semua span dari satu trace_id sampai lengkap sebelum decision dibuat. Tradeoff: semakin lama decision_wait, semakin akurat sampling tapi semakin tinggi memory usage di collector.

Untuk service dengan traffic sangat tinggi, tambahkan head-based sampling awal (misal 1-2% di SDK aplikasi) sebagai filter pertama, baru kemudian tail-based sampler di collector memfilter lebih lanjut. Ini mencegah collector overwhelmed.


6. Observability Cost Awareness #

Observability yang baik bukan observability yang paling lengkap — tapi yang paling proporsional dengan nilai bisnis dan biayanya. Cardinality tinggi, retention panjang, dan trace full-fidelity adalah tiga sumber biaya terbesar yang harus kita sadari sejak awal.

# ANTI-PATTERN: cardinality tanpa batas
# Setiap label yang nilainya unik per request akan meledakkan storage
metrics:
  - name: http_requests_total
    labels:
      - method
      - path           # /users/12345, /users/12346, ... = ribuan series
      - user_id        # satu series per user = JUTAAN series
      - request_id     # setiap request beda = storage OOM dalam jam

# BENAR: cardinality terkontrol
metrics:
  - name: http_requests_total
    labels:
      - method
      - path_template  # /users/:id, BUKAN /users/12345
      - status_class   # 2xx, 4xx, 5xx — bukan 200, 201, 404, 500, ...
    # Cardinality: 3 method × 50 path × 5 status = 750 series
    # Bandingkan dengan 3 × ribuan × jutaan × millions = kardinalitas meledak

Aturan praktis untuk cardinality Prometheus:

Label value yang AMAN:
  ✓ method (GET, POST, PUT)              → ~5 nilai
  ✓ status_class (2xx, 3xx, 4xx, 5xx)    → ~5 nilai
  ✓ environment (prod, staging, dev)     → ~3 nilai
  ✓ service (app, db, cache)             → ~10 nilai
  ✓ region (ap-southeast-1, us-east-1)   → ~5 nilai

Label value yang BERBAHAYA:
  ✗ user_id                               → jutaan
  ✗ request_id                            → tak terhingga
  ✗ email / nama                          → jutaan
  ✗ path dengan ID                        → ribuan per aplikasi
  ✗ timestamp                             → tak terhingga

Untuk data high-cardinality (user_id, request_id, email), gunakan log atau trace — bukan metric. Log dan trace memang lebih mahal per event, tapi jumlah event high-cardinality biasanya jauh lebih sedikit dari jumlah metric series. Tradeoff yang sehat.

Retention policy juga bagian dari cost awareness. Default Prometheus menyimpan data 15 hari, Loki 30 hari, Tempo 14 hari. Sesuaikan dengan kebutuhan compliance dan incident review: data yang lebih dari 90 hari biasanya hanya dibutuhkan untuk audit tahunan — pindahkan ke S3 cold storage yang jauh lebih murah.


7. Runbook yang Executable dan Alert-Owner #

Alert tanpa runbook adalah gangguan. Runbook tanpa owner yang jelas adalah dokumen yang berdebu. Kombinasi keduanya — alert yang menunjuk ke runbook yang executable dan runbook yang menunjuk ke owner yang jelas — adalah pondasi operasional yang bisa diandalkan saat insiden.

<!-- ANTI-PATTERN: runbook yang tidak executable dan tidak ada owner -->
# Runbook: Memory Low

Kalau memory penuh, restart service-nya.

<!-- Tidak ada: owner, langkah diagnosis, kapan escalate, rollback plan -->
<!-- BENAR: runbook yang executable dan jelas ownership-nya -->

# Runbook: Memory Kritis di Host Production

**Owner:** Tim Platform (on-call: @oncall-platform)
**Severity:** Critical
**Escalation:** Jika tidak resolve dalam 30 menit, page manager @platform-lead
**Dashboard:** https://grafana.company.com/d/memory
**SLO terkait:** availability 99.9% (lihat SLO runbook availability)

## Diagnosis (5 menit)

1. Buka dashboard memory, identifikasi host dari alert.
2. SSH ke host: `ssh {{ '{{' }} hostname {{ '}}' }}`
3. Cek proses dengan RSS tertinggi:
   ```bash
   ps aux --sort=-%mem | head -20
  1. Cek apakah OOM killer aktif:
    dmesg | grep -i "killed process" | tail -20
    
  2. Cek apakah ada proses yang memory-nya terus naik (memory leak):
    # Bandingkan RSS proses X dari 5 menit terakhir
    

Mitigasi Cepat (10 menit) #

  • Opsi A: Restart service (jika diketahui service mana)
    systemctl restart <service-name>
    
  • Opsi B: Drain traffic (jika tidak yakin root cause)
    # Hapus dari load balancer
    ansible-playbook -i inv/prod playbooks/drain-host.yml -e "host={{ '{{' }} hostname {{ '}}' }}"
    
  • Opsi C: Kill proses boros (jika satu proses nyata penyebab)
    # Konfirmasi di Slack #platform dulu
    kill -15 <pid>
    

Setelah Insiden #

  • Buat post-mortem (template: wiki.company.com/postmortem)
  • Tambah alert untuk early detection (jika belum ada)
  • Review apakah ini pattern yang akan terulang

Perhatikan struktur runbook yang benar: ada owner, ada escalation path, ada link ke dashboard, ada SLI/SLO yang terdampak, dan ada langkah diagnosis *sebelum* langkah mitigasi. Banyak tim loncat ke mitigasi tanpa diagnosis yang benar — yang sering membuat masalah datang balik dalam 1-2 jam.

Simpan runbook di repositori kode (GitOps) bukan di wiki yang terpisah. Ini memastikan runbook di-review saat ada perubahan, ada version history, dan bisa di-link langsung dari alert annotation.

---

## 8. Observability as Code via Ansible

Semua konfigurasi observability — Prometheus scrape config, alert rules, dashboard Grafana, Filebeat pipeline, Alertmanager routing — harus didefinisikan di Ansible dan di-version-control di Git. Tidak ada konfigurasi observability yang diubah manual lewat UI, kecuali untuk eksperimen sementara yang akan di-promosikan ke kode.

```yaml
# Struktur direktori role observability
roles/observability/
├── tasks/
│   ├── main.yml
│   ├── prometheus.yml
│   ├── grafana.yml
│   ├── alertmanager.yml
│   └── filebeat.yml
├── templates/
│   ├── prometheus.yml.j2
│   ├── alerts/
│   │   ├── infrastructure.yml.j2
│   │   ├── application.yml.j2
│   │   └── slo.yml.j2
│   ├── alertmanager.yml.j2
│   └── dashboards/
│       ├── api-overview.json.j2
│       └── slo-overview.json.j2
├── files/
│   └── dashboards/
│       ├── api-overview.json      # JSON statis, di-copy apa adanya
│       └── slo-overview.json
└── defaults/
    └── main.yml                    # retention, scrape interval, dll

Struktur di atas memisahkan tiga jenis file:

File template (.j2):
  → Konfigurasi yang butuh variabel Ansible
  → Prometheus scrape config, alert rules
  → Di-render saat ansible-playbook berjalan

File statis (JSON/YAML):
  → Konfigurasi yang tidak butuh variabel
  → Dashboard JSON dari Grafana export
  → Di-copy apa adanya via copy module

File defaults:
  → Variabel yang bisa di-override per environment
  → retention_days, scrape_interval, alertmanager_url

Workflow yang direkomendasikan:

1. Developer butuh dashboard baru untuk service X
   → Export JSON dari staging Grafana
   → Taruh di roles/observability/files/dashboards/service-x.json
   → Tambah task di roles/observability/tasks/grafana.yml

2. Developer butuh alert rule baru
   → Edit templates/alerts/application.yml.j2
   → Tambah receiver dan route di templates/alertmanager.yml.j2
   → Submit PR dengan label "observability"

3. CI menjalankan:
   - promtool check rules templates/alerts/*.yml
   - amtool check-config templates/alertmanager.yml.j2 --syntax-only
   - Grafana dashboard lint (json schema validation)
   - Test deployment ke staging

4. Setelah merge, ansible-playbook deploy ke production
   → Prometheus reload otomatis (notify handler)
   → Dashboard baru muncul di Grafana
   → Alert rule baru mulai evaluasi
# ANTI-PATTERN: konfigurasi observability diubah via UI Grafana/Prometheus
# "Coba dulu di UI, nanti kalau sudah oke baru di-export"
# Masalah:
#   - Tidak ada version history
#   - Tidak ada code review
#   - Drift antara staging dan production
#   - Re-deploy ulang = konfigurasi hilang

# BENAR: observability as code, semua lewat Ansible
# Semua perubahan lewat PR, di-review, di-test di staging, baru di-deploy
- name: Deploy Prometheus alert rules
  template:
    src: "alerts/{{ item }}.yml.j2"
    dest: "/etc/prometheus/rules/{{ item }}.yml"
    owner: prometheus
    mode: '0644'
    validate: "promtool check rules %s"
  loop:
    - infrastructure
    - application
    - slo
  notify: Reload Prometheus

Pendekatan ini mungkin terlihat lebih lambat di awal dibanding klik-klik di UI, tapi dalam skala produksi (10+ services, 50+ alerts, 20+ dashboards), observability as code adalah satu-satunya cara yang berkelanjutan. Tanpa itu, drift dan inkonsistensi akan menjadi sumber utama incident.


Anti-Pattern yang Harus Dihindari #

Berikut anti-pattern yang paling sering ditemukan di setup observability production. Setiap anti-pattern disertai dengan solusi ringkas yang bisa langsung kita adopsi.

# ✗ Anti-pattern 1: Cardinality tanpa batas
# Label dengan nilai unik per request/user/path
metrics:
  - name: api_requests_total
    labels: [user_id, path, request_id]
# Konsekuensi: Prometheus OOM dalam hitungan jam
# ✓ Solusi: gunakan path_template (low-cardinality) dan taruh user_id
#           di log/trace, bukan di metric

# ✗ Anti-pattern 2: Alert tanpa owner
- alert: HighErrorRate
  expr: rate(http_errors[5m]) > 0.05
  annotations:
    summary: "Error rate tinggi"
# Konsekuensi: tidak ada yang bertanggung jawab, alert di-snooze/di-ignore
# ✓ Solusi: tambahkan label team dan runbook URL,
#           integrasikan dengan PagerDuty rotation per-team

# ✗ Anti-pattern 3: Menyimpan log tanpa retention policy
# Log dari 2 tahun lalu masih ada, storage penuh, query lambat
# ✓ Solusi: tetapkan retention eksplisit
#   - Hot storage (Loki/ES): 30 hari
#   - Warm storage (S3 + Athena): 1 tahun
#   - Cold storage (Glacier): 5 tahun
#   Hapus otomatis setelah lewat retention, dan jangan lupa monitor
#   disk usage dari storage backend-nya sendiri.

# ✗ Anti-pattern 4: Dashboard yang tidak ada yang membaca
# Puluhan dashboard Grafana, tidak ada yang tahu mana yang dibuka saat insiden
# ✓ Solusi: buat "runbook dashboard" per service, satu dashboard
#           yang menampilkan semua yang dibutuhkan saat investigasi
#           (4 golden signals, dependency health, recent deploy, log).

# ✗ Anti-pattern 5: Alert storm saat ada masalah besar
# Satu masalah memicu 50 alert sekaligus karena semua metrik ikut terdampak
# ✓ Solusi: gunakan inhibit_rules di Alertmanager untuk menekan alert turunan,
#           dan error budget alert (lihat artikel SLO/SLA) yang alert sekali
#           per SLO violation, bukan per-metrik
# Contoh inhibit_rules untuk mengurangi alert storm
# alertmanager.yml
inhibit_rules:
  # Jika Alertmanager down, jangan kirim alert AlertmanagerDown dari host lain
  - source_match:
      alertname: AlertmanagerDown
    target_match:
      alertname: AlertmanagerDown
    equal: ['cluster']

  # Jika ada outage besar, jangan kirim warning untuk setiap service
  - source_match:
      severity: critical
    target_match:
      severity: warning
    equal: ['cluster', 'service']
# ✗ Anti-pattern 6: Log tanpa struktur yang konsisten
"Error: database connection failed"
"2024-03-15 ERROR db conn fail"
"[ERR] could not connect to postgres"
# Konsekuensi: tidak bisa di-query, tidak bisa di-filter,
#              tidak bisa di-correlate dengan trace_id

# ✓ Solusi: structured log dengan field yang konsisten
{"timestamp":"2024-03-15T14:30:00Z","level":"error","service":"myapp",
 "trace_id":"abc123","span_id":"def456","message":"database connection failed",
 "error":"dial tcp: connection refused","host":"app-01","db_host":"db-01"}
# Setiap field bisa di-filter, di-aggregate, dan di-correlate

# ✗ Anti-pattern 7: Observability stack yang berjalan tanpa backup konfigurasi
# Prometheus crash, restore dari backup tapi semua alert rules dan
# scrape config hilang karena tidak ada version control
# ✓ Solusi: git adalah backup. Setiap perubahan lewat PR,
#           Ansible deploy dari Git, recovery = git clone + ansible-playbook

Checklist Review Observability #

Gunakan checklist ini setiap kali setup observability untuk service baru, atau setiap kali review setup observability yang sudah ada. Centang semua item sebelum declare service “production-ready”.

LOGGING
  □ Log ditulis dalam format JSON terstruktur
  □ Field konsisten: timestamp, level, service, trace_id, message
  □ Level log digunakan dengan benar (debug/info/warn/error)
  □ Log dikirim ke backend terpusat (Loki/Elasticsearch) via Filebeat/Promtail
  □ Retention log ditetapkan (hot 30 hari, warm 1 tahun)
  □ Log rotation di sisi aplikasi/local file
  □ Tidak ada data sensitif (password, PII) di log
  □ Sampling log untuk level debug (jika volume tinggi)

MONITORING
  □ Node/system exporter berjalan di host
  □ Application metrics tersedia di /metrics (Prometheus format)
  □ RED metrics untuk service: Rate, Error, Duration
  □ USE metrics untuk host: Utilization, Saturation, Errors
  □ Service sudah menjadi scrape target di Prometheus
  □ Recording rules untuk query yang kompleks
  □ Dashboard service tersedia di Grafana
  □ Cardinality metric diaudit (tidak ada user_id/request_id)

ALERTING
  □ Alert untuk availability (error rate tinggi)
  □ Alert untuk latency (P99 tinggi)
  □ Alert untuk saturation (resource hampir penuh)
  □ Alert untuk SLO error budget burn rate
  □ Setiap alert punya label severity (critical/warning/info)
  □ Setiap alert punya label team (siapa yang dipanggil)
  □ Setiap alert punya runbook_url annotation
  □ Setiap alert punya dashboard_url annotation
  □ Inhibit_rules dikonfigurasi untuk mencegah alert storm
  □ Routing ke channel yang benar (PagerDuty critical, Slack warning)

TRACING
  □ Aplikasi instrumentasi dengan OpenTelemetry SDK
  □ Trace ID diteruskan antar service (context propagation)
  □ Tail-based sampling aktif (error dan slow selalu disimpan)
  □ Trace tersedia di Tempo/Jaeger
  □ Sampling rate disesuaikan dengan volume traffic
  □ Trace retention 7-14 hari
  □ Trace backend dimonitor (storage usage, ingestion rate)

HEALTH CHECK
  □ Endpoint /health/live (liveness) tersedia dan ringan
  □ Endpoint /health/ready (readiness) tersedia dan cek dependency
  □ Load balancer dikonfigurasi untuk health check
  □ Kubernetes probe dikonfigurasi (jika di K8s)
  □ Health check tidak cek dependency eksternal (untuk liveness)
  □ Startup probe dikonfigurasi untuk service yang lambat start

SLO
  □ SLI didefinisikan (availability, latency, throughput)
  □ SLO target ditetapkan (misal 99.9% request < 200ms)
  □ Error budget dikalkulasi per bulan
  □ Recording rules untuk SLI dibuat
  □ Multi-window burn rate alert dikonfigurasi (1h+5m, 6h+30m)
  □ Dashboard SLO menampilkan error budget remaining
  □ Laporan SLA bulanan di-generate otomatis

COST & PERFORMANCE
  □ Cardinality metric diaudit setiap quarter
  □ Retention policy eksplisit untuk semua storage backend
  □ Disk usage monitoring untuk Prometheus/Loki/Tempo storage
  □ Sampling rate tracing disesuaikan dengan traffic
  □ Log sampling di level debug/info
  □ Storage backend pakai tier (hot/warm/cold) sesuai umur data

CULTURE & PROCESS
  □ Runbook up-to-date untuk setiap alert
  □ Runbook punya owner dan escalation path
  □ On-call rotation jelas (per team, per severity)
  □ Blameless post-mortem untuk setiap insiden critical
  □ Observability review di setiap PR yang menambah service baru
  □ Konfigurasi observability via Ansible/Git, bukan UI click
  □ CI menjalankan lint untuk alert rules dan dashboard JSON
  □ Alert yang tidak firing dalam 3 bulan di-review (perlu atau tidak?)

Ringkasan #

  • Observability harus otomatis — integrasikan ke role common Ansible sehingga setiap server baru langsung termonitor, log-nya terpusat, dan health check berfungsi tanpa langkah manual.
  • Tiga pilar harus terhubung — gunakan label yang konsisten (service, environment, host, trace_id) di log, metrik, dan trace; navigasi silang di Grafana via derived fields memberikan nilai yang jauh lebih besar dari masing-masing pilar secara terpisah.
  • Alert harus actionable — setiap alert harus menjawab “apa yang harus dilakukan” dengan link ke runbook, dan “siapa yang dipanggil” dengan label team. Alert tanpa konteks adalah noise yang merusak trust tim.
  • Monitor infrastructure monitoring — Alertmanager yang down, Prometheus yang OOM, dan storage yang penuh adalah silent failure yang baru ketahuan saat terlambat. Alert untuk self-monitoring adalah investasi minimum.
  • Sampling trace yang ekonomis — tail-based sampling: simpan 100% trace error dan trace lambat, sample 5-10% trace normal. Storage hemat 90% tapi insight untuk debugging tetap utuh.
  • Cardinality dan retention adalah biaya utama — label high-cardinality (user_id, request_id) harus dihindari di metric; data semacam itu lebih cocok masuk log atau trace. Tetapkan retention eksplisit untuk semua storage backend.
  • Runbook executable dengan owner jelas — runbook harus berisi langkah diagnosis sebelum mitigasi, menyebutkan owner dan escalation path, dan disimpan di Git (bukan wiki terpisah) supaya ikut ter-review saat ada perubahan.
  • Observability as code via Ansible — semua konfigurasi (scrape config, alert rules, dashboard, alertmanager routing) lewat Ansible role dan Git, di-lint di CI, di-deploy lewat promtool/amtool validate. Tidak ada perubahan via UI klik.

← Sebelumnya: SLO & SLA   Berikutnya: Custom Module →

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