from flask import Blueprint, render_template, jsonify, request, session from functools import wraps import psycopg2 from psycopg2.extras import RealDictCursor import os import requests from datetime import datetime, timedelta import logging from utils.partition_helper import ensure_partition_exists, check_table_exists monitoring_bp = Blueprint('monitoring', __name__) logger = logging.getLogger(__name__) # Database connection def get_db_connection(): return psycopg2.connect( host=os.environ.get('POSTGRES_HOST', 'postgres'), database=os.environ.get('POSTGRES_DB', 'v2_adminpanel'), user=os.environ.get('POSTGRES_USER', 'postgres'), password=os.environ.get('POSTGRES_PASSWORD', 'postgres') ) # Login required decorator def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'logged_in' not in session: return render_template('error.html', error_message='Nicht autorisiert', details='Sie müssen angemeldet sein, um diese Seite zu sehen.') return f(*args, **kwargs) return decorated_function @monitoring_bp.route('/live-dashboard') @login_required def live_dashboard(): """Live Dashboard showing active customer sessions and analytics""" try: conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) # Check if license_heartbeats table exists if not check_table_exists(conn, 'license_heartbeats'): logger.warning("license_heartbeats table does not exist") # Return empty data return render_template('monitoring/live_dashboard.html', active_sessions=[], stats={'active_licenses': 0, 'active_devices': 0, 'total_heartbeats': 0}, validation_timeline=[], live_stats=[0, 0, 0, 0], validation_rates=[], recent_anomalies=[], top_licenses=[]) # Ensure current month partition exists ensure_partition_exists(conn, 'license_heartbeats', datetime.now()) # Get active customer sessions (last 5 minutes) cur.execute(""" SELECT l.id, l.license_key, c.name as company_name, lh.hardware_id, lh.ip_address, lh.timestamp as last_activity, lh.session_data, COUNT(DISTINCT lh.hardware_id) OVER (PARTITION BY l.id) as active_devices FROM license_heartbeats lh JOIN licenses l ON l.id = lh.license_id JOIN customers c ON c.id = l.customer_id WHERE lh.timestamp > NOW() - INTERVAL '5 minutes' AND l.is_active = true ORDER BY lh.timestamp DESC LIMIT 100 """) active_sessions = cur.fetchall() # Get session statistics cur.execute(""" SELECT COUNT(DISTINCT license_id) as active_licenses, COUNT(DISTINCT hardware_id) as active_devices, COUNT(*) as total_heartbeats FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '5 minutes' """) stats = cur.fetchone() # Get validations per minute (for both charts) cur.execute(""" SELECT DATE_TRUNC('minute', timestamp) as minute, COUNT(*) as validations FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '60 minutes' GROUP BY minute ORDER BY minute DESC LIMIT 60 """) validation_timeline = cur.fetchall() # Get live statistics for analytics cards cur.execute(""" SELECT COUNT(DISTINCT license_id) as active_licenses, COUNT(*) as total_validations, COUNT(DISTINCT hardware_id) as unique_devices, COUNT(DISTINCT ip_address) as unique_ips FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '5 minutes' """) live_stats_data = cur.fetchone() live_stats = [ live_stats_data['active_licenses'] or 0, live_stats_data['total_validations'] or 0, live_stats_data['unique_devices'] or 0, live_stats_data['unique_ips'] or 0 ] # Get validation rates for analytics chart (last 30 minutes) cur.execute(""" SELECT DATE_TRUNC('minute', timestamp) as minute, COUNT(*) as count FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '30 minutes' GROUP BY minute ORDER BY minute DESC LIMIT 30 """) validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()] # Get recent anomalies cur.execute(""" SELECT ad.*, l.license_key, c.name as customer_name FROM anomaly_detections ad LEFT JOIN licenses l ON l.id = ad.license_id LEFT JOIN customers c ON c.id = l.customer_id WHERE ad.detected_at > NOW() - INTERVAL '24 hours' ORDER BY ad.detected_at DESC LIMIT 10 """) recent_anomalies = cur.fetchall() # Get top active licenses cur.execute(""" SELECT l.license_key, c.name as customer_name, COUNT(DISTINCT lh.hardware_id) as device_count, COUNT(*) as validation_count, MAX(lh.timestamp) as last_seen FROM license_heartbeats lh JOIN licenses l ON l.id = lh.license_id JOIN customers c ON c.id = l.customer_id WHERE lh.timestamp > NOW() - INTERVAL '15 minutes' GROUP BY l.license_key, c.name ORDER BY validation_count DESC LIMIT 10 """) top_licenses = cur.fetchall() cur.close() conn.close() return render_template('monitoring/live_dashboard.html', active_sessions=active_sessions, stats=stats, validation_timeline=validation_timeline, live_stats=live_stats, validation_rates=validation_rates, recent_anomalies=recent_anomalies, top_licenses=top_licenses) except Exception as e: logger.error(f"Error in live dashboard: {str(e)}") return render_template('error.html', error_message='Fehler beim Laden des Dashboards', details=str(e)) @monitoring_bp.route('/system-status') @login_required def system_status(): """System status showing service health""" services = [] # Check each service service_checks = [ {'name': 'License Server', 'url': 'http://license-server:8443/health', 'port': 8443}, {'name': 'PostgreSQL', 'check': 'database'}, ] for service in service_checks: status = {'name': service['name'], 'status': 'unknown', 'response_time': None} try: if service.get('check') == 'database': # Check database start = datetime.now() conn = get_db_connection() conn.close() status['status'] = 'healthy' status['response_time'] = (datetime.now() - start).total_seconds() * 1000 elif service.get('url'): # Check HTTP service start = datetime.now() response = requests.get(service['url'], timeout=2) if response.status_code == 200: status['status'] = 'healthy' else: status['status'] = 'unhealthy' status['response_time'] = (datetime.now() - start).total_seconds() * 1000 except Exception as e: status['status'] = 'down' logging.error(f"Health check failed for {service['name']}: {str(e)}") services.append(status) # Get Prometheus metrics if available prometheus_data = None try: response = requests.get('http://prometheus:9090/api/v1/query', params={'query': 'up'}, timeout=2) if response.status_code == 200: prometheus_data = response.json() except: pass return render_template('monitoring/system_status.html', services=services, prometheus_data=prometheus_data) @monitoring_bp.route('/alerts') @login_required def alerts(): """Show active alerts from Alertmanager""" alerts = [] try: # Get alerts from Alertmanager response = requests.get('http://alertmanager:9093/api/v1/alerts', timeout=2) if response.status_code == 200: alerts = response.json() except: # Fallback to database anomalies conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT ad.*, l.license_key, c.name as company_name FROM anomaly_detections ad LEFT JOIN licenses l ON l.id = ad.license_id LEFT JOIN customers c ON c.id = l.customer_id WHERE ad.resolved = false ORDER BY ad.detected_at DESC LIMIT 50 """) alerts = cur.fetchall() cur.close() conn.close() return render_template('monitoring/alerts.html', alerts=alerts) @monitoring_bp.route('/analytics') @login_required def analytics(): """Combined analytics and license server status page""" try: conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) # Get live statistics for the top cards cur.execute(""" SELECT COUNT(DISTINCT license_id) as active_licenses, COUNT(*) as total_validations, COUNT(DISTINCT hardware_id) as unique_devices, COUNT(DISTINCT ip_address) as unique_ips FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '5 minutes' """) live_stats = cur.fetchone() live_stats = [ live_stats['active_licenses'] or 0, live_stats['total_validations'] or 0, live_stats['unique_devices'] or 0, live_stats['unique_ips'] or 0 ] # Get validation rates for chart cur.execute(""" SELECT DATE_TRUNC('minute', timestamp) as minute, COUNT(*) as count FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '30 minutes' GROUP BY minute ORDER BY minute DESC LIMIT 30 """) validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()] # Get recent anomalies cur.execute(""" SELECT ad.*, l.license_key, c.name as customer_name FROM anomaly_detections ad LEFT JOIN licenses l ON l.id = ad.license_id LEFT JOIN customers c ON c.id = l.customer_id WHERE ad.detected_at > NOW() - INTERVAL '24 hours' ORDER BY ad.detected_at DESC LIMIT 10 """) recent_anomalies = cur.fetchall() # Get top active licenses cur.execute(""" SELECT l.license_key, c.name as customer_name, COUNT(DISTINCT lh.hardware_id) as device_count, COUNT(*) as validation_count, MAX(lh.timestamp) as last_seen FROM license_heartbeats lh JOIN licenses l ON l.id = lh.license_id JOIN customers c ON c.id = l.customer_id WHERE lh.timestamp > NOW() - INTERVAL '15 minutes' GROUP BY l.license_key, c.name ORDER BY validation_count DESC LIMIT 10 """) top_licenses = cur.fetchall() cur.close() conn.close() return render_template('monitoring/analytics.html', live_stats=live_stats, validation_rates=validation_rates, recent_anomalies=recent_anomalies, top_licenses=top_licenses) except Exception as e: logger.error(f"Error in analytics: {str(e)}") return render_template('monitoring/analytics.html', live_stats=[0, 0, 0, 0], validation_rates=[], recent_anomalies=[], top_licenses=[]) # API endpoints for live data @monitoring_bp.route('/api/live-stats') @login_required def api_live_stats(): """API endpoint for live statistics""" try: conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) # Get current stats cur.execute(""" SELECT COUNT(DISTINCT license_id) as active_licenses, COUNT(DISTINCT hardware_id) as active_devices, COUNT(*) as validations_last_minute FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '1 minute' """) stats = cur.fetchone() cur.close() conn.close() return jsonify(stats) except Exception as e: return jsonify({'error': str(e)}), 500 @monitoring_bp.route('/api/active-sessions') @login_required def api_active_sessions(): """API endpoint for active customer sessions""" try: conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) # Get active sessions with geo data cur.execute(""" SELECT l.license_key, c.name as company_name, lh.hardware_id, lh.ip_address, lh.timestamp as last_activity, EXTRACT(EPOCH FROM (NOW() - lh.timestamp)) as seconds_ago, lh.session_data FROM license_heartbeats lh JOIN licenses l ON l.id = lh.license_id JOIN customers c ON c.id = l.customer_id WHERE lh.timestamp > NOW() - INTERVAL '5 minutes' ORDER BY lh.timestamp DESC LIMIT 50 """) sessions = cur.fetchall() cur.close() conn.close() return jsonify(sessions) except Exception as e: return jsonify({'error': str(e)}), 500