文件日志落地:FileHandler与RottingFileHandler,日志文件切割策略

日志光在控制台打印,那是给自己看的。真正上了生产环境,你得把日志写到文件里。为什么?因为控制台输出会丢失,服务器一重启,之前的日志就没了。我见过太多新手,排查线上问题的时候,翻遍服务器找不到日志文件,急得满头大汗。

今天我们就聊聊,怎么把日志老老实实写到文件里,并且做好切割策略。

FileHandler:最基础的日志写入方式

Python 的 logging 模块自带 FileHandler,用法很简单。你指定一个文件路径,日志就会追加写入。

import logging

logger = logging.getLogger('my_app')
handler = logging.FileHandler('app.log')
handler.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

logger.addHandler(handler)
logger.info('系统启动成功')

这段代码跑起来,同级目录下会生成一个 app.log 文件。每次写入都是追加模式,不会覆盖之前的内容。

注意:FileHandler 默认使用追加模式(mode='a')。如果你改成 mode='w',每次启动程序都会清空之前的日志。生产环境千万别这么干!

但是,FileHandler 有个致命问题——它不会自动切割。日志文件会一直增长,直到撑爆你的磁盘。我曾经接手过一个项目,日志文件 50 多个 G,服务器直接宕机。嗯,这就是典型的「日志写死」事故。

RotatingFileHandler:自动切割,按大小轮转

解决文件无限增长的办法,就是用 RotatingFileHandler。它会在文件达到指定大小时,自动重命名并创建新文件。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger('my_app')
handler = RotatingFileHandler(
    filename='app.log',
    maxBytes=10 * 1024 * 1024,  # 10MB
    backupCount=5,               # 保留5个备份
    encoding='utf-8'
)

handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

# 模拟写入大量日志
for i in range(10000):
    logger.info(f'这是第 {i} 条日志')

运行之后,你会看到目录下出现多个文件:

app.log          # 当前正在写入的日志
app.log.1        # 最旧的备份
app.log.2
app.log.3
app.log.4
app.log.5        # 最新的备份

当 app.log 达到 10MB 时,它会变成 app.log.1,然后新建一个 app.log。如果 app.log.1 已经存在,就依次往后推。backupCount=5 意味着最多保留 5 个备份文件,加上当前文件一共 6 个。

我的习惯:maxBytes 设置成 10MB 或 50MB 比较合适。太小了频繁切割影响性能,太大了单个文件不好查看。backupCount 我一般设 5-10 个,具体看磁盘空间和日志量。

TimedRotatingFileHandler:按时间切割

有些场景下,按大小切割不够直观。比如你想每天生成一个日志文件,方便按日期归档。这时候用 TimedRotatingFileHandler。

import logging
from logging.handlers import TimedRotatingFileHandler

logger = logging.getLogger('my_app')
handler = TimedRotatingFileHandler(
    filename='app.log',
    when='midnight',    # 每天午夜切割
    interval=1,         # 间隔1天
    backupCount=30,     # 保留30天
    encoding='utf-8'
)

handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('按天切割的日志示例')

when 参数支持多种模式:

参数值 含义 适用场景
'S' 每秒 调试用,生产别用
'M' 每分钟 高频调试
'H' 每小时 高并发系统
'D' 每天 通用场景
'W0'-'W6' 每周(0=周一) 低频系统
'midnight' 每天午夜 最常用
核心区别:RotatingFileHandler 按文件大小切割,适合控制单个文件体积。TimedRotatingFileHandler 按时间切割,适合按日期归档。实际项目中,我经常两者结合使用。

切割策略怎么选?

说白了,没有银弹。你得根据业务场景来定。

  • 高并发系统:日志量巨大,建议按大小切割(10MB-50MB),配合 backupCount=10。避免单个文件过大,影响写入性能。
  • 日常业务系统:按天切割最省心。每天一个文件,保留 30 天。查问题的时候直接看对应日期。
  • 合规审计场景:日志需要长期保存。按天切割,backupCount 设大一些,或者配合外部日志归档工具。
  • 磁盘空间紧张:严格控制总大小。比如 maxBytes=5MB,backupCount=3,总日志不超过 20MB。

我曾经踩过一个坑:给一个支付系统配了按天切割,backupCount 设了 365。结果一年下来,日志文件占了 200 多 G,运维同事差点没把我拉黑。后来改成按大小切割 + 30 天保留,问题就解决了。

实战:组合使用两种 Handler

有时候,一个 logger 可以挂多个 handler。比如同时按大小切割和按时间切割:

import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler

logger = logging.getLogger('payment_system')
logger.setLevel(logging.INFO)

# 按大小切割,防止单个文件过大
size_handler = RotatingFileHandler(
    'payment.log', maxBytes=50*1024*1024, backupCount=5, encoding='utf-8'
)
size_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))

# 按天切割,方便按日期归档
time_handler = TimedRotatingFileHandler(
    'payment_daily.log', when='midnight', interval=1, backupCount=30, encoding='utf-8'
)
time_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))

logger.addHandler(size_handler)
logger.addHandler(time_handler)

logger.info('双 handler 测试')

这样,同一个日志会同时写入两个文件。一个用于实时查看(按大小控制体积),一个用于归档(按日期管理)。

注意:双 handler 意味着双倍写入开销。如果日志量极大,要考虑磁盘 IO 压力。我一般只在关键业务模块用这种方案。

日志文件切割的核心逻辑

为了让你更直观地理解切割流程,我画了一张图:

日志文件切割流程 app.log (当前) 达到maxBytes 执行切割 app.log.1 (备份) 备份文件链(backupCount=3) app.log.3 app.log.2 app.log.1 app.log (新) 每次切割,编号依次递增,最旧的被删除 新日志写入 app.log,旧日志变成 app.log.1 关键参数:maxBytes=10MB | backupCount=5 | encoding='utf-8'

避坑指南

最后,分享几个我踩过的坑:

  • 编码问题:日志文件一定要指定 encoding='utf-8'。否则中文日志会乱码,排查问题的时候想哭。
  • 权限问题:确保日志目录有写入权限。我遇到过部署后日志写不进去,程序也不报错,白白浪费半天排查时间。
  • 多进程写入:RotatingFileHandler 不是线程安全的,更不是进程安全的。多进程场景下,日志可能会丢失或错乱。解决方案是用 ConcurrentLogHandler 或者单独跑日志服务。
  • 日志不刷新:默认情况下,日志是缓冲写入的。如果程序突然崩溃,最后几条日志可能没刷到磁盘。可以设置 delay=False 或者手动调用 flush()。
我的建议:刚开始做日志系统,别想得太复杂。先用 RotatingFileHandler 按大小切割,backupCount 设 5 个。跑一段时间,看看日志量,再调整策略。日志系统是慢慢迭代出来的,不是一步到位的。

好了,文件日志落地就聊这么多。记住一句话:日志写不好,排查两行泪。把切割策略配好,磁盘空间管住,你就能安心睡觉了。


专注资料整理