Học bằng cách đọc theo thứ tự

logging — Ghi lại thay vì print

Học 5 mức log và basicConfig cho định dạng, các mã %(levelname)s cho format, và FileHandler để ghi ra file — cách bỏ debug bằng print và chuyển sang log thật.

logging là module chuẩn để ghi lại hành vi của ứng dụng theo từng giai đoạn. Viết output debug bằng print gặp vấn đề: bạn quên xóa lúc production, không biết cái nào quan trọng, và chuyển sang ghi file sau này nghĩa là viết lại tất cả. Với logging, bạn được các level mức độ quan trọng (DEBUG / INFO / WARNING / ERROR / CRITICAL) để lọc output, Formatter để định dạng thống nhất, và Handler để chuyển đổi đích đến (stdout / file / remote).

logging xử lý một lệnh gọi thế nào
Codelogger.info(...)LoggerLọc theo levelHandlerChọn đích đếnFormatterRáp định dạng
Khi bạn gọi logger.info(...) từ code, luồng là Logger lọc theo levelHandler quyết định đích đếnFormatter ráp định dạng. Vì ba trách nhiệm tách bạch, đổi định dạng hoặc đích đến tại một chỗ đổi toàn bộ hành vi.

Level log — 5 mức độ quan trọng

logging có 5 level, và chỉ thông điệp ở mức bằng hoặc trên level đặt trên Logger mới được phát ra. Thực hành chuẩn là DEBUG lúc phát triển và INFO hoặc WARNING lúc production — và việc đổi lượng output từ một thiết lập duy nhất mà không chạm code là khác biệt lớn nhất với print.

5 level của logging
DEBUGChi tiếtINFOTiến triểnWARNINGCảnh báoERRORThất bạiCRITICALThảm họa
DEBUG (chi tiết) / INFO (tiến triển bình thường) / WARNING (cảnh báo) / ERROR (thất bại) / CRITICAL (thảm họa). Chỉ thông điệp ở mức bằng hoặc trên level của Logger được phát ra — DEBUG lúc phát triển, INFO/WARNING lúc production là pattern chuẩn.

Đầu tiên, cấu hình định dạng và level với basicConfig và phát một INFO log.

① Hãy import logging và cấu hình basicConfig với level=logging.INFO, format="[%(levelname)s] %(message)s", force=True

② Hãy lấy logger qua logger = logging.getLogger("app")

③ Hãy phát một log qua logger.info("Ứng dụng khởi động")

