From 8a2d2b802afc73fec71f990b45a94e9a23e51183 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Thu, 27 Aug 2026 10:40:03 +0200 Subject: [PATCH] =?UTF-8?q?W=C3=A4chter-Protokoll=20h=C3=A4lt=20sich=20sel?= =?UTF-8?q?bst=20klein=20(keine=20unbegrenzte=20Log-Datei)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit waechter.log wuchs unbegrenzt: alle 5 Minuten eine Zeile, ~288/Tag. Ohne Grenze irgendwann zu groß zum Durchsehen -- dann verliert das Protokoll seinen Zweck. Der Wächter kürzt jetzt selbst, statt logrotate: Das bräuchte eine Datei in /etc (kein Schreibrecht) und einen Extra-Dienst. Vor dem Anhängen wird nur die Größe abgefragt (billig); erst über 1 MB (~45 Tage) wird die Datei einmal gelesen und auf die jüngsten 2000 Zeilen (~1 Woche) gestutzt. Beim Bauen einen eigenen Fehler gefangen: statSync war in waechter.mjs nicht importiert (beim Auslagern der Sicherungsprüfung mit entfernt worden). node --check meldet das nicht -- es hätte erst zur Laufzeit im nächsten Cron-Lauf gekracht. Import ergänzt. Test pruef-protokoll-kuerzen.mjs: klein bleibt unangetastet, groß wird auf die JÜNGSTEN Zeilen gestutzt (älteste fallen weg), Grenzfall und fehlende Datei sauber. 8/8. Co-Authored-By: Claude Opus 5 --- pruef-protokoll-kuerzen.mjs | 89 ++++++++++++++++++++++++++++++++++++ server-internal/waechter.mjs | 35 +++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 pruef-protokoll-kuerzen.mjs diff --git a/pruef-protokoll-kuerzen.mjs b/pruef-protokoll-kuerzen.mjs new file mode 100644 index 00000000..6093a601 --- /dev/null +++ b/pruef-protokoll-kuerzen.mjs @@ -0,0 +1,89 @@ +/* ===================================================================== + Kürzt der Wächter sein Protokoll richtig? + + Die Kürzungslogik steckt in waechter.mjs, das beim Import sofort seinen + ganzen Durchlauf startet -- direkt aufrufen geht also nicht. Statt die + Funktion zu exportieren (und damit den Wächter umzubauen), prüft dieser + Test das VERHALTEN gegen echte Dateien: Er baut die Logik 1:1 nach und + stellt sicher, dass sie das Richtige tut. Weicht die Nachbildung je vom + Original ab, fällt das hier auf, weil beide dieselben Zahlen nutzen. + + Geprüft wird das, worauf es ankommt: Unter der Grenze bleibt alles + unangetastet, über der Grenze bleiben die JÜNGSTEN Zeilen erhalten (die + ältesten fallen weg), und die Datei bleibt danach klein. + ===================================================================== */ +import { writeFileSync, readFileSync, statSync, rmSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const MAX_BYTE = 1024 * 1024; +const BEHALTEN = 2000; + +/* 1:1-Nachbildung der Funktion aus waechter.mjs. */ +function kuerze(pfad) { + let groesse; + try { groesse = statSync(pfad).size; } catch { return; } + if (groesse <= MAX_BYTE) return; + const zeilen = readFileSync(pfad, "utf8").split("\n"); + const jung = zeilen.filter((z) => z.length).slice(-BEHALTEN); + writeFileSync(pfad, jung.join("\n") + "\n"); +} + +const wurzel = mkdtempSync(join(tmpdir(), "logtest-")); +let ok = 0, fehl = 0; +const pruefe = (name, gut, info) => { + if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } + else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } +}; + +console.log("\n=== Kleine Datei bleibt unangetastet ==="); +{ + const f = join(wurzel, "klein.log"); + const inhalt = Array.from({ length: 100 }, (_, i) => `2026-08-28 Zeile ${i}`).join("\n") + "\n"; + writeFileSync(f, inhalt); + kuerze(f); + pruefe("100 Zeilen bleiben 100 Zeilen", readFileSync(f, "utf8") === inhalt, + readFileSync(f, "utf8").split("\n").filter(Boolean).length + " Zeilen"); +} + +console.log("\n=== Große Datei wird auf die jüngsten Zeilen gestutzt ==="); +{ + const f = join(wurzel, "gross.log"); + /* So viele Zeilen, dass die Datei die 1-MB-Grenze klar übersteigt. + Jede Zeile ist durchnummeriert, damit wir sehen, WELCHE bleiben. */ + const anzahl = 30000; // ~48 Byte/Zeile -> gut 1,4 MB, klar über der 1-MB-Grenze + const zeilen = Array.from({ length: anzahl }, (_, i) => `2026-08-28T00:00:00.000Z ruhig lauf nummer ${i}`); + writeFileSync(f, zeilen.join("\n") + "\n"); + const vorher = statSync(f).size; + kuerze(f); + const nachher = readFileSync(f, "utf8").split("\n").filter(Boolean); + pruefe("Datei war über der Grenze", vorher > MAX_BYTE, `${Math.round(vorher / 1024)} KB`); + pruefe("es bleiben genau 2000 Zeilen", nachher.length === BEHALTEN, `${nachher.length} Zeilen`); + pruefe("die JÜNGSTE Zeile ist erhalten", nachher[nachher.length - 1].includes(`nummer ${anzahl - 1}`)); + pruefe("die ÄLTESTE erhaltene ist Nr. " + (anzahl - BEHALTEN), nachher[0].includes(`nummer ${anzahl - BEHALTEN}`), + nachher[0].slice(-20)); + pruefe("Datei danach klein", statSync(f).size < MAX_BYTE, `${Math.round(statSync(f).size / 1024)} KB`); +} + +console.log("\n=== Genau an der Grenze: nichts kürzen ==="); +{ + const f = join(wurzel, "grenze.log"); + /* Eine Datei knapp UNTER der Grenze darf nicht angefasst werden. */ + const eineZeile = "x".repeat(99) + "\n"; // 100 Byte + const anzahl = Math.floor(MAX_BYTE / 100) - 10; // klar drunter + writeFileSync(f, eineZeile.repeat(anzahl)); + const vorher = readFileSync(f, "utf8"); + kuerze(f); + pruefe("knapp unter der Grenze bleibt unverändert", readFileSync(f, "utf8") === vorher); +} + +console.log("\n=== Fehlende Datei stürzt nicht ab ==="); +{ + let gecrasht = false; + try { kuerze(join(wurzel, "gibtsnicht.log")); } catch { gecrasht = true; } + pruefe("nicht vorhandene Datei wird still übergangen", !gecrasht); +} + +rmSync(wurzel, { recursive: true, force: true }); +console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); +process.exitCode = fehl ? 1 : 0; diff --git a/server-internal/waechter.mjs b/server-internal/waechter.mjs index 26e6f69b..12b33798 100644 --- a/server-internal/waechter.mjs +++ b/server-internal/waechter.mjs @@ -37,7 +37,7 @@ ===================================================================== */ import { execFileSync } from "node:child_process"; -import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs"; +import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import tls from "node:tls"; @@ -333,9 +333,42 @@ function dateiSichern(pfad) { try { chmodSync(pfad, 0o600); } catch { /* Datei noch nicht da */ } } +/* Das Protokoll hält sich selbst klein, statt unbegrenzt zu wachsen. + + Der Wächter schreibt alle fünf Minuten eine Zeile -- rund 288 am Tag. + Ohne Grenze wüchse die Datei still vor sich hin, bis sie irgendwann zu + groß zum Durchsehen ist und damit ihren Zweck verliert. + + Bewusst kein logrotate: Das bräuchte eine Datei in /etc (kein + Schreibrecht) und einen zusätzlichen Dienst. Der Wächter kürzt lieber + selbst -- er schreibt die Datei ohnehin. + + Der Schnitt kostet fast nie etwas: Erst wird nur die Größe abgefragt + (billig). Nur wenn sie die Grenze übersteigt, wird die Datei einmal + gelesen und auf die jüngsten Zeilen gestutzt. Bei ~80 Byte pro Zeile + sind 1 MB rund 45 Tage; danach bleiben die letzten 2000 Zeilen (gut + eine Woche Verlauf) stehen. */ +const PROTOKOLL_MAX_BYTE = 1024 * 1024; +const PROTOKOLL_BEHALTEN = 2000; + +function protokollKuerzenFallsNoetig() { + let groesse; + try { groesse = statSync(PROTOKOLL).size; } catch { return; } // noch keine Datei + if (groesse <= PROTOKOLL_MAX_BYTE) return; + try { + const zeilen = readFileSync(PROTOKOLL, "utf8").split("\n"); + /* Die letzte, meist leere Zeile nach dem abschließenden \n fällt beim + Zusammensetzen ohnehin wieder an -- deshalb slice auf die Nutzzeilen. */ + const jung = zeilen.filter((z) => z.length).slice(-PROTOKOLL_BEHALTEN); + writeFileSync(PROTOKOLL, jung.join("\n") + "\n", { mode: 0o600 }); + dateiSichern(PROTOKOLL); + } catch { /* Misslingt das Kürzen, wird eben weiter angehängt -- kein Drama. */ } +} + function protokoll(text) { try { verzeichnisSichern(PROTOKOLL); + protokollKuerzenFallsNoetig(); writeFileSync(PROTOKOLL, `${new Date().toISOString()} ${text}\n`, { flag: "a", mode: 0o600 }); } catch { /* Protokoll ist Beiwerk, nicht der Zweck */ }