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
  • 多进程写入: RotatingFileHandlerTimedRotatingFileHandler 都不是线程安全的,更不是进程安全的。多进程场景下,轮转时可能会产生冲突。解决方案是用 ConcurrentLogHandler 或者把日志写到不同的文件里(比如每个进程一个日志文件)。
  • 日志不刷新: 默认情况下,日志是缓冲写入的。如果程序突然崩溃,最后几条日志可能没来得及写进去。可以设置 flush 或者用 logging.handlers.MemoryHandler 做批量写入。不过生产环境我一般不管这个,因为崩溃时的日志丢失概率很低,而且影响不大。
  • 磁盘写满: 即使设置了轮转,如果日志量太大,还是可能把磁盘写满。建议配合监控告警,当磁盘使用率超过 80% 时自动通知。我曾经遇到过日志把根分区写满,导致整个服务器挂掉的情况,那叫一个惨。

嗯,这一章的内容就到这里。日志文件输出和轮转,说白了就是三个核心点:写到哪里、怎么切分、保留多久。搞清楚了这三个问题,生产环境的日志管理基本就稳了。