.log.1 (eine vorhandene .1 wird ersetzt) und das aktive Log startet leer. * Es bleiben also höchstens zwei Generationen pro Log liegen. * 2. Aufbewahrungsfrist: Einträge, die älter als --keep-days sind, werden aus den * aktiven Logs entfernt; rotierte .1-Dateien jenseits der Frist gelöscht. Damit * gibt es eine echte Löschfrist statt nur einer Größengrenze — auf einer ruhigen * Vereinsseite läge ein Eintrag sonst jahrelang herum. * * Aufruf: php bin/log-rotate.php [--max-mb=2] [--keep-days=90] [-v] */ if (PHP_SAPI !== 'cli') { exit(1); } require dirname(__DIR__) . '/app/bootstrap.php'; $maxMb = 2.0; $keepDays = 90; $verbose = in_array('-v', $argv, true); foreach ($argv as $arg) { if (preg_match('/^--max-mb=([0-9.]+)$/', $arg, $m) === 1) { $maxMb = (float) $m[1]; } elseif (preg_match('/^--keep-days=(\d+)$/', $arg, $m) === 1) { $keepDays = max(1, (int) $m[1]); } } $limit = (int) ($maxMb * 1024 * 1024); $cutoff = time() - $keepDays * 86400; $logDir = STORAGE_PATH . '/logs'; /** * Zeitstempel am Zeilenanfang: `[ISO]` (log_write), `[27-Jul-2026 08:11:45 UTC]` * (PHP selbst) oder ein nackter ISO-Stempel. Null = keiner. */ $stampOf = static function (string $line): ?int { if (preg_match('/^\[([^\]]+)\]/', $line, $m) === 1 || preg_match('/^(\d{4}-\d{2}-\d{2}\S*)/', $line, $m) === 1) { $ts = strtotime($m[1]); return $ts === false ? null : $ts; } return null; }; /** * Einträge vor $cutoff entfernen. Zeilen ohne eigenen Zeitstempel (z. B. PHP-Stacktraces) * gehören zum vorherigen Eintrag und teilen dessen Schicksal. Geschrieben wird über eine * temporäre Datei + rename; ein Append genau in diesem Moment ginge verloren — bei einem * wöchentlichen Lauf nachts ein akzeptables Risiko, dafür bleibt die Datei immer konsistent. */ $prune = static function (string $file) use ($cutoff, $stampOf): int { $raw = (string) @file_get_contents($file); if ($raw === '') { return 0; } $keep = []; $dropping = false; $removed = 0; foreach (explode("\n", $raw) as $line) { $ts = $stampOf($line); if ($ts !== null) { $dropping = $ts < $cutoff; } if ($dropping) { if (trim($line) !== '') { $removed++; } continue; } $keep[] = $line; } if ($removed === 0) { return 0; } $tmp = $file . '.tmp'; if (@file_put_contents($tmp, implode("\n", $keep)) === false || !@rename($tmp, $file)) { @unlink($tmp); return 0; } @chmod($file, 0664); return $removed; }; $rotated = 0; $pruned = 0; $deleted = 0; foreach (glob($logDir . '/*.log') ?: [] as $log) { $name = basename($log); // 1. Alte Einträge raus (vor der Größenprüfung — spart oft die Rotation). $removed = $prune($log); if ($removed > 0) { $pruned += $removed; if ($verbose) { echo "gekürzt: {$name} ({$removed} Einträge älter als {$keepDays} Tage)\n"; } } // 2. Rotation nach Größe. $size = (int) @filesize($log); if ($size > $limit) { if (@rename($log, $log . '.1')) { @touch($log); @chmod($log, 0664); echo "rotiert: {$name} (" . round($size / 1048576, 1) . " MB)\n"; $rotated++; } else { fwrite(STDERR, "Rotation fehlgeschlagen: {$name}\n"); } } } // 3. Rotierte Generationen jenseits der Frist löschen. foreach (glob($logDir . '/*.log.1') ?: [] as $old) { if ((int) @filemtime($old) < $cutoff && @unlink($old)) { echo 'gelöscht: ' . basename($old) . " (älter als {$keepDays} Tage)\n"; $deleted++; } } echo $rotated === 0 && $pruned === 0 && $deleted === 0 ? "Nichts zu tun (alle Logs unter {$maxMb} MB, keine Einträge älter als {$keepDays} Tage).\n" : "Fertig: {$rotated} rotiert, {$pruned} alte Einträge entfernt, {$deleted} Altdatei(en) gelöscht.\n";