Python脚本日志记录如何配置滚动

wen 实用脚本 11

Python脚本日志记录如何配置滚动:从入门到生产级实践

目录导读

  1. 为什么需要日志滚动?
  2. Python日志系统核心组件解析
  3. 基于时间滚动的配置(TimedRotatingFileHandler)
  4. 基于文件大小滚动的配置(RotatingFileHandler)
  5. 高级滚动策略组合
  6. 日志滚动配置常见陷阱与解决方案
  7. 问答环节

为什么需要日志滚动?

在Python脚本长期运行或处理大量数据时,日志文件会无限制膨胀,一个未配置滚动的日志文件可能:

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.1app.log.2app.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__阶段,避免运行时动态修改造成日志丢失。

抱歉,评论功能暂时关闭!