(Nếu code chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

Chọn giữa WARNING / ERROR / DEBUG

Với logger đã cấu hình, gọi level khác INFO và quan sát output. Vì level đặt là INFO, DEBUG không cho output gì cả — hãy xác nhận điều đó. Đây là nền tảng của hành vi "đổi lượng output từ một thiết lập" của logging.

Tái sử dụng cấu hình logger từ Thực hành 1 và phát ở ba level. Lưu ý: ở INFO, DEBUG không hiện ra.

① Hãy phát WARNING qua logger.warning("Config cũ")

② Hãy phát ERROR qua logger.error("Kết nối DB thất bại")

③ Hãy gọi logger.debug("Trace chi tiết") — ở level INFO, không gì nên in

Python Editor

Chạy code để xem đầu ra

Phát log ra file

Dự án thực tế hầu như luôn cần log lưu vào file, không chỉ trên màn hình. Sau này bạn sẽ muốn grep để điều tra hậu sự cố hoặc đưa vào công cụ giám sát. Truyền filename="app.log" cho basicConfigđích đến của root logger chuyển sang file — làm cùng việc với print sẽ là viết lại mọi lệnh print, nhưng với logging bạn đổi một dòng cấu hình để chuyển hướng.

Output màn hình so với output file
logger.info(...)(chỉ màn hình)Terminal đóng→ Mất loglogger.info(...)filename=app.logSau này grep / nhậpvào công cụ giám sát
Output màn hình biến mất khi terminal đóng, nhưng output file sống sót để grep hậu sự cốnhập vào công cụ giám sát. Với logging, bạn flip giữa chúng trong một dòng cấu hình.

filemode"a" (mặc định, append) hoặc "w" (ghi đè). Append là lựa chọn production — giữ lịch sử. Ghi đè thường tiện hơn lúc phát triển — bắt đầu sạch mỗi lần. Để phát ra cả màn hình và file, bạn bỏ qua basicConfig và kết hợp StreamHandler + FileHandler rõ ràng (giới thiệu sau).

Kết hợp filemode và Handler
filemode="a" append→ Giữ lịch sử (prod)filemode="w" ghi đè→ Bảng sạch (dev)StreamHandler→ Output màn hìnhFileHandler→ Output file
filemode="a" append và giữ lịch sử (production), trong khi filemode="w" ghi đè cho bảng sạch mỗi lần chạy (phát triển). Để gửi cả tới màn hình và file, hãy xây StreamHandlerFileHandler riêng và addHandler mỗi cái.
import logging
import os

# Tạo folder cha trước (FileHandler không tự tạo)
os.makedirs("logs", exist_ok=True)

# Truyền filename cho basicConfig để chuyển đích đến sang file
logging.basicConfig(
    level=logging.INFO,
    format="[%(levelname)s] %(message)s",
    filename="logs/app.log",   # Output file
    filemode="w",               # "a" (append) hoặc "w" (ghi đè)
    force=True,
)
logger = logging.getLogger("app")

logger.info("Đã khởi động")
logger.warning("Config cũ")
logger.error("Kết nối DB thất bại")

# Đọc lại file để xác nhận nội dung
with open("logs/app.log") as f:
    print(f.read(), end="")

Truyền filename cho basicConfig để chuyển log sang file, rồi đọc lại nội dung từ đĩa.

① Hãy dùng os.makedirs("logs", exist_ok=True) để tạo folder cha trước (FileHandler không tự tạo)

② Hãy import logging và gọi basicConfig với level=logging.INFO, format="[%(levelname)s] %(message)s", filename="logs/app.log", filemode="w", force=True

③ Hãy lấy logger với logger = logging.getLogger("app")

④ Hãy phát 3 log ở INFO / WARNING / ERROR với thông điệp "Đã khởi động", "Config cũ", "Kết nối DB thất bại"

⑤ Hãy mở logs/app.log để đọc, rồi dưới tiêu đề --- nội dung của logs/app.log --- in nội dung không có newline cuối (print(content, end=""))

Python Editor

Chạy code để xem đầu ra

Định nghĩa handler trong file riêng

Khi ứng dụng lớn lên, tách cấu hình logger (định dạng, level, đích đến) ra một module riêng và để mỗi module import từ đó. Cấu hình logger không còn trùng lặp, và bạn đổi định dạng/đích đến tại một chỗ. Pattern: xây StreamHandler trực tiếp, gắn Formatter, đăng ký với logger.addHandler(...) — tất cả sống trong file riêng.

Tập trung log_setup; module import từ nó
log_setup.py(định dạng, đích, level)main.pyorders.pyusers.pyimportimportimport
log_setup.py là nguồn duy nhất cho cấu hình logger (định dạng, đích đến, level). Mỗi module (main.py / orders.py / users.py) chỉ gọi setup_logger(__name__) để có logger đã cấu hình. Để đổi định dạng hoặc đích đến, chỉ sửa log_setup.py.
# log_setup.py — module chuyên cho cấu hình logger
import logging

def setup_logger(name):
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)

    if not logger.handlers:                       # Ngăn đăng ký trùng
        handler = logging.StreamHandler()
        handler.setFormatter(
            logging.Formatter("[%(name)s] [%(levelname)s] %(message)s")
        )
        logger.addHandler(handler)
        logger.propagate = False                  # Không lan tới root
    return logger

# main.py — import log_setup và dùng
from log_setup import setup_logger

