unisbadri.com » Python Java Golang Typescript Kotlin Ruby Rust Dart PHP

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 memanggil logging.info() sebelum basicConfig(), root logger sudah dikonfigurasi dengan default dan panggilan basicConfig() berikutnya tidak akan berpengaruh. Selalu panggil basicConfig() 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 blok except — otomatis menyertakan traceback tanpa perlu exc_info=True.
  • RotatingFileHandler untuk rotasi berdasarkan ukuran file; TimedRotatingFileHandler untuk rotasi harian/mingguan.
  • Matikan log library pihak ketiga yang terlalu verbose dengan logging.getLogger("nama_lib").setLevel(logging.WARNING).
  • Gunakan dictConfig untuk konfigurasi logging yang kompleks agar mudah diubah dan dikelola.

← Sebelumnya: Collections   Berikutnya: Random →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact