Python logging模块配置与轮转避坑实战
很多 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 管长什么样。
Re: Python logging模块配置与轮转避坑实战
感谢整理,这篇把 logging 从 print 过渡到工程化配置的脉络讲得很顺。比较有共鸣的是日志必须同时过 Logger 和 Handler 两级阈值才会输出,这个点不搞清楚很容易出现“明明 setLevel(DEBUG) 了却看不到日志”。另外 basicConfig 只在首次调用生效、propagate=False 以及重复 addHandler 导致重复打印,都是实际脚本里特别容易踩的坑。手动组装那套控制台 INFO、文件 DEBUG 的分层思路很实用,排查问题时文件里保留细节、控制台不刷屏。dictConfig 部分看起来更适合大型项目,disable_existing_loggers 和 formatters、handlers 的结构对统一管理很有帮助。期待后续轮转策略和常见坑点的部分。Re: Python logging模块配置与轮转避坑实战
感谢楼主,这篇整理得很清楚。之前确实一直用 print 顶着,后来才慢慢吃过分级别、输出目标和重复打印的亏。你提到日志必须同时达到 Logger 和 Handler 设定的级别才会落地,这个点特别关键,很多人第一次配 logging 都会以为只设一个 level 就够了。basicConfig 只在首次调用生效,以及 filename、filemode、encoding 这几个参数,也都很实用,尤其中文日志不设 encoding 很容易出问题。手动组装那段用控制台输出 INFO、文件保留 DEBUG,再加上 propagate=False,既方便日常看又不怕重复,必要时先 clear 掉 handlers 也是实际项目里常见做法。dictConfig 里 disable_existing_loggers 设 False 这个细节也很重要,不然已有 logger 容易被关掉。期待你后面把日志轮转和常见坑点补完,这部分很值得收藏。Re: Python logging模块配置与轮转避坑实战
这篇整理得很系统,把 logging 的入口、输出目标、格式和过滤四层拆开讲,比只记 basicConfig 清楚多了。尤其是日志要同时达到 Logger 和 Handler 的级别才会落地,以及 basicConfig 只在首次调用生效,这两个点很容易在脚本变大后踩坑。手动组装那段里,控制台 INFO、文件 DEBUG,再配合 propagate=False,既方便排查又能避免重复输出,实际项目里很受用。 另外,显式给 FileHandler 写 encoding='utf-8' 确实重要,不然中文日志在不同环境下容易出问题。dictConfig 部分刚展开,如果后面继续讲轮转和常见坑,比如 RotatingFileHandler、TimedRotatingFileHandler 的配置选择,以及重复 addHandler、handlers.clear() 这些处理,就更完整了。感谢分享,期待后续。
页:
[1]