Job pipeline berjalan otomatis, sering di tengah malam, tanpa yang melihat layar. Ketika hasil pagi hari ternyata tidak lengkap, pertanyaan pertama operator adalah: langkah mana yang bermasalah, kapan kejadiannya, dan seberapa parah akibatnya. Fungsi print() tidak bisa menjawab — keluarannya tidak menyimpan waktu, tidak menyimpan tingkat keparahan, dan bercampur dengan data yang sengaja dicetak. Modul logging dari pustaka bawaan menyelesaikannya: pesan terstruktur dengan level, asal modul, dan waktu, yang bisa diarahkan ke berkas atau dikumpulkan oleh orchestration. Chapter ini membangun pipeline mini dua modul lalu menjalankannya sampai keluar log aslinya.
Mengapa print() Tidak Cukup untuk Pipeline #
Empat keterbatasan print() terasa ketika skrip berpindah dari meja kita ke server:
print()tidak mencatat waktu kejadian.print()tidak punya level; baris “berhasil” dan “gagal total” tampak sama.print()tidak menyebut asal; pada pipeline besar, semua tulisan bercampur di satu stdout.print()tidak bisa disaring atau dialihkan tanpa mengubah kode;loggingcukup diatur lewat satu konfigurasi di skrip utama.
Satu hal lagi: print() menulis ke stdout, logging bawaan menulis ke stderr — keluaran data tidak pernah tercampur dengan catatan kejadian.
Level dan basicConfig #
logging mengelompokkan pesan ke dalam lima level; tabel berikut menjadi acuan pemakaiannya:
| Level | Nilai | Kapan dipakai |
|---|---|---|
DEBUG | 10 | detail teknis saat menyelidiki masalah |
INFO | 20 | kemajuan normal: langkah dimulai, jumlah baris, hasil akhir |
WARNING | 30 | data dilewati atau nilai ganjil, tapi job masih sehat |
ERROR | 40 | satu langkah gagal; job bisa lanjut dengan hasil parsial |
CRITICAL | 50 | kegagalan yang menghentikan seluruh job |
Pemanggilan logging.basicConfig(...) di skrip utama mengatur level ambang serta format sekali untuk seluruh program. Format "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s" menghasilkan kolom waktu, level selebar delapan karakter, nama logger, lalu isi pesan. Panduan resminya ada di https://docs.python.org/3/howto/logging.html
.
Logger per Modul #
Setiap modul mengambil logger sendiri lewat logging.getLogger(__name__). Di modul etl_langkah, __name__ bernilai "etl_langkah"; di skrip yang dieksekusi langsung, "__main__". Nama tersebut muncul di kolom %(name)s, sehingga operator tahu asal setiap baris log.
Logger tersusun induk-anak mengikuti tanda titik: etl_langkah berada di bawah root dan mewarisi level dari root yang diatur basicConfig. Cukup satu baris konfigurasi di skrip utama; modul lain tidak perlu tahu apa pun tentangnya.
Pipeline Mini dengan Log Asli #
Pipeline terdiri dari dua berkas. Modul etl_langkah.py menampung dua langkah:
"""Modul langkah pipeline untuk demo logging."""
import logging
logger = logging.getLogger(__name__)
def baca_transaksi(baris_mentah):
"""Ubah list of string menjadi list of dict (langkah ingestion)."""
logger.info("Ingestion: membaca %d baris", len(baris_mentah))
hasil = []
for nomor, baris in enumerate(baris_mentah, start=1):
kolom = [k.strip() for k in baris.split(",")]
if len(kolom) != 4:
logger.warning("Baris %d dilewati: jumlah kolom %d, harus 4", nomor, len(kolom))
continue
id_trx, kasir, qty, harga = kolom
hasil.append({"id": id_trx, "kasir": kasir, "qty": qty, "harga": harga})
logger.info("Ingestion selesai: %d baris lolos", len(hasil))
return hasil
def hitung_total(barisan):
"""Hitung total pendapatan; baris rusak dicatat sebagai galat ERROR."""
logger.info("Transform: menghitung total %d baris", len(barisan))
total = 0
for baris in barisan:
try:
total += int(baris["qty"]) * int(baris["harga"])
except ValueError as galat:
logger.error("Baris %s gagal dikonversi: %s", baris["id"], galat)
logger.info("Transform selesai: total Rp%d", total)
return totalSkrip utama pipeline_mini.py mengonfigurasi logging lalu memanggil kedua langkah:
"""Pipeline mini: menggabungkan langkah-langkah dari modul etl_langkah."""
import logging
import etl_langkah
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
BARIS_MENTAH = [
"TRX-001, Budi, 2, 24000",
"TRX-002, Siti, tiga, 15000",
"TRX-003, Dewi, 1, 27000",
"TRX-004, Budi, 1",
]
logger.info("Pipeline dimulai")
barisan = etl_langkah.baca_transaksi(BARIS_MENTAH)
total = etl_langkah.hitung_total(barisan)
logger.info("Pipeline selesai: total pendapatan Rp%d", total)python3 pipeline_mini.pyOutput yang Dihasilkan:
09:25:37 | INFO | __main__ | Pipeline dimulai
09:25:37 | INFO | etl_langkah | Ingestion: membaca 4 baris
09:25:37 | WARNING | etl_langkah | Baris 4 dilewati: jumlah kolom 3, harus 4
09:25:37 | INFO | etl_langkah | Ingestion selesai: 3 baris lolos
09:25:37 | INFO | etl_langkah | Transform: menghitung total 3 baris
09:25:37 | ERROR | etl_langkah | Baris TRX-002 gagal dikonversi: invalid literal for int() with base 10: 'tiga'
09:25:37 | INFO | etl_langkah | Transform selesai: total Rp75000
09:25:37 | INFO | __main__ | Pipeline selesai: total pendapatan Rp75000Kolom waktu mengikuti jam saat skrip dijalankan, jadi di mesin Anda nilainya berbeda. Mari kita telaah baris-per-baris kode di atas:
- Baris 5
etl_langkah.py(getLogger(__name__)): modul mengambil logger bernamaetl_langkah; nama tersebut muncul di kolom ketiga pada setiap baris log-nya. - Baris 10 dan 15 (
logger.info/logger.warning): pesan memakai placeholder%d—logginghanya merangkai teks bila pesan itu akan diteruskan. - Baris 29-31 (
try/except+logger.error): barisTRX-002dengan qty"tiga"gagal dikonversi; galat dicatat sebagai ERROR dan loop berlanjut — job selesai dengan hasil parsialRp75000. - Baris 7-11
pipeline_mini.py(basicConfig): levelINFOberartiDEBUGtidak dicetak; format dandatefmtmembentuk empat kolom pada setiap baris. - Baris 22-25 (urutan langkah): log
__main__membingkai logetl_langkah— “Pipeline dimulai” lebih dulu, “Pipeline selesai” paling akhir.
Baca output di atas seperti laporan shift: satu WARNING untuk data yang dilewati, satu ERROR untuk konversi yang gagal, dan total akhir tetap terhitung. Ketika jumlah langkah bertambah, kita butuh wadah yang lebih rapi daripada fungsi lepas — class yang menampung data bersama perilakunya. Topik tersebut dibuka di chapter berikutnya, Pemrograman Berorientasi Objek.