logger = setup_logger("orders")
logger.info("Đã nhận đơn")

log_setup.py được cung cấp (mở panel file 📂 bên trái console để xem). Từ main.py, hãy import setup_logger và phát 3 log với logger tên "orders".

① Hãy import hàm qua from log_setup import setup_logger

② Hãy lấy logger với logger = setup_logger("orders")

③ Hãy phát 3 log ở INFO / WARNING / ERROR: "Đã nhận đơn", "Hàng tồn thấp", "API thanh toán trả về lỗi"

Python Editor

Chạy code để xem đầu ra

Định dạng chi tiết hơn — timestamp / module / dòng

Chuỗi định dạng Formatter chấp nhận mã thay thế kiểu %(...)s, cho phép bạn nhồi thông tin vận hành liên quan vào một dòng. Đây là sáu mã dùng nhiều nhất với ví dụ. Tên logger có thể phân cấp qua dạng phân tách bằng dấu chấm như parent.child.grandchild — gọi getLogger(__name__) từ mỗi module tự động ghi lại "module nào phát log này".

Cái nó outputVí dụ
%(asctime)sTimestamp2024-12-01 10:30:45
%(name)sTên logger (phân cấp bằng dấu chấm)app.orders
%(levelname)sLevel logINFO / WARNING / ERROR
%(funcName)sTên hàm gọiprocess_order
%(lineno)dSố dòng gọi42
%(message)sThân thông điệpĐang xử lý đơn
import logging

# Định dạng chi tiết cho production
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] [%(levelname)s] %(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    force=True,
)

logger = logging.getLogger("app.orders")

def process_order(order_id):
    logger.info(f"Đang xử lý đơn {order_id}")

process_order(1234)
# Output mẫu:
# 2024-12-01 10:30:45 [app.orders] [INFO] process_order:13 - Đang xử lý đơn 1234

Dùng 3 tên logger phân cấp — app / app.orders / app.orders.payment — và hiển thị mỗi log đến từ module nào với %(name)s. Trong production bạn sẽ thêm %(asctime)s cho timestamp, nhưng ở đây ta chỉ giữ phân cấp để output có tính xác định.

① Hãy import logging và cấu hình basicConfig với level=logging.INFO, format="[%(name)s] [%(levelname)s] %(message)s", force=True

② Hãy lấy 3 logger: logging.getLogger("app"), logging.getLogger("app.orders"), logging.getLogger("app.orders.payment")

③ Hãy phát một log từ mỗi cái: app gọi info("Ứng dụng khởi động"), app.orders gọi info("Đã nhận đơn"), app.orders.payment gọi error("Lỗi API thanh toán")

Python Editor

Chạy code để xem đầu ra

Tách cấu hình ra file yaml

Khi cấu hình logger phức tạp, thay vì hard-code định dạng / handler / level trong Python, tách chúng ra file cấu hình yaml. logging.config.dictConfig(...) chấp nhận một cấu hình dict, nên chỉ cần parse yaml và truyền vào để cấu hình toàn bộ logger. Codebase của bạn đột nhiên xử lý "file yaml khác cho production / staging / development" mà không thay đổi.

Luồng file cấu hình yaml → dictConfig
logging.ymlformat / handler / levelyaml.safe_load()+ dictConfig()Logger đã cấu hìnhlogger.info(...)
Viết định dạng / handler / level vào logging.yml, rồi chuyển nó thành dict qua yaml.safe_load() và truyền cho logging.config.dictConfig(...). Phía Python chỉ load yaml và gọi dictConfig, và logger được setup.
# logging.yml — nguồn duy nhất cho cấu hình logger
version: 1
disable_existing_loggers: false
formatters:
  default:
    format: "[%(name)s] [%(levelname)s] %(message)s"
handlers:
  console:
    class: logging.StreamHandler
    formatter: default
    level: INFO
loggers:
  app:
    level: INFO
    handlers: [console]
    propagate: false
