(六)记录器、处理器与输出位置
1. 跟踪一条记录经过的筛选
一条 INFO 记录从 readingapp.services 发出,先检查记录器的有效等级;通过后交给 handler。handler 再按自己的等级筛选,由 formatter 排版,最后送到终端或文件。propagate=True 会把记录交给祖先记录器的 handler。
例如记录器等级为 DEBUG,终端 handler 为 WARNING,文件 handler 为 DEBUG:INFO 只进文件;WARNING 同时进终端和文件;DEBUG 也只进文件。向祖先传播时,不会再按祖先记录器自身的等级重新筛一次,handler 的等级仍然参与判断。把同一 handler 同时挂在子记录器和根记录器上,还会重复输出。logging 接口与传播
# 项目文件:debug_demo/log_routes.py
# 一个记录器接两个处理器:终端显示警告,内存文本接收 DEBUG 以上记录。
import io
import logging
logger = logging.getLogger("route_demo")
logger.setLevel(logging.DEBUG)
logger.propagate = False
memory = io.StringIO()
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
saved = logging.StreamHandler(memory)
saved.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(levelname)s %(message)s")
for handler in (console, saved):
handler.setFormatter(formatter)
logger.addHandler(handler)
try:
logger.debug("检查字段")
logger.info("导入 2 本")
logger.warning("标签重复")
print(memory.getvalue().splitlines())
finally:
for handler in (console, saved):
logger.removeHandler(handler)
handler.close()终端日志只有 WARNING 标签重复;print 得到 ['DEBUG 检查字段', 'INFO 导入 2 本', 'WARNING 标签重复']。StringIO 沿用第五章的内存文本接口。这里移除并关闭自己建立的处理器,再次运行不会遗留重复的输出通道。
2. 在应用入口配置文件日志
本章项目使用一份 UTF-8 文件日志。RotatingFileHandler 达到指定大小后轮换备份文件,避免同一文件一直增长:
# 项目文件:quality_project/src/readingapp/logging_setup.py
# 入口配置根记录器;模块只调用 getLogger,不在导入时创建文件。
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
def configure_logging(path: Path, debug: bool = False) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
handler = RotatingFileHandler(path, maxBytes=1_000_000, backupCount=2, encoding="utf-8")
logging.basicConfig(
level=logging.DEBUG if debug else logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
handlers=[handler],
force=True,
)maxBytes 是轮换的大小阈值,backupCount=2 保留两份轮换文件。force=True 移除并关闭根记录器已有的 handler,让这次入口配置生效;basicConfig 默认遇到已有 handler 时不重新配置。因此这里由应用入口调用,测试普通业务函数时不调用它,以便 pytest 接收日志。