文件日志落地: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 有个致命问题——它不会自动切割。日志文件会一直增长,直到撑爆你的磁盘。我曾经接手过一个项目,日志文件 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 个。
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' | 每天午夜 | 最常用 |
切割策略怎么选?
说白了,没有银弹。你得根据业务场景来定。
- 高并发系统:日志量巨大,建议按大小切割(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 测试')
这样,同一个日志会同时写入两个文件。一个用于实时查看(按大小控制体积),一个用于归档(按日期管理)。
日志文件切割的核心逻辑
为了让你更直观地理解切割流程,我画了一张图:
避坑指南
最后,分享几个我踩过的坑:
- 编码问题:日志文件一定要指定 encoding='utf-8'。否则中文日志会乱码,排查问题的时候想哭。
- 权限问题:确保日志目录有写入权限。我遇到过部署后日志写不进去,程序也不报错,白白浪费半天排查时间。
- 多进程写入:RotatingFileHandler 不是线程安全的,更不是进程安全的。多进程场景下,日志可能会丢失或错乱。解决方案是用 ConcurrentLogHandler 或者单独跑日志服务。
- 日志不刷新:默认情况下,日志是缓冲写入的。如果程序突然崩溃,最后几条日志可能没刷到磁盘。可以设置 delay=False 或者手动调用 flush()。
好了,文件日志落地就聊这么多。记住一句话:日志写不好,排查两行泪。把切割策略配好,磁盘空间管住,你就能安心睡觉了。