KhóaVai tròGhi chú
versionPhiên bản schema dictConfigHiện tại chỉ 1. Bắt buộc.
disable_existing_loggersCó disable logger có sẵn khôngfalse được khuyên (true im lặng các logger như app.orders trước đó)
formattersDanh sách Formatter có tênĐịnh nghĩa dưới tên bất kỳ (ví dụ default), tham chiếu từ handlers
formatters.<name>.formatChuỗi định dạng (mã %(...)s)Giống đối số Formatter trong code
handlersDanh sách Handler (đích đến) có tênĐịnh nghĩa console / file / mail v.v.
handlers.<name>.classTên class Handler đầy đủlogging.StreamHandler / logging.FileHandler / logging.handlers.RotatingFileHandler v.v.
handlers.<name>.formatterTên Formatter để áp dụngDùng key đã định nghĩa dưới formatters
handlers.<name>.levelLevel theo HandlerLọc output tinh hơn ở mức Logger
loggersDanh sách Logger có tênloggers.app được lấy qua logging.getLogger("app")
loggers.<name>.handlersDanh sách tên Handler gắn với logger nàyNhiều cũng được, như [console, file]
loggers.<name>.propagateCó lan tới logger cha khôngfalse dừng lan tới root logger (tránh output kép)

logging.yml được cung cấp (mở panel file 📂 để xem). Parse thành dict qua yaml.safe_load, rồi truyền cho logging.config.dictConfig để cấu hình logger.

① Hãy import logging, logging.config, và yaml

② Hãy mở với with open("logging.yml") as f: và chuyển thành dict qua yaml.safe_load(f)

③ Hãy truyền dict cho logging.config.dictConfig(...) để áp dụng

④ Hãy lấy logger qua logger = logging.getLogger("app") và phát 1 INFO và 1 WARNING: "Khởi động với config yaml""WARNING via yaml"

Python Editor

Chạy code để xem đầu ra

Xoay log

Nếu log cứ lớn lên, file phình ra vô hạn, nên production cần xoayđổi tên file cũ sang tên riêng và xóa cuối cùng. logging.handlers đi kèm hai loại — theo sizetheo thời gian — và bạn chọn theo use case.

Hai loại: theo size và theo thời gian
RotatingFileHandlerTheo size(xoay khi vượt maxBytes)TimedRotatingFileHandlerTheo thời gian(xoay tại biên when)
Theo size hợp với log debug nơi bạn muốn giới hạn cứng dùng đĩa. Theo thời gian hợp với log truy cập bạn muốn cắt theo ngày để phân tích hoặc công cụ giám sát.

RotatingFileHandler — theo size

RotatingFileHandler(filename, maxBytes, backupCount) là Handler chuyển sang file mới khi maxBytes sẽ bị vượt. File cũ được đổi tên với hậu tố số (app.log.1 / app.log.2), và khi backupCount bị vượt thì cái cũ nhất bị xóa. Với maxBytes=10_000_000 (10 MB) + backupCount=5, log của bạn xoay vòng trong trần 60 MB.

RotatingFileHandler hoạt động thế nào
app.logĐang ghiVượt maxBytes→ xoayapp.log.1app.log.2 ...(backup)Vượt backupCount→ Cũ nhất bị xóa
Khi file đang ghi app.log sắp đạt maxBytes, nó được đổi tên app.log → app.log.1, và app.log mới được tạo để tiếp tục ghi. Nếu app.log.N đã có, nó tăng thành app.log.N → app.log.N+1, và file cũ nhất vượt backupCount bị xóa.
import logging
from logging.handlers import RotatingFileHandler

# Theo size: xoay khi maxBytes bị vượt
size_handler = RotatingFileHandler(
    "app.log",
    maxBytes=10_000_000,   # Xoay khi vượt cái này (10 MB)
    backupCount=5,         # Giữ app.log.1 đến app.log.5 (tối đa 60 MB tổng)
)
size_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))

