Tampilan
Kenapa penting
Health check dari bab Actuator hanya menjawab "hidup atau mati". Di production, pertanyaan yang lebih sering muncul adalah "seberapa sehat?": berapa request per detik yang masuk, berapa persen yang gagal, apakah checkout melambat sejak deploy tadi siang, apakah pool koneksi database hampir habis. Jawaban atas pertanyaan itu datang dari metrics — angka yang dikumpulkan terus-menerus dan bisa digambar sebagai grafik. Tanpa metrik, kamu baru tahu ada masalah setelah pelanggan komplain. Dengan metrik dan alert yang tepat, kamu tahu lebih dulu, dan punya data untuk membuktikan apakah perbaikanmu berhasil. Hampir semua tim backend Java memakai kombinasi Micrometer, Prometheus, dan Grafana untuk ini.
Konsep inti
Tiga pilar observability
Observability adalah kemampuan memahami apa yang terjadi di dalam sistem hanya dari data yang dikeluarkannya. Ada tiga jenis data utama:
| Pilar | Bentuk | Menjawab | Contoh |
|---|---|---|---|
| Metrics | angka agregat per waktu (time series) | "Ada masalah? Seberapa besar?" | 2% request checkout gagal, p95 = 800 ms |
| Logs | catatan kejadian satu per satu | "Apa yang terjadi persisnya?" | payment declined orderId=ORD-91 |
| Traces | perjalanan satu request lintas komponen | "Di bagian mana waktunya habis?" | 600 ms dari 800 ms ada di query stok |
Metrics murah disimpan karena sudah diagregasi, sehingga cocok untuk dashboard dan alert. Bab ini fokus di metrics; logs dan traces dibahas di bab berikutnya.
Micrometer: facade untuk metrik
Micrometer berperan seperti SLF4J untuk logging: kamu menulis kode ke satu API, lalu memilih backend (Prometheus, Datadog, OTLP, dsb) lewat dependency. Spring Boot Actuator sudah membawa Micrometer dan menyediakan bean MeterRegistry. Ada empat jenis meter yang paling sering dipakai:
| Meter | Sifat | Contoh pemakaian |
|---|---|---|
Counter | hanya naik | jumlah order dibuat, jumlah pembayaran gagal |
Gauge | nilai sesaat, bisa naik-turun | ukuran antrean, stok tersisa |
Timer | durasi + jumlah kejadian | lama memanggil payment gateway |
DistributionSummary | sebaran nilai non-waktu | nominal transaksi, ukuran payload |
java
// Potongan — service Spring dengan MeterRegistry dari Actuator
@Service
public class OrderMetrics {
private final Counter ordersCreated;
private final DistributionSummary orderAmount;
private final Timer gatewayTimer;
public OrderMetrics(MeterRegistry registry, PendingOrderQueue queue) {
this.ordersCreated = Counter.builder("orders.created")
.description("Jumlah order yang berhasil dibuat")
.register(registry);
this.orderAmount = DistributionSummary.builder("orders.amount")
.baseUnit("rupiah")
.register(registry);
this.gatewayTimer = Timer.builder("payment.gateway.duration")
.tag("provider", "midtrans")
.register(registry);
// Gauge membaca nilai saat di-scrape; registry hanya menyimpan weak reference ke queue
Gauge.builder("orders.pending", queue, PendingOrderQueue::size)
.register(registry);
}
public void recordCreated(long amount) {
ordersCreated.increment();
orderAmount.record(amount);
}
public PaymentResult charge(PaymentRequest request, PaymentGateway gateway) {
return gatewayTimer.record(() -> gateway.charge(request));
}
}Nama meter memakai titik (orders.created). Registry Prometheus otomatis mengubahnya ke konvensi Prometheus: orders_created_total untuk counter dan payment_gateway_duration_seconds_count/_sum/_max untuk timer.
Tag dan bahaya high cardinality
Tag (di Prometheus disebut label) memecah satu metrik menjadi beberapa time series, misalnya per outcome:
java
// Potongan
public void recordPayment(String outcome) { // "success", "declined", "error"
Counter.builder("payments.processed")
.tag("outcome", outcome)
.register(registry) // register idempotent: nama + tag sama → meter yang sama
.increment();
}Setiap kombinasi nilai tag adalah satu time series yang disimpan terpisah. Tiga outcome × lima provider = 15 series, aman. Tetapi tag orderId, userId, email, atau URL mentah dengan ID di dalamnya membuat series baru untuk setiap nilai — ini disebut high cardinality. Prometheus akan kehabisan memori dan query melambat. Aturan praktis: nilai tag harus berasal dari himpunan kecil yang bisa kamu sebutkan. Untuk menelusuri satu order tertentu, pakai log dan trace.
Metrik bawaan Spring Boot
Begitu Actuator terpasang, kamu langsung mendapat banyak metrik tanpa menulis kode:
| Metrik | Isi |
|---|---|
http.server.requests | timer per method, uri (template, mis. /orders/{id}), status, outcome |
jvm.memory.used, jvm.gc.pause | memori heap/non-heap dan jeda garbage collector |
jvm.threads.live | jumlah thread |
hikaricp.connections.active / .pending | koneksi pool DB yang dipakai dan yang menunggu |
process.cpu.usage | pemakaian CPU proses |
Perhatikan tag uri memakai template route, bukan path asli — itulah cara Spring mencegah high cardinality.
@Timed dan Observation API
Untuk mengukur sebuah method tanpa menulis Timer manual, kamu bisa memakai anotasi. Sejak Spring Boot 3, cara yang direkomendasikan adalah Observation API: satu instrumentasi menghasilkan metrik dan span trace sekaligus.
java
// Potongan — butuh spring-boot-starter-aspectj dan
// management.observations.annotations.enabled=true
@Service
public class StockService {
@Observed(name = "stock.reserve", contextualName = "reserve-stock")
public void reserve(String sku, int qty) {
// ...
}
@Timed(value = "stock.recount", description = "Lama menghitung ulang stok gudang")
public void recount() {
// ...
}
}Atau secara programatik, berguna bila kamu butuh tag yang dihitung saat runtime:
java
// Potongan — ObservationRegistry disediakan Spring Boot
Observation.createNotStarted("inventory.sync", observationRegistry)
.lowCardinalityKeyValue("warehouse", "JKT-1") // jadi tag metrik
.highCardinalityKeyValue("batch.id", batchId) // hanya di span trace, bukan di metrik
.observe(() -> syncBatch(batchId));Pemisahan lowCardinalityKeyValue dan highCardinalityKeyValue adalah cara Micrometer membantumu menghindari ledakan series: nilai berkardinalitas tinggi hanya ikut ke trace.
Prometheus scrape
Prometheus bekerja dengan model pull: setiap beberapa detik ia memanggil endpoint HTTP aplikasi dan menyimpan angkanya. Tambahkan registry Prometheus lalu ekspos endpoint-nya:
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>yaml
# application.yml
spring:
application:
name: order-service
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
tags:
application: ${spring.application.name} # tag umum di semua metrik
distribution:
percentiles-histogram:
http.server.requests: true # ekspor bucket histogram untuk p95/p99Isi /actuator/prometheus berupa teks seperti ini (dipersingkat):
text
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{application="order-service",method="POST",outcome="SUCCESS",status="201",uri="/orders",le="0.1"} 812
http_server_requests_seconds_bucket{application="order-service",method="POST",outcome="SUCCESS",status="201",uri="/orders",le="0.5"} 990
http_server_requests_seconds_count{application="order-service",method="POST",outcome="SUCCESS",status="201",uri="/orders"} 1003
http_server_requests_seconds_sum{application="order-service",method="POST",outcome="SUCCESS",status="201",uri="/orders"} 98.4Konfigurasi Prometheus untuk men-scrape aplikasi:
yaml
# prometheus.yml
scrape_configs:
- job_name: order-service
metrics_path: /actuator/prometheus
scrape_interval: 15s
static_configs:
- targets: ["order-service:8080"]PromQL dasar
Counter terus naik sejak aplikasi start, jadi nilai mentahnya jarang berguna. Fungsi rate mengubahnya menjadi laju per detik dalam sebuah jendela waktu, dan otomatis menangani reset saat aplikasi restart.
text
# Throughput: request per detik per URI
sum by (uri) (rate(http_server_requests_seconds_count[5m]))
# Rasio error 5xx
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count[5m]))
# Latency p95 POST /orders (butuh bucket histogram)
histogram_quantile(0.95,
sum by (le) (rate(http_server_requests_seconds_bucket{uri="/orders", method="POST"}[5m])))histogram_quantile memperkirakan persentil dari bucket. Karena dihitung di server dari bucket semua instance, hasilnya bisa diagregasi lintas instance — berbeda dengan persentil yang dihitung di dalam aplikasi (percentiles), yang tidak bisa dirata-rata dengan benar.
Dashboard Grafana: RED dan USE
Grafana membaca Prometheus sebagai data source dan menampilkan query PromQL sebagai panel. Supaya dashboard tidak jadi kumpulan grafik acak, pakai dua metode berikut:
| Metode | Untuk | Panel |
|---|---|---|
| RED | service (endpoint, API) | Rate (request/detik), Errors (rasio gagal), Duration (p50/p95/p99) |
| USE | resource (CPU, pool, memori) | Utilization, Saturation (antrean/menunggu), Errors |
Untuk order-service, baris atas berisi RED per endpoint; baris bawah berisi USE untuk pool HikariCP (koneksi aktif vs maksimum, pending), heap JVM, dan CPU.
Alert berbasis SLO
SLI (Service Level Indicator) adalah ukuran yang dirasakan pengguna, misalnya persentase request checkout yang sukses. SLO adalah targetnya, misalnya 99,9% sukses dalam 30 hari. Sisa 0,1% disebut error budget. Alert yang baik berbunyi ketika error budget terbakar terlalu cepat, bukan ketika CPU sempat tinggi sebentar.
yaml
# Potongan aturan alert Prometheus
groups:
- name: order-service-slo
rules:
- alert: CheckoutErrorBudgetBurn
expr: |
sum(rate(http_server_requests_seconds_count{uri="/checkout",status=~"5.."}[1h]))
/ sum(rate(http_server_requests_seconds_count{uri="/checkout"}[1h])) > 14.4 * 0.001
for: 5m
labels:
severity: page
annotations:
summary: "Error checkout membakar error budget 99,9% terlalu cepat"Angka 14.4 adalah burn rate: pada laju ini, budget 30 hari habis dalam kira-kira dua hari. Detail teknik multi-window bisa kamu pelajari dari Google SRE Workbook.
Contoh di dunia backend
Layanan pembayaran ingin tahu tiga hal: berapa transaksi per outcome, berapa lama gateway merespons, dan berapa nominal yang diproses. Semua dicatat di satu tempat dengan tag berkardinalitas rendah.
java
// Potongan — PaymentService dengan metrik bisnis
@Service
public class PaymentService {
private final PaymentGateway gateway;
private final MeterRegistry registry;
private final Timer gatewayLatency;
public PaymentService(PaymentGateway gateway, MeterRegistry registry) {
this.gateway = gateway;
this.registry = registry;
this.gatewayLatency = Timer.builder("payment.gateway.duration")
.description("Latency panggilan ke payment gateway")
.publishPercentileHistogram() // bucket untuk histogram_quantile
.register(registry);
}
public PaymentResult pay(PaymentRequest request) {
Timer.Sample sample = Timer.start(registry);
String outcome = "error";
try {
PaymentResult result = gateway.charge(request);
outcome = result.approved() ? "approved" : "declined";
if (result.approved()) {
DistributionSummary.builder("payment.amount")
.baseUnit("rupiah")
.tag("method", request.method().name()) // enum: CARD, VA, EWALLET
.register(registry)
.record(request.amount());
}
return result;
} finally {
sample.stop(gatewayLatency);
registry.counter("payment.processed", "outcome", outcome).increment();
}
}
}Lalu panel Grafana-nya:
text
# Persentase pembayaran ditolak
sum(rate(payment_processed_total{outcome="declined"}[5m])) / sum(rate(payment_processed_total[5m]))
# p95 latency gateway
histogram_quantile(0.95, sum by (le) (rate(payment_gateway_duration_seconds_bucket[5m])))
# Nominal per detik per metode
sum by (method) (rate(payment_amount_rupiah_sum[5m]))Perhatikan outcome dicatat di finally sehingga exception tetap terhitung sebagai error, dan orderId sengaja tidak dijadikan tag.
Pertanyaan interview
1. Apa beda Counter, Gauge, dan Timer?
Counter hanya naik dan dipakai untuk menghitung kejadian; lajunya dihitung di Prometheus dengan rate. Gauge mencerminkan nilai sesaat yang bisa naik-turun, seperti ukuran antrean atau koneksi aktif, dan dibaca saat scrape. Timer merekam durasi sekaligus jumlah kejadian, sehingga dari satu timer kamu mendapat throughput, rata-rata, dan (dengan histogram) persentil latency.
2. Kenapa memakai persentil (p95/p99) dan bukan rata-rata latency?
Rata-rata menyembunyikan ekor distribusi. Bila 95% request selesai 50 ms tetapi 5% butuh 4 detik, rata-ratanya terlihat wajar padahal satu dari dua puluh pengguna menunggu lama. p95 menyatakan "95% request lebih cepat dari angka ini", sehingga lebih jujur menggambarkan pengalaman pengguna. Persentil sebaiknya dihitung dari bucket histogram di Prometheus agar bisa diagregasi lintas instance.
3. Apa itu high cardinality dan bagaimana mencegahnya?
High cardinality terjadi ketika sebuah tag punya sangat banyak nilai unik, misalnya user ID atau path dengan ID. Setiap nilai menjadi time series baru, sehingga memori dan waktu query storage metrik membengkak. Pencegahannya: hanya pakai tag dengan himpunan nilai kecil, gunakan template URI, dan taruh identitas spesifik di log atau atribut trace (di Observation API lewat highCardinalityKeyValue).
Jebakan umum
- Tag berisi ID atau input user.
userId,orderId, email, atau pesan exception mentah sebagai tag akan meledakkan jumlah series. Pakai kategori sepertioutcomeatau nama class exception. - Lupa mengaktifkan histogram. Tanpa
percentiles-histogramataupublishPercentileHistogram(), tidak ada series_bucketdanhistogram_quantilekosong. - Memakai nilai counter mentah di dashboard. Counter reset ke nol saat restart; selalu bungkus dengan
rateatauincrease. - Gauge kehilangan objeknya. Micrometer menyimpan weak reference ke objek yang diukur gauge; bila objek itu tidak dipegang di tempat lain, ia bisa di-garbage-collect dan gauge melaporkan
NaN. - Mengekspos semua endpoint Actuator ke publik. Cukup
healthdanprometheus, dan sebaiknya batasi akses/actuator/prometheuske jaringan internal atau port management terpisah. - Alert untuk setiap gejala. Alert CPU atau heap yang sering berbunyi tanpa dampak ke pengguna membuat tim kebal terhadap alert. Alert yang membangunkan orang sebaiknya berbasis SLO.
Ringkasan
- Observability bertumpu pada metrics (agregat, murah, untuk alert), logs (detail kejadian), dan traces (alur request).
- Micrometer adalah facade metrik di Spring Boot:
Counter,Gauge,Timer,DistributionSummary, dengan tag berkardinalitas rendah. - Actuator memberi metrik bawaan HTTP, JVM, dan HikariCP; Prometheus men-scrape
/actuator/prometheus. ratemengubah counter menjadi laju;histogram_quantilemenghitung p95 dari bucket histogram.- Susun dashboard dengan RED untuk service dan USE untuk resource; pasang alert berdasarkan SLO dan error budget.