Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,465 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from datetime import datetime, timedelta
import os
import psycopg2
from psycopg2.extras import RealDictCursor
from psycopg2.pool import SimpleConnectionPool
import redis
import json
import logging
from functools import wraps
import jwt
from collections import defaultdict
import numpy as np
from prometheus_flask_exporter import PrometheusMetrics
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# Initialize Prometheus metrics
metrics = PrometheusMetrics(app)
metrics.info('analytics_service_info', 'Analytics Service Information', version='1.0.0')
# Configuration
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres@postgres:5432/v2_adminpanel')
REDIS_URL = os.environ.get('REDIS_URL', 'redis://redis:6379/2')
JWT_SECRET = os.environ.get('JWT_SECRET', 'your-secret-key')
SERVICE_PORT = 5003
# Database connection pool
db_pool = SimpleConnectionPool(1, 20, DATABASE_URL)
# Redis client
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
# Cache decorator
def cache_result(ttl=300):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
cache_key = f"analytics:{f.__name__}:{str(args)}:{str(kwargs)}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = f(*args, **kwargs)
redis_client.setex(cache_key, ttl, json.dumps(result, default=str))
return result
return wrapper
return decorator
# JWT validation decorator
def require_auth(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
request.jwt_payload = payload
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return wrapper
# Database query helper
def execute_query(query, params=None, fetchall=True):
conn = db_pool.getconn()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(query, params)
if fetchall:
return cur.fetchall()
return cur.fetchone()
finally:
db_pool.putconn(conn)
# Analytics calculations
class AnalyticsService:
@staticmethod
@cache_result(ttl=60)
def get_usage_statistics(customer_id=None, days=30):
"""Get usage statistics for licenses"""
base_query = """
SELECT
DATE(lh.timestamp) as date,
COUNT(DISTINCT lh.license_id) as active_licenses,
COUNT(DISTINCT lh.hardware_id) as active_devices,
COUNT(*) as total_heartbeats,
COUNT(DISTINCT lh.session_data->>'app_version') as app_versions
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
WHERE lh.timestamp >= NOW() - INTERVAL '%s days'
"""
params = [days]
if customer_id:
base_query += " AND l.customer_id = %s"
params.append(customer_id)
base_query += " GROUP BY DATE(lh.timestamp) ORDER BY date DESC"
return execute_query(base_query, params)
@staticmethod
@cache_result(ttl=300)
def get_performance_metrics(days=7):
"""Get system performance metrics"""
query = """
SELECT
DATE_TRUNC('hour', timestamp) as hour,
AVG(EXTRACT(EPOCH FROM (timestamp - LAG(timestamp) OVER (PARTITION BY license_id ORDER BY timestamp)))) as avg_heartbeat_interval,
COUNT(*) as validation_count,
COUNT(DISTINCT license_id) as unique_licenses,
COUNT(DISTINCT hardware_id) as unique_devices
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '%s days'
GROUP BY DATE_TRUNC('hour', timestamp)
ORDER BY hour DESC
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=120)
def get_anomaly_statistics(days=30):
"""Get anomaly detection statistics"""
query = """
SELECT
anomaly_type,
severity,
COUNT(*) as count,
COUNT(CASE WHEN resolved = false THEN 1 END) as unresolved_count,
AVG(CASE WHEN resolved = true THEN EXTRACT(EPOCH FROM (resolved_at - detected_at))/3600 END) as avg_resolution_hours
FROM anomaly_detections
WHERE detected_at >= NOW() - INTERVAL '%s days'
GROUP BY anomaly_type, severity
ORDER BY count DESC
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=300)
def get_license_distribution():
"""Get license distribution statistics"""
query = """
SELECT
l.license_type,
l.is_test,
COUNT(*) as total_count,
COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_count,
COUNT(CASE WHEN lh.timestamp >= NOW() - INTERVAL '1 hour' THEN 1 END) as recently_active,
AVG(l.device_limit) as avg_device_limit
FROM licenses l
LEFT JOIN LATERAL (
SELECT timestamp
FROM license_heartbeats
WHERE license_id = l.id
ORDER BY timestamp DESC
LIMIT 1
) lh ON true
GROUP BY l.license_type, l.is_test
"""
return execute_query(query)
@staticmethod
def get_revenue_impact(days=30):
"""Calculate revenue impact from license usage"""
query = """
WITH license_activity AS (
SELECT
l.id,
l.customer_id,
l.license_type,
l.price,
COUNT(DISTINCT DATE(lh.timestamp)) as active_days,
COUNT(DISTINCT lh.hardware_id) as devices_used
FROM licenses l
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp >= NOW() - INTERVAL '%s days'
WHERE l.is_test = false
GROUP BY l.id, l.customer_id, l.license_type, l.price
)
SELECT
license_type,
COUNT(*) as total_licenses,
SUM(price) as total_revenue,
AVG(active_days) as avg_active_days,
AVG(devices_used) as avg_devices_used,
SUM(CASE WHEN active_days > 0 THEN price ELSE 0 END) as active_revenue,
SUM(CASE WHEN active_days = 0 THEN price ELSE 0 END) as inactive_revenue
FROM license_activity
GROUP BY license_type
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=600)
def get_geographic_distribution():
"""Get geographic distribution of license usage"""
query = """
SELECT
lh.ip_address::text,
COUNT(DISTINCT lh.license_id) as license_count,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as total_validations,
MAX(lh.timestamp) as last_seen
FROM license_heartbeats lh
WHERE lh.timestamp >= NOW() - INTERVAL '24 hours'
AND lh.ip_address IS NOT NULL
GROUP BY lh.ip_address
ORDER BY total_validations DESC
LIMIT 100
"""
return execute_query(query)
@staticmethod
def get_usage_patterns(license_id=None):
"""Analyze usage patterns for predictive analytics"""
base_query = """
WITH hourly_usage AS (
SELECT
EXTRACT(HOUR FROM timestamp) as hour_of_day,
EXTRACT(DOW FROM timestamp) as day_of_week,
COUNT(*) as usage_count
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '30 days'
"""
params = []
if license_id:
base_query += " AND license_id = %s"
params.append(license_id)
base_query += """
GROUP BY hour_of_day, day_of_week
)
SELECT
hour_of_day,
day_of_week,
usage_count,
AVG(usage_count) OVER (PARTITION BY hour_of_day) as avg_hourly_usage,
AVG(usage_count) OVER (PARTITION BY day_of_week) as avg_daily_usage
FROM hourly_usage
ORDER BY day_of_week, hour_of_day
"""
return execute_query(base_query, params)
@staticmethod
def calculate_churn_risk():
"""Calculate churn risk based on usage patterns"""
query = """
WITH recent_activity AS (
SELECT
l.id,
l.customer_id,
l.expires_at,
MAX(lh.timestamp) as last_activity,
COUNT(DISTINCT DATE(lh.timestamp)) as active_days_30d,
COUNT(DISTINCT DATE(lh.timestamp)) FILTER (WHERE lh.timestamp >= NOW() - INTERVAL '7 days') as active_days_7d
FROM licenses l
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp >= NOW() - INTERVAL '30 days'
WHERE l.is_test = false
GROUP BY l.id, l.customer_id, l.expires_at
)
SELECT
customer_id,
COUNT(*) as total_licenses,
AVG(EXTRACT(EPOCH FROM (NOW() - last_activity))/86400) as avg_days_since_activity,
AVG(active_days_30d) as avg_active_days_30d,
AVG(active_days_7d) as avg_active_days_7d,
MIN(expires_at) as next_expiry,
CASE
WHEN AVG(active_days_7d) = 0 AND AVG(active_days_30d) > 0 THEN 'high'
WHEN AVG(active_days_30d) < 5 THEN 'medium'
ELSE 'low'
END as churn_risk
FROM recent_activity
GROUP BY customer_id
HAVING COUNT(*) > 0
ORDER BY churn_risk DESC, avg_days_since_activity DESC
"""
return execute_query(query)
# API Routes
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'service': 'analytics-service',
'timestamp': datetime.utcnow().isoformat()
})
@app.route('/api/v1/analytics/usage', methods=['GET'])
@require_auth
def get_usage_stats():
"""Get usage statistics"""
customer_id = request.args.get('customer_id')
days = int(request.args.get('days', 30))
stats = AnalyticsService.get_usage_statistics(customer_id, days)
return jsonify({
'success': True,
'data': stats,
'period_days': days,
'customer_id': customer_id
})
@app.route('/api/v1/analytics/performance', methods=['GET'])
@require_auth
def get_performance():
"""Get performance metrics"""
days = int(request.args.get('days', 7))
metrics = AnalyticsService.get_performance_metrics(days)
return jsonify({
'success': True,
'data': metrics,
'period_days': days
})
@app.route('/api/v1/analytics/anomalies', methods=['GET'])
@require_auth
def get_anomalies():
"""Get anomaly statistics"""
days = int(request.args.get('days', 30))
anomalies = AnalyticsService.get_anomaly_statistics(days)
return jsonify({
'success': True,
'data': anomalies,
'period_days': days
})
@app.route('/api/v1/analytics/distribution', methods=['GET'])
@require_auth
def get_distribution():
"""Get license distribution"""
distribution = AnalyticsService.get_license_distribution()
return jsonify({
'success': True,
'data': distribution
})
@app.route('/api/v1/analytics/revenue', methods=['GET'])
@require_auth
def get_revenue():
"""Get revenue impact analysis"""
days = int(request.args.get('days', 30))
revenue = AnalyticsService.get_revenue_impact(days)
return jsonify({
'success': True,
'data': revenue,
'period_days': days
})
@app.route('/api/v1/analytics/geographic', methods=['GET'])
@require_auth
def get_geographic():
"""Get geographic distribution"""
geo_data = AnalyticsService.get_geographic_distribution()
return jsonify({
'success': True,
'data': geo_data
})
@app.route('/api/v1/analytics/patterns', methods=['GET'])
@require_auth
def get_patterns():
"""Get usage patterns"""
license_id = request.args.get('license_id')
patterns = AnalyticsService.get_usage_patterns(license_id)
return jsonify({
'success': True,
'data': patterns,
'license_id': license_id
})
@app.route('/api/v1/analytics/churn-risk', methods=['GET'])
@require_auth
def get_churn_risk():
"""Get churn risk analysis"""
churn_data = AnalyticsService.calculate_churn_risk()
return jsonify({
'success': True,
'data': churn_data
})
@app.route('/api/v1/analytics/summary/<customer_id>', methods=['GET'])
@require_auth
def get_customer_summary(customer_id):
"""Get comprehensive analytics summary for a customer"""
usage = AnalyticsService.get_usage_statistics(customer_id, 30)
# Calculate summary metrics
total_heartbeats = sum(day['total_heartbeats'] for day in usage)
active_days = len([day for day in usage if day['active_licenses'] > 0])
return jsonify({
'success': True,
'customer_id': customer_id,
'summary': {
'total_heartbeats_30d': total_heartbeats,
'active_days_30d': active_days,
'average_daily_devices': np.mean([day['active_devices'] for day in usage]) if usage else 0,
'usage_trend': usage[:7] if len(usage) >= 7 else usage
}
})
# Real-time analytics endpoint (for websocket in future)
@app.route('/api/v1/analytics/realtime', methods=['GET'])
@require_auth
def get_realtime_stats():
"""Get real-time statistics for dashboard"""
# Get stats from last 5 minutes
query = """
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(DISTINCT hardware_id) as active_devices,
COUNT(*) as validations_5min,
COUNT(*) / 5.0 as validations_per_minute
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '5 minutes'
"""
realtime = execute_query(query, fetchall=False)
# Get current anomalies
anomaly_query = """
SELECT COUNT(*) as unresolved_anomalies
FROM anomaly_detections
WHERE resolved = false
"""
anomalies = execute_query(anomaly_query, fetchall=False)
return jsonify({
'success': True,
'timestamp': datetime.utcnow().isoformat(),
'data': {
'active_licenses': realtime['active_licenses'] or 0,
'active_devices': realtime['active_devices'] or 0,
'validations_5min': realtime['validations_5min'] or 0,
'validations_per_minute': float(realtime['validations_per_minute'] or 0),
'unresolved_anomalies': anomalies['unresolved_anomalies'] or 0
}
})
if __name__ == '__main__':
logger.info(f"Starting Analytics Service on port {SERVICE_PORT}")
app.run(host='0.0.0.0', port=SERVICE_PORT, debug=os.environ.get('FLASK_ENV') == 'development')