Tes memastikan aplikasi berfungsi saat dibangun; pertanyaan berbeda muncul setelah aplikasi berjalan berjam-jam: apakah basis data masih terhubung, apakah disk menipis, apakah instance siap menerima trafik? Spring Boot Actuator menjawabnya dengan menyuntikkan endpoint operasional ke dalam aplikasi — /actuator/health adalah yang paling terkenal, dibaca oleh load balancer dan orkestrator sebelum mengirim trafik. Pada Chapter ini kita bedah endpoint yang tersedia, cara mengeksposnya secara selektif, mekanisme HealthIndicator dengan agregasi worst-of, serta pasangan liveness dan readiness — lalu membuktikan seluruh alurnya dengan panel kesehatan Java murni.

Endpoint Bawaan Actuator #

Actuator diaktifkan lewat satu starter — sisanya otomatis:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Endpoint yang paling sering dipakai memetakan langsung ke kebutuhan operasional:

EndpointIsi
/actuator/healthstatus kesehatan agregat + komponen
/actuator/infoinfo build, git, versi aplikasi
/actuator/metricsdaftar metrik (heap, thread, HTTP)
/actuator/envsumber konfigurasi aktif (rahasia disaring)
/actuator/beansseluruh bean di konteks
/actuator/threaddumpjejak seluruh thread JVM

Aturan keamanannya tegas: hanya /health yang terbuka secara bawaan. Endpoint lain mengandung informasi internal (jalur kelas, variabel lingkungan) sehingga eksposurnya diatur eksplisit per lingkungan:

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics   # jangan pernah "*" di produksi
  endpoint:
    health:
      show-details: when-authorized    # detail hanya untuk peran berwenang

Bila Spring Security aktif, endpoint sensitif otomatis memerlukan autentikasi — kombinasikan dengan requestMatchers("/actuator/**").hasRole("ADMIN") dari Chapter Authentication dan Authorization, atau letakkan Actuator di port manajemen terpisah (management.server.port: 9090) yang hanya dijangkau jaringan internal.

HealthIndicator dan Agregasi Worst-of #

Nilai /actuator/health berasal dari kumpulan HealthIndicator — Actuator menyediakan indikator bawaan untuk database, disk, dan mail; kita menambah indikator bisnis sendiri dengan mendaftarkan bean HealthIndicator:

java
@Component
public class GatewayPembayaranHealthIndicator implements HealthIndicator {

    private final GatewayPembayaranKlien klien;

    public GatewayPembayaranHealthIndicator(GatewayPembayaranKlien klien) {
        this.klien = klien;
    }

    @Override
    public Health health() {
        if (!klien.ping()) {
            return Health.down()
                    .withDetail("galat", "gateway tidak merespons")
                    .build();
        }
        return Health.up().withDetail("mode", klien.mode()).build();
    }
}

Agregasi mengikuti aturan worst-of: status paling parah dari seluruh indikator menjadi status keseluruhan, dengan urutan keparahan DOWN > OUT_OF_SERVICE > UP > UNKNOWN — persis urutan bawaan SimpleStatusAggregator (status UP lebih menentukan daripada UNKNOWN). Satu indikator DOWN menjatuhkan seluruh respons menjadi {"status":"DOWN"} — klien load balancer cukup memeriksa satu kata untuk memutuskan menjauh.

Liveness dan Readiness #

Menjalankan aplikasi di Kubernetes mengubah health check menjadi dua pertanyaan berbeda, dan Actuator menjawabnya dengan health group:

yaml
management:
  endpoint:
    health:
      probes:
        enabled: true
      group:
        readiness:
          include: readinessState,db,mail
        liveness:
          include: livenessState
GrupPertanyaanReaksi gagalAnggota tepat
livenessapakah proses hidup?pod direstartinternal state saja
readinesssiap menerima trafik?trafik dihentikan, pod hidup+ dependensi eksternal

