很多 Python 脚本一开始用 print 调试,项目变大后问题会集中暴露:日志无法按级别过滤、程序退出后输出丢失、只能打印到控制台、级别不够时仍然拼接字符串、多线程输出容易交错。logging 模块把这条链路拆成 Logger、Handler、Formatter、Filter 四层,分别负责入口、输出目标、格式和细粒度过滤。下面按实际开发顺序梳理核心组件、配置方式、执行流程、日志轮转与常见坑点。
一、核心组件与执行链路
Logger 是直接调用的入口,推荐用 logging.getLogger(__name__) 创建,点号命名空间会形成父子层级。setLevel 设置该 Logger 的阈值。Handler 决定日志去哪,一个 Logger 可以绑定多个 Handler:StreamHandler 输出控制台,FileHandler 输出文件,RotatingFileHandler 按大小轮转,TimedRotatingFileHandler 按时间轮转,SMTPHandler 发邮件,HTTPHandler 发远程,QueueHandler 走异步队列,NullHandler 适合库开发,避免未配置时输出到 stderr。Formatter 决定日志长相,常用占位符包括 %(asctime)s、%(name)s、%(levelname)s、%(message)s、%(filename)s、%(lineno)d、%(funcName)s。Filter 通过 addFilter() 挂到 Logger 或 Handler 上,可做比级别更细的筛选。
调用 logger.info('msg') 时,内部大致流程是:创建 LogRecord,Logger 级别过滤,Filter 过滤,分发到所有 Handler,Handler 级别过滤,Formatter 格式化,最后 emit 输出。关键规则是日志必须同时达到 Logger 和 Handler 设定的级别才会落地。标准级别数值为 DEBUG 10、INFO 20、WARNING 30、ERROR 40、CRITICAL 50,默认级别是 WARNING。
二、简单脚本:basicConfig 一行配置- import logging
- logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S',
- filename='app.log',
- filemode='a',
- encoding='utf-8'
- )
- logger = logging.getLogger(__name__)
- logger.info('服务启动成功')
复制代码 basicConfig() 只在首次调用时生效,第二次调用不会覆盖已有配置。它适合单文件小脚本;filename、filemode、encoding 分别控制输出文件、追加模式和中文编码。
三、正式项目:手动组装 Logger、Handler、Formatter- import logging
- def setup_logger():
- logger = logging.getLogger(__name__)
- logger.setLevel(logging.DEBUG)
- logger.propagate = False
- console_handler = logging.StreamHandler()
- console_handler.setLevel(logging.INFO)
- file_handler = logging.FileHandler('app.log', encoding='utf-8')
- file_handler.setLevel(logging.DEBUG)
- formatter = logging.Formatter(
- '%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
- )
- console_handler.setFormatter(formatter)
- file_handler.setFormatter(formatter)
- logger.addHandler(console_handler)
- logger.addHandler(file_handler)
- return logger
- logger = setup_logger()
- logger.debug('调试信息')
- logger.info('正常信息')
- logger.error('错误信息', exc_info=True)
复制代码 这里把控制台限定为 INFO 及以上,文件保留 DEBUG,方便排查。propagate=False 可避免日志继续向 root Logger 传播;如果反复调用 addHandler 或 propagate 未关闭,容易出现重复打印,必要时先执行 logger.handlers.clear()。
四、大型项目:dictConfig 字典配置- import logging.config
- config = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'formatters': {
- 'standard': {
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
- }
- },
- 'handlers': {
- 'console': {
- 'class': 'logging.StreamHandler',
- 'level': 'DEBUG',
- 'formatter': 'standard',
- 'stream': 'ext://sys.stdout'
- },
- 'file': {
- 'class': 'logging.FileHandler',
- 'level': 'INFO',
- 'formatter': 'standard',
- 'filename': 'app.log',
- 'encoding': 'utf8'
- }
- },
- 'loggers': {
- 'my_app': {
- 'level': 'DEBUG',
- 'handlers': ['console', 'file'],
- 'propagate': False
- }
- }
- }
- logging.config.dictConfig(config)
- logger = logging.getLogger('my_app')
复制代码 dictConfig 适合从 JSON 或 YAML 加载配置,version 必须为 1,disable_existing_loggers 设为 False 可避免已有 Logger 被禁用。
五、常用方法与延迟格式化
logger.debug()、logger.info()、logger.warning()、logger.error()、logger.critical() 分别记录五个标准级别;logger.exception() 记录 ERROR 级别并附带完整异常堆栈,等价于 error(msg, exc_info=True);logger.log(level, msg) 可以动态指定级别。
记录变量时推荐使用 %s 占位符:- logger.info('用户 %s 登录成功,ID: %d', username, user_id)
复制代码 不推荐 f-string,因为即使当前级别不够,f-string 也会先完成字符串拼接,浪费性能。%s 占位符会延迟格式化,级别不足时直接跳过。
六、日志轮转:避免日志文件撑爆磁盘
长期运行的程序必须处理日志文件增长。按大小轮转可用 RotatingFileHandler:- from logging.handlers import RotatingFileHandler
- handler = RotatingFileHandler(
- 'app.log',
- maxBytes=10 * 1024 * 1024,
- backupCount=5,
- encoding='utf-8'
- )
复制代码 按时间轮转可用 TimedRotatingFileHandler:- from logging.handlers import TimedRotatingFileHandler
- handler = TimedRotatingFileHandler(
- 'app.log',
- when='D',
- interval=1,
- backupCount=7,
- encoding='utf-8'
- )
复制代码 when 支持 S、M、H、D、midnight 等单位,interval 表示间隔,backupCount 表示保留的历史文件数量。
七、新手常见坑点
DEBUG 日志不输出:默认级别是 WARNING,需要 logger.setLevel(logging.DEBUG)。
日志重复打印:多次 addHandler 或 propagate 未关闭,设置 propagate=False 或清理 handlers。
日志文件乱码:FileHandler 未指定编码,加上 encoding='utf-8'。
设置级别无效:只给 Logger 设了级别,Handler 也要设置。
f-string 性能差:改成 %s 占位符。
basicConfig 不生效:它只在第一次调用时起作用。
库开发输出到 stderr:给库 Logger 加 NullHandler。
生产环境慎用 DEBUG:高频路径输出 DEBUG 会影响性能。
异常排障:使用 exc_info=True 或 logger.exception() 记录完整堆栈。
八、生产级日志配置示例- import logging
- import logging.handlers
- import json
- from pathlib import Path
- class JsonFormatter(logging.Formatter):
- def format(self, record):
- log_data = {
- 'time': self.formatTime(record),
- 'level': record.levelname,
- 'logger': record.name,
- 'message': record.getMessage(),
- 'module': record.module,
- 'line': record.lineno,
- }
- if record.exc_info:
- log_data['exception'] = self.formatException(record.exc_info)
- return json.dumps(log_data, ensure_ascii=False)
- def setup_production_logging(log_dir='logs'):
- log_path = Path(log_dir)
- log_path.mkdir(exist_ok=True)
- root_logger = logging.getLogger()
- root_logger.setLevel(logging.DEBUG)
- fmt = logging.Formatter(
- '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
- )
- console = logging.StreamHandler()
- console.setLevel(logging.WARNING)
- console.setFormatter(fmt)
- file_handler = logging.handlers.RotatingFileHandler(
- log_path / 'app.log',
- maxBytes=10 * 1024 * 1024,
- backupCount=5,
- encoding='utf-8',
- )
- file_handler.setLevel(logging.DEBUG)
- file_handler.setFormatter(fmt)
- error_handler = logging.handlers.RotatingFileHandler(
- log_path / 'error.log',
- maxBytes=5 * 1024 * 1024,
- backupCount=3,
- encoding='utf-8',
- )
- error_handler.setLevel(logging.ERROR)
- error_handler.setFormatter(fmt)
- root_logger.addHandler(console)
- root_logger.addHandler(file_handler)
- root_logger.addHandler(error_handler)
- setup_production_logging()
- logger = logging.getLogger(__name__)
- logger.info('应用启动')
- logger.error('请求失败', exc_info=True)
复制代码 这个示例把控制台限定为 WARNING,app.log 记录所有级别并按 10MB 轮转保留 5 份,error.log 只记录 ERROR 及以上并按 5MB 轮转保留 3 份。JsonFormatter 可把日志输出为 JSON,便于 ELK、Loki 等系统采集。核心分工仍然是:Logger 管记录什么,Handler 管输出到哪,Formatter 管长什么样。 |