Health check memberi jawaban biner; metrik memberi sejarah berangka. “Berapa pesanan diproses per menit?”, “seberapa dalam antrean sekarang?”, “berapa persentase request di bawah 100 ms?” — pertanyaan-pertanyaan ini dijawab deret angka yang dicatat terus-menerus, dan deret itulah yang membuat grafik, peringatan dini, serta kapasitas bisa direncanakan dari data. Berbeda dari log yang menyimpan peristiwa satu per satu, metrik menyimpan agregat: satu angka per rentang waktu, murah disimpan bertahun-tahun. Chapter ini membangun registri metrik mini dengan tiga bentuk dasarnya, mengeksposnya dalam format Prometheus, lalu memetakan penerapan nyatanya lewat Micrometer.
Tiga Jenis Metrik #
| Bentuk | Sifat nilai | Contoh | Pertanyaan yang dijawab |
|---|---|---|---|
| Counter | hanya naik | pesanan_dibuat_total | sudah berapa banyak? |
| Gauge | naik-turun | antrean_pesanan | berapa sekarang? |
| Histogram | sebaran | latensi_pesanan_seconds | berapa lama, seberapa tersebar? |
Counter tidak pernah turun — aplikasi yang direstart memulai dari nol dan sistem pemantau menghitung laju kenaikannya (rate()). Gauge memotret kondisi sesaat. Histogram menukar nilai individual dengan sebaran: observasi dimasukkan ke bucket, menghasilkan kuantil seperti p95 tanpa menyimpan durasi mentah.
Model Pull dan Eksposur #
Prometheus — standar de-facto di ekosistem ini — bekerja dengan model pull: aplikasi mengekspos seluruh metriknya sebagai teks di GET /metrics, server pemantau datang mengambil setiap 15-30 detik. Konfigurasi sisi server:
scrape_configs:
- job_name: kasirapp
scrape_interval: 15s
metrics_path: /metrics
static_configs:
- targets: ["kasirapp-1.internal:8080", "kasirapp-2.internal:8080"]Isi endpoint itu bisa kita pahami utuh lewat registri mini berikut:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** Registri metrik mini: counter, gauge, histogram + eksposur format Prometheus. */
public class PenaMetrik {
/** Counter: nilai yang hanya naik — jumlah kejadian sejak aplikasi menyala. */
static class Counter {
private double nilai;
void inc() {
nilai += 1;
}
void inc(double n) {
nilai += n;
}
double nilai() {
return nilai;
}
}
/** Gauge: nilai yang bisa naik-turun — kondisi saat ini. */
static class Gauge {
private double nilai;
void set(double n) {
nilai = n;
}
double nilai() {
return nilai;
}
}
/** Histogram: sebaran durasi ke dalam bucket kumulatif. */
static class Histogram {
private final double[] batas;
private final long[] bucket;
private double jumlah;
private long cacah;
Histogram(double[] batas) {
this.batas = batas;
this.bucket = new long[batas.length];
}
void amati(double nilai) {
jumlah += nilai;
cacah++;
for (int i = 0; i < batas.length; i++) {
if (nilai <= batas[i]) {
bucket[i]++;
}
}
}
void ekspos(String nama, StringBuilder out) {
for (int i = 0; i < batas.length; i++) {
out.append(String.format("%s_bucket{le=\"%s\"} %d%n", nama,
(batas[i] >= Double.POSITIVE_INFINITY) ? "+Inf" : batas[i], bucket[i]));
}
out.append(String.format("%s_count %d%n", nama, cacah));
out.append(String.format("%s_sum %s%n", nama, jumlah));
}
}
public static void main(String[] args) {
Counter pesanan = new Counter();
pesanan.inc();
pesanan.inc();
pesanan.inc(2.0); // dua pesanan sekaligus dari satu pelanggan
Gauge antrean = new Gauge();
antrean.set(7);
Histogram latensi = new Histogram(new double[]{0.1, 0.5, 1.0, Double.POSITIVE_INFINITY});
for (double d : List.of(0.08, 0.12, 0.34, 0.41, 0.9, 1.4)) {
latensi.amati(d);
}
System.out.println("== Nilai internal registri ==");
System.out.printf("pesanan_dibuat_total = %.0f%n", pesanan.nilai());
System.out.printf("antrean_pesanan = %.0f%n", antrean.nilai());
System.out.println();
System.out.println("== Eksposur GET /metrics (format teks Prometheus, tanpa baris komentar) ==");
StringBuilder out = new StringBuilder();
out.append(String.format("pesanan_dibuat_total %.0f%n", pesanan.nilai()));
out.append(String.format("antrean_pesanan %.0f%n", antrean.nilai()));
latensi.ekspos("latensi_pesanan_seconds", out);
System.out.print(out);
System.out.println();
Map<String, Double> contohJvm = new TreeMap<>();
contohJvm.put("jvm_memory_used_bytes{area=\"heap\"}", 268435456.0);
contohJvm.put("jvm_memory_used_bytes{area=\"nonheap\"}", 71303168.0);
System.out.println("== Metrik JVM yang ikut terekspos otomatis (contoh) ==");
for (var e : contohJvm.entrySet()) {
System.out.printf("%s %.0f%n", e.getKey(), e.getValue());
}
}
}Output yang Dihasilkan:
== Nilai internal registri ==
pesanan_dibuat_total = 4
antrean_pesanan = 7
== Eksposur GET /metrics (format teks Prometheus, tanpa baris komentar) ==
pesanan_dibuat_total 4
antrean_pesanan 7
latensi_pesanan_seconds_bucket{le="0.1"} 1
latensi_pesanan_seconds_bucket{le="0.5"} 4
latensi_pesanan_seconds_bucket{le="1.0"} 5
latensi_pesanan_seconds_bucket{le="+Inf"} 6
latensi_pesanan_seconds_count 6
latensi_pesanan_seconds_sum 3.25
== Metrik JVM yang ikut terekspos otomatis (contoh) ==
jvm_memory_used_bytes{area="heap"} 268435456
jvm_memory_used_bytes{area="nonheap"} 71303168Mari kita bedah registri dan eksposurnya:
Counter.inc(): setiap kejadian menambah nilai — program mensimulasikan empat pesanan dari tiga kejadian (satuinc(2.0)untuk pesanan ganda). Nama berakhiran_totaladalah konvensi counter Prometheus.Gauge.set(...): nilai diperbarui saat kondisi berubah — antrean 7 saat pemotretan. Berbeda dari counter, gauge yang tidak diperbarui nilainya memang membusuk: aplikasi mati berarti antrean tidak terbaca lagi.Histogram.amati(...): enam observasi dimasukkan ke empat bucket. Barisle="0.5"terbaca “4 observasi ≤ 0,5 detik” — bucket Prometheus kumulatif, dan+Infselalu sama dengan_count. Nilaisum3,25 memungkinkan rata-rata; kuantil dihitung server dari perbandingan antar-bucket.- Baris komentar
# HELPdan# TYPE: pada endpoint sungguhan, tiap metrik didahului komentar satu baris (# TYPE pesanan_dibuat_total counter) yang menjelaskan jenis dan maknanya — program kita menghilangkan baris komentar itu agar keluaran ringkas. Server pemantau memakai baris tersebut untuk membedakan counter dari gauge tanpa mengandalkan konvensi nama saja. - Metrik JVM: pustaka JVM menambahkan deret otomatis — pemakaian heap/nonheap, jumlah thread, waktu GC — contoh dua barisnya terlihat di bagian akhir. Perhatikan label
{area="heap"}: metrik yang sama dengan label berbeda menjadi deret terpisah.
Penerapan Java dengan Micrometer #
Menulis counter sendiri berlebihan di proyek nyata — Micrometer menjadi fasad instrumentasi yang seragam untuk banyak backend (Prometheus, OpenTelemetry, Datadog):
Counter pesanan = Counter.builder("pesanan.dibuat")
.description("Jumlah pesanan yang berhasil dibuat")
.tag("app", "kasirapp")
.register(registry);
Timer latensi = Timer.builder("pesanan.latensi")
.publishPercentileHistogram()
.register(registry);
pesanan.increment();
latensi.record(Duration.ofMillis(240));Pada aplikasi Spring Boot, Actuator merangkai semuanya: anotasi @Timed, metrik JVM otomatis, dan endpoint /actuator/prometheus yang formatnya persis keluaran program kita di atas. Pustaka HTTP dan JDBC ikut menyumbang metrik bawaan — laju request per endpoint, waktu kueri per repositori.
Alerting dan Dashboard #
Angka baru berguna ketika dibandingkan: PromQL rate(pesanan_dibuat_total[5m]) menghasilkan pesanan per detik, dan histogram_quantile(0.95, rate(latensi_pesanan_seconds_bucket[5m])) menghasilkan latensi p95. Aturan peringatan didefinisikan dari ekspresi ini — “tingkat error > 2% selama 5 menit” membangunkan yang bertugas, sementara dashboard menampilkan empat sinyal emas: latensi, trafik, error, saturasi.
Metrik mengukur akibat — latensi naik, error bertambah. Tetapi mencari penyebabnya di dalam JVM membutuhkan alat yang melihat lebih dalam: profiler. Chapter berikutnya membahas JFR dan teknik profiling untuk menemukan method yang menyerap waktu prosesor.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PenaMetrik.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** Registri metrik mini: counter, gauge, histogram + eksposur format Prometheus. */
public class PenaMetrik {
/** Counter: nilai yang hanya naik — jumlah kejadian sejak aplikasi menyala. */
static class Counter {
private double nilai;
void inc() {
nilai += 1;
}
void inc(double n) {
nilai += n;
}
double nilai() {
return nilai;
}
}
/** Gauge: nilai yang bisa naik-turun — kondisi saat ini. */
static class Gauge {
private double nilai;
void set(double n) {
nilai = n;
}
double nilai() {
return nilai;
}
}
/** Histogram: sebaran durasi ke dalam bucket kumulatif. */
static class Histogram {
private final double[] batas;
private final long[] bucket;
private double jumlah;
private long cacah;
Histogram(double[] batas) {
this.batas = batas;
this.bucket = new long[batas.length];
}
void amati(double nilai) {
jumlah += nilai;
cacah++;
for (int i = 0; i < batas.length; i++) {
if (nilai <= batas[i]) {
bucket[i]++;
}
}
}
void ekspos(String nama, StringBuilder out) {
for (int i = 0; i < batas.length; i++) {
out.append(String.format("%s_bucket{le=\"%s\"} %d%n", nama,
(batas[i] >= Double.POSITIVE_INFINITY) ? "+Inf" : batas[i], bucket[i]));
}
out.append(String.format("%s_count %d%n", nama, cacah));
out.append(String.format("%s_sum %s%n", nama, jumlah));
}
}
public static void main(String[] args) {
Counter pesanan = new Counter();
pesanan.inc();
pesanan.inc();
pesanan.inc(2.0); // dua pesanan sekaligus dari satu pelanggan
Gauge antrean = new Gauge();
antrean.set(7);
Histogram latensi = new Histogram(new double[]{0.1, 0.5, 1.0, Double.POSITIVE_INFINITY});
for (double d : List.of(0.08, 0.12, 0.34, 0.41, 0.9, 1.4)) {
latensi.amati(d);
}
System.out.println("== Nilai internal registri ==");
System.out.printf("pesanan_dibuat_total = %.0f%n", pesanan.nilai());
System.out.printf("antrean_pesanan = %.0f%n", antrean.nilai());
System.out.println();
System.out.println("== Eksposur GET /metrics (format teks Prometheus, tanpa baris komentar) ==");
StringBuilder out = new StringBuilder();
out.append(String.format("pesanan_dibuat_total %.0f%n", pesanan.nilai()));
out.append(String.format("antrean_pesanan %.0f%n", antrean.nilai()));
latensi.ekspos("latensi_pesanan_seconds", out);
System.out.print(out);
System.out.println();
Map<String, Double> contohJvm = new TreeMap<>();
contohJvm.put("jvm_memory_used_bytes{area=\"heap\"}", 268435456.0);
contohJvm.put("jvm_memory_used_bytes{area=\"nonheap\"}", 71303168.0);
System.out.println("== Metrik JVM yang ikut terekspos otomatis (contoh) ==");
for (var e : contohJvm.entrySet()) {
System.out.printf("%s %.0f%n", e.getKey(), e.getValue());
}
}
}