Debugger adalah alat yang hebat di mesin developer, tetapi begitu aplikasi berjalan di server produksi, tidak ada orang yang menempelkan IDE ke proses JVM untuk melihat isi variabel. Satu-satunya jendela ke perilaku program di produksi adalah log: catatan peristiwa yang program tulis selama berjalan. Kualitas log menentukan kecepatan investigasi insiden — log yang baik membuat akar masalah terlihat dalam hitungan menit, log yang asal menyimpan justru membuang waktu. Pada Chapter ini kita akan memahami mengapa System.out.println tidak memadai, membaca anatomi baris log, dan membangun logger mini sendiri agar mekanismenya tidak terasa seperti kotak tertutup.

Mengapa println Tidak Memadai #

System.out.println memang mencetak teks, tetapi lima kemampuan berikut tidak dimiliki teks polos tersebut:

  • Level keparahan — semua baris berstatus sama; kita tidak bisa membedakan catatan rutin dari kegagalan yang mendesak.
  • Waktu kejadian — teks tidak menyimpan kapan baris ditulis, padahal urutan dan selisih waktu adalah informasi kunci.
  • Tujuan keluaran — println selalu mengalir ke satu stream; produksi membutuhkan berkas, rotasi, atau pengiriman ke pusat log.
  • Penyaringan — tidak ada saklar untuk mematikan sebagian keluaran tanpa mengubah kode dan men-deploy ulang.
  • Bentuk terstruktur — teks bebas sulit diurai mesin; pemformatan konsisten membuat log dapat dicari dan diagregasi.

Bayangkan satu baris println("gagal") di tengah jutaan baris operasi normal: tanpa level, tanpa waktu, tanpa konteks, baris itu nyaris mustahil ditemukan, apalagi dikorelasikan dengan permintaan mana yang gagal.

Anatomi Sebuah Baris Log #

Logger industri menulis baris dengan struktur konsisten, lazimnya seperti berikut:

text
2026-01-15 08:03:41.217 [main] INFO  PelangganService - Pesanan 1001 diproses

Dari kiri ke kanan: cap waktu (timestamp), thread aktif, level, nama logger — biasanya nama class atau package — lalu pesan. Setiap bidang punya peran: cap waktu mengukur durasi antar peristiwa, nama logger mengidentifikasi komponen, dan level menjadi dasar penyaringan. Ketika format ini seragam di seluruh aplikasi, alat pengumpul log dapat mengindeks, memfilter, dan memvisualisasikan jutaan baris dalam hitungan detik.

Logger Mini Buatan Sendiri #

Mari kita buktikan bahwa inti sebuah logger cukup sederhana: penyaring berdasarkan level plus pemformatan. Berkas berikut mendefinisikan LogMini — logger mini dengan lima level dan satu ambang (threshold):

java LogMini.java
class LogMini {
    enum Level { TRACE, DEBUG, INFO, WARN, ERROR }

    private Level ambang;

    LogMini(Level ambang) {
        this.ambang = ambang;
    }

    void setAmbang(Level ambang) {
        this.ambang = ambang;
    }

    private boolean aktif(Level level) {
        return level.ordinal() >= ambang.ordinal();
    }

    private void catat(Level level, String pesan) {
        if (aktif(level)) {
            System.out.println("[" + level + "] " + pesan);
        }
    }

    void trace(String pesan) { catat(Level.TRACE, pesan); }
    void debug(String pesan) { catat(Level.DEBUG, pesan); }
    void info(String pesan)  { catat(Level.INFO, pesan); }
    void warn(String pesan)  { catat(Level.WARN, pesan); }
    void error(String pesan) { catat(Level.ERROR, pesan); }
}

Kemudian program berikut menggunakan LogMini pada tiga pengaturan ambang yang berbeda:

java DemoLogMini.java
public class DemoLogMini {
    public static void main(String[] args) {
        System.out.println("-- ambang TRACE: semua pesan lolos --");
        LogMini log = new LogMini(LogMini.Level.TRACE);
        log.trace("Menyiapkan koneksi database");
        log.debug("Query pelanggan dieksekusi");
        log.info("Aplikasi siap menerima permintaan");
        log.warn("Stok menipis: 3 item tersisa");
        log.error("Koneksi database gagal");

        System.out.println();
        System.out.println("-- ambang INFO: TRACE dan DEBUG disaring --");
        log.setAmbang(LogMini.Level.INFO);
        log.trace("Menyiapkan koneksi database");
        log.debug("Query pelanggan dieksekusi");
        log.info("Aplikasi siap menerima permintaan");
        log.warn("Stok menipis: 3 item tersisa");
        log.error("Koneksi database gagal");

        System.out.println();
        System.out.println("-- ambang ERROR: hanya pesan error --");
        log.setAmbang(LogMini.Level.ERROR);
        log.info("Aplikasi siap menerima permintaan");
        log.error("Koneksi database gagal");
    }
}

Output yang Dihasilkan:

text
-- ambang TRACE: semua pesan lolos --
[TRACE] Menyiapkan koneksi database
[DEBUG] Query pelanggan dieksekusi
[INFO] Aplikasi siap menerima permintaan
[WARN] Stok menipis: 3 item tersisa
[ERROR] Koneksi database gagal

-- ambang INFO: TRACE dan DEBUG disaring --
[INFO] Aplikasi siap menerima permintaan
[WARN] Stok menipis: 3 item tersisa
[ERROR] Koneksi database gagal

