import logging import traceback import uuid from datetime import datetime, timezone from flask import request, g, has_request_context from extensions import db from api.models import ErrorLog import threading import pytz from .toolbox import get_app_timezone, get_current_timestamp class DatabaseLogHandler(logging.Handler): """ Custom logging handler that stores log records in the database. Configurable logging level via database settings. """ def __init__(self): super().__init__() self.setLevel(logging.WARNING) # Default level, will be updated from config self._app = None # Store app reference self._processed_records = set() # Track processed records to avoid duplicates self._max_cache_size = 1000 # Limit cache size to prevent memory issues def emit(self, record): """ Emit a log record to the database. This runs in a separate thread to avoid blocking the main application. """ # Import filter functions from .toolbox import get_filtered_loggers, get_filtered_message_patterns # Get configured filters filtered_loggers = get_filtered_loggers() filtered_patterns = get_filtered_message_patterns() # Skip database logging for filtered loggers to prevent feedback loops if record.name in filtered_loggers: return # Also filter out messages containing specific patterns message = record.getMessage().lower() for pattern in filtered_patterns: if pattern.lower() in message: return # Create a unique identifier for this record to prevent duplicates record_id = ( record.name, record.levelname, record.getMessage(), record.created, getattr(record, 'pathname', ''), getattr(record, 'lineno', 0) ) # Check if we've already processed this exact record if record_id in self._processed_records: return # Add to processed records cache self._processed_records.add(record_id) # Clean cache if it gets too large if len(self._processed_records) > self._max_cache_size: # Remove oldest half of entries (simple cleanup) self._processed_records = set(list(self._processed_records)[self._max_cache_size//2:]) # Store the app reference if we have an application context if not self._app: try: from flask import current_app self._app = current_app._get_current_object() except RuntimeError: # No application context available, try to import app try: from app import app self._app = app except ImportError: pass # Use a thread to avoid blocking the main application threading.Thread(target=self._emit_to_db, args=(record,), daemon=True).start() def _emit_to_db(self, record): """ Actually write the log record to the database. This method runs in a separate thread. """ try: # Use the stored app reference or try to get it app = self._app if not app: try: from flask import current_app app = current_app._get_current_object() except RuntimeError: # No application context, try to import app try: from app import app except ImportError: print("Could not import app for database logging") return with app.app_context(): # Extract request information if available request_id = None user_id = None remote_addr = None if has_request_context(): try: request_id = getattr(g, 'request_id', str(uuid.uuid4())[:8]) user_id = getattr(g, 'user_id', None) remote_addr = request.remote_addr except Exception: # If we can't get request context, continue without it pass # Format exception info if present exception_text = None if record.exc_info: exception_text = ''.join(traceback.format_exception(*record.exc_info)) # Create error log entry error_log = ErrorLog( level=record.levelname, logger_name=record.name, message=self.format(record), timestamp=get_current_timestamp(), pathname=record.pathname if hasattr(record, 'pathname') else None, lineno=record.lineno if hasattr(record, 'lineno') else None, request_id=request_id, user_id=user_id, remote_addr=remote_addr, exception=exception_text ) db.session.add(error_log) db.session.commit() except Exception as e: # If database logging fails, fall back to console logging # Don't raise the exception to avoid breaking the application print(f"Failed to log to database: {e}") def setup_database_logging(app): """ Set up database logging for the Flask application. """ # Create and configure the database handler db_handler = DatabaseLogHandler() db_handler._app = app # Store app reference # Set initial logging level from settings (will be updated dynamically) update_logging_level(app, db_handler) # Set a formatter for the database logs formatter = logging.Formatter( '%(name)s - %(levelname)s - %(message)s' ) db_handler.setFormatter(formatter) # Store handler reference in app for dynamic updates app.db_handler = db_handler # Only add to root logger to avoid duplicate logging # (app.logger propagates to root logger by default) root_logger = logging.getLogger() # Check if we already have this handler to avoid duplicates handler_exists = False for handler in root_logger.handlers: if isinstance(handler, DatabaseLogHandler): handler_exists = True break if not handler_exists: root_logger.addHandler(db_handler) # Add request ID to flask request context @app.before_request def add_request_id(): g.request_id = str(uuid.uuid4())[:8] if hasattr(g, 'api_key') and g.api_key and g.api_key.user_id: g.user_id = g.api_key.user_id elif hasattr(g, 'current_user') and g.current_user and hasattr(g.current_user, 'id'): g.user_id = g.current_user.id app.logger.info("Database logging initialized") def update_logging_level(app, db_handler=None): """ Update the database logging level from settings. """ try: with app.app_context(): from auth.models import Settings settings = Settings.query.first() if settings and hasattr(settings, 'log_level'): # Convert string to logging level level_map = { 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL } new_level = level_map.get(settings.log_level, logging.WARNING) # Update the handler if provided if db_handler: db_handler.setLevel(new_level) # Or get it from app if stored elif hasattr(app, 'db_handler'): app.db_handler.setLevel(new_level) app.logger.info(f"Database logging level updated to: {settings.log_level}") else: # Default to WARNING if no setting found if db_handler: db_handler.setLevel(logging.WARNING) elif hasattr(app, 'db_handler'): app.db_handler.setLevel(logging.WARNING) except Exception as e: print(f"Failed to update logging level: {e}") def get_available_log_levels(): """ Get list of available logging levels for admin configuration. """ return [ ('DEBUG', 'Debug - All messages'), ('INFO', 'Info - General information and above'), ('WARNING', 'Warning - Warnings and above (default)'), ('ERROR', 'Error - Errors and above'), ('CRITICAL', 'Critical - Only critical errors') ] def log_error(message, exception=None, level=logging.ERROR): """ Convenience function to log errors to the database. Args: message: Error message exception: Exception object (optional) level: Logging level (default: ERROR) """ logger = logging.getLogger(__name__) if exception: logger.log(level, message, exc_info=True) else: logger.log(level, message) def log_warning(message): """Convenience function to log warnings.""" log_error(message, level=logging.WARNING) def log_critical(message, exception=None): """Convenience function to log critical errors.""" log_error(message, exception, level=logging.CRITICAL)