import logging import sys import platform import os from pathlib import Path class SafeFormatter(logging.Formatter): """Custom formatter that safely handles non-ASCII characters""" def format(self, record): # Handle message string safely if hasattr(record, 'msg') and record.msg is not None: if isinstance(record.msg, str): # Strip non-ASCII characters to prevent encoding errors record.msg = record.msg.encode("ascii", "ignore").decode() elif isinstance(record.msg, bytes): # Handle bytes objects record.msg = record.msg.decode("utf-8", "ignore") # Handle args tuple if present if hasattr(record, 'args') and record.args: safe_args = [] for arg in record.args: if isinstance(arg, str): safe_args.append(arg.encode("ascii", "ignore").decode()) elif isinstance(arg, bytes): safe_args.append(arg.decode("utf-8", "ignore")) else: safe_args.append(str(arg)) record.args = tuple(safe_args) # Handle exc_text if present if hasattr(record, 'exc_text') and record.exc_text: if isinstance(record.exc_text, str): record.exc_text = record.exc_text.encode("ascii", "ignore").decode() return super().format(record) class SafeStreamHandler(logging.StreamHandler): """Stream handler that forces UTF-8 encoding where supported""" def __init__(self, stream=None): super().__init__(stream) # Try to set UTF-8 encoding on stdout/stderr if supported if hasattr(self.stream, 'reconfigure'): try: if platform.system() == "Windows": # On Windows, use errors='ignore' self.stream.reconfigure(encoding='utf-8', errors='ignore') else: # On Unix-like systems, use backslashreplace self.stream.reconfigure(encoding='utf-8', errors='backslashreplace') except (AttributeError, OSError): # If reconfigure is not available or fails, continue silently pass def setup_safe_logging(log_level=logging.INFO, log_file='logs/safe_logging.log'): """Setup logging with SafeFormatter and UTF-8 encoding Args: log_level: Logging level (default: INFO) log_file: Path to log file (default: logs/safe_logging.log) """ # Ensure logs directory exists log_path = Path(log_file) log_path.parent.mkdir(parents=True, exist_ok=True) # Clear existing handlers to avoid duplicates root_logger = logging.getLogger() for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) # Create handlers with proper encoding handlers = [] # Console handler with safe UTF-8 handling console_handler = SafeStreamHandler(sys.stdout) console_handler.setFormatter(SafeFormatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )) handlers.append(console_handler) # File handler with UTF-8 encoding and error handling try: encoding_kwargs = { "encoding": "utf-8", "errors": "ignore" if platform.system() == "Windows" else "backslashreplace" } file_handler = logging.FileHandler(log_file, **encoding_kwargs) file_handler.setFormatter(SafeFormatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )) handlers.append(file_handler) except (OSError, IOError) as e: # If file handler fails, just use console handler print(f"Warning: Could not create log file {log_file}: {e}", file=sys.stderr) # Configure root logger logging.basicConfig( level=log_level, handlers=handlers, force=True # Force reconfiguration ) # Apply SafeFormatter to all existing loggers safe_formatter = SafeFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') for logger_name in logging.Logger.manager.loggerDict: logger = logging.getLogger(logger_name) for handler in logger.handlers: handler.setFormatter(safe_formatter)