-- ambang ERROR: hanya pesan error --
[ERROR] Koneksi database gagal

Mari kita bedah mekanisme yang bermain di balik kedua berkas tersebut:

  1. enum Level { TRACE, DEBUG, INFO, WARN, ERROR }: urutan deklarasi enum menentukan tingkat keparahan — ordinal() TRACE adalah 0 dan ERROR adalah 4. Urutan ini menjadi dasar perbandingan.
  2. aktif(Level level): mengembalikan true hanya jika level sama atau lebih tinggi daripada ambang. Inilah penyaring: pesan di bawah ambang tidak pernah sampai ke keluaran.
  3. catat(Level, String): satu titik keluaran yang menjaga format [LEVEL] pesan tetap konsisten — prinsip yang sama dengan pattern pada Logback.
  4. log.setAmbang(...): mengubah ambang saat runtime meniru kelakuan logger sungguhan, yang levelnya dapat diubah lewat konfigurasi tanpa menyentuh kode.
  5. Perhatikan output kedua dan ketiga: kode program sama persis, tetapi keluaran berbeda hanya karena ambang berubah — bukti bahwa penyaringan adalah tanggung jawab logger, bukan kode pemanggil.

STDOUT dan STDERR #

JVM menyediakan dua stream keluaran: System.out untuk keluaran normal dan System.err untuk pesan galat. Konvensi Unix lama memakai pemisahan ini agar perintah seperti java Aplikasi > hasil.txt tetap menampung keluaran normal ke berkas sementara pesan galat tetap terlihat di terminal. Logger sungguhan melangkah lebih jauh: level ERROR dan WARN diarahkan ke appender terpisah (misalnya berkas galat.log) tanpa programmer menulis satu pun System.err.

Satu jebakan pemula layak disebut: System.out dan System.err masing-masing punya buffer, sehingga saat keduanya diarahkan ke terminal yang sama, urutan antar stream tidak terjamin. Itu alasan lain meninggalkan println — logger menulis lewat satu jalur tunggal sehingga urutan baris terjaga.

Mencatat Peristiwa, Bukan Aliran Data #

Prinsip terakhir yang menentukan kualitas log: catat peristiwa — keputusan, perubahan state, kegagalan — bukan setiap nilai yang lewat. Peristiwa yang layak dicatat antara lain: transaksi dimulai dan selesai (dengan hasil), kegagalan koneksi beserta upaya ulangnya, keputusan bisnis yang tidak biasa, dan ambang yang terlampaui. Sebaliknya, mencetak setiap baris hasil query menghasilkan log berukuran raksasa yang menutupi peristiwa penting dan membocorkan data pelanggan.

Ukuran praktisnya: untuk setiap baris log yang hendak kita tulis, tanyakan “jika baris ini muncul satu juta kali, apakah masih berguna?” Pesan level INFO yang jawabnya tidak sebaiknya turun menjadi DEBUG, atau tidak ditulis sama sekali.

Logger mini kita sudah menunjukkan mekanisme penyaringan level. Dalam proyek nyata, kita tidak membangun logger sendiri, melainkan memakai standar industri: SLF4J sebagai antarmuka dan Logback sebagai implementasi. Chapter berikutnya membahas pasangan tersebut dan pola facade yang membuatnya lentur.

Referensi #

Berkas Sumber

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

DemoLogMini.java

Unduh berkas
java
public class DemoLogMini {
    public static void main(String[] args) {
        System.out.println("-- ambang TRACE: semua pesan lolos --");
        LogMini log = new LogMini(LogMini.Level.TRACE);
        log.trace("Menyiapkan koneksi database");
        log.debug("Query pelanggan dieksekusi");
        log.info("Aplikasi siap menerima permintaan");
        log.warn("Stok menipis: 3 item tersisa");
        log.error("Koneksi database gagal");

        System.out.println();
        System.out.println("-- ambang INFO: TRACE dan DEBUG disaring --");
        log.setAmbang(LogMini.Level.INFO);
        log.trace("Menyiapkan koneksi database");
        log.debug("Query pelanggan dieksekusi");
        log.info("Aplikasi siap menerima permintaan");
        log.warn("Stok menipis: 3 item tersisa");
        log.error("Koneksi database gagal");

        System.out.println();
        System.out.println("-- ambang ERROR: hanya pesan error --");
        log.setAmbang(LogMini.Level.ERROR);
        log.info("Aplikasi siap menerima permintaan");
        log.error("Koneksi database gagal");
    }
}

LogMini.java

Unduh berkas
java
class LogMini {
    enum Level { TRACE, DEBUG, INFO, WARN, ERROR }

    private Level ambang;

    LogMini(Level ambang) {
        this.ambang = ambang;
    }

    void setAmbang(Level ambang) {
        this.ambang = ambang;
    }

    private boolean aktif(Level level) {
        return level.ordinal() >= ambang.ordinal();
    }

    private void catat(Level level, String pesan) {
        if (aktif(level)) {
            System.out.println("[" + level + "] " + pesan);
        }
    }

    void trace(String pesan) { catat(Level.TRACE, pesan); }
    void debug(String pesan) { catat(Level.DEBUG, pesan); }
    void info(String pesan)  { catat(Level.INFO, pesan); }
    void warn(String pesan)  { catat(Level.WARN, pesan); }
    void error(String pesan) { catat(Level.ERROR, pesan); }
}