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 #

BentukSifat nilaiContohPertanyaan yang dijawab
Counterhanya naikpesanan_dibuat_totalsudah berapa banyak?
Gaugenaik-turunantrean_pesananberapa sekarang?
Histogramsebaranlatensi_pesanan_secondsberapa 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:

yaml prometheus.yml — konfigurasi scrape
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:

java PenaMetrik.java
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:

text
== 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"} 71303168

Mari kita bedah registri dan eksposurnya:

  1. Counter.inc(): setiap kejadian menambah nilai — program mensimulasikan empat pesanan dari tiga kejadian (satu inc(2.0) untuk pesanan ganda). Nama berakhiran _total adalah konvensi counter Prometheus.
  2. 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.
  3. Histogram.amati(...): enam observasi dimasukkan ke empat bucket. Baris le="0.5" terbaca “4 observasi ≤ 0,5 detik” — bucket Prometheus kumulatif, dan +Inf selalu sama dengan _count. Nilai sum 3,25 memungkinkan rata-rata; kuantil dihitung server dari perbandingan antar-bucket.
  4. Baris komentar # HELP dan # 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.
  5. 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):

java Instrumentasi dengan Micrometer (pola umum)
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 berkas
java
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());
        }
    }
}