rotate_logging.yml được cung cấp (mở panel file 📂 để xem). Cấu hình RotatingFileHandler (maxBytes=60 / backupCount=2) sống trong yaml, và code chỉ load và dùng.

① Hãy import os / shutil, xóa và tạo lại folder rotate/. Cũng xóa mọi handler trên logger rotate_demo với close + removeHandler (tránh nhiễu khi chạy lại)

② Hãy mở yaml với with open("rotate_logging.yml"), chuyển qua yaml.safe_load(f), và truyền cho logging.config.dictConfig(...) để cấu hình handler

③ Hãy lấy logger = logging.getLogger("rotate_demo") và phát 15 log trong vòng lặp: for i in range(15): logger.info(f"event {i:02d}")

④ Từ os.listdir("rotate"), lấy chỉ file bắt đầu với app.log, sắp xếp, và dưới tiêu đề File sau khi xoay: ◯ in mỗi tên file dưới dạng - tên_file trên dòng riêng

Python Editor

Chạy code để xem đầu ra

TimedRotatingFileHandler — theo thời gian

TimedRotatingFileHandler(filename, when, interval, backupCount) xoay tại biên thời gian chỉ định bởi when. Dùng when="midnight" (mỗi ngày lúc 0:00), "H" (mỗi giờ), "M" (phút), "S" (giây), "D" (mỗi ngày), v.v. Backup mang hậu tố timestamp như app.log.2024-12-01_00-00-00, nên tên file nói thẳng "log này được viết khi nào".

TimedRotatingFileHandler hoạt động thế nào
app.logĐang ghiwhen tiếp theo (giờ)→ xoayapp.log.2024-12-01app.log.2024-12-02(hậu tố timestamp)Vượt backupCount→ Cũ nhất bị xóa
Tại thời gian chỉ định bởi when (mỗi ngày lúc 0:00, v.v.), app.log đang ghi được đổi tên với tên có hậu tố timestamp (ví dụ app.log.2024-12-01_00-00-00)app.log mới mở. File timestamp cũ hơn vượt backupCount bị tự xóa.
import logging
from logging.handlers import TimedRotatingFileHandler

# Theo thời gian: xoay mỗi ngày lúc nửa đêm
day_handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",   # "S" giây / "M" phút / "H" giờ / "D" ngày / "midnight" v.v.
    interval=1,         # Mỗi N đơn vị (when="H", interval=6 nghĩa là mỗi 6 giờ)
    backupCount=30,     # Giữ 30 ngày lịch sử
)
day_handler.setFormatter(
    logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)

time_logging.yml được cung cấp. Cấu hình TimedRotatingFileHandler (when: S (mỗi giây) / interval: 1 / backupCount: 3) sống trong yaml, và code Python dùng time.sleep để mô phỏng thời gian trôi để kích hoạt xoay.

① Hãy import logging / os / shutil / time. Xóa mọi handler trên logger trotate_demo với close + removeHandler, rồi xóa và tạo lại folder trotate/

② Hãy mở time_logging.yml, chuyển thành dict qua yaml.safe_load(f), và truyền cho logging.config.dictConfig(...) để cấu hình

③ Hãy lấy logger và phát 3 log với sleep ở giữa: logger.info(...)time.sleep(1.2)logger.info(...)time.sleep(1.2)logger.info(...), kích hoạt 2 lần xoay

④ Từ os.listdir("trotate"), hãy tách app.log (log hiện tại) khỏi mọi cái khác (backup timestamp), rồi in Log hiện tại có: True/FalseSố backup: ◯

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Với logging.basicConfig(level=logging.INFO), level nào bị triệt?

Câu 2Khi bạn truyền filename="app.log" cho logging.basicConfig, log đi đâu?

Câu 3Lợi ích chính của tách cấu hình logger (định dạng / level / handler) ra module riêng là gì?

Câu 4RotatingFileHandler với backupCount=2 giữ tối đa bao nhiêu file?