纯 shell 脚本实现日志轮转更轻量可控,核心是用日期比对+文件锁+追加写入保障原子性,避免跨天重复/漏切与并发冲突。

logrotate 不是唯一选择,但 shell 脚本自己实现更可控
直接用 logrotate 当然省事,但它依赖系统配置、需要 root 权限、行为不够透明。如果只是单个应用日志、想完全掌握切割时机和命名逻辑,纯 shell 脚本反而更轻量、更易调试。
用 date + mv 实现按天重命名,关键在时间戳生成方式
核心思路是:每天首次写入前,把旧日志重命名为带日期后缀的归档文件。不能简单用 date +%Y%m%d 拼接,否则跨天时可能重复或漏切。
- 正确做法:用上一次写入时的日期作为当前日志的“归属日”,所以应在每次追加前检查当前日期是否变化
- 用
stat -c "%y" logfile 2>/dev/null | cut -d' ' -f1获取最后修改日期(兼容 GNU coreutils) - 或者更稳妥:记录一个
.last_rotate_date文件,每次启动/写入前读取并比对$(date +%Y-%m-%d) - 重命名命令必须加判断,避免重复执行导致
mv: cannot stat 'app.log': No such file
if [ "$today" != "$last_date" ]; then [ -s "app.log" ] && mv app.log "app.log.$last_date" echo "$(date '+%Y-%m-%d %H:%M:%S') - rotated" > app.log echo "$today" > .last_rotate_date fi
追加日志时别用 > 覆盖,要用 >> 并确保目录可写
常见错误是脚本里用 echo "msg" > app.log,这会清空当天日志;或者没检查 app.log 所在目录是否存在、权限是否允许创建文件。
- 所有日志写入统一用
echo "[$(date '+%H:%M:%S')] $msg" >> app.log - 脚本开头加
mkdir -p ./logs && cd ./logs确保路径存在 - 如果日志路径含变量(如
$LOG_DIR),务必用[ -d "$LOG_DIR" ] || mkdir -p "$LOG_DIR"防错 - 注意 shell 的
date命令在 macOS 上不支持%N,跨平台建议避开纳秒级格式
定时触发靠 cron,但得防重复执行和并发写冲突
很多人把切割逻辑塞进 cron 每分钟跑一次,结果在临界时刻(比如 23:59:59 和 00:00:01)可能被两个实例同时执行,导致日志丢失或重命名失败。
- 推荐方案:cron 每 5 分钟检查一次,切割逻辑内部用
flock加锁(flock -n /tmp/rotate.lock -c 'bash rotate.sh') - 如果没有
flock,可用if mkdir /tmp/rotate.lock 2>/dev/null; then ... rmdir /tmp/rotate.lock; fi模拟排他锁 - 不要在切割脚本里直接
killall tail -f或重启服务——除非你明确知道日志文件被进程 hold 住且需 reload
真正难的不是怎么切,而是怎么让“正在写的日志”和“刚切走的归档”之间不丢一行、不重一记。时间判断粒度、锁机制、写入原子性,这三个点漏掉任一个,线上查问题时就会多花两小时。











