Logging #
Setiap program yang berjalan di production membutuhkan logging yang baik. Tanpa log, kamu tidak bisa mendiagnosis bug yang hanya muncul di server, melacak alur eksekusi yang salah, atau memantau performa aplikasi. Python menyediakan modul logging yang sangat lengkap — jauh lebih baik dari print() untuk hampir semua kasus nyata. Memahami cara kerjanya akan menghemat banyak waktu debugging di kemudian hari.
Mengapa Bukan print()
#
# ANTI-PATTERN: debug dengan print()
def proses_pembayaran(order_id, jumlah):
print(f"Memproses pembayaran order {order_id}") # tidak ada timestamp
print(f"Jumlah: {jumlah}") # tidak ada level severity
# bagaimana matikan semua print ini di production?
# bagaimana kirim log ini ke file?
# bagaimana filter hanya error saja?
# BENAR: gunakan logging
import logging
logger = logging.getLogger(__name__)
def proses_pembayaran(order_id, jumlah):
logger.info("Memproses pembayaran order %s", order_id)
logger.debug("Detail jumlah: %s", jumlah)
# bisa dikontrol level-nya, dikirim ke file, diformat, difilter
Level Log #
Python mendefinisikan lima level log standar, dari yang paling rendah ke paling tinggi:
DEBUG (10) -- informasi detail untuk debugging, biasanya dimatikan di production
INFO (20) -- konfirmasi bahwa sesuatu berjalan sesuai harapan
WARNING (30) -- sesuatu yang tidak terduga terjadi, tapi program masih berjalan
ERROR (40) -- error yang menyebabkan fungsi gagal, tapi program masih jalan
CRITICAL (50) -- error serius yang mungkin menghentikan program
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("Query database selesai dalam 12ms")
logging.info("User login berhasil: user_id=42")
logging.warning("Koneksi database lambat, response time > 500ms")
logging.error("Gagal mengirim email ke [email protected]")
logging.critical("Database tidak bisa dihubungi, semua request gagal")
Output:
DEBUG:root:Query database selesai dalam 12ms
INFO:root:User login berhasil: user_id=42
WARNING:root:Koneksi database lambat, response time > 500ms
ERROR:root:Gagal mengirim email ke [email protected]
CRITICAL:root:Database tidak bisa dihubungi, semua request gagal
basicConfig — Setup Cepat #
logging.basicConfig() adalah cara paling cepat untuk mengkonfigurasi logging. Cukup untuk script sederhana atau saat prototyping.
import logging
# Log ke konsol dengan format lengkap
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Log ke file
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
filename="app.log",
filemode="a", # "a" untuk append, "w" untuk overwrite tiap run
encoding="utf-8",
)
basicConfig()hanya berefek jika root logger belum punya handler. Jika kamu memanggillogging.info()sebelumbasicConfig(), root logger sudah dikonfigurasi dengan default dan panggilanbasicConfig()berikutnya tidak akan berpengaruh. Selalu panggilbasicConfig()di awal program sebelum logging apapun.
Logger, Handler, dan Formatter #
Untuk aplikasi yang lebih kompleks, kamu perlu memahami tiga komponen utama sistem logging Python:
Logger -- titik masuk yang kamu gunakan di kode (getLogger)
│
▼
Handler -- menentukan ke mana log dikirim (file, konsol, email, dll)
│
▼
Formatter -- menentukan bagaimana format pesan log
Logger #
import logging
# Selalu gunakan __name__ sebagai nama logger
# Ini membuat nama logger mengikuti hierarki modul: "myapp.services.payment"
logger = logging.getLogger(__name__)
# Jangan gunakan root logger langsung di modul library/aplikasi
# ANTI-PATTERN:
logging.info("pesan") # menggunakan root logger -- sulit dikontrol
# BENAR:
logger = logging.getLogger(__name__)
logger.info("pesan") # menggunakan named logger
Handler #
Handler menentukan tujuan output log. Satu logger bisa punya beberapa handler sekaligus.
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG) # level minimum yang diproses logger ini
# StreamHandler -- log ke konsol (stdout/stderr)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # filter: hanya INFO ke atas
# FileHandler -- log ke file
file_handler = logging.FileHandler("app.log", encoding="utf-8")
file_handler.setLevel(logging.DEBUG) # filter: semua level ke file
# Tambahkan handler ke logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Sekarang: DEBUG dan di atas masuk ke file, INFO dan di atas juga ke konsol
logger.debug("detail debug") # hanya ke file
logger.info("info penting") # ke file DAN konsol
logger.error("ada error!") # ke file DAN konsol
Formatter #
import logging
# Format string yang umum digunakan
FORMAT_SEDERHANA = "%(levelname)s: %(message)s"
FORMAT_LENGKAP = "%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s"
FORMAT_JSON_LIKE = '{"time": "%(asctime)s", "level": "%(levelname)s", "msg": "%(message)s"}'
# Atribut yang tersedia dalam format string:
# %(asctime)s -- waktu log dibuat
# %(name)s -- nama logger
# %(levelname)s -- level sebagai string (DEBUG, INFO, dll)
# %(levelno)d -- level sebagai angka (10, 20, dll)
# %(message)s -- pesan log
# %(filename)s -- nama file
# %(lineno)d -- nomor baris
# %(funcName)s -- nama fungsi
# %(process)d -- process ID
# %(thread)d -- thread ID
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
Konfigurasi Lengkap untuk Aplikasi #
Berikut pola konfigurasi yang umum digunakan untuk aplikasi production — log ke konsol sekaligus ke file, dengan level yang berbeda.
import logging
import logging.handlers
from pathlib import Path
def setup_logging(log_level: str = "INFO", log_file: str = "app.log"):
"""Konfigurasi logging untuk aplikasi."""
# Buat direktori log jika belum ada
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
# Format
fmt = "%(asctime)s [%(levelname)-8s] %(name)s - %(message)s"
formatter = logging.Formatter(fmt=fmt, datefmt="%Y-%m-%d %H:%M:%S")
# Root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # tangkap semua, filter di handler
# Handler 1: konsol -- hanya INFO ke atas
console = logging.StreamHandler()
console.setLevel(getattr(logging, log_level.upper()))
console.setFormatter(formatter)
# Handler 2: file dengan rotasi -- semua level
# RotatingFileHandler: rotasi saat file mencapai ukuran tertentu
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10 * 1024 * 1024, # 10 MB per file
backupCount=5, # simpan 5 file lama
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
root_logger.addHandler(console)
root_logger.addHandler(file_handler)
# Panggil sekali di entry point aplikasi
setup_logging(log_level="INFO", log_file="logs/app.log")
RotatingFileHandler vs TimedRotatingFileHandler #
import logging.handlers
# RotatingFileHandler -- rotasi berdasarkan ukuran file
rotating = logging.handlers.RotatingFileHandler(
"app.log",
maxBytes=10_000_000, # 10 MB
backupCount=5, # simpan app.log.1 sampai app.log.5
)
# TimedRotatingFileHandler -- rotasi berdasarkan waktu
timed = logging.handlers.TimedRotatingFileHandler(
"app.log",
when="midnight", # rotasi setiap tengah malam
interval=1, # setiap 1 hari
backupCount=30, # simpan 30 hari terakhir
encoding="utf-8",
)
# File lama: app.log.2024-01-15, app.log.2024-01-14, dst.
Hierarki Logger #
Logger di Python mengikuti hierarki berdasarkan nama, dipisah titik. Ini memungkinkan kontrol logging per modul atau per subsistem.
import logging
# Hierarki logger:
# root
# └── myapp
# ├── myapp.services
# │ └── myapp.services.payment
# └── myapp.api
# Log dari child diteruskan ke parent secara default (propagate=True)
logger_payment = logging.getLogger("myapp.services.payment")
logger_api = logging.getLogger("myapp.api")
logger_app = logging.getLogger("myapp")
# Atur level berbeda per subsistem
logging.getLogger("myapp").setLevel(logging.INFO)
logging.getLogger("myapp.services.payment").setLevel(logging.DEBUG)
# -- payment logger mencatat DEBUG, tapi subsistem lain hanya INFO
# Matikan propagasi jika tidak ingin log diteruskan ke parent
logger_terlalu_verbose = logging.getLogger("library.noisy")
logger_terlalu_verbose.propagate = False
Logging Exception #
Sertakan traceback exception di log untuk memudahkan debugging.
import logging
logger = logging.getLogger(__name__)
def bagi(a, b):
try:
return a / b
except ZeroDivisionError:
# ANTI-PATTERN: log tanpa traceback
logger.error("Terjadi error pembagian")
# BENAR: sertakan traceback dengan exc_info=True
logger.error("Gagal membagi %s dengan %s", a, b, exc_info=True)
# Atau gunakan logger.exception() -- otomatis sertakan traceback
logger.exception("Gagal membagi %s dengan %s", a, b)
return None
bagi(10, 0)
Output dengan logger.exception():
ERROR:__main__:Gagal membagi 10 dengan 0
Traceback (most recent call last):
File "app.py", line 6, in bagi
return a / b
ZeroDivisionError: division by zero
Extra dan LoggerAdapter #
Tambahkan konteks tambahan ke setiap pesan log — berguna untuk melacak request ID, user ID, atau informasi sesi.
import logging
logger = logging.getLogger(__name__)
# extra -- tambahkan field satu kali per pemanggilan
logger.info(
"Pembayaran berhasil",
extra={"user_id": 42, "order_id": "ORD-001", "amount": 150000}
)
# LoggerAdapter -- tambahkan konteks yang sama ke semua pesan
class RequestLogger(logging.LoggerAdapter):
def process(self, msg, kwargs):
return f"[req:{self.extra['request_id']}] {msg}", kwargs
# Buat adapter dengan konteks request
request_logger = RequestLogger(logger, {"request_id": "abc-123"})
request_logger.info("Menerima request")
request_logger.info("Validasi input selesai")
request_logger.error("Gagal memproses")
# Semua pesan otomatis disertai [req:abc-123]
Menonaktifkan Log dari Library Pihak Ketiga #
Library seperti urllib3, boto3, atau sqlalchemy sering menghasilkan log yang terlalu banyak. Cara membungkamnya:
import logging
# Naikkan level ke WARNING agar hanya error yang muncul
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("boto3").setLevel(logging.WARNING)
logging.getLogger("botocore").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
# Atau matikan sama sekali
logging.getLogger("library.yang.berisik").disabled = True
Konfigurasi via dictConfig
#
Untuk aplikasi yang lebih besar, pisahkan konfigurasi logging ke dictionary atau file YAML/JSON agar mudah diubah tanpa menyentuh kode.
import logging
import logging.config
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s [%(levelname)-8s] %(name)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"simple": {
"format": "%(levelname)s: %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "standard",
"filename": "logs/app.log",
"maxBytes": 10485760,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"myapp": {
"level": "DEBUG",
"handlers": ["console", "file"],
"propagate": False,
},
"myapp.services.payment": {
"level": "DEBUG",
"handlers": ["file"],
"propagate": True,
},
},
"root": {
"level": "WARNING",
"handlers": ["console"],
},
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("myapp")
logger.info("Aplikasi dimulai")
Ringkasan #
- Jangan gunakan
print()untuk logging — tidak ada level, tidak ada timestamp, tidak bisa dikontrol, tidak bisa dikirim ke file.- Selalu gunakan
logging.getLogger(__name__)di setiap modul — ini membuat nama logger mengikuti hierarki modul dan mudah dikontrol per subsistem.- Set level di logger dan handler secara terpisah — logger menentukan level minimum yang diproses, handler menentukan level minimum yang dikirim ke tujuannya.
- Gunakan
logger.exception()di dalam blokexcept— otomatis menyertakan traceback tanpa perluexc_info=True.RotatingFileHandleruntuk rotasi berdasarkan ukuran file;TimedRotatingFileHandleruntuk rotasi harian/mingguan.- Matikan log library pihak ketiga yang terlalu verbose dengan
logging.getLogger("nama_lib").setLevel(logging.WARNING).- Gunakan
dictConfiguntuk konfigurasi logging yang kompleks agar mudah diubah dan dikelola.