feat: 日志轮转(保留 14 天)

main.log 无轮转、已涨到 34MB,且每晚任务都会继续追加。

- logrotate/xwlb 模板 + scripts/install_logrotate.sh(幂等,装到 /etc/logrotate.d/xwlb)
- 每天轮转、保留 14 份、立即压缩 → main.log-YYYYMMDD.gz
- 必须用 copytruncate:脚本以 >>main.log 长开句柄,改名式轮转会
  让当晚日志写进归档、新文件空着
- journald 同步设 MaxRetentionSec=14day + SystemMaxUse=500M
- 安装脚本自带 logrotate -d 校验(这条立刻抓到"指令行尾注释"被当成参数值的错误)
- tests/test_logrotate.py 19 项,锁住保留天数、行尾注释、占位符替换

实测:34MB → 2.9MB,归档 32453 行与轮转前完全一致
This commit is contained in:
2026-09-25 12:50:11 +08:00
parent b996b444c4
commit 1d3ba02703
7 changed files with 226 additions and 5 deletions
+86
View File
@@ -0,0 +1,86 @@
"""日志轮转配置回归测试(不依赖 pytest,不需要 root)
python tests/test_logrotate.py
覆盖:
- 保留 14 天等关键指令齐全;
- **指令后面不能有行尾注释**(logrotate 会把注释当成参数值,
实测报错 `bad rotation count '14 # 保留 14 份'`,这条就是那次踩坑的回归);
- 占位符齐全,且安装脚本确实会替换(否则装过去的路径是字面量 @PROJECT_DIR@);
- 若本机已安装,装好的文件里不应残留占位符。
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CONF = ROOT / 'logrotate' / 'xwlb'
INSTALLER = ROOT / 'scripts' / 'install_logrotate.sh'
INSTALLED = Path('/etc/logrotate.d/xwlb')
def directives(text):
"""取配置块里的指令行(去注释、去空行、去大括号)"""
out = []
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith('#') or line in ('{', '}'):
continue
out.append(line)
return out
def main():
failed = 0
def check(desc, got, want):
nonlocal failed
if got == want:
print(f" ✓ {desc}: {got!r}")
else:
failed += 1
print(f" ✗ {desc}: 期望 {want!r},实际 {got!r}")
print("配置文件:")
check('模板存在', CONF.exists(), True)
text = CONF.read_text(encoding='utf-8')
lines = directives(text)
def has_directive(name):
return any(re.match(rf'^{name}(\s|$)', ln) for ln in lines)
for d in ('daily', 'rotate', 'missingok', 'notifempty', 'compress',
'copytruncate', 'dateext', 'dateformat', 'su'):
check(f'含指令 {d}', has_directive(d), True)
print("\n保留天数:")
rotate = [ln for ln in lines if ln.startswith('rotate')][0]
check('rotate 值为 14(保留 14 天)', int(rotate.split()[1]), 14)
print("\n行尾注释(logrotate 会把注释吃成参数值):")
bad = [ln for ln in lines if '#' in ln]
check('指令行无行尾注释', bad, [])
print("\n占位符与安装替换:")
check('含路径占位符 @PROJECT_DIR@', '@PROJECT_DIR@' in text, True)
check('含属主占位符 @OWNER@', '@OWNER@' in text, True)
installer = INSTALLER.read_text(encoding='utf-8')
check('安装脚本会替换路径占位符', 's#@PROJECT_DIR@#' in installer, True)
check('安装脚本会替换属主占位符', 's#@OWNER@#' in installer, True)
check('安装脚本会做语法校验(logrotate -d)', 'logrotate -d' in installer, True)
print("\n已安装文件(若本机装过):")
if INSTALLED.exists():
installed = INSTALLED.read_text(encoding='utf-8')
check('无残留占位符', [p for p in ('@PROJECT_DIR@', '@OWNER@', '@GROUP@')
if p in installed], [])
check('已指向真实项目路径', str(ROOT) in installed, True)
else:
print(" · 本机未安装,跳过(未运行 scripts/install_logrotate.sh)")
print(f"\n结果: {'全部通过' if failed == 0 else f'{failed} 项失败'}")
return 1 if failed else 0
if __name__ == '__main__':
sys.exit(main())