# Multi-Tenant Integration in Flask App Hier sind die konkreten Änderungen für `Web/app.py`, um Multi-Tenant Funktionalität zu aktivieren. ## Änderung 1: Imports hinzufügen **VORHER** (Zeile 1-60 in app.py): ```python from flask import Flask, render_template, request, ... from werkzeug.utils import secure_filename # ... weitere imports ``` **NACHHER** (Zusätzliche Imports): ```python # Multi-Tenant Imports from tenant import get_tenant_context, require_tenant, get_tenant_db from session_manager import create_redis_session_interface from query_cache import get_cache_manager, cached_query, invalidate_cache ``` --- ## Änderung 2: Redis Session Backend konfigurieren **NACH** `app = Flask(...)` (ca. Zeile 65): ```python app = Flask(__name__, static_folder='static') app.logger.setLevel(logging.WARNING) app.secret_key = cfg.SECRET_KEY # ========== MULTI-TENANT KONFIGURATION ========== # Aktiviere Redis Session Backend statt Filesystem if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis': try: app.session_interface = create_redis_session_interface(app) app.logger.info("Redis session backend enabled") except Exception as e: app.logger.warning(f"Redis session backend failed, using default: {e}") # ================================================ ``` --- ## Änderung 3: Health Check Endpoint hinzufügen **Neuer Route** (nach allen anderen Routes, vor `if __name__ == '__main__'`): ```python @app.route('/health') def health_check(): """ Health check endpoint für Nginx Load Balancer. Wird regelmäßig von Nginx aufgerufen (30s interval). Rückgabe: 200 OK wenn app bereit, sonst 503 """ try: # Check Database Connection from settings import MongoClient mongo = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) mongo.admin.command('ping') # Check Redis Connection (falls Redis Session aktiv) if os.getenv('INVENTAR_SESSION_BACKEND') == 'redis': cache_mgr = get_cache_manager() if cache_mgr and cache_mgr.redis: cache_mgr.redis.ping() return {'status': 'healthy'}, 200 except Exception as e: app.logger.error(f"Health check failed: {e}") return {'status': 'unhealthy', 'error': str(e)}, 503 ``` --- ## Änderung 4: Tenant Context in bestehende Database-Calls **WICHTIG**: Alle `MongoClient` Zugriffe müssen durch Tenant-Context gehen. ### Beispiel 1: Bestehender Code (VORHER) ```python # VORHER: Direkter DB-Zugriff @app.route('/items') def get_items(): client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] # ← PROBLEM: Alle Tenants teilen sich die gleiche DB items = db['items'].find().to_list(100) return jsonify(items) ``` ### Beispiel 1: Mit Tenant-Routing (NACHHER) ```python # NACHHER: Tenant-Isolierte DB @app.route('/items') @require_tenant # ← Decorator setzt Tenant Context def get_items(): client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) ctx = get_tenant_context() # Richtige Datenbank für diesen Tenant db = client[ctx.db_name] # z.B. "inventar_schule1" items = db['items'].find().to_list(100) return jsonify(items) ``` ### Oder kürzere Variante: ```python @app.route('/items') @require_tenant def get_items(): db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)) items = db['items'].find().to_list(100) return jsonify(items) ``` --- ## Änderung 5: Query Caching für häufige Abfragen **VORHER**: ```python def load_user_profile(user_id): db = client[cfg.MONGODB_DB] # Direkter DB-Zugriff bei jedem Request return db['users'].find_one({'_id': ObjectId(user_id)}) ``` **NACHHER**: ```python @cached_query(category='user', ttl=7*24*3600) # 7 Tage Cache def load_user_profile(user_id): db = get_tenant_db(MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)) return db['users'].find_one({'_id': ObjectId(user_id)}) ``` Nach Update muss Cache invalidiert werden: ```python def update_user_profile(user_id, updates): db = get_tenant_db(MongoClient(...)) db['users'].update_one({'_id': ObjectId(user_id)}, {'$set': updates}) # Cache invalidieren nach Write ctx = get_tenant_context() invalidate_cache(ctx.tenant_id, 'user') return True ``` --- ## Änderung 6: Debugging - Tenant-Info in Logs **Logging Helper** (am besten in einem bestehenden Logging-Block): ```python def log_with_tenant(level, message): """Helper um Tenant-ID in Logs zu erfassen.""" ctx = get_tenant_context() tenant_id = ctx.tenant_id if ctx else 'unknown' prefixed_msg = f"[{tenant_id}] {message}" if level == 'error': app.logger.error(prefixed_msg) elif level == 'warning': app.logger.warning(prefixed_msg) elif level == 'info': app.logger.info(prefixed_msg) else: app.logger.debug(prefixed_msg) # Nutzung: log_with_tenant('info', 'User login successful') # Output: "[schule1] User login successful" ``` --- ## Änderung 7: Test-Routes (optional, für Debugging) ```python @app.route('/debug/tenant') def debug_tenant_info(): """Debug-Endpoint: Zeigt aktuellen Tenant-Context.""" ctx = get_tenant_context() cache_mgr = get_cache_manager() return { 'tenant_id': ctx.tenant_id if ctx else None, 'db_name': ctx.db_name if ctx else None, 'subdomain': ctx.subdomain if ctx else None, 'cache_enabled': cache_mgr is not None, 'cache_stats': cache_mgr.get_stats(ctx.tenant_id) if ctx and cache_mgr else None, 'request_host': request.host, 'request_headers': dict(request.headers) } @app.route('/debug/cache/', methods=['POST']) def debug_cache_control(action): """Debug-Endpoint: Cache Kontrolle.""" ctx = get_tenant_context() cache_mgr = get_cache_manager() if action == 'clear': if cache_mgr: cache_mgr.invalidate_tenant(ctx.tenant_id) return {'status': 'Cache cleared for tenant', 'tenant': ctx.tenant_id} return {'status': 'unknown action'}, 400 ``` --- ## Änderung 8: Umgebungsvariablen (.env) ```bash # Multi-Tenant Konfiguration INVENTAR_MULTITENANT_ENABLED=true INVENTAR_SESSION_BACKEND=redis INVENTAR_REDIS_HOST=redis INVENTAR_REDIS_PORT=6379 INVENTAR_REDIS_DB=0 # Query Caching INVENTAR_QUERY_CACHE_ENABLED=true INVENTAR_CACHE_DB=1 # Logging (auf WARNING reduzieren) INVENTAR_LOG_LEVEL=WARNING # Performance INVENTAR_WORKERS=4 INVENTAR_WORKER_CLASS=gevent INVENTAR_WORKER_CONNECTIONS=100 ``` --- ## Migration-Strategie ### Phase 1: Compatibility Mode (Keine Breaking Changes) - ✓ Beide Mode laufen gleichzeitig (Single + Multi) - ✓ Alte Routes funktionieren ohne Änderung - ✓ Neue Routes können `@require_tenant` nutzen - ✓ Session-Fallback wenn Redis nicht verfügbar ### Phase 2: Graduelle Migration 1. Starten mit SINGLE Instance + Single Database ```bash docker-compose -f docker-compose-multitenant.yml up -d --scale app=1 ``` 2. Redis Session Backend aktivieren ```bash INVENTAR_SESSION_BACKEND=redis ``` 3. Einzelne Routes mit `@require_tenant` decorator markieren 4. Query Caching für häufige Abfragen hinzufügen 5. Testing mit Multi-Subdomain (test1.local, test2.local) 6. Full Multi-Tenant in Production ```bash docker-compose -f docker-compose-multitenant.yml up -d --scale app=5 ``` --- ## Performance-Vergleich ### Single Instance (Vorher) ``` 1 App Instance - Memory: 200MB - Startup: 8s - Max Users: ~50 (gleichzeitig) - DB Load: 100% - Sessions: Filesystem I/O ``` ### Multi-Instance (Nachher) ``` 3 App Instances - Memory: 3 × 100MB = 300MB (gesamt) - Startup: 3s pro Instance - Max Users: ~150 (gleichzeitig, 50 pro instance) - DB Load: 30% (durch Caching) - Sessions: Redis (keine I/O) ``` --- ## Checkliste für Deployment - [ ] Docker-compose-multitenant.yml durchgelesen - [ ] Tenant-Module (tenant.py, session_manager.py, query_cache.py) im Web/ Ordner - [ ] app.py mit Multi-Tenant Imports aktualisiert - [ ] Redis Session Backend aktiviert (INVENTAR_SESSION_BACKEND=redis) - [ ] Health Check Endpoint implementiert - [ ] Nginx multitenant.conf konfiguriert - [ ] SSL Wildcard Zertifikat erstellt - [ ] DNS Wildcard Record konfiguriert (*.example.com) - [ ] First Tenant als test registriert - [ ] Health Checks funktionieren: curl https://test.example.com/health - [ ] Cache Stats verfügbar: curl https://test.example.com/debug/tenant - [ ] Load Test mit 2-3 Tenants durchgeführt - [ ] Monitoring Setup (Docker Stats, Nginx Logs) --- ## Schul-Konfiguration pro Tenant Die Datei `config.json` unterstützt jetzt einen `tenants`-Block. Damit kann jede Schule eigene Modul-Schalter bekommen, ohne dass das ganze System global umgestellt werden muss. ```json { "tenants": { "schule1": { "modules": { "library": { "enabled": true }, "student_cards": { "enabled": false } } }, "schule2": { "modules": { "library": { "enabled": false } } } } } ``` Wenn ein Request über Subdomain oder `X-Tenant-ID` aufgelöst wird, liest die App diese Werte automatisch aus und blendet die Bibliothek bzw. andere Module nur für diesen Tenant ein oder aus. --- ## Support & Debugging **Fragen?** 1. Logs prüfen: `docker-compose -f docker-compose-multitenant.yml logs -f app` 2. Tenant-Info prüfen: `curl https://your-tenant.com/debug/tenant` 3. Cache Stats: `curl https://your-tenant.com/debug/cache-stats` 4. Redis Stats: `docker exec inventarsystem-redis redis-cli info stats` **Häufige Fehler:** - `X-Tenant-ID Header missing` → Nginx nutzt alte Konfiguration - `Redis connection refused` → Redis Container nicht gestartet - `Database not found` → Tenant nicht registriert (auto-create bei erstem Request) - `Out of memory` → Memory Limit zu niedrig oder zu viele Instanzen