Python脚本日志记录如何配置滚动:从入门到生产级实践
目录导读
- 为什么需要日志滚动?
- Python日志系统核心组件解析
- 基于时间滚动的配置(TimedRotatingFileHandler)
- 基于文件大小滚动的配置(RotatingFileHandler)
- 高级滚动策略组合
- 日志滚动配置常见陷阱与解决方案
- 问答环节
为什么需要日志滚动?
在Python脚本长期运行或处理大量数据时,日志文件会无限制膨胀,一个未配置滚动的日志文件可能:

- 占用GB级磁盘空间,导致服务器磁盘告警
- 日志读写性能急剧下降(单个大文件IO效率低)
- 难以检索特定时间段的日志信息
日志滚动(Log Rotation) 正是解决这些问题的机制:当日志达到预设条件(时间/大小)时,自动将当前日志重命名备份,并创建新文件继续写入,同时可设置保留备份数量,自动清理过期日志。
Python日志系统核心组件解析
Python的logging模块内置滚动支持,无需额外依赖,核心组件:
| 组件 | 作用 | 滚动相关实现 |
|---|---|---|
Logger |
日志记录器,提供接口 | 无需配置 |
Handler |
日志处理器,控制输出 | RotatingFileHandler / TimedRotatingFileHandler |
Formatter |
日志格式定义 | 按需定义 |
Filter |
日志过滤(可选) | 用于复杂过滤场景 |
关键点:滚动配置在Handler层面完成,一个Logger可以绑定多个Handler实现混合输出(如同时控制台+文件+滚动日志)。
基于时间滚动的配置(TimedRotatingFileHandler)
适用于需要按时间周期归档的场景(如每日、每小时生成独立日志文件)。
1 基础配置代码
import logging
import logging.handlers
import time
def setup_time_rotation_logger(log_path="app.log", when="midnight", interval=1, backup_count=30):
"""
when: 滚动周期单位(S-秒, M-分钟, H-小时, D-天, W0-W6-星期几, midnight-午夜)
interval: 间隔数量,配合when使用(如when='H', interval=2表示每2小时滚动)
backup_count: 保留的备份文件数,超过则删除最旧的
"""
logger = logging.getLogger("time_rotation_logger")
logger.setLevel(logging.DEBUG)
# 创建TimedRotatingFileHandler
handler = logging.handlers.TimedRotatingFileHandler(
log_path,
when="midnight", # 每天午夜滚动
interval=1,
backupCount=backup_count,
encoding="utf-8"
)
# 设置备份文件名格式(重要)
handler.suffix = "%Y-%m-%d.log" # 仅在when为'D'或'midnight'时生效
# 设置日志格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
# 使用示例
log = setup_time_rotation_logger()
for i in range(5):
log.info(f"测试日志第{i+1}条 - 时间:{time.strftime('%H:%M:%S')}")
time.sleep(1)
2 关键参数详解
-
when参数高级用法:W0-W6:每周指定日滚动(0=周一,6=周日),需配合interval(如when='W6', interval=1表示每周六滚动)midnight:每天0点执行滚动,等价于when='D'但更精准(会在0点准时执行)
-
suffix格式化:控制备份文件命名,当when='midnight'时,建议使用%Y-%m-%d避免时区问题。
3 生产环境改进建议
# 增加时区支持(避免服务器时区与UTC不一致导致滚动时间错乱)
import pytz
from logging.handlers import TimedRotatingFileHandler
class TZTimedRotatingFileHandler(TimedRotatingFileHandler):
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=True, atTime=None, tz=None):
self.tz = tz or pytz.timezone('Asia/Shanghai')
super().__init__(filename, when, interval, backupCount, encoding, delay, utc, atTime)
def _computeRollover(self, current_time):
# 重写计算逻辑,使用本地时间(此处简化示例)
return super()._computeRollover(current_time)
# 使用中国时区
handler = TZTimedRotatingFileHandler("app.log", when="midnight", interval=1, backupCount=30, tz=pytz.timezone('Asia/Shanghai'))
基于文件大小滚动的配置(RotatingFileHandler)
适用于日志生成速度不固定、希望控制单个文件大小的场景。
1 基础配置
import logging
from logging.handlers import RotatingFileHandler
def setup_size_rotation_logger(log_path="app.log", max_bytes=10*1024*1024, backup_count=5):
"""
maxBytes: 单个日志文件最大字节数(10MB示例)
backupCount: 保留的备份文件数量
当文件达到maxBytes时,会将当前日志重命名为app.log.1,并新建app.log继续写入
"""
logger = logging.getLogger("size_rotation_logger")
logger.setLevel(logging.DEBUG)
handler = RotatingFileHandler(
log_path,
maxBytes=max_bytes, # 10MB
backupCount=backup_count, # 保留5个备份
encoding="utf-8"
)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
# 快速测试:将maxBytes设小,观察滚动
log = setup_size_rotation_logger(max_bytes=1024, backup_count=3)
for i in range(1000):
log.info(f"这是一条测试日志,用于快速填满文件并触发滚动 #{i}")
2 备份文件命名规则
RotatingFileHandler的备份文件命名固定:原文件名.1、原文件名.2…
- 注意:最大的数字表示最旧的备份文件(与直觉相反)
- 当backup_count=3且已有
app.log.1、app.log.2、app.log.3时,新滚动产生的旧文件app.log会变成app.log.1,原app.log.3被删除
3 多线程/多进程安全
Python的RotatingFileHandler不是线程安全的(多线程并发写入可能导致数据错乱或丢失),解决方案:
# 使用QueueHandler + QueueListener实现异步安全日志(推荐用于生产环境)
import logging
import logging.handlers
import queue
from concurrent.futures import ThreadPoolExecutor
log_queue = queue.Queue(-1) # 无限队列
queue_handler = logging.handlers.QueueHandler(log_queue)
queue_listener = None
def setup_safe_logger():
global queue_listener
logger = logging.getLogger("safe_logger")
logger.setLevel(logging.DEBUG)
logger.addHandler(queue_handler)
# 实际写入的Handler(带滚动)
file_handler = RotatingFileHandler("safe_app.log", maxBytes=10*1024*1024, backupCount=5)
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(threadName)s - %(message)s'))
# 创建监听器,自动从队列取出日志并分发到handler
queue_listener = logging.handlers.QueueListener(log_queue, file_handler, respect_handler_level=True)
queue_listener.start()
return logger
# 多线程测试
log = setup_safe_logger()
with ThreadPoolExecutor(max_workers=10) as executor:
for i in range(100):
executor.submit(log.info, f"线程安全日志测试 {i}")
高级滚动策略组合
1 双条件滚动(时间+大小)
无法直接通过单个Handler实现,但可以配置两个Handler:
import logging
def setup_dual_rotation_logger():
logger = logging.getLogger("dual_logger")
logger.setLevel(logging.DEBUG)
# Handler1:按天滚动
time_handler = logging.handlers.TimedRotatingFileHandler(
"app_daily.log", when="midnight", interval=1, backupCount=30
)
time_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
# Handler2:按大小滚动(10MB)
size_handler = logging.handlers.RotatingFileHandler(
"app_size.log", maxBytes=10*1024*1024, backupCount=10
)
size_handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(time_handler)
logger.addHandler(size_handler)
return logger
# 注意:这样会产生两份日志文件,适合需要双重归档的场景
2 带压缩的滚动(减少磁盘占用)
Python内置Handler不支持自动压缩,但可以结合gzip:
import gzip
import shutil
import os
from logging.handlers import RotatingFileHandler
class CompressedRotatingHandler(RotatingFileHandler):
def doRollover(self):
# 先执行父类的滚动逻辑(产生备份文件)
super().doRollover()
# 找到刚刚创建的备份文件,压缩
if self.backupCount > 0:
for i in range(self.backupCount, 0, -1):
sfn = f"{self.baseFilename}.{i}"
if os.path.exists(sfn):
with open(sfn, 'rb') as f_in:
with gzip.open(sfn + '.gz', 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(sfn) # 删除原文件
# 使用:CompressedRotatingHandler("app.log", maxBytes=10*1024*1024, backupCount=5)
日志滚动配置常见陷阱与解决方案
陷阱1:日志被多个进程同时写入导致混乱
方案:使用ConcurrentRotatingFileHandler(第三方库concurrent-log-handler),或使用Socket/TCP将日志发送到统一日志服务(如Logstash)。
陷阱2:滚动时文件被其他进程锁定(Windows常见)
方案:设置delay=True(延迟创建文件直到第一条日志写入),并确保文件权限正确,Windows下建议避免多个进程操作同一文件。
陷阱3:备份文件命名混乱
方案:自定义Handler重写rotation_filename方法(Python 3.2+支持):
class CustomNamedHandler(RotatingFileHandler):
def rotation_filename(self, default_name):
# 将备份文件存储在子目录
backup_dir = "logs/archive"
os.makedirs(backup_dir, exist_ok=True)
return os.path.join(backup_dir, f"rotated_{os.path.basename(default_name)}")
陷阱4:日期滚动时跨天边界问题
方案:永远使用midnight而不是when='D',后者在某些实现中会在启动后24小时才首次滚动,而非0点。
问答环节
Q1:日志滚动配置后,旧日志立即被删除吗?
A:不会。backupCount参数控制保留的备份数量,例如backupCount=7意味着最多保留7个旧日志文件,第8次滚动时最早的备份文件会被自动删除。
Q2:脚本程序崩溃重启后,日志滚动会重置计数吗?
A:会,因为滚动计数(如大小滚动的文件序号)存储在内存中,重启后,如果app.log已存在且超过maxBytes,会立即触发一次滚动,但之前产生的备份文件(app.log.1等)不受影响。
Q3:生产环境应该使用多大的日志文件?
A:推荐范围:10MB-100MB,太小(如1MB)导致频繁滚动影响性能;太大(如200MB+)不便于手动查看,且单次滚动IO压力大,按天滚动时建议保留30天(配合磁盘监控)。
Q4:如何使用logging.config.dictConfig配置滚动?
A:通过配置文件(YAML/JSON)实现:
import logging.config
import yaml
config = {
'version': 1,
'handlers': {
'time_handler': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'app.log',
'when': 'midnight',
'interval': 1,
'backupCount': 30,
'encoding': 'utf-8'
}
},
'root': {
'level': 'DEBUG',
'handlers': ['time_handler']
}
}
logging.config.dictConfig(config)
Q5:能否在运行时动态修改滚动配置?
A:可以,但不推荐直接修改Handler参数,正确的做法是:通过removeHandler移除旧Handler,再addHandler新Handler,注意需要先flush()并close()旧Handler。
Python日志滚动配置的核心在于根据业务场景选择合适的Handler类型(时间或大小),并处理好备份数量、文件命名、多线程安全等细节,生产环境强烈建议使用QueueHandler+QueueListener或第三方库concurrent-log-handler确保写入安全,日志配置通常放在__init__阶段,避免运行时动态修改造成日志丢失。