Python 日志按大小轮转:用 RotatingFileHandler 控制备份数量
自动化脚本每天往同一个 task.log 追加内容,几周后文件就可能大到难以打开,甚至挤占磁盘。本文用 Python 标准库 RotatingFileHandler 按大小轮转日志,只保留指定数量的备份,并验证任务失败时仍有异常栈和非零退出码。
示例面向初学者到中级读者,适用于同一时刻只有一个进程写日志的批处理脚本。如果服务器已经满盘,先按 Linux 删除文件后磁盘仍满的排查步骤恢复空间,再添加轮转策略。
1. 方法选择:让 Python 管理自己的日志
这里由 Python 负责轮转。活动文件是 task.log,最近一次备份是 task.log.1,更旧的依次为 .2、.3。达到保留数量后,旧备份会被替换。
Python 官方处理器文档说明,maxBytes 和 backupCount 都必须非零才会轮转。本文用 4096 字节和 3 个备份方便观察;实际任务可以选更大的阈值。
不要同时让其他轮转工具重命名这一组文件。应用内轮转和系统侧轮转应选定一个负责人。本文只处理通过这个 logger 写出的应用日志,print() 和子进程输出不会自动进入它。
2. 准备环境
需要 Python 3.9 或更新版本,无第三方依赖。示例已在 Linux、Python 3.11.4 下验证,shell 命令适用于 bash、zsh 等 POSIX shell。
- 创建新的演示目录;重复实验时换一个新目录名,避免混入旧日志:
python3 --version
mkdir rotating-log-demo
cd rotating-log-demo
- 将下一节的完整代码保存为
rotate_job.py。
3. 完整实现与运行
"""Rotate one Python process's application log. Standard library only."""
import argparse
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
import sys
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--log-dir", type=Path, default=Path("logs"))
parser.add_argument("--count", type=int, default=200)
parser.add_argument("--max-bytes", type=int, default=4096)
parser.add_argument("--backups", type=int, default=3)
parser.add_argument("--fail", action="store_true")
args = parser.parse_args()
if args.count < 0 or args.max_bytes <= 0 or args.backups <= 0:
parser.error("count must be nonnegative; max-bytes and backups must be positive")
log_path = args.log_dir / "task.log"
try:
args.log_dir.mkdir(parents=True, exist_ok=True)
handler = RotatingFileHandler(
log_path, maxBytes=args.max_bytes, backupCount=args.backups,
encoding="utf-8",
)
except OSError as exc:
print(f"ERROR: cannot open log: {exc}", file=sys.stderr)
return 1
handler.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%S",
))
logger = logging.getLogger("rotation_demo")
logger.setLevel(logging.INFO)
logger.propagate = False
logger.addHandler(handler)
try:
logger.info("START count=%d", args.count)
# Replace this loop with your synchronous application work.
for item in range(args.count):
logger.info("item=%04d status=ok payload=%s", item, "x" * 120)
if args.fail:
raise RuntimeError("demo job failed")
logger.info("DONE count=%d", args.count)
except Exception:
logger.exception("FAILED")
print(f"FAILED: see {log_path}", file=sys.stderr)
return 2
finally:
logger.removeHandler(handler)
handler.close()
print(f"OK: log={log_path} count={args.count}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
脚本先创建日志目录,再配置 UTF-8 编码。propagate = False 避免同一条记录又被根 logger 的处理器输出。处理器只在入口配置一次,结束时关闭;不要在每个循环里重复添加它。
- 查看帮助并写入 200 条演示记录:
python3 rotate_job.py --help
python3 rotate_job.py --count 200
ls -lh logs/task.log*
tail -n 2 logs/task.log
写入记录的命令输出为:
OK: log=logs/task.log count=200
目录中应出现 task.log 与 .1、.2、.3 三个备份,活动文件末尾含有 DONE count=200。日志中的时间戳使用本机时间;文件大小以实际检查结果为准。
- 接入自己的任务时,用实际处理逻辑替换循环里的演示消息,保留开始、结束和异常记录。若还要运行外部工具,可参考子进程超时与日志教程单独捕获输出;把输出重定向到普通文件并不会启用这个轮转处理器。
4. 验证文件数量和保留内容
将下面代码保存为 check_rotation.py,在同一目录执行 python3 check_rotation.py:
from pathlib import Path
folder = Path("logs")
files = sorted(folder.glob("task.log*"))
assert [p.name for p in files] == [
"task.log", "task.log.1", "task.log.2", "task.log.3",
]
# These demo messages are short ASCII records, not arbitrary application data.
assert all(0 < p.stat().st_size <= 4096 for p in files)
assert "DONE count=200" in (folder / "task.log").read_text(encoding="utf-8")
for path in files:
print(f"{path.name}: {path.stat().st_size} bytes")
print("PASS: one active log, three backups, latest completion retained")
最后应输出:
PASS: one active log, three backups, latest completion retained
再执行一次 python3 rotate_job.py --count 200 和 python3 check_rotation.py,确认重复运行后仍只有这四个文件,最新的完成记录仍在活动文件中。这里的大小断言只针对本文的短 ASCII 演示消息。超长单条消息、多字节编码等情况可能超过阈值;maxBytes 不是硬磁盘配额。
轮转会淘汰旧记录,不适合必须永久保留的审计数据。对这类数据应另行归档;给每次运行改一个新文件名,也需要另一套清理策略。
5. 验证失败与调整保留策略
- 逐条执行以下命令。失败演示故意返回 2,不要把它直接放进遇错立即退出的验证脚本:
python3 rotate_job.py --count 2 --fail
printf 'exit=%s\n' "$?"
tail -n 8 logs/task.log
预期终端显示 FAILED: see logs/task.log 和 exit=2,日志末尾包含 FAILED、Traceback 和 RuntimeError: demo job failed。正常调用返回 0;日志目录创建或文件打开失败返回 1;参数校验失败由 argparse 返回 2。
- 验证完小阈值后,可以在同一目录试用 10 MiB 和 5 个备份:
python3 rotate_job.py --count 200 --max-bytes 10485760 --backups 5
这是一个活动文件加最多五个备份,并非保留五天。小阈值时已被淘汰的记录不会恢复。约 60 MiB 只能作为短记录情况下的容量估算,不能当作严格上限;降低备份数也不会立即清掉所有原有的高编号文件。
接入已有自动化入口时,使用解释器、脚本和 --log-dir 的绝对路径,并保留脚本退出码。同一个日志路径只允许一个写入进程;需要并发工作时,Python 官方多进程日志指南建议通过队列或 socket 汇集到单个写入端。
6. 常见问题与直接修复
| 现象 | 操作 |
|---|---|
一直没有 .1 文件 |
检查两个轮转参数都大于零;用 --count 200 --max-bytes 4096 --backups 3 重跑演示。 |
ERROR: cannot open log |
检查路径是否被普通文件占用;把 --log-dir 指向当前用户可写的目录。 |
| 同一消息出现多次 | 检查是否重复添加处理器,以及是否继续向根 logger 传播。 |
| 多个进程同时轮转时报错或记录丢失 | 停止共享文件写入,改为单个写入端;不要直接将本例用于多 worker 服务。 |
No space left on device |
先检查 df -h . 和 du -sh logs,恢复可用空间后再运行。轮转不能凭空提供磁盘空间。 |
| 找不到旧错误 | 旧备份可能已被淘汰;根据每小时产生日志量提高阈值或归档频率。 |
写入或重命名期间的 I/O 错误可能由 logging 的 handleError处理,而不是让业务自动失败。应同时观察 stderr、磁盘空间和实际日志内容;本例的退出码不是日志持久化保证。
查找错误时要搜索活动文件和备份;可按日志排障教程用 rg 缩小范围。轮转后的编号表示新旧顺序,不要只查看最大的编号。
总结
给单进程 Python 脚本配置非零大小阈值和备份数,日志就能按既定策略保留。先用小阈值验证轮转、重复运行和失败记录,再换成适合实际任务的容量。并发写入和永久归档需要单独设计,文件数量检查不能替代业务结果验证。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-rotating-logs.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。