diff --git a/oai.py b/oai.py index 1ab479e..26b92ef 100644 --- a/oai.py +++ b/oai.py @@ -30,7 +30,7 @@ from packaging import version as pkg_version import io # Added for custom handler # App version. Changes by author with new releases. -version = '1.9.5' +version = '1.9.6' app = typer.Typer() @@ -369,41 +369,132 @@ class RotatingRichHandler(RotatingFileHandler): except Exception: self.handleError(record) -# Get log configuration from DB +# ============================================================================ +# LOGGING SETUP - MUST BE DONE AFTER CONFIG IS LOADED +# ============================================================================ + +# Load log configuration from DB FIRST (before creating handler) LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10") LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2") LOG_LEVEL_STR = get_config('log_level') or "info" LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO) -# Create the custom rotating handler -app_handler = RotatingRichHandler( - filename=str(log_file), - maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, # Convert MB to bytes - backupCount=LOG_BACKUP_COUNT, - encoding='utf-8' -) +# Global reference to the handler for dynamic reloading +app_handler = None +app_logger = None -logging.basicConfig( - level=logging.NOTSET, - format="%(message)s", # Rich formats it - datefmt="[%X]", - handlers=[app_handler] -) +def setup_logging(): + """Setup or reset logging configuration with current settings.""" + global app_handler, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, app_logger + + # Get the root logger + root_logger = logging.getLogger() + + # Remove existing handler if present + if app_handler is not None: + root_logger.removeHandler(app_handler) + try: + app_handler.close() + except: + pass + + # Check if log file needs immediate rotation + if os.path.exists(log_file): + current_size = os.path.getsize(log_file) + max_bytes = LOG_MAX_SIZE_MB * 1024 * 1024 + + if current_size >= max_bytes: + # Perform immediate rotation + import shutil + timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + backup_file = f"{log_file}.{timestamp}" + try: + shutil.move(str(log_file), backup_file) + except Exception as e: + print(f"Warning: Could not rotate log file: {e}") + + # Clean up old backups if exceeding limit + log_dir = os.path.dirname(log_file) + log_basename = os.path.basename(log_file) + backup_pattern = f"{log_basename}.*" + + import glob + backups = sorted(glob.glob(os.path.join(log_dir, backup_pattern))) + + # Keep only the most recent backups + while len(backups) > LOG_BACKUP_COUNT: + oldest = backups.pop(0) + try: + os.remove(oldest) + except: + pass + + # Create new handler with current settings + app_handler = RotatingRichHandler( + filename=str(log_file), + maxBytes=LOG_MAX_SIZE_MB * 1024 * 1024, + backupCount=LOG_BACKUP_COUNT, + encoding='utf-8' + ) + + # Set handler level to NOTSET so it processes all records + app_handler.setLevel(logging.NOTSET) + + # Configure root logger - set to WARNING to suppress third-party library noise + root_logger.setLevel(logging.WARNING) + root_logger.addHandler(app_handler) + + # Suppress noisy third-party loggers + # These libraries create DEBUG logs that pollute our log file + logging.getLogger('asyncio').setLevel(logging.WARNING) + logging.getLogger('urllib3').setLevel(logging.WARNING) + logging.getLogger('requests').setLevel(logging.WARNING) + logging.getLogger('httpx').setLevel(logging.WARNING) + logging.getLogger('httpcore').setLevel(logging.WARNING) + logging.getLogger('openai').setLevel(logging.WARNING) + logging.getLogger('openrouter').setLevel(logging.WARNING) + + # Get or create app logger and set its level (this filters what gets logged) + app_logger = logging.getLogger("oai_app") + app_logger.setLevel(LOG_LEVEL) + # Don't propagate to avoid root logger filtering + app_logger.propagate = True + + return app_logger -app_logger = logging.getLogger("oai_app") -app_logger.setLevel(LOG_LEVEL) +# Initial logging setup +app_logger = setup_logging() def set_log_level(level_str: str) -> bool: """Set the application log level. Returns True if successful.""" - global LOG_LEVEL, LOG_LEVEL_STR + global LOG_LEVEL, LOG_LEVEL_STR, app_logger level_str_lower = level_str.lower() if level_str_lower not in VALID_LOG_LEVELS: return False LOG_LEVEL = VALID_LOG_LEVELS[level_str_lower] LOG_LEVEL_STR = level_str_lower - app_logger.setLevel(LOG_LEVEL) + + # Update the logger level immediately + if app_logger: + app_logger.setLevel(LOG_LEVEL) + return True +def reload_logging_config(): + """Reload logging configuration from database and reinitialize handler.""" + global LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger + + # Reload from database + LOG_MAX_SIZE_MB = int(get_config('log_max_size_mb') or "10") + LOG_BACKUP_COUNT = int(get_config('log_backup_count') or "2") + LOG_LEVEL_STR = get_config('log_level') or "info" + LOG_LEVEL = VALID_LOG_LEVELS.get(LOG_LEVEL_STR.lower(), logging.INFO) + + # Reinitialize logging + app_logger = setup_logging() + + return app_logger + # ============================================================================ # END OF LOGGING SETUP # ============================================================================ @@ -699,7 +790,7 @@ def show_command_help(command: str): app_logger.info(f"Displayed detailed help for command: {command}") -# Load configs +# Load configs (AFTER logging is set up) API_KEY = get_config('api_key') OPENROUTER_BASE_URL = get_config('base_url') or "https://openrouter.ai/api/v1" STREAM_ENABLED = get_config('stream_enabled') or "on" @@ -909,7 +1000,7 @@ def display_paginated_table(table: Table, title: str): @app.command() def chat(): - global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR + global API_KEY, OPENROUTER_BASE_URL, STREAM_ENABLED, MAX_TOKEN, COST_WARNING_THRESHOLD, DEFAULT_ONLINE_MODE, LOG_MAX_SIZE_MB, LOG_BACKUP_COUNT, LOG_LEVEL, LOG_LEVEL_STR, app_logger session_max_token = 0 session_system_prompt = "" session_history = [] @@ -1319,9 +1410,13 @@ def chat(): console.print("[bold yellow]Usage: /middleout on|off (or /middleout to view status)[/]") continue elif user_input.lower() == "/reset": - confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False) - if not confirm: - console.print("[bold yellow]Reset cancelled.[/]") + try: + confirm = typer.confirm("Reset conversation context? This clears history and prompt.", default=False) + if not confirm: + console.print("[bold yellow]Reset cancelled.[/]") + continue + except (EOFError, KeyboardInterrupt): + console.print("\n[bold yellow]Reset cancelled.[/]") continue session_history = [] current_index = -1 @@ -1538,9 +1633,13 @@ def chat(): new_size_mb = 100 set_config('log_max_size_mb', str(new_size_mb)) LOG_MAX_SIZE_MB = new_size_mb - console.print(f"[bold green]Log size limit set to {new_size_mb} MB.[/]") - console.print("[bold yellow]⚠️ Restart the application for this change to take effect.[/]") - app_logger.info(f"Log size limit updated to {new_size_mb} MB (requires restart)") + + # Reload logging configuration immediately + app_logger = reload_logging_config() + + console.print(f"[bold green]Log size limit set to {new_size_mb} MB and applied immediately.[/]") + console.print(f"[dim cyan]Log file rotated if it exceeded the new limit.[/]") + app_logger.info(f"Log size limit updated to {new_size_mb} MB and reloaded") except ValueError: console.print("[bold red]Invalid size. Provide a number in MB.[/]") elif args.startswith("online"): @@ -1778,7 +1877,7 @@ def chat(): ) help_table.add_row( "/config log [size_mb]", - "Set log file size limit in MB. Older logs are rotated automatically. Requires restart.", + "Set log file size limit in MB. Older logs are rotated automatically. Takes effect immediately.", "/config log 20" ) help_table.add_row( @@ -1930,8 +2029,7 @@ def chat(): if not selected_model: console.print("[bold yellow]Select a model first with '/model'.[/]") continue - - # Process file attachments with PDF support + # Process file attachments with PDF support content_blocks = [] text_part = user_input file_attachments = []