Files
iwz/lib/analyze.py
T
Michael Stangl 2379f4ac9f @
Skripte nach bin/ und lib/ umstrukturiert + WSL-Log-Analyse

Alle .bat-Skripte liegen jetzt in bin/, alle .py in lib/, .env(.example)
in bin/. setenv.bat setzt IWZ_* (inkl. IWZ_ENV) und PYTHONPATH; die
Launcher rufen setenv, aktivieren die .venv falls vorhanden und starten
das zugehoerige lib/-Skript. Die Python-Skripte lesen die .env ueber
IWZ_ENV (Fallback bin/.env).

Neu: Nutzungsanalyse per GoAccess (analyze.py/.bat) - laedt die
IONOS-Access-Logs per SFTP (paramiko) und erzeugt einen HTML-Report.
GoAccess laeuft ueber WSL (GOACCESS_MODE=wsl, Pfad-Uebersetzung nach
/mnt/c). install_py.bat prueft WSL+GoAccess und zeigt sonst den
Installationsweg. Doku in doc/analyse.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:44:56 +02:00

227 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""
IWZ Ferienwohnungen - Nutzungsanalyse
Laedt die Apache-Access-Logs per SFTP von IONOS herunter, entpackt die
rotierten .gz-Dateien und erzeugt mit GoAccess einen HTML-Report.
Kein JavaScript-Tracking, keine Cookies beim Besucher -> DSGVO-freundlich.
Die Auswertung passiert rein aus den Server-Logs.
Voraussetzungen:
- pip install -r requirements.txt (paramiko)
- GoAccess Windows-Build (Pfad in .env: GOACCESS_EXE)
- .env mit IONOS_SFTP_* befuellt (siehe .env.example)
Aufruf:
python analyze.py # Logs laden + Report bauen + oeffnen
python analyze.py --no-download # nur neu auswerten (schon geladene Logs)
"""
import gzip
import os
import shutil
import stat
import subprocess
import sys
import webbrowser
from datetime import date
from pathlib import Path
try:
import paramiko
except ImportError:
print("FEHLER: paramiko fehlt. Bitte 'pip install -r requirements.txt' ausfuehren.")
sys.exit(1)
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
LOGS_DIR = PROJECT_ROOT / "logs" # heruntergeladene Rohlogs (in .gitignore)
RESULTS_DIR = PROJECT_ROOT / "results" # erzeugte Reports (in .gitignore)
SFTP_PORT = 22
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
# Nur diese Dateien aus dem IONOS /logs-Ordner interessieren uns.
# Alles andere (sftp.log, traffic.db, forensic/, info.*) wird ignoriert.
ACCESS_LOG_PREFIX = "access.log"
def load_env(path):
env = {}
if not path.exists():
return env
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
env[key.strip()] = value.strip()
return env
def download_access_logs(host, port, user, password, remote_dir):
"""Laedt alle access.log*-Dateien aus dem IONOS /logs-Ordner nach LOGS_DIR."""
print(f"\n-> Verbinde per SFTP mit {host}:{port} ...")
transport = paramiko.Transport((host, port))
try:
transport.connect(username=user, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.chdir(remote_dir)
LOGS_DIR.mkdir(exist_ok=True)
count = 0
for attr in sftp.listdir_attr("."):
name = attr.filename
if not name.startswith(ACCESS_LOG_PREFIX):
continue
if stat.S_ISDIR(attr.st_mode):
continue
local_path = LOGS_DIR / name
# Schon vorhanden und gleich gross? -> ueberspringen (rotierte Logs
# aendern sich nicht mehr; nur .current / aktueller Tag waechst).
if (local_path.exists()
and local_path.stat().st_size == attr.st_size
and not name.endswith(".current")):
continue
print(f" lade {name} ({attr.st_size} Bytes)")
sftp.get(name, str(local_path))
count += 1
sftp.close()
print(f" {count} Datei(en) geladen/aktualisiert.")
finally:
transport.close()
def collect_log_lines():
"""Fuehrt alle access.log*-Dateien (auch .gz) zu einem kombinierten Log zusammen.
Gibt den Pfad zur kombinierten Klartext-Logdatei zurueck.
"""
combined = LOGS_DIR / "_combined.log"
sources = sorted(
p for p in LOGS_DIR.iterdir()
if p.is_file()
and p.name.startswith(ACCESS_LOG_PREFIX)
and p.name != combined.name
)
if not sources:
print("\nFEHLER: keine access.log*-Dateien in ./logs/ gefunden.")
print("Erst 'python analyze.py' (ohne --no-download) ausfuehren.")
sys.exit(1)
print(f"\n-> Fuehre {len(sources)} Logdatei(en) zusammen ...")
with open(combined, "wb") as out:
for src in sources:
if src.suffix == ".gz":
with gzip.open(src, "rb") as f:
shutil.copyfileobj(f, out)
else:
with open(src, "rb") as f:
shutil.copyfileobj(f, out)
return combined
def to_wsl_path(win_path):
"""Uebersetzt einen Windows-Pfad (C:\\...) in einen WSL-Pfad (/mnt/c/...)."""
p = Path(win_path).resolve()
drive = p.drive.rstrip(":").lower() # "C:" -> "c"
rest = p.as_posix()[len(p.drive):] # Rest ohne Laufwerk, mit /
return f"/mnt/{drive}{rest}"
def goaccess_args(log_format):
"""Gemeinsame GoAccess-Optionen (ohne Ein-/Ausgabedateien)."""
return [
"--log-format=" + log_format,
"--anonymize-ip", # DSGVO: IPs im Report anonymisieren
"--ignore-crawlers", # Bots aus den Besucherzahlen filtern
"--html-report-title=IWZ Ferienwohnungen - Zugriffsstatistik",
]
def run_goaccess(mode, goaccess_exe, log_file, report_file, log_format):
"""Ruft GoAccess auf und erzeugt einen eigenstaendigen HTML-Report.
mode="wsl": GoAccess laeuft in der WSL (Standard), Pfade werden nach
/mnt/c/... uebersetzt.
mode="exe": nativer Windows-Build unter GOACCESS_EXE.
"""
print(f"\n-> GoAccess ({mode}) -> {report_file.name}")
if mode == "wsl":
cmd = [
"wsl", "goaccess",
to_wsl_path(log_file),
"-o", to_wsl_path(report_file),
] + goaccess_args(log_format)
not_found_hint = (
"GoAccess ist in der WSL nicht installiert (oder WSL fehlt).\n"
" In der WSL installieren: wsl -e sudo apt update ^&^& "
"sudo apt install -y goaccess\n"
"Siehe auch bin/install_py.bat (prueft WSL + GoAccess) und doc/analyse.md."
)
else:
cmd = [
goaccess_exe,
str(log_file),
"-o", str(report_file),
] + goaccess_args(log_format)
not_found_hint = (
f"GoAccess-EXE nicht gefunden ('{goaccess_exe}').\n"
" Windows-Build laden: https://goaccess.io/download, entpacken,\n"
" dann GOACCESS_EXE in bin/.env auf die goaccess.exe setzen."
)
try:
result = subprocess.run(cmd)
except FileNotFoundError:
print("\nFEHLER: GoAccess konnte nicht gestartet werden.")
print(not_found_hint)
sys.exit(1)
if result.returncode != 0:
print("\nFEHLER: GoAccess fehlgeschlagen.")
print("Pruefe die GoAccess-Installation und das Log-Format (LOG_FORMAT).")
sys.exit(1)
if __name__ == "__main__":
print("=== IWZ Nutzungsanalyse ===")
no_download = "--no-download" in sys.argv
env = {**load_env(env_file()), **os.environ}
host = env.get("IONOS_SFTP_HOST")
port = int(env.get("IONOS_SFTP_PORT", SFTP_PORT))
user = env.get("IONOS_SFTP_USER")
password = env.get("IONOS_SFTP_PASSWORD")
remote_dir = env.get("IONOS_LOGS_DIR", "/logs")
# GoAccess-Aufruf: "wsl" (Standard) oder "exe" (nativer Windows-Build).
goaccess_mode = env.get("GOACCESS_MODE", "wsl").strip().lower()
goaccess_exe = env.get("GOACCESS_EXE", "goaccess")
# IONOS liefert i.d.R. Apache "Combined Log Format". Bei anderen
# Formaten hier anpassen (siehe doc/analyse.md).
log_format = env.get("LOG_FORMAT", "COMBINED")
if not no_download:
if not host or not user or not password:
print("\nFEHLER: IONOS_SFTP_HOST / _USER / _PASSWORD fehlen in bin/.env.")
print("Siehe bin/.env.example. Oder mit --no-download nur neu auswerten.")
sys.exit(1)
download_access_logs(host, port, user, password, remote_dir)
else:
print("\n-> Download uebersprungen (--no-download).")
combined = collect_log_lines()
RESULTS_DIR.mkdir(exist_ok=True)
report_file = RESULTS_DIR / f"report_{date.today().isoformat()}.html"
run_goaccess(goaccess_mode, goaccess_exe, combined, report_file, log_format)
print(f"\nFertig! Report: {report_file}")
webbrowser.open(report_file.as_uri())