Logpy-AgentOne/pipe/protokoll.py
Jeuner ab2bd949be feat: Log-Rotation fuer Rohlogs + Aufraeumen alter Verlauf-Archivkopien
Audit ergab: "Tag archivieren" sichert/leert nur verlauf.log. Die rohen
stdout/stderr-Logs (telefon/autostart.log vom launchd-Watchdog
net.dillenberg.fonagent, alle 5 Min; watcher.log/leitstand.log/monitor.log)
wachsen unbegrenzt - autostart.log stand bei 523 KB/16595 Zeilen ohne jede
Bereinigung. audit.log ist bewusst permanent (Compliance, unveraendert).

starten.sh kuerzt diese vier Dateien jetzt ab 2 MB auf die letzten 2000
Zeilen (in-place, gleiches Inode) - laeuft ohnehin alle 5 Min per Watchdog.
Redirects von > auf >> umgestellt (O_APPEND), damit die In-Place-Kuerzung
fuer die noch laufenden Prozesse sicher ist (kein Datenverlust durch
gecachte Schreib-Offsets).

pipe.protokoll.raeume_archiv_auf() loescht verlauf-archiv/*.log aelter als
90 Tage - lief bisher unbegrenzt an. Eingehaengt in pipe.watch's ohnehin
taeglichen Check (bisher nur fuer LOESCHFRIST_TAGE), jetzt unabhaengig davon.

Getestet: Kuerz-Logik (3,8 MB -> 2000 Zeilen) und raeume_archiv_auf()
(gezielt gealterte Testdatei geloescht, Rest unangetastet) isoliert
verifiziert, echte Dateien nicht angefasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbdWYnDtyMgxsq4eN1WnrQ
2026-08-28 19:07:31 +02:00

105 lines
3.5 KiB
Python

"""Gemeinsames Ereignisprotokoll der Pipe.
Jede Zeile ist ein JSON-Objekt (JSONL) — maschinenlesbar für das Dashboard und
trotzdem mit ``tail -f`` lesbar. Bewusst schlank: anhängen, nie sperren, nie
scheitern. Ein kaputtes Protokoll darf keinen Anruf kosten.
Diese Datei ist reine UI-Historie (siehe MAX_ZEILEN/kuerze) - fuer einen
dauerhaften Nachweis (Compliance, u.a. Loeschungen) schreibt schreibe()
jedes Ereignis zusaetzlich unveraenderlich nach audit.log, siehe pipe.audit.
"""
from __future__ import annotations
import json
import shutil
import time
from datetime import datetime
from pathlib import Path
from . import audit, config
DATEI = config.TELEFON / "verlauf.log"
MAX_ZEILEN = 2000 # darüber wird beim Schreiben gekürzt
ARCHIV_ORDNER = config.TELEFON / "verlauf-archiv"
def schreibe(art: str, text: str, **felder) -> None:
"""Hängt ein Ereignis an (UI-Verlauf + dauerhaftes Audit-Log). Wirft nie."""
try:
DATEI.parent.mkdir(parents=True, exist_ok=True)
eintrag = {"zeit": datetime.now().isoformat(timespec="seconds"),
"art": art, "text": text, **felder}
with DATEI.open("a", encoding="utf-8") as f:
f.write(json.dumps(eintrag, ensure_ascii=False) + "\n")
except Exception:
pass
audit.schreibe(art, text, **felder)
def lies(anzahl: int = 200) -> list[dict]:
"""Liest die letzten Ereignisse. Defekte Zeilen werden übersprungen."""
if not DATEI.is_file():
return []
try:
zeilen = DATEI.read_text(encoding="utf-8").splitlines()[-anzahl:]
except Exception:
return []
eintraege = []
for z in zeilen:
try:
eintraege.append(json.loads(z))
except json.JSONDecodeError:
continue
return eintraege
def kuerze() -> None:
"""Hält das Protokoll auf MAX_ZEILEN. Wirft nie."""
try:
if not DATEI.is_file():
return
zeilen = DATEI.read_text(encoding="utf-8").splitlines()
if len(zeilen) > MAX_ZEILEN:
DATEI.write_text("\n".join(zeilen[-MAX_ZEILEN:]) + "\n", encoding="utf-8")
except Exception:
pass
def archiviere_und_leere() -> Path | None:
"""Sichert den aktuellen Verlauf als Kopie und leert ihn danach - fuer
einen frischen Verlauf nach "Tag archivieren". Das dauerhafte audit.log
ist davon unberuehrt (behaelt ohnehin alles). Wirft nie. Gibt den Pfad
der Archivkopie zurueck, oder None wenn nichts zu sichern war."""
try:
if not DATEI.is_file() or not DATEI.stat().st_size:
return None
ARCHIV_ORDNER.mkdir(parents=True, exist_ok=True)
ziel = ARCHIV_ORDNER / f"verlauf_{datetime.now():%Y%m%d-%H%M%S}.log"
shutil.copy2(DATEI, ziel)
DATEI.write_text("", encoding="utf-8")
return ziel
except Exception:
return None
ARCHIV_FRIST_TAGE = 90
def raeume_archiv_auf(tage: int = ARCHIV_FRIST_TAGE) -> int:
"""Löscht Verlauf-Archivkopien (nicht audit.log!) älter als `tage` Tage.
Ein neues verlauf_*.log entsteht bei jedem "Tag archivieren" - ohne
Aufräumen sammeln sich diese sonst unbegrenzt an. Wirft nie, gibt die
Anzahl gelöschter Dateien zurück."""
if not ARCHIV_ORDNER.is_dir():
return 0
grenze = time.time() - tage * 86400
geloescht = 0
for datei in ARCHIV_ORDNER.glob("verlauf_*.log"):
try:
if datei.stat().st_mtime < grenze:
datei.unlink()
geloescht += 1
except Exception:
continue
return geloescht