Hướng dẫn logging trong Python: Debug nhanh hơn, vận hành dễ hơn

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Vấn đề khi không có logging

Dự án automation của mình ban đầu chỉ có 200 dòng — mọi thứ chạy ngon, debug bằng print() là đủ. Rồi project phình lên 2000 dòng, chạy như một service 24/7 trên VPS. Lúc đó mới vỡ ra: khi có lỗi xảy ra lúc 3 giờ sáng, không có gì để nhìn lại ngoài… màn hình đen.

print() không lưu vào file. Không có timestamp. Không biết lỗi xuất hiện ở module nào. Và quan trọng hơn — khi production crash, bạn cần biết chuyện gì đã xảy ra trước khi crash, không chỉ thông báo lỗi cuối cùng.

Module logging trong Python standard library sinh ra để giải quyết đúng bài toán đó. Học một lần, dùng mãi.

Khái niệm cốt lõi cần nắm

Log Level là gì?

Python logging có 5 mức độ, từ thấp đến cao:

  • DEBUG (10) — Chi tiết kỹ thuật, dùng khi develop
  • INFO (20) — Thông tin hoạt động bình thường
  • WARNING (30) — Cảnh báo, chưa phải lỗi nhưng cần chú ý
  • ERROR (40) — Lỗi xảy ra, nhưng chương trình vẫn chạy được
  • CRITICAL (50) — Lỗi nghiêm trọng, có thể làm chương trình dừng

Logger chỉ ghi những message có level bằng hoặc cao hơn mức đã set. Set WARNING thì DEBUGINFO bị bỏ qua hoàn toàn — hữu ích để giảm noise trên production.

Ba thành phần cần hiểu

Ba thứ này phối hợp với nhau, thiếu một cái là log hoặc không ra, hoặc ra không đúng chỗ:

  • Logger — Nơi bạn gọi log.info(), log.error()… Mỗi logger có tên riêng, thường đặt theo tên module.
  • Handler — Quyết định log đi đâu: console, file, email, HTTP endpoint…
  • Formatter — Quyết định log trông như thế nào: có timestamp không, có tên module không, format ra sao.

Thực hành chi tiết

Bước 1: Setup cơ bản trong 5 dòng

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

logging.info("Script bắt đầu chạy")
logging.warning("Cảnh báo: config file không tìm thấy, dùng default")
logging.error("Lỗi kết nối database")

Output:

2026-03-25 10:15:32,104 - INFO - Script bắt đầu chạy
2026-03-25 10:15:32,105 - WARNING - Cảnh báo: config file không tìm thấy, dùng default
2026-03-25 10:15:32,106 - ERROR - Lỗi kết nối database

Chạy được ngay, nhưng log chỉ hiện trên console. Tắt terminal là mất sạch.

Bước 2: Ghi log ra file

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler()  # Vẫn hiện trên console
    ]
)

log = logging.getLogger(__name__)
log.info("Đã kết nối database thành công")

Đặt tên logger bằng __name__ là cách làm chuẩn — khi project có chục module, bạn nhìn vào log là biết ngay message đến từ file nào, không cần đoán.

Bước 3: Log rotation — tránh file log phình to

Chạy service 24/7 vài tháng mà không có rotation, file log dễ chiếm vài GB. Trên VPS 20GB thì đó là vấn đề thật. RotatingFileHandler giải quyết gọn:

import logging
from logging.handlers import RotatingFileHandler

log = logging.getLogger("myapp")
log.setLevel(logging.DEBUG)

