mirror of
https://github.com/Jeuners/Logpy-AgentOne.git
synced 2026-09-09 23:12:32 +02:00
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
105 lines
3.5 KiB
Python
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
|