diff --git a/api/logging_config.py b/api/logging_config.py index d3561a5b..d2726c62 100644 --- a/api/logging_config.py +++ b/api/logging_config.py @@ -1,16 +1,26 @@ import logging import os from pathlib import Path +from logging.handlers import RotatingFileHandler + class IgnoreLogChangeDetectedFilter(logging.Filter): def filter(self, record: logging.LogRecord): return "Detected file change in" not in record.getMessage() + def setup_logging(format: str = None): """ - Configure logging for the application. - Reads LOG_LEVEL and LOG_FILE_PATH from environment (defaults: INFO, logs/application.log). - Ensures log directory exists, and configures both file and console handlers. + Configure logging for the application with log rotation. + + Environment variables: + LOG_LEVEL: Log level (default: INFO) + LOG_FILE_PATH: Path to log file (default: logs/application.log) + LOG_MAX_SIZE: Max size in MB before rotating (default: 10MB) + LOG_BACKUP_COUNT: Number of backup files to keep (default: 5) + + Ensures log directory exists, prevents path traversal, and configures + both rotating file and console handlers. """ # Determine log directory and default file path base_dir = Path(__file__).parent @@ -18,37 +28,58 @@ def setup_logging(format: str = None): log_dir.mkdir(parents=True, exist_ok=True) default_log_file = log_dir / "application.log" - # Get log level and file path from environment + # Get log level from environment log_level_str = os.environ.get("LOG_LEVEL", "INFO").upper() log_level = getattr(logging, log_level_str, logging.INFO) - log_file_path = Path(os.environ.get( - "LOG_FILE_PATH", str(default_log_file))) - # ensure log_file_path is within the project's logs directory to prevent path traversal + # Get log file path + log_file_path = Path(os.environ.get("LOG_FILE_PATH", str(default_log_file))) + + # Secure path check: must be inside logs/ directory log_dir_resolved = log_dir.resolve() resolved_path = log_file_path.resolve() if not str(resolved_path).startswith(str(log_dir_resolved) + os.sep): - raise ValueError( - f"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'" - ) - # Ensure parent dirs exist for the log file + raise ValueError(f"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'") + + # Ensure parent directories exist resolved_path.parent.mkdir(parents=True, exist_ok=True) - # Configure logging handlers and format - logging.basicConfig( - level=log_level, - format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s", - handlers=[ - logging.FileHandler(resolved_path), - logging.StreamHandler() - ], - force=True - ) - - # Ignore log file's change detection - for handler in logging.getLogger().handlers: - handler.addFilter(IgnoreLogChangeDetectedFilter()) + # Get max log file size (default: 10MB) + try: + max_mb = int(os.environ.get("LOG_MAX_SIZE", 10)) # 10MB default + max_bytes = max_mb * 1024 * 1024 + except (TypeError, ValueError): + max_bytes = 10 * 1024 * 1024 # fallback to 10MB on error - # Initial debug message to confirm configuration + # Get backup count (default: 5) + try: + backup_count = int(os.environ.get("LOG_BACKUP_COUNT", 5)) + except ValueError: + backup_count = 5 + + # Configure format + log_format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s" + + # Create handlers + file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8") + console_handler = logging.StreamHandler() + + # Set format for both handlers + formatter = logging.Formatter(log_format) + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Add filter to suppress "Detected file change" messages + file_handler.addFilter(IgnoreLogChangeDetectedFilter()) + console_handler.addFilter(IgnoreLogChangeDetectedFilter()) + + # Apply logging configuration + logging.basicConfig(level=log_level, handlers=[file_handler, console_handler], force=True) + + # Log configuration info logger = logging.getLogger(__name__) - logger.debug(f"Log level set to {log_level_str}, log file: {resolved_path}") + logger.debug( + f"Logging configured: level={log_level_str}, " + f"file={resolved_path}, max_size={max_bytes} bytes, " + f"backup_count={backup_count}" + )