Logger mini pada Chapter sebelumnya mengajarkan mekanismenya, tetapi proyek nyata menuntut standar industri: cap waktu akurat, keluaran ke berkas dengan rotasi otomatis, konfigurasi tanpa kompilasi ulang, dan ekosistem alat yang mengenali formatnya. Di dunia Java, kombinasi paling umum adalah SLF4J (Simple Logging Facade for Java) sebagai antarmuka dan Logback sebagai implementasi. Pemisahan dua peran ini bukan gaya-gayaan: kode aplikasi hanya mengenal antarmuka stabil, sementara mesin log dapat diganti tanpa menyentuh satu baris kode bisnis. Pada Chapter ini kita akan memahami pola facade tersebut, membaca konfigurasi logback.xml, dan membuktikan keunggulan parameterized message dengan program penghitung biaya format.

Pola Facade pada SLF4J #

SLF4J adalah facade: satu lapisan API (class Logger dan LoggerFactory) yang kode kita panggil, plus mekanisme binding yang mencari implementasi pada classpath saat aplikasi berjalan. Alurnya tampak seperti ini:

Manfaat pemisahan ini nyata dalam dua skenario. Pertama, pustaka pihak ketiga yang kita gunakan juga menulis log; jika seluruh pustaka memanggil SLF4J, semua keluarannya menyatu di satu mesin Logback dengan satu konfigurasi. Kedua, jika suatu saat tim memutuskan pindah ke implementasi lain (misalnya Log4j 2), cukup ganti berkas binding di classpath — kode aplikasi tidak berubah sama sekali.

Kode aplikasi sendiri hanya mengenal dua class dari SLF4J:

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PelangganService {
    private static final Logger log =
            LoggerFactory.getLogger(PelangganService.class);

    public void proses(long pesananId) {
        log.info("Pesanan {} mulai diproses", pesananId);
    }
}

Deklarasi LoggerFactory.getLogger(PelangganService.class) menjadikan nama class sebagai nama logger, sehingga konfigurasi dapat mengatur level per class atau per package. Perhatikan juga pemanggilan log.info dengan penanda {} — topik yang kita bedah tuntas di bagian akhir chapter ini.

Konfigurasi Logback #

Logback membaca berkas logback.xml dari classpath saat aplikasi dimulai. Konfigurasi minimal dengan dua appender tampak seperti berikut:

xml
<configuration>
  <appender name="KONSOL" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="BERKAS" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>log/aplikasi.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>log/aplikasi.%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="com.toko.pembayaran" level="DEBUG"/>

  <root level="INFO">
    <appender-ref ref="KONSOL"/>
    <appender-ref ref="BERKAS"/>
  </root>
</configuration>

Empat bagian konfigurasi tersebut punya peran masing-masing:

  1. ConsoleAppender: menulis ke System.out — berguna di lingkungan development.
  2. RollingFileAppender: menulis ke berkas dengan rotasi berbasis waktu; fileNamePattern memuat berkas lama per hari dan maxHistory menjaga hanya 30 hari terakhir agar disk tidak penuh.
  3. logger per package: menaikkan com.toko.pembayaran ke DEBUG sementara seluruh aplikasi (root) tetap INFO — penyaringan selektif yang kita simulasi dengan ambang pada Chapter sebelumnya.
  4. pattern: pola keluaran; %d cap waktu, %-5level level rata kiri lima karakter, %logger{36} nama logger dipangkas 36 karakter, %msg pesan, %n baris baru.

Parameterized Message dan Biaya Format #

Fitur SLF4J yang paling sering dijelaskan adalah parameterized message: penanda {} diisi nilai hanya jika level aktif. Untuk memahami mengapa hal ini penting, kita bandingkan dua bentuk pemanggilan pada logger mini berikut. Program menghitung berapa kali string pesan benar-benar dibangun ketika level TRACE sedang nonaktif:

java LogBiaya.java
public class LogBiaya {
    static int pesanDibangun = 0;     // penggabungan string di call site
    static int formatDijalankan = 0;  // penggabungan di dalam logger

    public static void main(String[] args) {
        LoggerMini log = new LoggerMini(LevelMini.WARN);

        System.out.println("== Bentuk penggabungan string, TRACE nonaktif ==");
        pesanDibangun = 0;
        for (int i = 1; i <= 3; i++) {
            log.trace("Data ke-" + i + " bernilai " + bangunDetail(i));
        }
        System.out.println("String pesan dibangun  : " + pesanDibangun + " kali");
        System.out.println("Baris yang tercetak    : 0");

        System.out.println();
        System.out.println("== Bentuk parameter {}, TRACE nonaktif ==");
        formatDijalankan = 0;
        for (int i = 1; i <= 3; i++) {
            log.trace("Data ke-{} bernilai {}", i, i * i);
        }
        System.out.println("Operasi format dijalankan: " + formatDijalankan + " kali");

        System.out.println();
        System.out.println("== Level WARN aktif: biaya dibayar hanya saat tercetak ==");
        formatDijalankan = 0;
        log.warn("Stok tinggal {} item", 3);
        System.out.println("Operasi format dijalankan: " + formatDijalankan + " kali");
    }

    static String bangunDetail(int i) {
        pesanDibangun++;
        return String.valueOf(i * i);
    }
}

enum LevelMini { TRACE, DEBUG, INFO, WARN, ERROR }