Pemisahan ini krusial: gateway pembayaran yang mogok tidak berarti proses aplikasi mati. Bila gateway ikut kelompok liveness, orkestrator me-restart seluruh pod — menghapus sesi pengguna dan heap hangat — padahal yang tepat hanya menahan trafik sejenak hingga gateway pulih. Aturan praktisnya: liveness memuat pemeriksaan internal (state proses), readiness boleh memuat dependensi eksternal.

Panel Kesehatan Mini #

Mekanisme agregasi dan health group layak dilihat dari dalam. Program Java murni berikut membangun empat indikator — db, diskSpace, mail (DOWN), dan ping — lalu mengagregasi worst-of dan mengevaluasi dua group ala probe orkestrator:

import java.util.ArrayList;
import java.util.List;

public class PanelKesehatan {

    // Status ala Spring Boot Actuator.
    enum Status {
        UP, DOWN, OUT_OF_SERVICE, UNKNOWN
    }

    // Urutan keparahan persis SimpleStatusAggregator bawaan:
    // DOWN, OUT_OF_SERVICE, UP, UNKNOWN — makin kecil makin parah.
    static int keparahan(Status s) {
        return switch (s) {
            case DOWN -> 0;
            case OUT_OF_SERVICE -> 1;
            case UP -> 2;
            case UNKNOWN -> 3;
        };
    }

    record Laporan(Status status, String detail) { }

    // Padanan HealthIndicator: satu komponen, satu keputusan.
    interface Indikator {

        String nama();

        Laporan periksa();
    }

    static class IndikatorDb implements Indikator {

        @Override
        public String nama() {
            return "db";
        }

        @Override
        public Laporan periksa() {
            // Uji nyata: kumpulan koneksi + SELECT 1.
            return new Laporan(Status.UP, "kumpulan=aktif seleksi1=ok");
        }
    }

    static class IndikatorDisk implements Indikator {

        @Override
        public String nama() {
            return "diskSpace";
        }

        @Override
        public Laporan periksa() {
            long sisaGb = 42;   // nilai tetap agar contoh deterministik
            return sisaGb >= 10
                    ? new Laporan(Status.UP, "sisa=" + sisaGb + "GB ambang=10GB")
                    : new Laporan(Status.DOWN, "sisa=" + sisaGb + "GB");
        }
    }

    static class IndikatorMail implements Indikator {

        @Override
        public String nama() {
            return "mail";
        }

        @Override
        public Laporan periksa() {
            // SMTP sedang tidak merespons.
            return new Laporan(Status.DOWN, "galat=smtp tidak merespons");
        }
    }

    // Liveness: apakah proses masih hidup? Tidak memeriksa dependensi eksternal.
    static class IndikatorPing implements Indikator {

        @Override
        public String nama() {
            return "ping";
        }

        @Override
        public Laporan periksa() {
            return new Laporan(Status.UP, "proses=hidup");
        }
    }

    // Padanan health group: sekumpulan indikator dengan nama.
    record Grup(String nama, List<Indikator> anggota) {

        Status status() {
            Status parah = Status.UP;
            for (Indikator i : anggota) {
                Laporan l = i.periksa();
                if (keparahan(l.status()) < keparahan(parah)) {
                    parah = l.status();
                }
            }
            return parah;
        }
    }

