Câu 1Với logging.basicConfig(level=logging.INFO), level nào bị triệ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).
logger.info(...) từ code, luồng là Logger lọc theo level → Handler quyết định đích đến → Formatter 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.
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.
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 và đí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.
logging, bạn flip giữa chúng trong một dòng cấu hình.filemode là "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).
StreamHandler và FileHandler 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="")
Đị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.
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")
Đị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".
| Mã | Cái nó output | Ví dụ |
|---|---|---|
| %(asctime)s | Timestamp | 2024-12-01 10:30:45 |
| %(name)s | Tên logger (phân cấp bằng dấu chấm) | app.orders |
| %(levelname)s | Level log | INFO / WARNING / ERROR |
| %(funcName)s | Tên hàm gọi | process_order |
| %(lineno)d | Số dòng gọi | 42 |
| %(message)s | Thâ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
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.
# 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óa | Vai trò | Ghi chú |
|---|---|---|
| version | Phiên bản schema dictConfig | Hiện tại chỉ 1. Bắt buộc. |
| disable_existing_loggers | Có disable logger có sẵn không | false được khuyên (true im lặng các logger như app.orders trước đó) |
| formatters | Danh 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>.format | Chuỗi định dạng (mã %(...)s) | Giống đối số Formatter trong code |
| handlers | Danh sách Handler (đích đến) có tên | Định nghĩa console / file / mail v.v. |
| handlers.<name>.class | Tên class Handler đầy đủ | logging.StreamHandler / logging.FileHandler / logging.handlers.RotatingFileHandler v.v. |
| handlers.<name>.formatter | Tên Formatter để áp dụng | Dùng key đã định nghĩa dưới formatters |
| handlers.<name>.level | Level theo Handler | Lọc output tinh hơn ở mức Logger |
| loggers | Danh sách Logger có tên | loggers.app được lấy qua logging.getLogger("app") |
| loggers.<name>.handlers | Danh sách tên Handler gắn với logger này | Nhiều cũng được, như [console, file] |
| loggers.<name>.propagate | Có lan tới logger cha không | false dừng lan tới root logger (tránh output kép) |
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 size và theo thời gian — và bạn chọn theo use case.
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.
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"))
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".
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")
)
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi mộ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?