# Tối đa 5MB mỗi file, giữ lại 3 file backup
handler = RotatingFileHandler(
    "app.log",
    maxBytes=5 * 1024 * 1024,  # 5MB
    backupCount=3,
    encoding="utf-8"
)
formatter = logging.Formatter(
    "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
log.addHandler(handler)

log.info("Logger đã sẵn sàng")

Kết quả: app.log, app.log.1, app.log.2, app.log.3 — tổng tối đa khoảng 20MB, tự xoay vòng khi đầy.

Bước 4: Tổ chức logging theo module

Project lớn lên, mỗi module cần logger riêng. Đừng copy-paste setup code khắp nơi — tạo một hàm setup tập trung, gọi ở đâu cũng được:

# logger.py
import logging
from logging.handlers import RotatingFileHandler

def setup_logger(name: str, log_file: str = "app.log", level=logging.INFO):
    logger = logging.getLogger(name)
    if logger.handlers:  # Tránh thêm handler trùng lặp
        return logger

    logger.setLevel(level)
    fmt = logging.Formatter(
        "%(asctime)s [%(name)s] %(levelname)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    fh = RotatingFileHandler(log_file, maxBytes=5_000_000, backupCount=3)
    fh.setFormatter(fmt)

    ch = logging.StreamHandler()
    ch.setFormatter(fmt)

    logger.addHandler(fh)
    logger.addHandler(ch)
    return logger

Mỗi module chỉ cần hai dòng:

# database.py
from logger import setup_logger

log = setup_logger(__name__)

def connect(url):
    log.info(f"Kết nối tới {url}")
    # ...
    log.debug("Connection pool initialized")

Bước 5: Log exception đúng cách

Lỗi hay gặp nhất: log exception nhưng quên traceback, xong không biết lỗi xảy ra ở dòng nào.

# SAI — chỉ có message, mất hết traceback
try:
    result = 10 / 0
except ZeroDivisionError as e:
    log.error(f"Lỗi: {e}")

# ĐÚNG — giữ nguyên traceback đầy đủ
try:
    result = 10 / 0
except ZeroDivisionError:
    log.exception("Phép tính thất bại")  # Tự động đính kèm stack trace

# Cách khác, tương đương
try:
    result = 10 / 0
except ZeroDivisionError:
    log.error("Phép tính thất bại", exc_info=True)

log.exception() chỉ dùng được bên trong except block. Nó tương đương log.error() kèm stack trace — khi debug production issue lúc 2 giờ sáng, traceback đầy đủ là thứ bạn muốn nhất.

Bước 6: Thêm context vào log — trace khi có nhiều task chạy song song

Service xử lý 10 task đồng thời, log ra cùng một file — nhìn vào chỉ thấy hỗn độn. LoggerAdapter giúp gán context (ví dụ: task ID) vào mọi dòng log trong một scope:

import logging

# Formatter phải có %(task_id)s để hiện context
fmt = logging.Formatter("%(asctime)s [%(task_id)s] %(levelname)s: %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(fmt)

base_log = logging.getLogger("worker")
base_log.addHandler(handler)
base_log.setLevel(logging.DEBUG)

def process_task(task_id: str, data: dict):
    log = logging.LoggerAdapter(base_log, {"task_id": task_id})
    log.info("Bắt đầu xử lý")
    log.info(f"Xử lý {len(data)} items")
    # Output:
    # 2026-03-25 10:00:01 [abc-123] INFO: Bắt đầu xử lý
    # 2026-03-25 10:00:01 [abc-123] INFO: Xử lý 42 items

process_task("abc-123", {})
process_task("xyz-456", {})

Khi log của hai task xen kẽ nhau, bạn vẫn lọc được đúng task cần xem — chỉ cần grep abc-123 app.log.

Kết luận

Nhìn lại dự án automation của mình — từ 200 dòng print() đến 2000 dòng với hệ thống log đầy đủ — sự khác biệt rõ nhất không phải lúc đang code, mà lúc có sự cố. Log rotation 5MB × 3 backup cho phép nhìn lại cả tuần hoạt động. log.exception() đảm bảo không bao giờ mất traceback.

Mấy điểm quan trọng cần nhớ:

  • Đặt logging.getLogger(__name__) thay print() ngay từ đầu — chi phí thấp, lợi ích lớn
  • Cấu hình RotatingFileHandler để log không phình vô hạn
  • Luôn dùng log.exception() trong except block — đừng để mất traceback
  • Tách hàm setup logger ra module riêng, tránh copy-paste
  • Production dùng INFO, development dùng DEBUG — đừng để DEBUG lên production

Project tiếp theo, setup logging trước khi viết business logic. Sau này khi có sự cố — và sẽ có — bạn sẽ cảm ơn bản thân vì điều đó.

Share: