import logging from logging.handlers import TimedRotatingFileHandler import os from pathlib import Path from dotenv import load_dotenv load_dotenv() # Custom log level (optional). Keep for backward compatibility. CUSTOM_ERROR_LEVEL = 35 logging.addLevelName(CUSTOM_ERROR_LEVEL, "CUSTOM_ERROR") def custom_error(self, message, *args, **kwargs): """Helper to log at the custom error level.""" if self.isEnabledFor(CUSTOM_ERROR_LEVEL): self._log(CUSTOM_ERROR_LEVEL, message, args, **kwargs) # Attach helper to the Logger class so callers can do: logging.getLogger().custom_error(...) logging.Logger.custom_error = custom_error def _ensure_dir_for_file(path: str): """Ensure the parent directory for `path` exists.""" Path(path).resolve().parent.mkdir(parents=True, exist_ok=True) def _create_timed_handler(path: str, level=None, when='midnight', interval=1, backupCount=7, fmt=None): """ Create and configure a TimedRotatingFileHandler. Uses the handler's built-in rotation logic which is more robust and easier to maintain than a custom doRollover implementation. """ _ensure_dir_for_file(path) handler = TimedRotatingFileHandler(path, when=when, interval=interval, backupCount=backupCount, encoding='utf-8') # Default behavior is to append to the existing log file. # Rotation happens when 'when' occurs. if fmt: handler.setFormatter(fmt) if level is not None: handler.setLevel(level) return handler def setup_logging(): """ Configure logging for the application and return (logger, info_handler, error_handler, warning_handler). This version uses standard TimedRotatingFileHandler to keep the logic simple and avoid fragile file-renaming on Windows. """ # Select a format depending on environment for easier debugging in dev if os.getenv('ACH_ENV') == 'development': log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d') elif os.getenv('ACH_ENV') == 'production': log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') else: log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s') error_log_path = os.getenv('ERROR_LOG_FILE_PATH', "./logs/ACH_media_import_errors.log") warning_log_path = os.getenv('WARNING_LOG_FILE_PATH', "./logs/ACH_media_import_warnings.log") if os.getenv('WARING_LOG_FILE_PATH'): # Fix typo in .env if present warning_log_path = os.getenv('WARING_LOG_FILE_PATH') info_log_path = os.getenv('INFO_LOG_FILE_PATH', "./logs/ACH_media_import_info.log") # Create three handlers: info (all), warning (warning+), error (error+) info_handler = _create_timed_handler(info_log_path, level=logging.INFO, fmt=log_formatter, backupCount=int(os.getenv('LOG_BACKUP_COUNT', '7'))) warning_handler = _create_timed_handler(warning_log_path, level=logging.WARNING, fmt=log_formatter, backupCount=int(os.getenv('LOG_BACKUP_COUNT', '7'))) error_handler = _create_timed_handler(error_log_path, level=logging.ERROR, fmt=log_formatter, backupCount=int(os.getenv('LOG_BACKUP_COUNT', '7'))) console_handler = logging.StreamHandler() console_handler.setFormatter(log_formatter) # Configure root logger explicitly root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # Clear existing handlers to avoid duplicate logs when unit tests or reloads occur root_logger.handlers = [] root_logger.addHandler(info_handler) root_logger.addHandler(warning_handler) root_logger.addHandler(error_handler) root_logger.addHandler(console_handler) # Return the root logger and handlers (so callers can do manual rollovers if they truly need to) return root_logger, info_handler, error_handler, warning_handler