    public static void main(String[] args) {
        List<Indikator> semua = List.of(
                new IndikatorDb(),
                new IndikatorDisk(),
                new IndikatorMail());

        System.out.println("== Indikator kesehatan ==");
        for (Indikator i : semua) {
            Laporan l = i.periksa();
            System.out.printf("%-10s %-6s %s%n", i.nama(), l.status(), l.detail());
        }

        // Agregasi worst-of: satu komponen DOWN menjatuhkan seluruh status.
        Status keseluruhan = new Grup("keseluruhan", semua).status();
        System.out.println();
        System.out.println("== Agregasi (worst-of: DOWN > OUT_OF_SERVICE > UP > UNKNOWN) ==");
        System.out.println("status /actuator/health: " + keseluruhan);

        Grup liveness = new Grup("livenessProbe", List.of(new IndikatorPing()));
        Grup readiness = new Grup("readinessProbe",
                List.of(new IndikatorDb(), new IndikatorMail()));

        System.out.println();
        System.out.println("== Health groups (probe orkestrator) ==");
        System.out.printf("%-15s %-6s indikator: ping        -> pod TIDAK direstart%n",
                liveness.nama(), liveness.status());
        System.out.printf("%-15s %-6s indikator: db, mail    -> trafik dihentikan, pod tetap hidup%n",
                readiness.nama(), readiness.status());

        System.out.println();
        System.out.println("== Keputusan orkestrator ==");
        System.out.println("liveness  UP  : jangan restart, proses sehat");
        System.out.println("readiness DOWN: jangan kirim trafik baru ke instance ini");
    }
}

Output yang Dihasilkan:

text
== Indikator kesehatan ==
db         UP     kumpulan=aktif seleksi1=ok
diskSpace  UP     sisa=42GB ambang=10GB
mail       DOWN   galat=smtp tidak merespons

== Agregasi (worst-of: DOWN > OUT_OF_SERVICE > UP > UNKNOWN) ==
status /actuator/health: DOWN

== Health groups (probe orkestrator) ==
livenessProbe   UP     indikator: ping        -> pod TIDAK direstart
readinessProbe  DOWN   indikator: db, mail    -> trafik dihentikan, pod tetap hidup

== Keputusan orkestrator ==
liveness  UP  : jangan restart, proses sehat
readiness DOWN: jangan kirim trafik baru ke instance ini

Mari kita bedah panel tersebut:

  1. enum Status dan keparahan(...): empat status Actuator dengan urutan keparahan yang ditulis eksplisit dalam switch — DOWN paling parah (0), UNKNOWN paling belakang (3). Angka inilah yang dipakai agregator untuk membandingkan, menggantikan pembandingan string yang rawan salah urutan.
  2. IndikatorMail.periksa(): satu-satunya yang mengembalikan DOWN — smtp tidak merespons. Indikator db dan diskSpace sehat dengan detail tetap (nilai beku, bukan pembacaan disk nyata, agar keluaran dapat diverifikasi ulang).
  3. Grup.status(): agregasi worst-of — loop menyimpan status terparah (keparahan terkecil) dari seluruh anggota. Diterapkan pada grup keseluruhan berisi tiga indikator, hasilnya DOWN meski dua dari tiga komponen UP: itulah makna “satu komponen menjatuhkan semuanya”.
  4. livenessProbe vs readinessProbe: grup liveness hanya berisi ping (state internal) sehingga tetap UP — orkestrator tidak me-restart pod. Grup readiness berisi db dan mail; karena mail DOWN, readiness turun ke DOWN — trafik dihentikan sementara proses tetap hidup. Justru perbedaan dua baris terakhir inilah inti pemisahan probe.
  5. Konsekuensi cetak: pod TIDAK direstart pada liveness dan trafik dihentikan, pod tetap hidup pada readiness merangkum kontrak kedua probe — satu kata status berbeda menghasilkan tindakan orkestrator yang sama sekali berbeda.

Health memberi jawaban biner saat ditanya; pertanyaan berikutnya adalah memahami mengapa dan kapan — tren latensi, laju error, jejak request lintas service. Tiga pilar observability itu — log terstruktur, metrik, dan tracing — menyatu di Spring Boot lewat Observation API dan Micrometer, pokok bahasan Chapter Observability Aplikasi berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PanelKesehatan.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;

public class PanelKesehatan {

    // Status ala Spring Boot Actuator.
    enum Status {
        UP, DOWN, OUT_OF_SERVICE, UNKNOWN
    }

