fix(archive): Error-Logs ohne Zeitzone wurden STILL verworfen + Backfill-Skript
BEFUND (echter Datenverlust, gemessen gegen KeyHelp-Formate):
KeyHelp/Debian schreiben error.log im Access-Format, aber OHNE Zeitzone:
[01/Sep/2026:10:00:05] [core:error] [pid 9] AH00124: ...
TS_FORMATS kannte nur "%d/%b/%Y:%H:%M:%S %z" (MIT Zone). parse_ts() gab None,
hour wurde "unknown", und der Schreiber ueberspringt unknown-Zeilen:
apache_archive_errors_total fehlte KOMPLETT — kein Fehler, kein Log, Panel leer.
Genau die stille Fehlerklasse, die nicht passieren darf.
FIX:
- Format "%d/%b/%Y:%H:%M:%S" (ohne Zone) ergaenzt.
- Unparsbare Zeitstempel werden GEZAEHLT und laut gemeldet (Domain + Beispiel +
Hinweis auf TS_FORMATS) statt still zu verschwinden.
- Neu: scripts/x22-log-backfill.sh — messen (read-only) und parsen in einem
Aufruf, mit Selbstpruefung (Einheiten, Zukunft, Domain-Anzahl) VOR der
Uebergabe und dem sicheren Import-Befehl am Ende.
Verifiziert im Container gegen echte KeyHelp-Formate inkl. rotierter .1 und
.gz-Datei: Messmodus aendert nichts (MD5-Vergleich), Backfill erzeugt alle
fuenf Metriken inkl. errors_total{level=error} und {level=warn}.
This commit is contained in:
1 parent
7d3ce17c53
commit
194fa98f97
2 files changed
+186
-4
No files matched your search
@@ -65,7 +65,22 @@ RE_LINE = re.compile(
|
||||
)
|
||||
RE_ERR = re.compile(r'^\[(?P<ts>[^\]]+)\] \[(?P<mod>[^\]:]+):(?P<lvl>\w+)\]')
|
||||
|
||||
TS_FORMATS = ["%d/%b/%Y:%H:%M:%S %z", "%a %b %d %H:%M:%S.%f %Y", "%a %b %d %H:%M:%S %Y"]
|
||||
TS_FORMATS = [
|
||||
# access.log (Apache Access, MIT Zeitzone)
|
||||
"%d/%b/%Y:%H:%M:%S %z",
|
||||
# ERROR-LOGS OHNE ZEITZONE — der Apache-Standard in error.log:
|
||||
# [Mon Sep 14 00:10:01.123456 2026] [core:error] ...
|
||||
# [Mon Sep 14 00:10:01 2026] [php:warn] ...
|
||||
"%a %b %d %H:%M:%S.%f %Y",
|
||||
"%a %b %d %H:%M:%S %Y",
|
||||
# KeyHelp/Debian schreiben error.log AUCH im Access-Format, aber OHNE
|
||||
# Zeitzone: [01/Sep/2026:10:00:05] [core:error] ...
|
||||
# Fehlte hier bisher -> parse_ts() gab None -> hour="unknown" -> die
|
||||
# Error-Metrik wurde STILL verworfen (kein Fehler, kein Log, das Panel
|
||||
# blieb leer). Real gemessen: error.log-Zeilen landeten nur in
|
||||
# log_lines_total, apache_archive_errors_total fehlte komplett.
|
||||
"%d/%b/%Y:%H:%M:%S",
|
||||
]
|
||||
|
||||
# Metriknamen, die das Skript erzeugt — auch fuer den Import-Loader relevant.
|
||||
METRICS = [
|
||||
@@ -112,6 +127,9 @@ def collect(patterns):
|
||||
clients = defaultdict(set) # (hour, domain) -> {ip}
|
||||
errors = defaultdict(int) # (hour, domain, level) -> count
|
||||
lines = defaultdict(int) # (domain, kind) -> count
|
||||
# Zeitstempel, die KEIN Format getroffen haben. Frueher still verworfen —
|
||||
# dadurch fehlte die Error-Metrik komplett, ohne Fehlermeldung.
|
||||
unparsed_ts = defaultdict(int) # (domain, roh) -> count
|
||||
|
||||
for path in files:
|
||||
domain = os.path.basename(os.path.dirname(path)) or os.path.basename(path)
|
||||
@@ -146,10 +164,15 @@ def collect(patterns):
|
||||
lines[(domain, "error")] += 1
|
||||
ts = parse_ts(m.group("ts"))
|
||||
hour = ts.strftime("%Y-%m-%dT%H:00:00Z") if ts else "unknown"
|
||||
errors[(hour, domain, m.group("lvl").lower())] += 1
|
||||
if ts:
|
||||
errors[(hour, domain, m.group("lvl").lower())] += 1
|
||||
else:
|
||||
# NICHT still verwerfen: ein unparsbarer Zeitstempel hat
|
||||
# vorher die ganze Error-Metrik gekostet, ohne jede Spur.
|
||||
unparsed_ts[(domain, m.group("ts")[:40])] += 1
|
||||
continue
|
||||
lines[(domain, "unparsed")] += 1
|
||||
return files, requests, bytes_by_domain, bytes_by_hour, clients, errors, lines
|
||||
return files, requests, bytes_by_domain, bytes_by_hour, clients, errors, lines, unparsed_ts
|
||||
|
||||
|
||||
def write_openmetrics(path, node, requests, bytes_by_domain, bytes_by_hour, clients, errors, lines):
|
||||
@@ -258,7 +281,8 @@ def main():
|
||||
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
print(f"[1/5] Logs einlesen …")
|
||||
files, requests, bytes_by_domain, bytes_by_hour, clients, errors, lines = collect(PATTERNS)
|
||||
(files, requests, bytes_by_domain, bytes_by_hour,
|
||||
clients, errors, lines, unparsed_ts) = collect(PATTERNS)
|
||||
if not files:
|
||||
print("Keine Logdateien gefunden. Muster:", ", ".join(PATTERNS[:4]), file=sys.stderr)
|
||||
return 1
|
||||
@@ -266,6 +290,17 @@ def main():
|
||||
print(f" {len(files)} Dateien, {total_lines:,} Zeilen")
|
||||
print(f" {sum(requests.values()):,} Zugriffe, {sum(errors.values()):,} Fehlereintraege")
|
||||
|
||||
# Unparsbare Zeitstempel LAUT melden. Sie kosteten frueher die ganze
|
||||
# Error-Metrik, ohne dass irgendwo etwas stand.
|
||||
if unparsed_ts:
|
||||
n = sum(unparsed_ts.values())
|
||||
print(f" WARNUNG: {n:,} Eintraege mit unlesbarem Zeitstempel — "
|
||||
f"diese fehlen in den Zeitreihen (nicht in den Rohdaten).", file=sys.stderr)
|
||||
for (dom, raw), cnt in sorted(unparsed_ts.items(), key=lambda x: -x[1])[:5]:
|
||||
print(f" {cnt:>6}x domain={dom} Beispiel: {raw}", file=sys.stderr)
|
||||
print(" Ein fehlendes Format in TS_FORMATS ergaenzen, dann erneut laufen.",
|
||||
file=sys.stderr)
|
||||
|
||||
print(f"[2/5] OpenMetrics schreiben …")
|
||||
om = os.path.join(args.out, "apache-archive.openmetrics")
|
||||
n = write_openmetrics(om, args.node, requests, bytes_by_domain, bytes_by_hour,
|
||||
|
||||
Reference in new issue
Block a user