Membangun logger sendiri cocok untuk belajar, namun di proyek nyata kita memakai library yang sudah matang. Pola standar industri di JVM adalah SLF4J sebagai façade dan Logback sebagai implementasi. SLF4J menyediakan API yang seragam, sementara Logback melakukan pekerjaan menulis log ke console, file, atau tujuan lain. Pemisahan ini membuat kita dapat mengganti implementasi (mis. ke Log4j2) tanpa mengubah kode aplikasi. Pada chapter ini kita akan mensimulasikan pola tersebut tanpa dependensi eksternal, dengan struktur yang sama persis dengan SLF4J + Logback.
Pola Façade dan Implementasi #
SLF4J adalah façade: interface org.slf4j.Logger yang dipakai kode aplikasi. Implementasi konkret disuntikkan pada runtime melalui ServiceLoader (file META-INF/services/org.slf4j.spi.SLF4JServiceProvider). Pada simulasi kita, kelas LoggerFactory berperan sebagai SLF4J, dan LogbackLogger berperan sebagai implementasi Logback.
Konfigurasi logback.xml #
Pada Logback nyata, file logback.xml di classpath memuat konfigurasi: appender (tujuan log), pattern (format), dan level per-logger. Contoh ringkas:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<encoder>
<pattern>%d %level %logger - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>Kode Demo SLF4J + Logback #
Berikut simulasi penuh yang memperlihatkan binding façade ke implementasi, konfigurasi dua appender, dan ganti implementasi di runtime.
// Simulasi pola SLF4J (façade) + Logback (implementasi) tanpa dependensi eksternal.
// Pola sebenarnya: `org.slf4j.Logger` sebagai façade, `ch.qos.logback.Logger` sebagai impl.
// === Layer SLF4J (façade) ===
interface LoggerFaçade {
fun trace(msg: String)
fun debug(msg: String)
fun info(msg: String)
fun warn(msg: String)
fun error(msg: String)
}
object LoggerFactory {
// Singleton binding: di dunia nyata diatur via ServiceLoader/META-INF.
private var provider: () -> LoggerFaçade = ::LogbackLogger
fun bind(provider: () -> LoggerFaçade) {
this.provider = provider
}
fun getLogger(nama: String): LoggerFaçade = provider().also {
// Set nama logger saat konstruksi (mirip `LoggerFactory.getLogger(Kelas::class.java)`)
if (it is LogbackLogger) it.nama = nama
}
}
// === Layer Logback (implementasi) ===
class LogbackLogger(var nama: String = "root") : LoggerFaçade {
var levelAktif: String = "INFO"
private fun emit(level: String, msg: String) {
if (ordinal(level) >= ordinal(levelAktif)) {
println("2026-01-15 10:00:00 [$level] $nama - $msg")
}
}
private fun ordinal(level: String): Int = when (level) {
"TRACE" -> 0; "DEBUG" -> 1; "INFO" -> 2; "WARN" -> 3; "ERROR" -> 4
else -> 5
}
override fun trace(p: String) = emit("TRACE", p)
override fun debug(p: String) = emit("DEBUG", p)
override fun info(p: String) = emit("INFO", p)
override fun warn(p: String) = emit("WARN", p)
override fun error(p: String) = emit("ERROR", p)
}
// Simulasi `logback.xml`: appender console + file
object LogbackConfig {
fun muatKonfigurasi() {
println("[logback] Memuat konfigurasi dari classpath:logback.xml")
println("[logback] Appender [CONSOLE] aktif ke System.out")
println("[logback] Appender [FILE] aktif ke logs/app.log")
}
}
data class Tugas(val id: Int, val deskripsi: String)
class ManajerTugas {
private val log = LoggerFactory.getLogger(ManajerTugas::class.simpleName ?: "ManajerTugas")
fun simpan(tugas: Tugas) {
log.debug("Menyimpan tugas id=${tugas.id}")
log.info("Tugas ${tugas.id} tersimpan: ${tugas.deskripsi}")
}
fun hapus(id: Int) {
log.warn("Menghapus tugas id=$id (operasi destruktif terhadap memori)")
}
}
fun main() {
println("=== Demo SLF4J + Logback (Simulasi) ===")
LogbackConfig.muatKonfigurasi()
val manajer = ManajerTugas()
manajer.simpan(Tugas(1, "Belajar Kotlin"))
manajer.simpan(Tugas(2, "Review PR tim"))
manajer.hapus(1)
println()
println("=== Ganti Implementasi (mis. ke console-only) ===")
LoggerFactory.bind(::ConsoleOnlyLogger)
val m2 = ManajerTugas()
m2.simpan(Tugas(3, "Tugas baru"))
}
class ConsoleOnlyLogger : LoggerFaçade {
override fun trace(p: String) = println("[c] TRACE $p")
override fun debug(p: String) = println("[c] DEBUG $p")
override fun info(p: String) = println("[c] INFO $p")
override fun warn(p: String) = println("[c] WARN $p")
override fun error(p: String) = println("[c] ERROR $p")
}Output yang Dihasilkan:
=== Demo SLF4J + Logback (Simulasi) ===
[logback] Memuat konfigurasi dari classpath:logback.xml
[logback] Appender [CONSOLE] aktif ke System.out
[logback] Appender [FILE] aktif ke logs/app.log
2026-01-15 10:00:00 [INFO] ManajerTugas - Tugas 1 tersimpan: Belajar Kotlin
2026-01-15 10:00:00 [INFO] ManajerTugas - Tugas 2 tersimpan: Review PR tim
2026-01-15 10:00:00 [WARN] ManajerTugas - Menghapus tugas id=1 (operasi destruktif terhadap memori)
=== Ganti Implementasi (mis. ke console-only) ===
[c] DEBUG Menyimpan tugas id=3
[c] INFO Tugas 3 tersimpan: Tugas baruBedah Baris-Per-Baris #
interface LoggerFaçade: kontrak lima methodtrace/debug/info/warn/error. Di SLF4J nyata, interface ini bernamaorg.slf4j.Logger.object LoggerFactory: factory tunggal yang memegangprovider: () -> LoggerFaçade. MethodgetLogger(nama)mengembalikan instance baru dari provider; bila implementasinyaLogbackLogger, fieldnamadi-set.LogbackConfig.muatKonfigurasi(): mensimulasikan pemuatanlogback.xmlyang mendaftarkan dua appender (CONSOLE dan FILE). Pada Logback nyata, ini dilakukan otomatis olehLoggerContextsaat startup.ManajerTugas.log: field logger privat dengan nama = simple name class. Pola ini adalah standar:private val log = LoggerFactory.getLogger(MyClass::class.java).LoggerFactory.bind(::ConsoleOnlyLogger): mengganti provider di runtime. Di SLF4J nyata, ganti implementasi dilakukan dengan mengganti JAR di classpath; pola ini hanya untuk demonstrasi.
Appender Console dan File #
Appender menentukan ke mana log dikirim. Console appender menulis ke System.out; file appender menulis ke file. Rolling file appender membuat file baru berdasarkan ukuran atau tanggal, sehingga log lama tidak menumpuk tanpa batas. Pada konfigurasi logback.xml, appender di-attach ke logger via <appender-ref>.
Referensi Lanjutan #
- SLF4J — dokumentasi resmi SLF4J untuk binding dan facade logging
- Logback — dokumentasi resmi Logback untuk konfigurasi appender dan pattern
Penutup #
Pola façade + implementasi adalah salah satu contoh terbaik dari prinsip Dependency Inversion yang akan kita pelajari nanti. Pada chapter berikutnya kita akan membahas kapan menggunakan masing-masing level log dalam praktik nyata, lengkap dengan contoh skenario produksi.