class LoggerMini {
    private LevelMini ambang;

    LoggerMini(LevelMini ambang) {
        this.ambang = ambang;
    }

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

    void trace(String pesan) {
        if (aktif(LevelMini.TRACE)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[TRACE] " + pesan);
        }
    }

    void trace(String format, Object a, Object b) {
        if (aktif(LevelMini.TRACE)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[TRACE] " + format
                    .replaceFirst("\\{}", String.valueOf(a))
                    .replaceFirst("\\{}", String.valueOf(b)));
        }
    }

    void warn(String format, Object a) {
        if (aktif(LevelMini.WARN)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[WARN] " + format.replaceFirst("\\{}", String.valueOf(a)));
        }
    }
}

Output yang Dihasilkan:

text
== Bentuk penggabungan string, TRACE nonaktif ==
String pesan dibangun  : 3 kali
Baris yang tercetak    : 0

== Bentuk parameter {}, TRACE nonaktif ==
Operasi format dijalankan: 0 kali

== Level WARN aktif: biaya dibayar hanya saat tercetak ==
[WARN] Stok tinggal 3 item
Operasi format dijalankan: 1 kali

Mari kita bedah hasil penghitungan tersebut:

  1. Bagian pertama — penggabungan string: ekspresi "Data ke-" + i + " bernilai " + bangunDetail(i) dievaluasi sebelum method trace dipanggil, apa pun levelnya. Hasilnya tiga string dibangun (pesanDibangun = 3) meskipun nol baris tercetak — pekerjaan sia-sia murni karena compiler menyusun penggabungan itu tanpa peduli level.
  2. Bagian kedua — parameter {}: nilai i dan i * i memang tetap dievaluasi sebagai argumen, tetapi penggabungan teksnya ditunda ke dalam logger — dan karena aktif(TRACE) bernilai false, penggabungan tidak pernah dijalankan (formatDijalankan = 0).
  3. Bagian ketiga — level aktif: ketika warn melewati penyaring, baris tercetak dan penghitung naik menjadi 1. Biaya format hanya dibayar ketika pesan benar-benar keluar.
  4. Implikasinya: pada loop panjang dengan level DEBUG nonaktif, bentuk penggabungan tetap membangun ribuan string; bentuk {} tidak membangun satu pun.

Inilah alasan dokumentasi SLF4J menyarankan bentuk log.debug("Nilai {}", nilai) menggantikan log.debug("Nilai " + nilai): keluaran identik saat level aktif, tetapi biaya jauh lebih rendah saat level dimatikan.

Kini kita mengenal kedua sisi pasangan ini: SLF4J sebagai antarmuka yang dipanggil kode, Logback sebagai mesin yang dikonfigurasi lewat XML. Pertanyaan berikutnya bersifat praktis sehari-hari: pada situasi apa kita memilih TRACE, DEBUG, INFO, WARN, atau ERROR? Chapter berikutnya menyusun tabel keputusan untuk memilih level secara konsisten.

Referensi #

Berkas Sumber

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

LogBiaya.java

Unduh berkas
java
public class LogBiaya {
    static int pesanDibangun = 0;     // penggabungan string di call site
    static int formatDijalankan = 0;  // penggabungan di dalam logger

    public static void main(String[] args) {
        LoggerMini log = new LoggerMini(LevelMini.WARN);

        System.out.println("== Bentuk penggabungan string, TRACE nonaktif ==");
        pesanDibangun = 0;
        for (int i = 1; i <= 3; i++) {
            log.trace("Data ke-" + i + " bernilai " + bangunDetail(i));
        }
        System.out.println("String pesan dibangun  : " + pesanDibangun + " kali");
        System.out.println("Baris yang tercetak    : 0");

        System.out.println();
        System.out.println("== Bentuk parameter {}, TRACE nonaktif ==");
        formatDijalankan = 0;
        for (int i = 1; i <= 3; i++) {
            log.trace("Data ke-{} bernilai {}", i, i * i);
        }
        System.out.println("Operasi format dijalankan: " + formatDijalankan + " kali");

        System.out.println();
        System.out.println("== Level WARN aktif: biaya dibayar hanya saat tercetak ==");
        formatDijalankan = 0;
        log.warn("Stok tinggal {} item", 3);
        System.out.println("Operasi format dijalankan: " + formatDijalankan + " kali");
    }

    static String bangunDetail(int i) {
        pesanDibangun++;
        return String.valueOf(i * i);
    }
}

enum LevelMini { TRACE, DEBUG, INFO, WARN, ERROR }

class LoggerMini {
    private LevelMini ambang;

    LoggerMini(LevelMini ambang) {
        this.ambang = ambang;
    }

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

    void trace(String pesan) {
        if (aktif(LevelMini.TRACE)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[TRACE] " + pesan);
        }
    }

    void trace(String format, Object a, Object b) {
        if (aktif(LevelMini.TRACE)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[TRACE] " + format
                    .replaceFirst("\\{}", String.valueOf(a))
                    .replaceFirst("\\{}", String.valueOf(b)));
        }
    }

    void warn(String format, Object a) {
        if (aktif(LevelMini.WARN)) {
            LogBiaya.formatDijalankan++;
            System.out.println("[WARN] " + format.replaceFirst("\\{}", String.valueOf(a)));
        }
    }
}