Prometheus mengumpulkan angka-angka (metrics) dari aplikasimu secara berkala dan menyimpannya per waktu, lalu Grafana menggambarnya jadi dashboard dan Prometheus membunyikan alert kalau angkanya melewati batas. Bayangin petugas PDAM. Tiap bulan dia datang ke rumahmu dan mencatat angka meteran air. Meterannya cuma bisa naik. Pemakaian bulan ini = angka sekarang dikurangi angka bulan lalu. Kalau meterannya diganti baru dan mulai dari nol, petugas paham itu meteran baru, bukan pemakaian negatif. Prometheus adalah petugasnya (datang sendiri, menarik angka), counter adalah meterannya, dan rate() adalah hitungan pemakaiannya.
Pelajaran Metrics & Monitoring sudah memperkenalkan golden signals dan jenis metric. Sekarang kita pasang semuanya untuk catatan-api di web-1, dan menghitung query-nya sendiri.
🧪 Tebakan cepat: Selasa, 13 Oktober 2026, pukul 19:05 WIB, panel latency p95 menunjukkan tepat 2,5 detik. Padahal request yang gagal menunggu 5 detik sebelum error. Grafana-nya salah hitung? Jawabannya ada di bagian "histogram_quantile: p95 dari bucket".
Prometheus menarik, aplikasi menyediakan /metrics
Prometheus berbasis pull: dia yang mendatangi aplikasi, bukan aplikasi yang mengirim. Aplikasi cukup menyediakan endpoint /metrics berisi angka terkini, dan Prometheus men-scrape (mengambil) endpoint itu tiap scrape_interval. Prometheus dan Grafana jalan sebagai service di docker-compose.yml yang sama dengan app:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 1m
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: catatan-api
static_configs:
- targets: ["app:3000"]
- Tiap 15 detik Prometheus mengambil
http://app:3000/metrics(path/metricsadalah bawaannya). - Tiap sampel dapat label tambahan
job="catatan-api"daninstance="app:3000". evaluation_interval: 1m: aturan alert dialerts.ymldicek tiap menit.
Grafana disambungkan ke Prometheus lewat datasource dengan url: http://prometheus:9090. Grafana gak menyimpan metrics, dia cuma bertanya ke Prometheus tiap kali panel digambar.
Counter dan histogram di catatan-api
Ada empat jenis metric. Counter cuma bisa naik: total request, total error, total pembayaran gagal. Gauge bisa naik turun: koneksi aktif, memori. Histogram mencatat sebaran nilai ke dalam bucket, misalnya durasi request, supaya p50, p95, p99 bisa dihitung. Summary mirip histogram, tapi persentilnya dihitung di aplikasi. catatan-api memakai counter dan histogram lewat library prom-client:
const client = require("prom-client");
const httpRequests = new client.Counter({
name: "http_requests_total",
help: "Jumlah request HTTP yang sudah dilayani",
labelNames: ["method", "route", "status"],
});
const httpDuration = new client.Histogram({
name: "http_request_duration_seconds",
help: "Durasi request HTTP dalam detik",
labelNames: ["method", "route"],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5],
});
app.use((req, res, next) => {
const selesai = httpDuration.startTimer();
res.on("finish", () => {
const route = req.route ? req.baseUrl + req.route.path : "tidak-cocok";
httpRequests.inc({ method: req.method, route, status: res.statusCode });
selesai({ method: req.method, route });
});
next();
});
app.get("/metrics", async (req, res) => {
res.set("Content-Type", client.register.contentType);
res.end(await client.register.metrics());
});
labelNamesmemecah satu metric jadi beberapa series, satu per kombinasi label.routediambil dari pola Express (/catatan/:id), bukan path mentah, supaya jumlah series gak meledak.startTimer()mulai mengukur, danselesai(...)mencatat durasinya ke bucket yang cocok.
Ini isi /metrics yang diambil Prometheus pukul 19:02:52, khusus GET /catatan:
deploy@web-1:~$ curl -s http://127.0.0.1:3000/metrics | grep 'method="GET",route="/catatan"'
http_requests_total{method="GET",route="/catatan",status="200"} 61418
http_requests_total{method="GET",route="/catatan",status="500"} 274
http_request_duration_seconds_bucket{le="0.05",method="GET",route="/catatan"} 36967
http_request_duration_seconds_bucket{le="0.1",method="GET",route="/catatan"} 51758
http_request_duration_seconds_bucket{le="0.25",method="GET",route="/catatan"} 59153
http_request_duration_seconds_bucket{le="0.5",method="GET",route="/catatan"} 60880
http_request_duration_seconds_bucket{le="1",method="GET",route="/catatan"} 61373
http_request_duration_seconds_bucket{le="2.5",method="GET",route="/catatan"} 61644
http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/catatan"} 61692
http_request_duration_seconds_sum{method="GET",route="/catatan"} 5171.664
http_request_duration_seconds_count{method="GET",route="/catatan"} 61692
Bucket-nya kumulatif: le="0.25" berisi semua request yang selesai dalam 0,25 detik atau kurang, jadi le="+Inf" = semua request.
rate(): dari total jadi laju per detik
Angka 61418 sendirian gak berguna: dia cuma bilang "sejak proses ini jalan". Yang kamu mau adalah laju: berapa request per detik sekarang. rate(http_requests_total[5m]) mengambil sampel 5 menit terakhir dan menghitung kenaikannya per detik. Pukul 19:00:00, jendela 5 menit berisi 20 sampel, dari 18:55:07 (53.712) sampai 18:59:52 (58.464):
function rate(samples, end, rangeDetik) {
const isi = samples.filter((s) => s.t > end - rangeDetik && s.t <= end);
let naik = isi[isi.length - 1].v - isi[0].v;
for (let i = 1; i < isi.length; i++) {
if (isi[i].v < isi[i - 1].v) naik += isi[i - 1].v; // counter reset: proses mulai dari 0
}
return naik / (isi[isi.length - 1].t - isi[0].t);
}
(58.464 − 53.712) / 285 detik = 16,674 request per detik berstatus 200. Status 500: (222 − 213) / 285 = 0,032. Totalnya sum(rate(...)) = 16,705, kira-kira 1.000 request per menit. Prometheus juga memperpanjang hitungannya sedikit ke tepi jendela, tapi untuk sampel yang rapi tiap 15 detik seperti ini hasilnya sama persis.
Baris if itu penting. Pukul 19:14:45 container catatan-api diganti (rollback), dan counter proses baru mulai dari 0: sampel 19:14:37 bernilai 63.772, sampel 19:14:52 cuma 115. Selisih biasa jadi negatif. rate() menganggap penurunan sebagai counter reset dan menambahkan nilai sebelumnya, persis petugas PDAM dengan meteran baru. Karena itu rate() hanya untuk counter, jangan untuk gauge.
histogram_quantile: p95 dari bucket
p95 artinya 95% request selesai dalam waktu segitu atau kurang. Prometheus menghitungnya dari laju tiap bucket: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))). Pukul 19:00:00:
le |
laju (per detik) |
|---|---|
| 0.05 | 10,000 |
| 0.1 | 14,004 |
| 0.25 | 16,004 |
| 0.5 | 16,470 |
| 1 | 16,604 |
| 2.5 | 16,705 |
| +Inf | 16,705 |
Posisi ke-95% = 0,95 × 16,705 = 15,870. Bucket pertama yang mencapainya adalah le="0.25". Di antara 0,1 (14,004) dan 0,25 (16,004), Prometheus menganggap request tersebar rata, jadi p95 = 0,1 + 0,15 × (15,870 − 14,004) / (16,004 − 14,004) = 0,240 detik.
function histogramQuantile(q, buckets) { // [{ le, rate }], terakhir le Infinity
const rank = q * buckets[buckets.length - 1].rate;
const i = buckets.findIndex((b) => b.rate >= rank);
if (buckets[i].le === Infinity) return buckets[i - 1].le; // jatuh di +Inf
const bawah = i === 0 ? 0 : buckets[i - 1].le;
const sebelum = i === 0 ? 0 : buckets[i - 1].rate;
return bawah + (buckets[i].le - bawah) * ((rank - sebelum) / (buckets[i].rate - sebelum));
}
Pukul 19:05, 80% request menunggu 5 detik lalu error. Bucket le="2.5" cuma 10,926 per detik, belum mencapai posisi 15,880, jadi posisinya jatuh di +Inf. Prometheus gak tahu seberapa jauh di atas 2,5 request itu, jadi dia mengembalikan batas bucket terbesar yang terhingga: 2,5. Grafana gak salah hitung. Itu jawaban tebakannya. Kalau angka di atas 2,5 detik penting buatmu, tambahkan bucket 5 dan 10.
Panel Grafana dan alert rule
Panel error rate memakai query ini:
sum(rate(http_requests_total{route="/catatan",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{route="/catatan"}[5m]))
Pembilangnya laju request berstatus 5xx (=~"5.." artinya regex: diawali 5, lalu dua karakter apa saja), penyebutnya laju semua request. Hasilnya proporsi request yang gagal dengan error server: 0,0019 (0,19%) pukul 19:00. Di Grafana, panel timeseries dengan unit percentunit menampilkannya sebagai persen. Alert-nya ditulis di alerts.yml:
groups:
- name: catatan-api
rules:
- alert: CatatanApiErrorRateTinggi
expr: |
sum(rate(http_requests_total{route="/catatan",status=~"5.."}[5m]))
/ sum(rate(http_requests_total{route="/catatan"}[5m])) > 0.05
for: 2m
labels:
severity: page
annotations:
summary: "Error rate GET /catatan {{ $value | humanizePercentage }}"
Tiap menit Prometheus mengecek expr. Begitu benar, alert jadi pending. Kalau masih benar setelah for: 2m, alert jadi firing dan dikirim ke Alertmanager, yang meneruskannya ke on-call (pelajaran Alerting & On-Call). Malam itu: 19:04 pending (17,95%), 19:06 firing (51,54%). Rollback 19:14:45, tapi alert baru kembali inactive pukul 19:20, karena jendela 5 menit masih memuat menit-menit yang buruk.
⚠️ Jebakan yang sering ketemu
- Label dengan nilai tak terbatas.
routeberisi path mentah (/catatan/91) atau labeluser_idmembuat ribuan series, dan Prometheus kehabisan memori. Pakai pola route. - Menggambar counter mentah. Grafiknya cuma naik terus dan anjlok saat restart. Selalu bungkus dengan
rate(). - Jendela terlalu pendek.
rate(...[15s])dengan scrape tiap 15 detik sering cuma punya satu sampel, dan hasilnya kosong. Pakai jendela minimal 4 kaliscrape_interval. /metricsterbuka ke internet. Isinya membocorkan route dan traffic. Prometheus mengambilnya dari jaringan Docker, jadi blokir path itu di nginx.
TL;DR: Prometheus menarik (pull) /metrics dari aplikasi tiap scrape_interval (di sini app:3000 tiap 15 detik), Grafana menggambar hasil query-nya. Counter cuma naik (total request, total pembayaran gagal), gauge naik turun, histogram mencatat sebaran ke bucket kumulatif. rate(x[5m]) mengubah counter jadi laju per detik dan menangani counter reset saat proses restart. histogram_quantile(0.95, sum by (le) (rate(..._bucket[5m]))) menghitung p95 dengan interpolasi di dalam bucket, dan kalau posisinya jatuh di +Inf, hasilnya batas bucket terbesar (2,5). Error rate = laju 5xx dibagi laju semua request. Alert rule dengan for: 2m melewati pending dulu sebelum firing. Label harus terbatas, jendela minimal 4 kali scrape interval, dan /metrics jangan dibuka ke publik.