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 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) DISABLE_FILE_LOGGING: Set to "true" to disable file logging (default: false) Ensures log directory exists, prevents path traversal, and configures both rotating file and console handlers. Falls back to console-only logging if file logging fails due to permissions. """ # Check if file logging should be disabled disable_file_logging = os.environ.get("DISABLE_FILE_LOGGING", "false").lower() == "true" # Determine log directory and default file path base_dir = Path(__file__).parent # In containerized environments, use /tmp for logs if the default location isn't writable try: log_dir = base_dir / "logs" log_dir.mkdir(parents=True, exist_ok=True) default_log_file = log_dir / "application.log" except (PermissionError, OSError): # Fall back to /tmp for containerized environments log_dir = Path("/tmp/deepwiki-logs") try: log_dir.mkdir(parents=True, exist_ok=True) default_log_file = log_dir / "application.log" except (PermissionError, OSError): # If even /tmp fails, disable file logging disable_file_logging = True default_log_file = None # Get log level from environment - default to WARNING to reduce noise in production log_level_str = os.environ.get("LOG_LEVEL", "WARNING").upper() log_level = getattr(logging, log_level_str, logging.WARNING) # Get log file path (only if file logging is enabled) resolved_path = None if not disable_file_logging and default_log_file: 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): print(f"Warning: LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'. Falling back to console logging.") disable_file_logging = True resolved_path = None # Ensure parent directories exist if resolved_path: try: resolved_path.parent.mkdir(parents=True, exist_ok=True) except (PermissionError, OSError): print("Warning: Cannot create log directory. Falling back to console logging.") disable_file_logging = True resolved_path = None # 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 # 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 list handlers = [] # Create console handler (always present) console_handler = logging.StreamHandler() formatter = logging.Formatter(log_format) console_handler.setFormatter(formatter) console_handler.addFilter(IgnoreLogChangeDetectedFilter()) handlers.append(console_handler) # Create file handler only if file logging is enabled and path is available file_handler = None if not disable_file_logging and resolved_path: try: file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8") file_handler.setFormatter(formatter) file_handler.addFilter(IgnoreLogChangeDetectedFilter()) handlers.append(file_handler) except (PermissionError, OSError) as e: print(f"Warning: Cannot create file handler: {e}. Using console logging only.") # Apply logging configuration logging.basicConfig(level=log_level, handlers=handlers, force=True) # Log configuration info logger = logging.getLogger(__name__) if file_handler and 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}" ) else: logger.debug(f"Logging configured: level={log_level_str}, console only (file logging disabled)")