Memahami level log adalah hal mudah; memilih level yang tepat untuk setiap pesan adalah keterampilan tersendiri. Pesan dengan level terlalu tinggi akan membanjiri log produksi; pesan dengan level terlalu rendah akan terlewat saat insiden terjadi. Pada chapter ini kita akan mendiskusikan pedoman pemilihan level dengan contoh konkret, lalu mendemonstrasikan bagaimana perubahan level aktif memengaruhi output yang muncul. Kode demo akan menunjukkan empat skenario berbeda dengan level aktif berbeda.

Pedoman Pemilihan Level #

Pedoman berikut adalah konsensus luas di komunitas:

LevelTujuanPertanyaan kunci
TRACEDetail ekstrem, alur method“Apakah saya butuh jejak masuk-keluar setiap method?”
DEBUGInformasi dev, state variabel“Apakah pesan ini berguna saat saya debugging lokal?”
INFOPeristiwa signifikan normal“Apakah operator perlu tahu ini terjadi?”
WARNMasalah dapat dipulihkan“Apakah ada yang perlu diperhatikan, tapi bukan error?”
ERRORKegagalan yang mempengaruhi user“Apakah user terdampak atau operasi penting gagal?”

Kode Demo Empat Skenario #

Program berikut mendefinisikan LayananPengguna dengan method cari dan validasi. Kita akan menjalankan skenario yang sama dengan level aktif berbeda untuk melihat perbedaan output.

kotlin
// Demo pemilihan level log berdasarkan konteks kejadian.

enum class LogLevel { TRACE, DEBUG, INFO, WARN, ERROR }

class Logger(val nama: String, var levelAktif: LogLevel = LogLevel.INFO) {
    private fun cetak(level: LogLevel, pesan: String) {
        if (level.ordinal >= levelAktif.ordinal) {
            println("2026-01-15 [${level.name}] $nama - $pesan")
        }
    }
    fun trace(p: String) = cetak(LogLevel.TRACE, p)
    fun debug(p: String) = cetak(LogLevel.DEBUG, p)
    fun info(p: String) = cetak(LogLevel.INFO, p)
    fun warn(p: String) = cetak(LogLevel.WARN, p)
    fun error(p: String) = cetak(LogLevel.ERROR, p)
}

data class Pengguna(val id: Int, val nama: String, val email: String)

class LayananPengguna {
    private val log = Logger("LayananPengguna")

    fun cari(id: Int): Pengguna? {
        log.trace("Memasuki cari(id=$id)")
        val pengguna = dbMock[id]
        if (pengguna == null) {
            log.warn("Pengguna id=$id tidak ditemukan")
        } else {
            log.debug("Pengguna ditemukan: ${pengguna.nama}")
        }
        log.trace("Meninggalkan cari(id=$id)")
        return pengguna
    }

    fun validasi(p: Pengguna) {
        if (p.email.isBlank()) {
            log.error("Validasi gagal: email kosong untuk pengguna ${p.id}")
            throw IllegalArgumentException("Email kosong")
        }
        log.info("Pengguna ${p.id} tervalidasi")
    }

    private val dbMock = mapOf(
        1 to Pengguna(1, "Budi", "[email protected]"),
        2 to Pengguna(2, "Siti", "[email protected]")
    )
}

fun main() {
    println("=== Demo Pemilihan Level Log ===")

    // Skenario 1: level INFO (default produksi)
    println("--- Skenario 1: Level INFO (produksi) ---")
    val svc1 = LayananPengguna()
    svc1.cari(1)
    svc1.cari(99)

    // Skenario 2: level TRACE (saat debugging mendalam)
    println()
    println("--- Skenario 2: Level TRACE (debugging) ---")
    val svc2 = LayananPengguna()
    // Untuk simulasi, kita tukar level lewat refleksi atau konstruksi ulang:
    val log2 = Logger("LayananPengguna", LogLevel.TRACE)
    val svcTrace = LayananPenggunaDenganLogger(log2)
    svcTrace.cari(1)
    svcTrace.cari(99)

    // Skenario 3: ERROR saja (sistem minim)
    println()
    println("--- Skenario 3: Level ERROR (sistem minim) ---")
    val log3 = Logger("LayananPengguna", LogLevel.ERROR)
    val svcError = LayananPenggunaDenganLogger(log3)
    svcError.cari(1)
    svcError.cari(99)

    // Skenario 4: ERROR yang sebenarnya
    println()
    println("--- Skenario 4: Validasi gagal -> ERROR ---")
    val svc4 = LayananPengguna()
    try {
        svc4.validasi(Pengguna(5, "Kosong", ""))
    } catch (e: IllegalArgumentException) {
        println("Ditangkap: ${e.message}")
    }
}

