auto: 일일 백업 2026-09-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""로그 로테이션 — ~/.openclaw/logs 크기 상한 유지 (LLM 미경유).
|
||||
|
||||
로그가 무한정 쌓여 1.0GB 가 된 걸 정리하고, 그 상태로 유지한다.
|
||||
대부분은 종료된 claude-session 로그였다(단일 파일 422MB).
|
||||
|
||||
정책 두 갈래:
|
||||
- **오래된 로그**(mtime > STALE_DAYS): 종료된 세션·고아 로그로 보고 trash.
|
||||
살아있는 데몬 로그는 매 사이클 쓰이므로 mtime 이 항상 최신이라 여기 걸리지 않는다.
|
||||
- **크고 활성인 로그**(size > MAX_MB): 최근 KEEP_LINES 줄만 남기고 **in-place** 축소.
|
||||
|
||||
⚠️ **in-place 여야 한다** — rename 후 새 파일을 만들면 launchd 데몬은 이미 열어둔
|
||||
옛 inode 에 계속 쓰므로 디스크는 그대로고 새 파일만 영원히 비어 있다. 그래서
|
||||
같은 fd 를 유지한 채 앞으로 당겨쓰고 truncate 한다.
|
||||
⚠️ 이 방식은 데몬이 **O_APPEND** 로 열었을 때만 안전하다(launchd 의 StandardOutPath/
|
||||
StandardErrorPath 는 O_APPEND 로 연다). append 가 아니면 데몬 fd 의 offset 이 옛 위치에
|
||||
남아 다음 write 가 그 자리에 떨어지며 sparse hole 로 파일 크기가 되돌아간다.
|
||||
2026-09-10 behive-web(112MB, 매분 기록)으로 실측해 되돌아가지 않음을 확인했다.
|
||||
|
||||
⚠️ 게이트웨이 로그는 대상이 아니다 — 2026.9.3 이후 `~/Library/Logs/openclaw/` 로
|
||||
옮겨가 본체가 자체 관리한다. `logs/gateway.*` 는 그때 남은 고아라 STALE 로 정리된다.
|
||||
|
||||
CLI: python3 log_rotate.py [--dry-run]
|
||||
launchd: ai.openclaw.log-rotate (매주 일요일 04:00)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
LOG_DIR = os.path.expanduser("~/.openclaw/logs")
|
||||
STALE_DAYS = 30 # 이보다 오래 안 쓰인 로그 = 종료된 세션·고아 → trash
|
||||
MAX_MB = 20 # 활성 로그 상한
|
||||
KEEP_LINES = 3000 # 축소 시 남길 최근 줄 수
|
||||
READ_CHUNK = 1 << 20
|
||||
|
||||
|
||||
def _tail_bytes(path: str, keep_lines: int) -> bytes:
|
||||
"""파일 끝에서 keep_lines 줄만 역방향으로 읽어 반환."""
|
||||
with open(path, "rb") as f:
|
||||
f.seek(0, os.SEEK_END)
|
||||
end = f.tell()
|
||||
buf = b""
|
||||
pos = end
|
||||
while pos > 0 and buf.count(b"\n") <= keep_lines:
|
||||
step = min(READ_CHUNK, pos)
|
||||
pos -= step
|
||||
f.seek(pos)
|
||||
buf = f.read(step) + buf
|
||||
lines = buf.splitlines(keepends=True)
|
||||
return b"".join(lines[-keep_lines:])
|
||||
|
||||
|
||||
def _shrink(path: str, dry: bool) -> int:
|
||||
"""최근 KEEP_LINES 줄만 남기고 in-place 축소. 회수한 바이트 반환."""
|
||||
before = os.path.getsize(path)
|
||||
data = _tail_bytes(path, KEEP_LINES)
|
||||
if len(data) >= before:
|
||||
return 0
|
||||
if not dry:
|
||||
# inode 유지 — 데몬이 열어둔 fd 가 계속 유효해야 한다
|
||||
with open(path, "r+b") as f:
|
||||
f.write(data)
|
||||
f.truncate()
|
||||
return before - len(data)
|
||||
|
||||
|
||||
def _trash(paths: list[str], dry: bool) -> None:
|
||||
if dry or not paths:
|
||||
return
|
||||
subprocess.run(["/usr/bin/trash", *paths], check=False,
|
||||
capture_output=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
args = ap.parse_args()
|
||||
dry = args.dry_run
|
||||
|
||||
if not os.path.isdir(LOG_DIR):
|
||||
print(f"[skip] {LOG_DIR} 없음", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
now = time.time()
|
||||
stale_cut = now - STALE_DAYS * 86400
|
||||
max_bytes = MAX_MB * 1024 * 1024
|
||||
|
||||
total_before = 0
|
||||
stale: list[str] = []
|
||||
shrunk: list[tuple[str, int]] = []
|
||||
|
||||
for name in sorted(os.listdir(LOG_DIR)):
|
||||
if not name.endswith(".log"):
|
||||
continue
|
||||
path = os.path.join(LOG_DIR, name)
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
continue
|
||||
total_before += st.st_size
|
||||
|
||||
if st.st_mtime < stale_cut:
|
||||
stale.append(path)
|
||||
elif st.st_size > max_bytes:
|
||||
freed = _shrink(path, dry)
|
||||
if freed:
|
||||
shrunk.append((name, freed))
|
||||
|
||||
stale_bytes = sum(os.path.getsize(p) for p in stale if os.path.exists(p))
|
||||
_trash(stale, dry)
|
||||
|
||||
tag = "[dry-run] " if dry else ""
|
||||
freed_total = stale_bytes + sum(f for _, f in shrunk)
|
||||
print(f"{tag}로그 {total_before/1048576:.0f}MB → "
|
||||
f"{(total_before - freed_total)/1048576:.0f}MB "
|
||||
f"(회수 {freed_total/1048576:.0f}MB)")
|
||||
if stale:
|
||||
print(f"{tag}오래된 로그 {len(stale)}개 정리 "
|
||||
f"({stale_bytes/1048576:.0f}MB, {STALE_DAYS}일 초과)")
|
||||
for p in stale[:5]:
|
||||
print(f" - {os.path.basename(p)}")
|
||||
if len(stale) > 5:
|
||||
print(f" … 외 {len(stale)-5}개")
|
||||
for name, freed in shrunk:
|
||||
print(f"{tag}축소 {name}: -{freed/1048576:.0f}MB "
|
||||
f"(최근 {KEEP_LINES}줄 보존)")
|
||||
if not stale and not shrunk:
|
||||
print(f"{tag}정리 대상 없음")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user