    // Urutan keparahan persis SimpleStatusAggregator bawaan:
    // DOWN, OUT_OF_SERVICE, UP, UNKNOWN — makin kecil makin parah.
    static int keparahan(Status s) {
        return switch (s) {
            case DOWN -> 0;
            case OUT_OF_SERVICE -> 1;
            case UP -> 2;
            case UNKNOWN -> 3;
        };
    }

    record Laporan(Status status, String detail) { }

    // Padanan HealthIndicator: satu komponen, satu keputusan.
    interface Indikator {

        String nama();

        Laporan periksa();
    }

    static class IndikatorDb implements Indikator {

        @Override
        public String nama() {
            return "db";
        }

        @Override
        public Laporan periksa() {
            // Uji nyata: kumpulan koneksi + SELECT 1.
            return new Laporan(Status.UP, "kumpulan=aktif seleksi1=ok");
        }
    }

    static class IndikatorDisk implements Indikator {

        @Override
        public String nama() {
            return "diskSpace";
        }

        @Override
        public Laporan periksa() {
            long sisaGb = 42;   // nilai tetap agar contoh deterministik
            return sisaGb >= 10
                    ? new Laporan(Status.UP, "sisa=" + sisaGb + "GB ambang=10GB")
                    : new Laporan(Status.DOWN, "sisa=" + sisaGb + "GB");
        }
    }

    static class IndikatorMail implements Indikator {

        @Override
        public String nama() {
            return "mail";
        }

        @Override
        public Laporan periksa() {
            // SMTP sedang tidak merespons.
            return new Laporan(Status.DOWN, "galat=smtp tidak merespons");
        }
    }

    // Liveness: apakah proses masih hidup? Tidak memeriksa dependensi eksternal.
    static class IndikatorPing implements Indikator {

        @Override
        public String nama() {
            return "ping";
        }

        @Override
        public Laporan periksa() {
            return new Laporan(Status.UP, "proses=hidup");
        }
    }

    // Padanan health group: sekumpulan indikator dengan nama.
    record Grup(String nama, List<Indikator> anggota) {

        Status status() {
            Status parah = Status.UP;
            for (Indikator i : anggota) {
                Laporan l = i.periksa();
                if (keparahan(l.status()) < keparahan(parah)) {
                    parah = l.status();
                }
            }
            return parah;
        }
    }

    public static void main(String[] args) {
        List<Indikator> semua = List.of(
                new IndikatorDb(),
                new IndikatorDisk(),
                new IndikatorMail());

        System.out.println("== Indikator kesehatan ==");
        for (Indikator i : semua) {
            Laporan l = i.periksa();
            System.out.printf("%-10s %-6s %s%n", i.nama(), l.status(), l.detail());
        }

        // Agregasi worst-of: satu komponen DOWN menjatuhkan seluruh status.
        Status keseluruhan = new Grup("keseluruhan", semua).status();
        System.out.println();
        System.out.println("== Agregasi (worst-of: DOWN > OUT_OF_SERVICE > UP > UNKNOWN) ==");
        System.out.println("status /actuator/health: " + keseluruhan);

        Grup liveness = new Grup("livenessProbe", List.of(new IndikatorPing()));
        Grup readiness = new Grup("readinessProbe",
                List.of(new IndikatorDb(), new IndikatorMail()));

        System.out.println();
        System.out.println("== Health groups (probe orkestrator) ==");
        System.out.printf("%-15s %-6s indikator: ping        -> pod TIDAK direstart%n",
                liveness.nama(), liveness.status());
        System.out.printf("%-15s %-6s indikator: db, mail    -> trafik dihentikan, pod tetap hidup%n",
                readiness.nama(), readiness.status());

        System.out.println();
        System.out.println("== Keputusan orkestrator ==");
        System.out.println("liveness  UP  : jangan restart, proses sehat");
        System.out.println("readiness DOWN: jangan kirim trafik baru ke instance ini");
    }
}