Pukul 03:12 alert freshness untuk mart_penjualan berbunyi di ponsel on-call. Pipeline harian gagal, dashboard GMV berpotensi kosong saat tim finance mulai bekerja pukul 07:00, dan tanpa proses yang jelas tiga engineer akan memeriksa log yang sama sementara tim finance justru mengetahui kejadian lebih dulu dari rapat pagi. Incident response adalah proses terkoordinasi untuk situasi seperti ini: menentukan keparahan, membagi peran, mengeksekusi mitigasi, mengomunikasikan status, lalu menutup insiden dengan postmortem. Chapter ini menyusun proses tersebut untuk konteks data, lengkap dengan timeline insiden yang dibangun nyata dari berkas events JSONL.

Tingkat Keparahan untuk Insiden Data #

Severity menentukan siapa yang dibangunkan dan seberapa cepat status harus diperbarui. Insiden data punya ciri khas: pipeline bisa tampak hijau di orkestrator sementara angkanya salah secara bisnis, sehingga severity harus dinilai dari dampak ke konsumen data, bukan dari warna status task. Contoh severity untuk platform data TokoKita:

SeverityDefinisiContoh insiden dataRespons
SEV1data untuk keputusan bisnis atau pihak eksternal salah dan tidak ada solusi sementaraangka pajak yang salah sudah terkirim ke auditor; warehouse tidak bisa dibaca saat flash salepage on-call 24/7, incident commander ditunjuk, update tiap 30 menit
SEV2satu domain data salah atau terlambat, konsumen penting terdampak, ada solusi sementaraGMV harian kosong karena nilai status upstream berubahpage on-call, update tiap 2 jam
SEV3gangguan minor tanpa konsumen real-timemart_stok tertunda 3 jam, hanya dibaca batch berikutnyaditangani pada jam kerja, cukup tiket

Alert yang memicu page pada tabel di atas berasal dari praktik chapter Monitoring dan Alerting. Ketika angka dashboard terindikasi salah, penelusuran akar masalahnya mengikuti pohon diagnostik dari chapter Investigasi Kegagalan Data — incident response fokus pada proses pengelolaan kejadian, bukan pada teknik investigasinya.

Peran Saat Insiden Berlangsung #

Insiden yang dikelola baik memisahkan pengambil keputusan dari pelaksana. Pola peran berikut diadaptasi dari praktik SRE:

PeranTugas utamaBukan tugas peran tersebut
Incident Commandermemegang kendali, membagi tugas, memutuskan mitigasiikut menyelidiki akar masalah
Operations Leadmengeksekusi mitigasi: pause export, rollback, reprocessmenunggu perintah selangkah demi selangkah
Communications Leadmengirim status berkala ke konsumen datamenjanjikan ETA tanpa dasar data
Scribemencatat timeline: siapa, melakukan apa, kapanmerekonstruksi timeline dari ingatan

Membangun Timeline dan Menghitung MTTA/MTTR #

Timeline insiden terbaik dicatat saat kejadian berlangsung, bukan direkonstruksi sehari kemudian. Kita menyimpan setiap kejadian sebagai satu baris JSON di events.jsonl — pola yang selaras dengan log terstruktur pada chapter Logging dan Tracing:

json
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T03:12:00", "tipe": "deteksi", "aktor": "alert-freshness (Grafana)", "detail": "mart_penjualan stale > 6 jam, page on-call"}
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T03:19:00", "tipe": "ack", "aktor": "Dewi (on-call DE)", "detail": "konfirmasi dashboard GMV 7 November kosong"}
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T03:24:00", "tipe": "mitigasi", "aktor": "Dewi", "detail": "pause export mart_penjualan ke laporan_finance"}
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T03:41:00", "tipe": "update", "aktor": "Siti (communications lead)", "detail": "status update #1 ke tim finance"}
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T04:05:00", "tipe": "identifikasi", "aktor": "Budi (SME pipeline)", "detail": "upstream API v2 mengubah nilai status, filter tidak cocok"}
{"insiden": "INS-2026-11-08", "sev": "SEV2", "waktu": "2026-11-08T04:31:00", "tipe": "resolusi", "aktor": "Dewi", "detail": "hotfix normalisasi status, run ulang sukses, GMV tampil"}
{"insiden": "INS-2026-11-21", "sev": "SEV3", "waktu": "2026-11-21T08:47:00", "tipe": "deteksi", "aktor": "digest harian", "detail": "mart_stok tertunda 3 jam, tanpa page"}
{"insiden": "INS-2026-11-21", "sev": "SEV3", "waktu": "2026-11-21T09:29:00", "tipe": "ack", "aktor": "Budi (on-call DE)", "detail": "diperiksa setelah jam kerja dimulai"}
{"insiden": "INS-2026-11-21", "sev": "SEV3", "waktu": "2026-11-21T10:05:00", "tipe": "identifikasi", "aktor": "Budi", "detail": "worker ETL kehabisan scratch saat join"}
{"insiden": "INS-2026-11-21", "sev": "SEV3", "waktu": "2026-11-21T11:52:00", "tipe": "resolusi", "aktor": "Budi", "detail": "naikkan scratch worker, run ulang sukses"}

