Konfigurasi basicConfig() memasang satu format untuk satu tujuan — cukup untuk skrip kecil, tetapi aplikasi nyata butuh lebih: pesan WARNING ke atas tampil di layar sementara log lengkap level DEBUG tersimpan rapi di berkas, masing-masing dengan format berbeda. Module logging memisahkan dua keputusan tersebut ke dalam dua komponen: Formatter mengatur bagaimana sebuah baris log ditulis, dan Handler mengatur ke mana baris tersebut dikirim. Pada Chapter ini kita akan memakai keduanya, mengatur level per handler, memahami flag propagate, lalu merangkum semuanya lewat dictConfig.
Formatter: Menyusun Isi Baris Log #
Object Formatter menerima string template dengan placeholder %(namaatribut)s. Atribut yang paling sering dipakai: asctime (stempel waktu), name (nama logger), levelname (nama level), dan message (isi pesan). basicConfig() sebenarnya hanya jalan pintas untuk membuat formatter tersebut:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
)
logging.warning("Stok kopi habis")Output yang Dihasilkan (dicetak module logging ke stderr):
2025-06-15 09:30:45,120 | root | WARNING | Stok kopi habisPlaceholder lain yang sering berguna: %(funcName)s untuk nama function pengirim pesan dan %(lineno)d untuk nomor barisnya — kombinasi yang sangat membantu saat menelusuri sumber log di file besar.
Handler: Menentukan Tujuan Log #
Handler adalah object yang benar-benar menulis baris log ke suatu tempat. Dua handler bawaan yang paling sering dipakai:
| Handler | Tujuan | Cocok untuk |
|---|---|---|
StreamHandler() | stream (default stderr) | pesan yang perlu dilihat langsung di terminal |
FileHandler("nama.log") | sebuah berkas | jejak lengkap yang diperiksa belakangan |
Keunggulan arsitektur handler adalah kombinasi: beberapa handler bisa dipasang pada satu logger, dan setiap handler punya saringan level sendiri. Pola berikut memasang dua handler — layar hanya menerima WARNING ke atas, berkas menerima semuanya:
import logging
logger = logging.getLogger("toko")
logger.setLevel(logging.DEBUG)
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
file_handler = logging.FileHandler("toko.log", mode="w", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
)
logger.addHandler(console)
logger.addHandler(file_handler)
logger.debug("Detail internal")
logger.warning("Perlu perhatian")Output yang Dihasilkan (di terminal, dari StreamHandler):
WARNING: Perlu perhatianIsi berkas toko.log (stempel waktunya berubah setiap eksekusi):
09:30:45 DEBUG Detail internal
09:30:45 WARNING Perlu perhatianMari kita bedah pengaturannya:
- Baris 4 (
logger.setLevel(logging.DEBUG)): saringan level logger dibuka penuh; tanpa ini, pesan DEBUG akan terbuang sebelum sempat sampai ke handler mana pun. - Baris 6-8 (
console): StreamHandler dengan level WARNING dan format ringkas tanpa stempel waktu. - Baris 10-14 (
file_handler): FileHandler membukatoko.logdengan mode"w"agar berkas ditulis ulang dari kosong setiap eksekusi (ganti ke"a"untuk menambah). Level DEBUG membuatnya menerima semua pesan. - Baris 16-17 (
addHandler): kedua handler dipasang pada loggertoko.
Satu pesan logger.warning("Perlu perhatian") kini ditulis dua kali dengan dua wajah berbeda — ringkas di layar, berstempel waktu di berkas.
Flag propagate #
Perilaku bawaan logger adalah meneruskan pesan naik ke seluruh leluhurnya sampai ke root. Flag propagate mengendalikan penerusan tersebut. Mematikan flag ini mencegah pesan tampil dua kali saat logger anak sudah punya handler sendiri:
import logging
logging.basicConfig(format="root: %(message)s", level=logging.INFO)
anak = logging.getLogger("toko.kasir")
handler_anak = logging.StreamHandler()
handler_anak.setFormatter(logging.Formatter("anak: %(message)s"))
anak.addHandler(handler_anak)
anak.info("dengan propagate aktif")
anak.propagate = False
anak.info("setelah propagate dimatikan")Output yang Dihasilkan (dicetak module logging ke stderr):
anak: dengan propagate aktif
root: dengan propagate aktif
anak: setelah propagate dimatikanPesan pertama ditulis dua kali: sekali oleh handler milik logger anak, sekali lagi oleh handler root yang menerima terusan. Setelah propagate = False, terusan ke root terputus dan hanya handler anak yang bekerja.
Konfigurasi Terpusat dengan dictConfig #
Memasang handler satu per satu lewat kode cepat berantakan begitu jumlah handler bertambah. Function logging.config.dictConfig() menerima seluruh konfigurasi sebagai satu dictionary — pemisahan yang enak dibaca karena deklaratif:
import logging
import logging.config
config = {
"version": 1,
"formatters": {
"ringkas": {"format": "%(levelname)s %(name)s: %(message)s"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "ringkas",
},
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
}
logging.config.dictConfig(config)
logging.getLogger("toko.kasir").warning("Struk gagal dicetak")Output yang Dihasilkan (dicetak module logging ke stderr):
WARNING toko.kasir: Struk gagal dicetakStruktur dictionary tersebut memetakan ulang konsep-konsep yang sudah kita kenal: key formatters mendefinisikan formatter bernama, key handlers mendefinisikan handler yang merujuk formatter tersebut lewat key formatter, dan key root memasang semuanya pada root logger beserta levelnya. Konfigurasi ini biasa dipindah ke berkas JSON atau YAML agar tim bisa mengubah perilaku log tanpa menyentuh kode.
Kita kini menguasai bentuk dan tujuan log. Bab berikutnya mengangkat satu lompatan lagi: membuat baris log yang bisa dibaca mesin, bukan hanya manusia.