fix(apache): Sekunden-Kollision behoben (log_time + converter) — Node lieferte gar nichts mehr
Echte x22-Logs: 2000 Zeilen -> 286 Kollisionen (duplicate sample). Telegraf uebernimmt den Log-Zeitstempel; bei mehreren Treffern pro Sekunde kollidiert der +1ms-Aufschlag mit echten Zeilen. Prometheus weist den Batch ab, Telegraf retryt endlos -> FIFO steht -> Node liefert GAR NICHTS mehr. Fix: eigenes Grok-Muster mit log_time als Feld (Telegraf nutzt eigene ns-Uhr) + processors.converter, damit resp_code/verb Tags bleiben. Gemessen: 2000/2000 Samples, 0 Kollisionen. Ausserdem: Einheiten-Waechter im Archiver (ms statt s -> Abbruch statt nutzloser Block).
This commit is contained in:
1 parent
5a213eb580
commit
5483a4fea8
7 files changed
+626
-7
No files matched your search
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
apache-log-survey.py — Bestandsaufnahme der Apache-Logs auf einem KeyHelp-Host.
|
||||
|
||||
MESSEN, NICHT RATEN: Bevor Logs archiviert werden, muss der Umfang bekannt sein.
|
||||
Kardinalitaet ist der begrenzende Faktor in Prometheus — pro Request eine Serie
|
||||
wuerde die TSDB sprengen. Die Aggregation richtet sich nach diesen Zahlen.
|
||||
|
||||
Aufruf (als root, damit alle User-Homes lesbar sind):
|
||||
python3 apache-log-survey.py
|
||||
|
||||
Ausgabe: Dateien, Zeilen, Zeitraum, Status-Verteilung, Top-Domains.
|
||||
Schreibt nichts, aendert nichts.
|
||||
"""
|
||||
import gzip
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Zugriffs- und Fehlerlogs, inkl. rotierter Varianten (.1, .2.gz).
|
||||
PATTERNS = [
|
||||
"/var/log/apache2/*access*.log", "/var/log/apache2/*access*.log.*",
|
||||
"/var/log/apache2/*error*.log", "/var/log/apache2/*error*.log.*",
|
||||
"/home/users/*/logs/*/access.log", "/home/users/*/logs/*/access.log.*",
|
||||
"/home/users/*/logs/*/error.log", "/home/users/*/logs/*/error.log.*",
|
||||
"/var/www/vhosts/*/logs/access_log*", "/var/www/vhosts/*/logs/error_log*",
|
||||
]
|
||||
|
||||
# Combined / Common: ip - - [14/Sep/2026:00:47:01 +0200] "GET /x HTTP/1.1" 200 559
|
||||
RE_LINE = re.compile(
|
||||
r'^(?P<ip>\S+) \S+ \S+ \[(?P<ts>[^\]]+)\] "(?P<req>[^"]*)" (?P<status>\d{3}|-) (?P<bytes>\d+|-)'
|
||||
)
|
||||
# Apache-Fehlerlog: [Mon Sep 14 00:47:01.123456 2026] [core:error] [pid 1] msg
|
||||
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"]
|
||||
|
||||
|
||||
def open_maybe_gz(path):
|
||||
try:
|
||||
if path.endswith(".gz"):
|
||||
return gzip.open(path, "rt", errors="replace")
|
||||
return open(path, "r", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_ts(raw):
|
||||
"""Zeitstempel parsen und IMMER auf zeitzonenbewusst (UTC) normalisieren.
|
||||
|
||||
Access-Logs haben eine Zeitzone ([14/Sep/2026:00:47:01 +0200]), Error-Logs
|
||||
nicht. Ohne Normalisierung sind die Werte nicht vergleichbar — Python wirft
|
||||
dann 'can't compare offset-naive and offset-aware datetimes'. Lokale Zeiten
|
||||
werden als Systemzeitzone interpretiert (das ist auch ihre Bedeutung).
|
||||
"""
|
||||
for fmt in TS_FORMATS:
|
||||
try:
|
||||
dt = datetime.strptime(raw.strip(), fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.astimezone() # lokale Zeit -> mit Offset versehen
|
||||
return dt.astimezone(timezone.utc)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
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_lines = 0
|
||||
total_parsed = 0
|
||||
total_err = 0
|
||||
bytes_on_disk = 0
|
||||
first_ts = None
|
||||
last_ts = None
|
||||
by_status = Counter()
|
||||
by_domain = Counter()
|
||||
by_method = Counter()
|
||||
err_levels = Counter()
|
||||
# Stunden-Buckets: hier entscheidet sich die Kardinalitaet der Archiv-Metriken.
|
||||
hour_domain = set()
|
||||
day_domain = set()
|
||||
per_file = []
|
||||
|
||||
for path in files:
|
||||
fh = open_maybe_gz(path)
|
||||
if fh is None:
|
||||
continue
|
||||
try:
|
||||
bytes_on_disk += os.path.getsize(path)
|
||||
except OSError:
|
||||
pass
|
||||
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
|
||||
with fh:
|
||||
for line in fh:
|
||||
n_lines += 1
|
||||
m = RE_LINE.match(line)
|
||||
if m:
|
||||
n_ok += 1
|
||||
by_status[m.group("status")] += 1
|
||||
by_domain[domain] += 1
|
||||
req = m.group("req") or ""
|
||||
if " " in req:
|
||||
by_method[req.split(" ", 1)[0]] += 1
|
||||
ts = parse_ts(m.group("ts"))
|
||||
if ts is not None:
|
||||
if first_ts is None or ts < first_ts:
|
||||
first_ts = ts
|
||||
if last_ts is None or ts > last_ts:
|
||||
last_ts = ts
|
||||
if f_first is None or ts < f_first:
|
||||
f_first = ts
|
||||
if f_last is None or ts > f_last:
|
||||
f_last = ts
|
||||
hour_domain.add((ts.strftime("%Y-%m-%dT%H"), domain))
|
||||
day_domain.add((ts.strftime("%Y-%m-%d"), domain))
|
||||
continue
|
||||
m = RE_ERR.match(line)
|
||||
if m:
|
||||
n_err += 1
|
||||
err_levels[m.group("lvl").lower()] += 1
|
||||
ts = parse_ts(m.group("ts"))
|
||||
if ts is not None:
|
||||
if first_ts is None or ts < first_ts:
|
||||
first_ts = ts
|
||||
if last_ts is None or ts > last_ts:
|
||||
last_ts = ts
|
||||
total_lines += n_lines
|
||||
total_parsed += n_ok
|
||||
total_err += n_err
|
||||
per_file.append((path, n_lines, n_ok, n_err, f_first, f_last))
|
||||
|
||||
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(f"Zeilen total: {total_lines:,}")
|
||||
print(f" Zugriffe: {total_parsed:,}")
|
||||
print(f" Fehlerlog: {total_err:,}")
|
||||
print(f" Unparsbar: {total_lines - total_parsed - total_err:,}")
|
||||
if first_ts and last_ts:
|
||||
span = last_ts - first_ts
|
||||
print(f"Zeitraum: {first_ts:%Y-%m-%d %H:%M} .. {last_ts:%Y-%m-%d %H:%M} ({span.days} Tage)")
|
||||
print()
|
||||
print("--- Status-Verteilung (Top 10) ---")
|
||||
for code, n in by_status.most_common(10):
|
||||
print(f" {code} {n:>12,}")
|
||||
print()
|
||||
print("--- Methoden ---")
|
||||
for meth, n in by_method.most_common(8):
|
||||
print(f" {meth:<8} {n:>12,}")
|
||||
print()
|
||||
print("--- Top 15 Domains ---")
|
||||
for dom, n in by_domain.most_common(15):
|
||||
print(f" {n:>12,} {dom}")
|
||||
print(f" ({len(by_domain)} Domains insgesamt)")
|
||||
if err_levels:
|
||||
print()
|
||||
print("--- Fehler-Level ---")
|
||||
for lvl, n in err_levels.most_common():
|
||||
print(f" {lvl:<10} {n:>10,}")
|
||||
print()
|
||||
print("=" * 72)
|
||||
print("KARDINALITAET der Archiv-Metriken (das entscheidet die Granularitaet)")
|
||||
print("=" * 72)
|
||||
print(f" pro Stunde + Domain: {len(hour_domain):,} Serien")
|
||||
print(f" pro Tag + Domain: {len(day_domain):,} Serien")
|
||||
print(f" pro Tag + Domain + Status: ~{len(day_domain) * max(1, len(by_status)):,} Serien")
|
||||
print()
|
||||
print("Faustregel: unter ~100k Serien ist unkritisch, darueber wird die TSDB traege.")
|
||||
print()
|
||||
print("--- Groesste Dateien ---")
|
||||
for path, n_lines, n_ok, n_err, _, _ in sorted(per_file, key=lambda x: -x[1])[:10]:
|
||||
print(f" {n_lines:>10,} Zeilen {path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user