Dua chapter sebelumnya sudah ada yang mengambil bagian ini: Monitoring Workflow membahas pemantauan dari sisi orchestrator, dan Metrics dan Alerting di section Data Quality dan Observability menghitung metrik dari sisi isi data — volume baris, kualitas, dan label severity. Chapter ini mengambil sudut yang berbeda: pipeline sebagai beban operasional yang harus tersedia setiap hari. Pertanyaannya bukan “apakah datanya benar”, melainkan “apakah run-nya sehat”: berapa tingkat keberhasilan, berapa lama kasus terberat, apakah kegagalan menumpuk, dan apakah resource menipis. Jawaban angkanya dihitung dari run ledger; jawaban tindakannya adalah routing — notifikasi mana yang membangunkan orang, mana yang cukup menjadi tiket pagi, dan mana yang sengaja dibiarkan diam.

Golden Signals untuk Pipeline #

Empat golden signals dari praktik SRE — latensi, trafik, error, saturasi — terpetakan rapi ke dunia pipeline batch:

Golden signalPadanan pipelineSumber data
Latencydurasi run, khususnya p95durasi_menit di ledger
Trafficvolume baris per runrows di ledger
Errorsrun gagal, kegagalan berurutanstatus di ledger
SaturationCPU puncak, memori, concurrencymetrik resource per run

Volume baris sebagai sinyal trafik sudah dibahas habis di chapter Metrics dan Alerting bersama deteksi penurunan; tiga sisanya adalah wilayah chapter ini, dan semuanya tersedia dari ledger yang sama.

Menghitung Kesehatan dari Run Ledger #

Skema ledger meneruskan chapter Scheduling dan Production Runs dengan satu kolom tambahan: cpu_peak_pct, saturasi yang tercatat setiap run. Riwayat berikut memuat pekan yang bermasalah — satu kegagalan di jendela rawat terjadwal, lalu dua kegagalan berurutan:

python
"""Kesehatan run pipeline dari ledger + aturan routing alert (page/tiket/diam)."""
import json
from datetime import datetime

RIWAYAT = """\
{"run_id":"r-115","tanggal":"2026-10-02","mulai":"02:00","status":"success","duration_s":208,"rows":53550,"cpu_peak_pct":55}
{"run_id":"r-116","tanggal":"2026-10-03","mulai":"02:00","status":"success","duration_s":226,"rows":52900,"cpu_peak_pct":60}
{"run_id":"r-117","tanggal":"2026-10-04","mulai":"02:00","status":"success","duration_s":204,"rows":53300,"cpu_peak_pct":57}
{"run_id":"r-118","tanggal":"2026-10-05","mulai":"02:10","status":"failed","duration_s":38,"rows":0,"cpu_peak_pct":12}
{"run_id":"r-119","tanggal":"2026-10-06","mulai":"02:00","status":"success","duration_s":296,"rows":51700,"cpu_peak_pct":78}
{"run_id":"r-120","tanggal":"2026-10-07","mulai":"02:00","status":"failed","duration_s":341,"rows":21850,"cpu_peak_pct":88}
{"run_id":"r-121","tanggal":"2026-10-08","mulai":"02:00","status":"failed","duration_s":328,"rows":0,"cpu_peak_pct":91}
"""

JENDELA_RAWAT = [{"nama": "upgrade PostgreSQL production",
                  "mulai": "2026-10-05T02:00", "akhir": "2026-10-05T03:00"}]
BATAS_CPU = 90  # persen; di atas ini pipeline dianggap jenuh


def persentil(nilai, p):
    data = sorted(nilai)
    pos = (len(data) - 1) * p / 100
    bawah = int(pos)
    atas = min(bawah + 1, len(data) - 1)
    return data[bawah] + (data[atas] - data[bawah]) * (pos - bawah)


def dalam_rawat(run):
    for rawat in JENDELA_RAWAT:
        mulai = datetime.fromisoformat(rawat["mulai"])
        akhir = datetime.fromisoformat(rawat["akhir"])
        mulai_run = datetime.fromisoformat(f"{run['tanggal']}T{run['mulai']}")
        if mulai <= mulai_run < akhir:
            return rawat["nama"]
    return None

Bagian kedua menghitung metrik, mengevaluasi aturan, lalu menyusun kiriman akhir per saluran:

python
runs = [json.loads(baris) for baris in RIWAYAT.splitlines()]
jendela = runs[-7:]
sukses = [r for r in jendela if r["status"] == "success"]

success_rate = len(sukses) / len(jendela)
streak = 0
for r in reversed(jendela):
    if r["status"] == "failed":
        streak += 1
    else:
        break
durasi_p95 = persentil([r["duration_s"] for r in jendela], 95)
cpu_puncak = max(r["cpu_peak_pct"] for r in jendela)

print("== Kesehatan 7 run terakhir (etl_pesanan_harian) ==")
print(f"success_rate        : {success_rate:.2f} ({len(sukses)}/{len(jendela)})")
print(f"kegagalan_berurutan : {streak}")
print(f"durasi_p95          : {durasi_p95:.1f} detik")
print(f"cpu_puncak          : {cpu_puncak}% (batas {BATAS_CPU}%)")

