Observability adalah kemampuan menjawab pertanyaan baru tentang sistem yang sedang jalan hanya dari output-nya (metrics, traces, logs), tanpa menambah kode atau deploy ulang. Di distributed system ini penting, karena satu request melewati banyak service dan masalahnya sering muncul di tempat yang gak kamu duga. Bayangin jasa ekspedisi. Dashboard kantor pusat bilang "paket terlambat naik sejak jam 3 sore": itu metric. Lacak resi satu paket menunjukkan ia nyangkut 2 hari di gudang transit: itu trace. Catatan kurir bilang "gudang transit tutup, dialihkan": itu log. Yang menyambungkan ketiganya adalah nomor resi, dan di software namanya trace_id.
🧪 Tebakan cepat: mulai jam 19:30:30, p99 checkout TiketNada melompat dari 260-an ms ke 2.258 ms, tapi p50 tetap sekitar 180 ms. Berapa bagian pembeli yang kena: hampir semua, sekitar seperempat, atau cuma 1 dari 100? Tebak dulu, jawabannya ada di bagian "Pecah per field: siapa yang lambat".
Tiga sinyal, tiga pertanyaan
Lesson Monitoring & Logging sudah mengenalkan tiga pilar. Kuncinya: tiap sinyal menjawab pertanyaan yang berbeda.
| Sinyal | Menjawab | Di kasus ini |
|---|---|---|
| Metrics | apakah ada masalah? | p99 checkout naik |
| Traces | di mana masalahnya? | span bayar yang menunggu provider |
| Logs | kenapa terjadi? | provider timeout, coba lagi |
Ketiganya baru berguna bersama kalau disambung trace_id yang sama: dibawa header traceparent dari gateway ke tiap service. Cara memasangnya ada di lesson OpenTelemetry Fundamentals dan Distributed Tracing (track DevOps). Lesson ini tentang memakainya untuk menyelidiki.
Metric: lonjakan jam 19:30:30
Model contoh: satu menit checkout TiketNada, 19:30:00 sampai 19:31:00, 4 checkout per detik, 240 total. Metric checkout_duration_seconds per 10 detik:
| Jam | p50 | p99 |
|---|---|---|
| 19:30:00 | 200 ms | 267 ms |
| 19:30:10 | 180 ms | 273 ms |
| 19:30:20 | 182 ms | 260 ms |
| 19:30:30 | 183 ms | 2.258 ms |
| 19:30:40 | 187 ms | 2.272 ms |
| 19:30:50 | 157 ms | 2.253 ms |
p50 tetap, p99 melompat hampir 9 kali. Artinya sebagian pembeli lambat sekali, sebagian besar normal. Metric menjawab "ada masalah", tapi belum bilang siapa yang kena dan kenapa.
Pecah per field: siapa yang lambat
Tiap checkout tercatat sebagai kejadian lengkap dengan field-nya: trace_id, user_id, region, metode bayar, dan versi app. Jadi kamu bisa memecah 120 checkout di jendela lonjakan (19:30:30 sampai 19:31:00) per field apa pun:
| Dipecah per | Hasil |
|---|---|
| region | jakarta, surabaya, medan, makassar: semua p99 di atas 2.200 ms. Bukan pembedanya. |
| versi app | keempat versi juga di atas 2.200 ms. Bukan pembedanya. |
| metode | ewallet: 33 checkout, p50 2.240 ms, p99 2.272 ms; kartu p99 219 ms; va p99 150 ms |
Jawaban tebakannya: sekitar seperempat. 33 dari 120 checkout (27,5%) lambat, dan semuanya pembayaran e-wallet. Itulah bedanya dengan monitoring: monitoring adalah dashboard dan alert yang disiapkan untuk masalah yang sudah kamu duga ("CPU naik? alert"). Observability adalah bisa mengajukan pertanyaan baru, misalnya "kenapa pembeli e-wallet lambat tapi pembeli kartu gak?", tanpa deploy ulang, karena data yang dibutuhkan sudah ada di tiap kejadian.
// 8 checkout pertama dari lonjakan 19:30:30 (data Lab)
const checkout = [
{ user: "u-7914", region: "jakarta", metode: "kartu", ms: 163 },
{ user: "u-3032", region: "surabaya", metode: "kartu", ms: 185 },
{ user: "u-5029", region: "jakarta", metode: "va", ms: 118 },
{ user: "u-8711", region: "surabaya", metode: "va", ms: 135 },
{ user: "u-8179", region: "jakarta", metode: "ewallet", ms: 2218 },
{ user: "u-9074", region: "jakarta", metode: "va", ms: 129 },
{ user: "u-7756", region: "jakarta", metode: "ewallet", ms: 2244 },
{ user: "u-8944", region: "surabaya", metode: "ewallet", ms: 2223 },
];
// persentil nearest rank: urutkan, ambil urutan ke ceil(p% × n)
const persen = (xs, p) => [...xs].sort((a, b) => a - b)[Math.ceil((p * xs.length) / 100) - 1];
function pecah(data, field) {
const grup = new Map();
for (const c of data) grup.set(c[field], [...(grup.get(c[field]) || []), c.ms]);
for (const [nilai, xs] of grup) {
console.log(field, nilai, "n", xs.length, "p50", persen(xs, 50), "p99", persen(xs, 99));
}
}
pecah(checkout, "region"); // jakarta p99 2244, surabaya p99 2223: sama-sama lambat
pecah(checkout, "metode"); // kartu p99 185, va p99 135, ewallet p99 2244
grupmengumpulkan durasi per nilai field, misalnya semua checkoutewalletjadi satu daftar.persenmemakai nearest rank, sama dengan lesson Metrics & Monitoring.pecah(checkout, "metode")adalah pertanyaan baru yang dijawab dari data yang sudah ada. Ganti"metode"dengan field lain, gak perlu deploy.
High-cardinality: field di kejadian, bukan di label
Kenapa gak langsung pasang semua field itu sebagai label metric? Karena tiap nilai label jadi time series sendiri. Histogram checkout_duration_seconds di sini punya 7 bucket plus _sum dan _count, jadi 9 series per kombinasi label. Label metode (3 nilai) = 27 series, murah. Label user_id dengan 1.000.000 pengguna aktif harian = 9.000.000 series, dan penyimpanan metric meledak. Field yang nilainya sangat banyak disebut high-cardinality.
Aturannya: metric tetap kecil dengan label sedikit. Field high-cardinality seperti user_id dan trace_id ditaruh di span dan log, yang memang dicari per kejadian. Di sana field itu justru paling berharga: "kenapa checkout u-8177 lambat?" langsung terjawab.
Exemplar: dari titik di grafik ke satu trace
Bagaimana pindah dari batang p99 yang merah ke satu request yang sungguhan lambat? Lewat exemplar: contoh satu observasi yang ditempel ke bucket histogram, lengkap dengan trace_id-nya. Di format OpenMetrics (yang dibaca Prometheus), bentuknya:
checkout_duration_seconds_bucket{le="2.5"} 240 # {trace_id="5dffd064889e792b4ba982466d9637c9"} 2.253
Grafana menggambarnya sebagai titik di atas grafik. Klik titiknya, dan trace-nya terbuka:
| Span | Mulai | Durasi |
|---|---|---|
| gateway POST /api/orders | 0 ms | 2.253 ms |
| akun: cek login | 2 ms | 6 ms |
| konser: TahanKursi | 10 ms | 14 ms |
| order: simpan pesanan | 26 ms | 13 ms |
| bayar: PotongKartu | 41 ms | 2.210 ms |
| dompetku POST /charge (percobaan 1) | 41 ms | 2.000 ms, timeout |
| dompetku POST /charge (percobaan 2) | 2.041 ms | 210 ms, 200 |
Trace menjawab di mana: dari 2.253 ms, 2.000 ms habis menunggu provider e-wallet dompetku di percobaan pertama. Pecah per field memberi tahu siapa yang kena, trace menunjukkan service mana yang membuatnya lambat.
Log dengan trace_id yang sama
Sekarang cari semua log dari semua service dengan trace_id itu. Dari ratusan baris, tersisa empat (skema dari lesson Structured Logging Patterns):
{"ts":"2026-10-09T19:30:57.250+07:00","level":"info","service":"order","trace_id":"5dffd064889e792b4ba982466d9637c9","user_id":"u-8177","metode":"ewallet","msg":"checkout dimulai"}
{"ts":"2026-10-09T19:30:59.291+07:00","level":"warn","service":"bayar","trace_id":"5dffd064889e792b4ba982466d9637c9","provider":"dompetku","attempt":1,"timeout_ms":2000,"msg":"provider timeout, coba lagi"}
{"ts":"2026-10-09T19:30:59.501+07:00","level":"info","service":"bayar","trace_id":"5dffd064889e792b4ba982466d9637c9","attempt":2,"duration_ms":210,"msg":"charge sukses"}
{"ts":"2026-10-09T19:30:59.503+07:00","level":"info","service":"order","trace_id":"5dffd064889e792b4ba982466d9637c9","status":201,"duration_ms":2253,"msg":"checkout selesai"}
Baris warn menjawab kenapa: dompetku gak menjawab percobaan pertama, bayar menunggu sampai timeout_ms 2000, lalu mencoba lagi dan sukses. Langkah berikutnya jelas: hubungi dompetku, dan pertimbangkan timeout yang lebih pendek atau circuit breaker (lesson Resilience Patterns).
RED untuk service, USE untuk resource
Untuk memasang alert, ada dua resep:
- RED untuk service yang digerakkan request, seperti API
order: Rate (request per detik), Errors (persen gagal), Duration (p50, p95, p99). - USE untuk resource seperti CPU, memory, disk, network, dan connection pool: Utilization (persen terpakai), Saturation (antrean yang menunggu), Errors (jumlah error).
Jadi API order dipantau dengan RED, disk di node PostgreSQL dengan USE. Detail tiap angka ada di lesson Metrics & Monitoring.
🎯 SLO dan error budget
SLO (Service Level Objective) adalah target internal, misalnya "99% checkout selesai di bawah 1 detik, dihitung per 30 hari". Sisa 1%-nya disebut error budget: jatah yang boleh gagal. Di lonjakan tadi, 27,5% checkout melewati 1 detik, jadi budget-nya terbakar sekitar 27 kali lebih cepat dari jatahnya (burn rate 27,5). Alert pada burn rate berbunyi dalam hitungan menit. Alert pada CPU gak akan berbunyi: bayar cuma menunggu jaringan, CPU-nya santai.
⚠️ Jebakan yang sering ketemu
- Alert cuma di CPU dan memory. Masalah yang dirasakan pembeli (checkout lambat) bisa terjadi saat CPU santai. Alert pada SLO dan RED.
user_idatautrace_idsebagai label metric. Jutaan time series. Taruh di span dan log, sambungkan dengan exemplar.- trace_id gak diteruskan. Log dari
orderdanbayargak bisa disatukan, dan penyelidikan berhenti di metric. - Log kalimat bebas.
"error bayar"gak bisa difilter perprovideratauattempt. Pakai JSON dengan field tetap.
TL;DR: Observability = bisa menjawab pertanyaan baru tentang sistem dari output-nya tanpa deploy ulang; monitoring = dashboard dan alert untuk masalah yang sudah diduga. Metrics menjawab apakah ada masalah, traces di mana, logs kenapa, dan trace_id menyambungkan ketiganya. Di TiketNada, p99 checkout melompat ke 2.258 ms jam 19:30:30 sementara p50 tetap 180-an ms; dipecah per metode, cuma ewallet yang lambat (33 dari 120, 27,5%). Field high-cardinality (user_id, trace_id) disimpan di span dan log, bukan label metric (1.000.000 user × 9 series = 9.000.000 time series). Exemplar menempelkan trace_id ke bucket histogram, sehingga satu klik membuka trace: 2.000 ms habis menunggu dompetku sampai timeout. Log dengan trace_id yang sama menjelaskan kenapa. RED untuk service, USE untuk resource, dan alert pada SLO serta burn rate, bukan CPU.