class LayananPenggunaDenganLogger(private val log: Logger) {
    fun cari(id: Int): Pengguna? {
        log.trace("Memasuki cari(id=$id)")
        return null
    }
}

Output yang Dihasilkan:

text
=== Demo Pemilihan Level Log ===
--- Skenario 1: Level INFO (produksi) ---
2026-01-15 [WARN] LayananPengguna - Pengguna id=99 tidak ditemukan

--- Skenario 2: Level TRACE (debugging) ---
2026-01-15 [TRACE] LayananPengguna - Memasuki cari(id=1)
2026-01-15 [TRACE] LayananPengguna - Memasuki cari(id=99)

--- Skenario 3: Level ERROR (sistem minim) ---

--- Skenario 4: Validasi gagal -> ERROR ---
2026-01-15 [ERROR] LayananPengguna - Validasi gagal: email kosong untuk pengguna 5
Ditangkap: Email kosong

TRACE: Saat Mencari Alur Eksekusi #

Pada Skenario 2, kita lihat TRACE menampilkan “Memasuki cari(id=1)” dan “Meninggalkan cari(id=1)”. Pesan ini berguna saat kita ingin memahami alur eksekusi secara detail, terutama pada codebase besar dengan banyak method bertingkat. Tanpa TRACE, kita akan kesulitan mengetahui method mana yang dipanggil sebelum bug terjadi.

DEBUG: Informasi State Internal #

DEBUG digunakan untuk mencatat state yang berguna saat debugging lokal, misalnya nilai variabel atau cache hit/miss. Pada demo, “Pengguna ditemukan: Budi” adalah pesan DEBUG. Di produksi, pesan ini tidak perlu; saat developer mencoba mereproduksi bug di lokal, pesan ini sangat berharga.

INFO: Peristiwa Signifikan Normal #

INFO untuk peristiwa yang operator produksi ingin tahu terjadi: pengguna login, pesanan dibuat, job terjadwal selesai. Pada demo, “Pengguna X tervalidasi” adalah INFO. Volume INFO harus relatif rendah (sekitar 1-10 pesan per request) agar log tetap dapat dipindai.

WARN: Masalah yang Dapat Dipulihkan #

WARN untuk situasi abnormal tetapi tidak fatal: koneksi database retry lalu berhasil, request lambat melebihi threshold, atau pengguna tidak ditemukan. Pada demo, “Pengguna id=99 tidak ditemukan” adalah WARN karena pelanggan meminta id yang tidak ada — mungkin bug di klien, mungkin input user, tetapi bukan kegagalan sistem.

ERROR: Kegagalan yang Memengaruhi User #

ERROR untuk kegagalan operasi penting: gagal menyimpan pesanan, validasi input gagal, exception tidak tertangkap. Pada demo Skenario 4, validasi email kosong melempar exception dan dicatat sebagai ERROR. Pesan ERROR harus selalu disertai context yang cukup (id, parameter, stack trace) untuk investigasi.

Referensi Lanjutan #

  • SLF4J — dokumentasi SLF4J untuk level log TRACE, DEBUG, INFO, WARN, ERROR
  • Logback — dokumentasi Logback untuk konfigurasi threshold level per appender

Penutup #

Pemilihan level yang tepat adalah keseimbangan antara konteks yang cukup dan volume yang manageable. Pada chapter berikutnya kita akan beralih dari logging ke topik lain: static analysis, yaitu menemukan masalah dalam kode tanpa menjalankannya.