print()
print("== Evaluasi aturan -> routing ==")
page, tiket, diam = [], [], []
for r in jendela:
    if r["status"] == "failed" and dalam_rawat(r):
        nama = dalam_rawat(r)
        diam.append(f"[P3] {r['run_id']} gagal dalam jendela rawat: {nama}")
        print(f"  [P3 diam ] {r['run_id']} gagal di jendela rawat: {nama}")
if streak >= 2:
    page.append(f"[P1] {streak} kegagalan berurutan (run terakhir gagal total)")
    print(f"  [P1 page ] {streak} kegagalan berurutan")
if success_rate < 0.85:
    page.append(f"[P1] success_rate 7 hari {success_rate:.2f} < 0.85")
    print(f"  [P1 page ] success_rate 7 hari {success_rate:.2f} < 0.85")
if durasi_p95 > 300:
    tiket.append(f"[P2] durasi_p95 {durasi_p95:.0f}s > 300s (latensi memburuk)")
    print(f"  [P2 tiket] durasi_p95 {durasi_p95:.0f}s > 300s")
if cpu_puncak >= BATAS_CPU:
    tiket.append(f"[P2] cpu_puncak {cpu_puncak}% >= {BATAS_CPU}% (saturasi)")
    print(f"  [P2 tiket] cpu_puncak {cpu_puncak}% >= {BATAS_CPU}%")

print()
print("== Kiriman akhir (setelah dedup per insiden) ==")
print(f"  PAGE  -> on-call: {len(page)} aturan menjadi 1 notifikasi; "
      f"pager dibunyikan untuk insiden etl_pesanan_harian")
print(f"  TIKET -> antrean pagi: {len(tiket)} aturan menjadi 1 tiket")
print(f"  DIAM  -> hanya tercatat: {len(diam)} run gagal di jendela rawat, "
      f"terjadwal dan diketahui")

Output yang Dihasilkan:

text
== Kesehatan 7 run terakhir (etl_pesanan_harian) ==
success_rate        : 0.57 (4/7)
kegagalan_berurutan : 2
durasi_p95          : 337.1 detik
cpu_puncak          : 91% (batas 90%)

== Evaluasi aturan -> routing ==
  [P3 diam ] r-118 gagal di jendela rawat: upgrade PostgreSQL production
  [P1 page ] 2 kegagalan berurutan
  [P1 page ] success_rate 7 hari 0.57 < 0.85
  [P2 tiket] durasi_p95 337s > 300s
  [P2 tiket] cpu_puncak 91% >= 90%

== Kiriman akhir (setelah dedup per insiden) ==
  PAGE  -> on-call: 2 aturan menjadi 1 notifikasi; pager dibunyikan untuk insiden etl_pesanan_harian
  TIKET -> antrean pagi: 2 aturan menjadi 1 tiket
  DIAM  -> hanya tercatat: 1 run gagal di jendela rawat, terjadwal dan diketahui

Mari kita telaah bagian-bagian kuncinya:

  1. streak dihitung mundur dari run terakhir: kegagalan berurutan adalah sinyal on-call yang lebih tajam daripada success rate — satu kegagalan bisa jadi insiden acak, dua berturut-turut berarti sesuatu rusak dan run berikutnya kemungkinan besar juga gagal.
  2. dalam_rawat membandingkan waktu mulai run dengan jendela rawat berkalender: kegagalan r-118 dikenali sebagai akibat upgrade yang terjadwal, sehingga tidak perlu membangunkan siapa pun — penyebabnya sudah diketahui sebelum run dimulai.
  3. Dua aturan P1 menyala dari satu insiden: kegagalan berurutan dan success rate rendah berasal dari kegagalan yang sama; kiriman akhir menggabungkannya menjadi satu notifikasi agar pager tidak berbunyi dua kali untuk satu masalah.
  4. P2 dikumpulkan ke satu tiket: latensi dan saturasi saling terkait (CPU menipis membuat run lambat) — satu tiket pagi dengan dua temuan lebih mudah ditindaklanjuti daripada dua tiket terpisah.

Dashboard dan Mutu Alert #

Angka-angka di atas layak jadi dashboard: panel success rate dan durasi p95 per pipeline, panel kegagalan berurutan, dan panel saturasi CPU — pola yang didukung langsung oleh Grafana untuk visualisasi dan Prometheus untuk penyimpanan time series serta evaluasi aturan alertnya. Aturan routing di dunia nyata dinyatakan sebagai konfigurasi Alertmanager: pengelompokan per insiden, penundaan pengulangan, dan prioritas saluran.

Notifikasi yang benar sasaran hanyalah setengah dari kecepatan pemulihan; setengah lainnya adalah tahu ke mana melihat setelah pager berbunyi. Ketika alert P1 tiba pukul 03.00, on-call membutuhkan kronologi lengkap satu run lintas layanan dalam hitungan detik — dan itu pekerjaan logging serta tracing, materi chapter Logging dan Tracing.