diff --git a/.gitignore b/.gitignore index b290f55..a5f8631 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .venv/ __pycache__/ *.py[cod] +# Logs +logs/ *.log .env .DS_Store diff --git a/LOGGING.md b/LOGGING.md new file mode 100644 index 0000000..832a178 --- /dev/null +++ b/LOGGING.md @@ -0,0 +1,75 @@ +# Logging-System + +Das Wordle-Helper Logging-System protokolliert Seitenaufrufe und Suchanfragen ohne personenbezogene Daten wie IP-Adressen. + +## Log-Datei + +- **Verzeichnis:** `logs/` +- **Datei:** `logs/app.log` +- **Format:** UTF-8 +- **Rotation:** Keine automatische Rotation (manuell oder über externe Tools) +- **Verzeichnis:** Wird automatisch erstellt, falls es nicht existiert + +## Protokollierte Ereignisse + +### 1. Seitenaufrufe (PAGE_VIEW) + +``` +2024-01-01 12:00:00 - INFO - PAGE_VIEW: index | User-Agent: Mozilla/5.0... +2024-01-01 12:00:01 - INFO - PAGE_VIEW: manifest | User-Agent: Mozilla/5.0... +2024-01-01 12:00:02 - INFO - PAGE_VIEW: service_worker | User-Agent: Mozilla/5.0... +``` + +### 2. Suchanfragen (SEARCH) + +``` +2024-01-01 12:00:05 - INFO - SEARCH: pos='hallo' includes='' excludes='' sources=['OT'] | User-Agent: Mozilla/5.0... +2024-01-01 12:00:10 - INFO - SEARCH: pos='' includes='aei' excludes='rst' sources=['OT', 'WF'] | User-Agent: Mozilla/5.0... +``` + +## Log-Format + +- **Zeitstempel:** ISO-Format (YYYY-MM-DD HH:MM:SS) +- **Ereignistyp:** PAGE_VIEW oder SEARCH +- **Details:** Spezifische Informationen je nach Ereignistyp +- **User-Agent:** Gekürzt auf 100 Zeichen (ohne IP-Adressen) + +## Datenschutz + +- **Keine IP-Adressen** werden protokolliert +- **Keine persönlichen Daten** werden gespeichert +- **User-Agent** wird gekürzt und anonymisiert +- **Logs** werden nicht ins Git-Repository übertragen (`.gitignore`) + +## Konfiguration + +Das Logging ist in `app.py` konfiguriert: + +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('app.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +``` + +## Log-Analyse + +Die Log-Datei kann mit Standard-Tools analysiert werden: + +```bash +# Alle Suchanfragen anzeigen +grep "SEARCH:" logs/app.log + +# Seitenaufrufe zählen +grep "PAGE_VIEW:" logs/app.log | wc -l + +# Häufigste Suchparameter +grep "SEARCH:" logs/app.log | cut -d'|' -f1 + +# Logs-Verzeichnis anzeigen +ls -la logs/ +``` diff --git a/app.py b/app.py index b8b1de1..718b49c 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,52 @@ from pathlib import Path import json +import logging +from datetime import datetime from typing import Tuple, Dict, List from flask import Flask, render_template, request, send_from_directory app = Flask(__name__) +# Logging konfigurieren +import os + +# Logs-Verzeichnis erstellen, falls es nicht existiert +logs_dir = Path(__file__).parent / "logs" +logs_dir.mkdir(exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(logs_dir / 'app.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +def log_page_view(page: str, user_agent: str = None): + """Protokolliert Seitenaufrufe ohne IP-Adressen""" + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + user_agent_clean = user_agent[:100] if user_agent else 'Unknown' + logger.info(f"PAGE_VIEW: {page} | User-Agent: {user_agent_clean}") + +def log_search_query(search_params: dict, user_agent: str = None): + """Protokolliert Suchanfragen ohne IP-Adressen""" + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + user_agent_clean = user_agent[:100] if user_agent else 'Unknown' + + # Suchparameter für Logging vorbereiten + pos_str = ''.join(search_params.get('pos', [''] * 5)) + includes = search_params.get('includes', '') + excludes = search_params.get('excludes', '') + sources = [] + if search_params.get('use_ot'): + sources.append('OT') + if search_params.get('use_wf'): + sources.append('WF') + + logger.info(f"SEARCH: pos='{pos_str}' includes='{includes}' excludes='{excludes}' sources={sources} | User-Agent: {user_agent_clean}") + def load_words() -> Tuple[List[str], Dict[str, List[str]]]: data_dir = Path(__file__).parent / "data" @@ -50,6 +92,9 @@ def filter_words(words: List[str], position_letters: List[str], includes_text: s @app.route("/", methods=["GET", "POST"]) def index(): + # Seitenaufruf protokollieren + log_page_view("index", request.headers.get('User-Agent')) + all_words, sources_map = load_words() results_display: List[str] | None = None pos: List[str] = ["", "", "", "", ""] @@ -70,6 +115,16 @@ def index(): use_ot = request.form.get("use_ot") is not None use_wf = request.form.get("use_wf") is not None + # Suchanfrage protokollieren + search_params = { + 'pos': pos, + 'includes': includes, + 'excludes': excludes, + 'use_ot': use_ot, + 'use_wf': use_wf + } + log_search_query(search_params, request.headers.get('User-Agent')) + # 1) Buchstaben-/Positionssuche über alle Wörter matched = filter_words(all_words, pos, includes, excludes) # 2) Quellen-Filter nur auf Ergebnisansicht anwenden @@ -99,12 +154,14 @@ def index(): @app.route('/manifest.webmanifest') def manifest_file(): + log_page_view("manifest", request.headers.get('User-Agent')) return send_from_directory(Path(__file__).parent / 'static', 'manifest.webmanifest', mimetype='application/manifest+json') @app.route('/sw.js') def service_worker(): # Service Worker muss auf Top-Level liegen + log_page_view("service_worker", request.headers.get('User-Agent')) return send_from_directory(Path(__file__).parent / 'static', 'sw.js', mimetype='application/javascript')