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 os
import re import re
import sys import sys
import time
from collections import Counter, defaultdict from collections import Counter, defaultdict
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -68,11 +69,54 @@ def parse_ts(raw):
def main(): 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)}) files = sorted({f for pat in PATTERNS for f in glob.glob(pat)})
if not files: if not files:
print("Keine Apache-Logs gefunden. Laeuft Apache? (apache2ctl -v)") print("Keine Apache-Logs gefunden. Laeuft Apache? (apache2ctl -v)")
return 1 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_lines = 0
total_parsed = 0 total_parsed = 0
total_err = 0 total_err = 0
@@ -88,14 +132,17 @@ def main():
day_domain = set() day_domain = set()
per_file = [] per_file = []
for path in files: t0 = time.time()
for _idx, path in enumerate(files, 1):
fh = open_maybe_gz(path) fh = open_maybe_gz(path)
if fh is None: if fh is None:
print(f" [{_idx}/{len(files)}] NICHT LESBAR: {path}", flush=True)
continue continue
try: try:
bytes_on_disk += os.path.getsize(path) _sz = os.path.getsize(path)
bytes_on_disk += _sz
except OSError: except OSError:
pass _sz = 0
domain = os.path.basename(os.path.dirname(path)) or os.path.basename(path) domain = os.path.basename(os.path.dirname(path)) or os.path.basename(path)
n_lines = n_ok = n_err = 0 n_lines = n_ok = n_err = 0
f_first = f_last = None f_first = f_last = None
@@ -137,18 +184,14 @@ def main():
total_parsed += n_ok total_parsed += n_ok
total_err += n_err total_err += n_err
per_file.append((path, n_lines, n_ok, n_err, f_first, f_last)) 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): print()
for u in ("B", "KB", "MB", "GB"): print(f"Scan fertig in {time.time() - t0:.1f} s.")
if n < 1024: print()
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(f"Zeilen total: {total_lines:,}") print(f"Zeilen total: {total_lines:,}")
print(f" Zugriffe: {total_parsed:,}") print(f" Zugriffe: {total_parsed:,}")
print(f" Fehlerlog: {total_err:,}") print(f" Fehlerlog: {total_err:,}")