Skrip berikut membaca seluruh berkas, mencetak timeline terformat per insiden, lalu menghitung MTTA (mean time to acknowledge: deteksi ke ack) dan MTTR (mean time to resolve: deteksi ke resolusi) — dua metrik yang menilai kecepatan respons dan kecepatan pemulihan:

python
"""Pembangun timeline insiden: baca events JSONL, cetak timeline terformat,
hitung MTTA dan MTTR per insiden, lalu bandingkan dengan target per severity."""
import json
from datetime import datetime
from pathlib import Path

TARGET_MTTA = {"SEV1": 15, "SEV2": 15, "SEV3": 60}     # menit
TARGET_MTTR = {"SEV1": 60, "SEV2": 240, "SEV3": 1440}  # menit


def waktu(teks):
    return datetime.fromisoformat(teks)


def durasi(menit):
    jam, sisa = divmod(menit, 60)
    return f"{jam}j {sisa}m" if jam else f"{sisa}m"


insiden = {}
for baris in Path("events.jsonl").read_text().splitlines():
    e = json.loads(baris)
    insiden.setdefault(e["insiden"], {"sev": e["sev"], "kejadian": []})
    insiden[e["insiden"]]["kejadian"].append(e)

ringkasan = []
for id_ins in sorted(insiden):
    data = insiden[id_ins]
    kejadian = sorted(data["kejadian"], key=lambda e: e["waktu"])
    deteksi = next(e for e in kejadian if e["tipe"] == "deteksi")
    ack = next(e for e in kejadian if e["tipe"] == "ack")
    resolusi = next(e for e in kejadian if e["tipe"] == "resolusi")
    t0 = waktu(deteksi["waktu"])

    print(f"=== {id_ins} ({data['sev']}) ===")
    for e in kejadian:
        m = int((waktu(e["waktu"]) - t0).total_seconds() // 60)
        jam = waktu(e["waktu"]).strftime("%H:%M")
        print(f"  T+{m:>3}m  {jam}  {e['tipe']:<12} {e['aktor']:<24} {e['detail']}")

    mtta = int((waktu(ack["waktu"]) - t0).total_seconds() // 60)
    mttr = int((waktu(resolusi["waktu"]) - t0).total_seconds() // 60)
    status = ("dalam target" if mttr <= TARGET_MTTR[data["sev"]] else "melebihi target")
    ringkasan.append((id_ins, data["sev"], mtta, mttr, status))
    print(f"  -> MTTA {durasi(mtta)}, MTTR {durasi(mttr)} "
          f"(target MTTR {durasi(TARGET_MTTR[data['sev']])}: {status})\n")

print("| Insiden | SEV | MTTA | MTTR | Target MTTR | Status |")
print("|---|---|---|---|---|---|")
for id_ins, sev, mtta, mttr, status in ringkasan:
    print(f"| {id_ins} | {sev} | {durasi(mtta)} | {durasi(mttr)} |"
          f" {durasi(TARGET_MTTR[sev])} | {status} |")
rata_mtta = sum(r[2] for r in ringkasan) / len(ringkasan)
rata_mttr = sum(r[3] for r in ringkasan) / len(ringkasan)
print(f"\nrata-rata MTTA {rata_mtta:.0f} menit, rata-rata MTTR {rata_mttr:.0f} menit")

Output yang Dihasilkan:

text
=== INS-2026-11-08 (SEV2) ===
  T+  0m  03:12  deteksi      alert-freshness (Grafana) mart_penjualan stale > 6 jam, page on-call
  T+  7m  03:19  ack          Dewi (on-call DE)        konfirmasi dashboard GMV 7 November kosong
  T+ 12m  03:24  mitigasi     Dewi                     pause export mart_penjualan ke laporan_finance
  T+ 29m  03:41  update       Siti (communications lead) status update #1 ke tim finance
  T+ 53m  04:05  identifikasi Budi (SME pipeline)      upstream API v2 mengubah nilai status, filter tidak cocok
  T+ 79m  04:31  resolusi     Dewi                     hotfix normalisasi status, run ulang sukses, GMV tampil
  -> MTTA 7m, MTTR 1j 19m (target MTTR 4j 0m: dalam target)

=== INS-2026-11-21 (SEV3) ===
  T+  0m  08:47  deteksi      digest harian            mart_stok tertunda 3 jam, tanpa page
  T+ 42m  09:29  ack          Budi (on-call DE)        diperiksa setelah jam kerja dimulai
  T+ 78m  10:05  identifikasi Budi                     worker ETL kehabisan scratch saat join
  T+185m  11:52  resolusi     Budi                     naikkan scratch worker, run ulang sukses
  -> MTTA 42m, MTTR 3j 5m (target MTTR 24j 0m: dalam target)

| Insiden | SEV | MTTA | MTTR | Target MTTR | Status |
|---|---|---|---|---|---|
| INS-2026-11-08 | SEV2 | 7m | 1j 19m | 4j 0m | dalam target |
| INS-2026-11-21 | SEV3 | 42m | 3j 5m | 24j 0m | dalam target |

rata-rata MTTA 24 menit, rata-rata MTTR 132 menit

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-9 (TARGET_MTTA, TARGET_MTTR): target respons per severity dinyatakan sebagai kamus, sehingga setiap insiden dibandingkan dengan target yang sesuai bobotnya — SEV3 memang boleh lebih lambat daripada SEV1.
  2. Baris 20-23 (grup per insiden): setdefault mengelompokkan baris JSON ke satu insiden; satu berkas bisa menampung banyak insiden sekaligus.
  3. Baris 28-30 (next): deteksi, ack, dan resolusi dicari berdasarkan tipe; sorted memastikan timeline tercetak menurut waktu.
  4. Baris 39-40 (MTTA/MTTR): kedua metrik dihitung sebagai selisih menit dari waktu deteksi t0 — definisi yang sama di seluruh insiden sehingga rata-ratanya bisa dibandingkan langsung.

Dua angka pada tabel ringkasan layak digarisbawahi. MTTA 7 menit untuk SEV2 menunjukkan jalur page bekerja: alert langsung membangunkan on-call. Sebaliknya, MTTA 42 menit untuk SEV3 adalah akibat kebijakan yang memang disengaja — insiden minor tidak mem-page siapa pun, sehingga baru ditangani setelah jam kerja dimulai. Keduanya masih dalam target, dan selisih terhadap target itulah yang menjadi bahan evaluasi postmortem, bukan sekadar angka mentahnya.

Postmortem Tanpa Menyalahkan #

Postmortem blameless berangkat dari asumsi: setiap orang bertindak masuk akal dengan informasi yang tersedia saat kejadian. Pertanyaan yang dicari adalah mengapa sistem memungkinkan kejadian tersebut, bukan siapa yang bersalah — budaya yang diuraikan tuntas di Google SRE Book: Postmortem Culture . Template berikut mengikat hasilnya menjadi aksi yang bisa diperiksa:

markdown
## Ringkasan
Satu paragraf: apa yang terjadi, konsumen mana yang terdampak, berapa lama.

## Dampak
- SEV2; GMV 7 November kosong pada dashboard selama 1 jam 19 menit
- Konsumen terdampak: dashboard_gmv, laporan_finance

## Timeline
(ditempel dari output timeline_insiden.py)

## Akar Masalah
Upstream merilis API v2 yang mengubah nilai status tanpa pemberitahuan;
pipeline menyalin nilai baru sehingga filter `status = 'lunas'` gagal cocok.

## Yang Sudah Berjalan Baik
- Alert freshness mem-page on-call dalam satu menit setelah ambang terlampaui
- Solusi sementara (pause export) mencegah angka salah sampai ke laporan

## Aksi Lanjutan
| Aksi | Pemilik | Tenggat |
|---|---|---|
| Validasi kosakata status di pintu masuk pipeline | Dewi | 15 Des |
| Perjanjian perubahan nilai kolom kunci dengan tim kasir | Budi | 20 Des |

Kerangka pengelolaan insiden lengkap dari deteksi sampai aksi lanjutan ada di Google SRE Workbook: Incident Response . Namun insiden yang berakhir pada status “resolved” sering kali meninggalkan pekerjaan terbesar: data yang salah harus diproses ulang dan konsumen harus menerima angka yang telah dikoreksi — pokok bahasan Chapter Recovery dan Reprocessing.