""" Comprehensive Exception Handling and Logging for Subscription System Provides robust error handling, logging, and monitoring for the usage-based subscription system. """ import traceback import json from datetime import datetime from typing import Dict, Any, Optional, Union, List from enum import Enum from loguru import logger from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError from models.subscription_models import APIProvider, UsageAlert class SubscriptionErrorType(Enum): USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded" PRICING_ERROR = "pricing_error" TRACKING_ERROR = "tracking_error" DATABASE_ERROR = "database_error" API_PROVIDER_ERROR = "api_provider_error" AUTHENTICATION_ERROR = "authentication_error" BILLING_ERROR = "billing_error" CONFIGURATION_ERROR = "configuration_error" class SubscriptionErrorSeverity(Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class SubscriptionException(Exception): """Base exception for subscription system errors.""" def __init__( self, message: str, error_type: SubscriptionErrorType, severity: SubscriptionErrorSeverity = SubscriptionErrorSeverity.MEDIUM, user_id: str = None, provider: APIProvider = None, context: Dict[str, Any] = None, original_error: Exception = None ): self.message = message self.error_type = error_type self.severity = severity self.user_id = user_id self.provider = provider self.context = context or {} self.original_error = original_error self.timestamp = datetime.utcnow() super().__init__(message) def to_dict(self) -> Dict[str, Any]: """Convert exception to dictionary for logging/storage.""" return { "message": self.message, "error_type": self.error_type.value, "severity": self.severity.value, "user_id": self.user_id, "provider": self.provider.value if self.provider else None, "context": self.context, "timestamp": self.timestamp.isoformat(), "original_error": str(self.original_error) if self.original_error else None, "traceback": traceback.format_exc() if self.original_error else None } class UsageLimitExceededException(SubscriptionException): """Exception raised when usage limits are exceeded.""" def __init__( self, message: str, user_id: str, provider: APIProvider, limit_type: str, current_usage: Union[int, float], limit_value: Union[int, float], context: Dict[str, Any] = None ): context = context or {} context.update({ "limit_type": limit_type, "current_usage": current_usage, "limit_value": limit_value, "usage_percentage": (current_usage / max(limit_value, 1)) * 100 }) super().__init__( message=message, error_type=SubscriptionErrorType.USAGE_LIMIT_EXCEEDED, severity=SubscriptionErrorSeverity.HIGH, user_id=user_id, provider=provider, context=context ) class PricingException(SubscriptionException): """Exception raised for pricing calculation errors.""" def __init__( self, message: str, provider: APIProvider = None, model_name: str = None, context: Dict[str, Any] = None, original_error: Exception = None ): context = context or {} if model_name: context["model_name"] = model_name super().__init__( message=message, error_type=SubscriptionErrorType.PRICING_ERROR, severity=SubscriptionErrorSeverity.MEDIUM, provider=provider, context=context, original_error=original_error ) class TrackingException(SubscriptionException): """Exception raised for usage tracking errors.""" def __init__( self, message: str, user_id: str = None, provider: APIProvider = None, context: Dict[str, Any] = None, original_error: Exception = None ): super().__init__( message=message, error_type=SubscriptionErrorType.TRACKING_ERROR, severity=SubscriptionErrorSeverity.MEDIUM, user_id=user_id, provider=provider, context=context, original_error=original_error ) class SubscriptionExceptionHandler: """Comprehensive exception handler for the subscription system.""" def __init__(self, db: Session = None): self.db = db self._setup_logging() def _setup_logging(self): """Setup structured logging for subscription errors.""" from utils.logger_utils import get_service_logger return get_service_logger("subscription_exception_handler") def handle_exception( self, error: Union[Exception, SubscriptionException], context: Dict[str, Any] = None, log_level: str = "error" ) -> Dict[str, Any]: """Handle and log subscription system exceptions.""" context = context or {} # Convert regular exceptions to SubscriptionException if not isinstance(error, SubscriptionException): error = SubscriptionException( message=str(error), error_type=self._classify_error(error), severity=self._determine_severity(error), context=context, original_error=error ) # Log the error error_data = error.to_dict() error_data.update(context) log_message = f"Subscription Error: {error.message}" if log_level == "critical": logger.critical(log_message, extra={"error_data": error_data}) elif log_level == "error": logger.error(log_message, extra={"error_data": error_data}) elif log_level == "warning": logger.warning(log_message, extra={"error_data": error_data}) else: logger.info(log_message, extra={"error_data": error_data}) # Store critical errors in database for alerting if error.severity in [SubscriptionErrorSeverity.HIGH, SubscriptionErrorSeverity.CRITICAL]: self._store_error_alert(error) # Return formatted error response return self._format_error_response(error) def _classify_error(self, error: Exception) -> SubscriptionErrorType: """Classify an exception into a subscription error type.""" error_str = str(error).lower() error_type_name = type(error).__name__.lower() if "limit" in error_str or "exceeded" in error_str: return SubscriptionErrorType.USAGE_LIMIT_EXCEEDED elif "pricing" in error_str or "cost" in error_str: return SubscriptionErrorType.PRICING_ERROR elif "tracking" in error_str or "usage" in error_str: return SubscriptionErrorType.TRACKING_ERROR elif "database" in error_str or "sql" in error_type_name: return SubscriptionErrorType.DATABASE_ERROR elif "api" in error_str or "provider" in error_str: return SubscriptionErrorType.API_PROVIDER_ERROR elif "auth" in error_str or "permission" in error_str: return SubscriptionErrorType.AUTHENTICATION_ERROR elif "billing" in error_str or "payment" in error_str: return SubscriptionErrorType.BILLING_ERROR else: return SubscriptionErrorType.CONFIGURATION_ERROR def _determine_severity(self, error: Exception) -> SubscriptionErrorSeverity: """Determine the severity of an error.""" error_str = str(error).lower() error_type = type(error) # Critical errors if isinstance(error, (SQLAlchemyError, ConnectionError)): return SubscriptionErrorSeverity.CRITICAL # High severity errors if "limit exceeded" in error_str or "unauthorized" in error_str: return SubscriptionErrorSeverity.HIGH # Medium severity errors if "pricing" in error_str or "tracking" in error_str: return SubscriptionErrorSeverity.MEDIUM # Default to low return SubscriptionErrorSeverity.LOW def _store_error_alert(self, error: SubscriptionException): """Store critical errors as alerts in the database.""" if not self.db or not error.user_id: return try: alert = UsageAlert( user_id=error.user_id, alert_type="system_error", threshold_percentage=0, provider=error.provider, title=f"System Error: {error.error_type.value}", message=error.message, severity=error.severity.value, billing_period=datetime.now().strftime("%Y-%m") ) self.db.add(alert) self.db.commit() except Exception as e: logger.error(f"Failed to store error alert: {e}") def _format_error_response(self, error: SubscriptionException) -> Dict[str, Any]: """Format error for API response.""" response = { "success": False, "error": { "type": error.error_type.value, "message": error.message, "severity": error.severity.value, "timestamp": error.timestamp.isoformat() } } # Add context for debugging (non-sensitive info only) if error.context: safe_context = { k: v for k, v in error.context.items() if k not in ["password", "token", "key", "secret"] } response["error"]["context"] = safe_context # Add user-friendly message based on error type user_messages = { SubscriptionErrorType.USAGE_LIMIT_EXCEEDED: "You have reached your usage limit. Please upgrade your plan or wait for the next billing cycle.", SubscriptionErrorType.PRICING_ERROR: "There was an issue calculating the cost for this request. Please try again.", SubscriptionErrorType.TRACKING_ERROR: "Unable to track usage for this request. Please contact support if this persists.", SubscriptionErrorType.DATABASE_ERROR: "A database error occurred. Please try again later.", SubscriptionErrorType.API_PROVIDER_ERROR: "There was an issue with the API provider. Please try again.", SubscriptionErrorType.AUTHENTICATION_ERROR: "Authentication failed. Please check your credentials.", SubscriptionErrorType.BILLING_ERROR: "There was a billing-related error. Please contact support.", SubscriptionErrorType.CONFIGURATION_ERROR: "System configuration error. Please contact support." } response["error"]["user_message"] = user_messages.get( error.error_type, "An unexpected error occurred. Please try again or contact support." ) return response # Utility functions for common error scenarios def handle_usage_limit_error( user_id: str, provider: APIProvider, limit_type: str, current_usage: Union[int, float], limit_value: Union[int, float], db: Session = None ) -> Dict[str, Any]: """Handle usage limit exceeded errors.""" handler = SubscriptionExceptionHandler(db) error = UsageLimitExceededException( message=f"Usage limit exceeded for {limit_type}", user_id=user_id, provider=provider, limit_type=limit_type, current_usage=current_usage, limit_value=limit_value ) return handler.handle_exception(error, log_level="warning") def handle_pricing_error( message: str, provider: APIProvider = None, model_name: str = None, original_error: Exception = None, db: Session = None ) -> Dict[str, Any]: """Handle pricing calculation errors.""" handler = SubscriptionExceptionHandler(db) error = PricingException( message=message, provider=provider, model_name=model_name, original_error=original_error ) return handler.handle_exception(error) def handle_tracking_error( message: str, user_id: str = None, provider: APIProvider = None, original_error: Exception = None, db: Session = None ) -> Dict[str, Any]: """Handle usage tracking errors.""" handler = SubscriptionExceptionHandler(db) error = TrackingException( message=message, user_id=user_id, provider=provider, original_error=original_error ) return handler.handle_exception(error) def log_usage_event( user_id: str, provider: APIProvider, action: str, details: Dict[str, Any] = None ): """Log usage events for monitoring and debugging.""" details = details or {} log_data = { "user_id": user_id, "provider": provider.value, "action": action, "timestamp": datetime.utcnow().isoformat(), **details } logger.info(f"Usage Tracking: {action}", extra={"usage_data": log_data}) # Decorator for automatic exception handling def handle_subscription_errors(db: Session = None): """Decorator to automatically handle subscription-related exceptions.""" def decorator(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except SubscriptionException as e: handler = SubscriptionExceptionHandler(db) return handler.handle_exception(e) except Exception as e: handler = SubscriptionExceptionHandler(db) return handler.handle_exception(e) return wrapper return decorator