3、日志文件输出:FileHandler使用、日志轮转(RotatingFileHandler)、按时间轮转(TimedRotatingFileHandler)
日志光在控制台打印,说实话,在开发环境还行。一旦上了生产环境,你不可能一直盯着终端看。日志必须落地到文件里,方便事后排查。这一章,我们就来聊聊怎么把日志写到文件里,以及怎么处理日志文件越滚越大的问题。
3.1 基础文件输出:FileHandler
Python 的 logging 模块里,FileHandler 是最基础的日志输出方式。说白了,就是把日志内容追加到一个文本文件里。
用法很简单,看代码:
import logging
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建一个 FileHandler,写入到 app.log
file_handler = logging.FileHandler('app.log', encoding='utf-8')
file_handler.setLevel(logging.INFO)
# 设置格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
# 把 handler 加到 logger 上
logger.addHandler(file_handler)
logger.info('服务启动成功')
logger.warning('数据库连接池使用率超过 80%')
这里有个小细节——encoding='utf-8'。我个人习惯是一定要加上这个参数。为什么?因为默认是系统编码,在 Windows 上可能是 GBK,日志里一旦出现中文,直接乱码。我曾经因为这个排查了半天,最后发现是编码问题,气得不行。
FileHandler 默认是追加模式,也就是每次启动程序,日志会接着上次的写,不会清空。如果你想每次启动都重新开始,可以传 mode='w'。不过生产环境我建议你别这么干,万一重启把历史日志丢了,排查问题就抓瞎了。
3.2 日志轮转:RotatingFileHandler
日志文件一直写下去,会越来越大。你想想看,一个日志文件几个 GB,打开都费劲,更别说用 grep 查了。所以我们需要日志轮转——当文件达到一定大小,自动切分成多个文件。
RotatingFileHandler 就是干这个的。它按文件大小来轮转。
from logging.handlers import RotatingFileHandler
# 每个日志文件最大 10MB,保留 5 个备份
rotating_handler = RotatingFileHandler(
'app.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
rotating_handler.setLevel(logging.INFO)
rotating_handler.setFormatter(formatter)
logger.addHandler(rotating_handler)
运行之后,你会看到这样的文件:
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 变成 app.log.2,以此类推。超过 backupCount 数量的旧文件会被删除。
ConcurrentRotatingFileHandler 这类第三方库。
3.3 按时间轮转:TimedRotatingFileHandler
按大小轮转有个问题——你没法控制日志按天或按小时切分。比如你想每天凌晨切一个日志文件,方便归档和清理。这时候就需要 TimedRotatingFileHandler。
from logging.handlers import TimedRotatingFileHandler
import time
# 按天轮转,保留 30 天的日志
timed_handler = TimedRotatingFileHandler(
'app.log',
when='midnight', # 每天午夜轮转
interval=1, # 间隔 1 天
backupCount=30, # 保留 30 个备份
encoding='utf-8'
)
timed_handler.setLevel(logging.INFO)
timed_handler.setFormatter(formatter)
logger.addHandler(timed_handler)
when 参数支持多种时间单位:
| 参数值 | 含义 | 适用场景 |
|---|---|---|
'S' |
每秒 | 调试用,生产基本不用 |
'M' |
每分钟 | 高频日志调试 |
'H' |
每小时 | 流量大的系统 |
'D' |
每天 | 最常用 |
'midnight' |
每天午夜 | 和 'D' 类似,但精确在 0 点轮转 |
'W0' - 'W6' |
每周指定天 | W0 是周一,W6 是周日 |
轮转后的文件名会带上时间戳,比如:
app.log.2025-01-15
app.log.2025-01-16
app.log.2025-01-17
when='midnight' 配合 backupCount=30。这样每天一个日志文件,保留最近 30 天。配合 crontab 或者日志收集工具(比如 Filebeat),把旧日志归档到对象存储或者 Elasticsearch 里,既省磁盘又方便检索。
3.4 组合使用:大小 + 时间双保险
有些场景下,光按时间轮转还不够。比如某个服务突然打印大量错误日志,一天之内就把磁盘写满了。这时候如果同时按大小轮转,至少能控制单个文件的大小,方便排查。
你可以同时添加两个 handler:
# 按大小轮转,防止单个文件过大
rotating_handler = RotatingFileHandler('app.log', maxBytes=100*1024*1024, backupCount=5, encoding='utf-8')
# 按时间轮转,方便按天归档
timed_handler = TimedRotatingFileHandler('app_archive.log', when='midnight', backupCount=30, encoding='utf-8')
logger.addHandler(rotating_handler)
logger.addHandler(timed_handler)
这样,app.log 负责控制单个文件大小,app_archive.log 负责按天切分。不过要注意,两个 handler 会写两份日志,磁盘占用会翻倍。我个人习惯是只用一个 TimedRotatingFileHandler,然后配合外部工具(比如 logrotate)做二次压缩和清理。
3.5 避坑指南
最后分享几个我踩过的坑:
- 权限问题: 日志文件所在的目录,应用进程必须有写权限。我曾经部署到 Docker 容器里,忘了挂载目录的权限,结果日志写不进去,排查了半天才发现是
Permission denied。 - 多进程写入:
RotatingFileHandler和TimedRotatingFileHandler都不是线程安全的,更不是进程安全的。多进程场景下,轮转时可能会产生冲突。解决方案是用ConcurrentLogHandler或者把日志写到不同的文件里(比如每个进程一个日志文件)。 - 日志不刷新: 默认情况下,日志是缓冲写入的。如果程序突然崩溃,最后几条日志可能没来得及写进去。可以设置
flush或者用logging.handlers.MemoryHandler做批量写入。不过生产环境我一般不管这个,因为崩溃时的日志丢失概率很低,而且影响不大。 - 磁盘写满: 即使设置了轮转,如果日志量太大,还是可能把磁盘写满。建议配合监控告警,当磁盘使用率超过 80% 时自动通知。我曾经遇到过日志把根分区写满,导致整个服务器挂掉的情况,那叫一个惨。
嗯,这一章的内容就到这里。日志文件输出和轮转,说白了就是三个核心点:写到哪里、怎么切分、保留多久。搞清楚了这三个问题,生产环境的日志管理基本就稳了。