""" Analytics Cache Service for Backend Provides intelligent caching for expensive analytics API calls """ import time import json from typing import Dict, Any, Optional, List from datetime import datetime, timedelta from loguru import logger import hashlib class AnalyticsCacheService: def __init__(self): # In-memory cache (in production, consider Redis) self.cache: Dict[str, Dict[str, Any]] = {} # Cache TTL configurations (in seconds) self.TTL_CONFIG = { 'platform_status': 30 * 60, # 30 minutes 'analytics_data': 60 * 60, # 60 minutes 'user_sites': 120 * 60, # 2 hours 'bing_analytics': 60 * 60, # 1 hour for expensive Bing calls 'gsc_analytics': 60 * 60, # 1 hour for GSC calls 'bing_sites': 120 * 60, # 2 hours for Bing sites (rarely change) } # Cache statistics self.stats = { 'hits': 0, 'misses': 0, 'sets': 0, 'invalidations': 0 } logger.info("AnalyticsCacheService initialized with TTL config: {ttl}", ttl=self.TTL_CONFIG) def _generate_cache_key(self, prefix: str, user_id: str, **kwargs) -> str: """Generate a unique cache key from parameters""" # Create a deterministic key from parameters params_str = json.dumps(kwargs, sort_keys=True) if kwargs else "" key_data = f"{prefix}:{user_id}:{params_str}" # Use hash to keep keys manageable return hashlib.md5(key_data.encode()).hexdigest() def _is_expired(self, entry: Dict[str, Any]) -> bool: """Check if cache entry is expired""" if 'timestamp' not in entry: return True ttl = entry.get('ttl', 0) age = time.time() - entry['timestamp'] return age > ttl def get(self, prefix: str, user_id: str, **kwargs) -> Optional[Any]: """Get cached data if valid""" cache_key = self._generate_cache_key(prefix, user_id, **kwargs) if cache_key not in self.cache: logger.debug("Cache MISS: {key}", key=cache_key) self.stats['misses'] += 1 return None entry = self.cache[cache_key] if self._is_expired(entry): logger.debug("Cache EXPIRED: {key}", key=cache_key) del self.cache[cache_key] self.stats['misses'] += 1 return None logger.debug("Cache HIT: {key} (age: {age}s)", key=cache_key, age=int(time.time() - entry['timestamp'])) self.stats['hits'] += 1 return entry['data'] def set(self, prefix: str, user_id: str, data: Any, ttl_override: Optional[int] = None, **kwargs) -> None: """Set cached data with TTL""" cache_key = self._generate_cache_key(prefix, user_id, **kwargs) ttl = ttl_override or self.TTL_CONFIG.get(prefix, 300) # Default 5 minutes self.cache[cache_key] = { 'data': data, 'timestamp': time.time(), 'ttl': ttl, 'created_at': datetime.now().isoformat() } logger.info("Cache SET: {prefix} for user {user_id} (TTL: {ttl}s)", prefix=prefix, user_id=user_id, ttl=ttl) self.stats['sets'] += 1 def invalidate(self, prefix: str, user_id: Optional[str] = None, **kwargs) -> int: """Invalidate cache entries matching pattern""" pattern_key = self._generate_cache_key(prefix, user_id or "*", **kwargs) pattern_prefix = pattern_key.split(':')[0] + ':' keys_to_delete = [] for key in self.cache.keys(): if key.startswith(pattern_prefix): if user_id is None or user_id in key: keys_to_delete.append(key) for key in keys_to_delete: del self.cache[key] logger.info("Cache INVALIDATED: {count} entries matching {pattern}", count=len(keys_to_delete), pattern=pattern_prefix) self.stats['invalidations'] += len(keys_to_delete) return len(keys_to_delete) def invalidate_user(self, user_id: str) -> int: """Invalidate all cache entries for a specific user""" keys_to_delete = [key for key in self.cache.keys() if user_id in key] for key in keys_to_delete: del self.cache[key] logger.info("Cache INVALIDATED: {count} entries for user {user_id}", count=len(keys_to_delete), user_id=user_id) self.stats['invalidations'] += len(keys_to_delete) return len(keys_to_delete) def cleanup_expired(self) -> int: """Remove expired entries from cache""" keys_to_delete = [] for key, entry in self.cache.items(): if self._is_expired(entry): keys_to_delete.append(key) for key in keys_to_delete: del self.cache[key] if keys_to_delete: logger.info("Cache CLEANUP: Removed {count} expired entries", count=len(keys_to_delete)) return len(keys_to_delete) def get_stats(self) -> Dict[str, Any]: """Get cache statistics""" total_requests = self.stats['hits'] + self.stats['misses'] hit_rate = (self.stats['hits'] / total_requests * 100) if total_requests > 0 else 0 return { 'cache_size': len(self.cache), 'hit_rate': round(hit_rate, 2), 'total_requests': total_requests, 'hits': self.stats['hits'], 'misses': self.stats['misses'], 'sets': self.stats['sets'], 'invalidations': self.stats['invalidations'], 'ttl_config': self.TTL_CONFIG } def clear_all(self) -> None: """Clear all cache entries""" self.cache.clear() logger.info("Cache CLEARED: All entries removed") def get_cache_info(self) -> Dict[str, Any]: """Get detailed cache information for debugging""" cache_info = {} for key, entry in self.cache.items(): age = int(time.time() - entry['timestamp']) remaining_ttl = max(0, entry['ttl'] - age) cache_info[key] = { 'age_seconds': age, 'remaining_ttl_seconds': remaining_ttl, 'created_at': entry.get('created_at', 'unknown'), 'data_size': len(str(entry['data'])) if entry['data'] else 0 } return cache_info # Global cache instance analytics_cache = AnalyticsCacheService() # Cleanup expired entries every 5 minutes import threading import time def cleanup_worker(): """Background worker to clean up expired cache entries""" while True: try: time.sleep(300) # 5 minutes analytics_cache.cleanup_expired() except Exception as e: logger.error("Cache cleanup error: {error}", error=e) # Start cleanup thread cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True) cleanup_thread.start() logger.info("Analytics cache cleanup thread started")