fix(survey): Fortschritt sichtbar — der leere bestand.txt war kein Fehler, sondern ein laufender Scan

BEFUND (x22, gemessen und reproduziert):
Der Aufruf war 'python3 apache-log-survey.py 2>&1 | tee bestand.txt'. Das Skript
gab ALLES erst nach der Scan-Schleife aus (alle print() standen dahinter), und
Python puffert stdout in eine PIPE blockweise (4-8 KB). Die Gesamtausgabe ist
aber nur ~1 KB -> sie landete erst beim Prozessende in der Datei.

Messung mit 2 Mio Zeilen (140 MB):
  mit tee:     nach 5 s = 0 Bytes, fertig nach 41 s -> dann 1028 Bytes
  ohne Pipe:   nach 1 s = 0 Bytes
  python3 -u:  nach 1 s = 0 Bytes
=> Die Datei war leer, weil der Scan LIEF. Auf einem KeyHelp-Host mit tausenden
   Logdateien dauert das Minuten und liest sich wie ein Haenger oder Absturz.

FIX (drei Massnahmen, damit das nie wieder passiert):
1. sys.stdout.reconfigure(line_buffering=True) — jede Zeile geht sofort raus,
   auch durch eine Pipe.
2. Startbanner VOR dem Scan: Anzahl und Gesamtgroesse der Dateien.
3. Fortschrittszeile pro Datei (Nummer/Gesamtzahl, Zeilen, Groesse, Pfad) plus
   eine Zeile fuer nicht lesbare Dateien.

Verifiziert mit 1,4 Mio Zeilen durch eine Pipe: nach 2 s steht das Banner, drei
Fortschrittszeilen erscheinen waehrend des Scans. Gegenprobe mit dem alten Code:
nach 3 s 16 Bytes, alles erst am Ende.
This commit is contained in:
mm committed 2026-09-14 12:08:48 +02:00
1 parent 194fa98f97
commit df2ee94613
1 file changed
+57 -14
+57 -14
View File
@@ -17,6 +17,7 @@ import glob
import os
import re
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
@@ -68,11 +69,54 @@ def parse_ts(raw):
def main():
# ----------------------------------------------------------------------
# FORTSCHRITT SICHTBAR MACHEN — sonst sieht der Scan wie ein Haenger aus.
#
# Real passiert (x22): der Aufruf war `python3 survey.py 2>&1 | tee bestand.txt`.
# Das Skript gibt ALLES erst am Ende aus (alle print() stehen hinter der
# Scan-Schleife), und Python puffert stdout in eine PIPE blockweise (4-8 KB,
# nicht zeilenweise). Die Gesamtausgabe ist aber nur ~1 KB — sie landet also
# erst beim Prozessende in der Datei. Bei 2 Mio Zeilen gemessen: 41 s lang
# 0 Bytes, dann alles auf einmal. Auf einem KeyHelp-Host mit tausenden
# Logdateien dauert das Minuten und liest sich wie ein Absturz.
#
# Drei Gegenmaßnahmen:
# 1. Zeilenpufferung erzwingen (reconfigure), damit jede Zeile sofort raus ist
# 2. einen Startbanner VOR dem Scan ausgeben
# 3. pro Datei eine Fortschrittszeile (mit flush)
# ----------------------------------------------------------------------
try:
sys.stdout.reconfigure(line_buffering=True)
sys.stderr.reconfigure(line_buffering=True)
except (AttributeError, ValueError):
pass # aeltere Python-Version: dann eben gepuffert
files = sorted({f for pat in PATTERNS for f in glob.glob(pat)})
if not files:
print("Keine Apache-Logs gefunden. Laeuft Apache? (apache2ctl -v)")
return 1
total_bytes = 0
for _f in files:
try:
total_bytes += os.path.getsize(_f)
except OSError:
pass
def human(n):
for u in ("B", "KB", "MB", "GB"):
if n < 1024:
return f"{n:.1f} {u}"
n /= 1024
return f"{n:.1f} TB"
print("=" * 72)
print("APACHE-LOG BESTANDSAUFNAHME")
print("=" * 72)
print(f"Scanne {len(files)} Dateien ({human(total_bytes)} auf Platte) …")
print("(grosse Bestaende dauern; dieser Fortschritt laeuft mit)")
print()
total_lines = 0
total_parsed = 0
total_err = 0
@@ -88,14 +132,17 @@ def main():
day_domain = set()
per_file = []
for path in files:
t0 = time.time()
for _idx, path in enumerate(files, 1):
fh = open_maybe_gz(path)
if fh is None:
print(f" [{_idx}/{len(files)}] NICHT LESBAR: {path}", flush=True)
continue
try:
bytes_on_disk += os.path.getsize(path)
_sz = os.path.getsize(path)
bytes_on_disk += _sz
except OSError:
pass
_sz = 0
domain = os.path.basename(os.path.dirname(path)) or os.path.basename(path)
n_lines = n_ok = n_err = 0
f_first = f_last = None
@@ -137,18 +184,14 @@ def main():
total_parsed += n_ok
total_err += n_err
per_file.append((path, n_lines, n_ok, n_err, f_first, f_last))
# Fortschritt pro Datei — und bei grossen Dateien auch bei jeder Zeile
# die Marke, damit lange Einzeldateien nicht stumm wirken.
print(f" [{_idx}/{len(files)}] {n_lines:>10,} Zeilen "
f"{human(_sz):>9} {path}", flush=True)
def human(n):
for u in ("B", "KB", "MB", "GB"):
if n < 1024:
return f"{n:.1f} {u}"
n /= 1024
return f"{n:.1f} TB"
print("=" * 72)
print("APACHE-LOG BESTANDSAUFNAHME")
print("=" * 72)
print(f"Dateien: {len(files)} ({human(bytes_on_disk)} auf Platte)")
print()
print(f"Scan fertig in {time.time() - t0:.1f} s.")
print()
print(f"Zeilen total: {total_lines:,}")
print(f" Zugriffe: {total_parsed:,}")
print(f" Fehlerlog: {total_err:,}")