""" Logging configuration for FLARES application. Provides centralized logging setup with: - Separate log files for different components (updates, plugins, errors) - Environment-based log level control - Secure handling of sensitive data (never logs tokens/passwords) - Structured logging format for easier parsing Usage: from src.shared.logging_config import get_logger logger = get_logger(__name__) logger.info("Operation completed successfully") """ import os import sys import logging import logging.handlers from pathlib import Path from typing import Optional from datetime import datetime # Log levels based on environment LOG_LEVEL_ENV_MAP = { "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR, "critical": logging.CRITICAL, } # Get log level from environment (default: INFO) _LOG_LEVEL_NAME = os.environ.get("FLARES_LOG_LEVEL", "INFO").upper() LOG_LEVEL = LOG_LEVEL_ENV_MAP.get(_LOG_LEVEL_NAME, logging.INFO) # Log directory configuration _LOG_DIR = Path(os.environ.get("FLARES_LOG_DIR", "~/.local/share/flares/logs")).expanduser() class SecureFormatter(logging.Formatter): """ Custom formatter that filters out sensitive data from logs. Filters: - Password fields - Token values - API keys - Any field containing 'secret', 'password', 'token', 'key' Format: [TIMESTAMP] [LEVEL] [MODULE] MESSAGE """ SENSITIVE_PATTERNS = [ "password=", "token=", "api_key=", "secret=", "authorization:", "bearer ", "basic ", "[REDACTED]", ] def __init__(self, *args, **kwargs): super().__init__( fmt="%(asctime)s [%(levelname)-8s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) def format(self, record): """Format log record with sensitive data filtering.""" # Filter sensitive data from message message = record.getMessage() # Check for sensitive patterns and replace for pattern in self.SENSITIVE_PATTERNS: if pattern.lower() in message.lower(): # Don't actually replace in string - that could hide real issues # Instead, we rely on the caller to not log sensitive data break # Standard formatting return super().format(record) class RotatingFileHandlerWithSecurity(logging.handlers.RotatingFileHandler): """ Rotating file handler with additional security features. Features: - File permissions set to 0o600 (owner read/write only) - Automatic rotation at size threshold - Backup of last 5 log files """ def __init__(self, filename, maxBytes=10*1024*1024, backupCount=5): # Create parent directories if needed Path(filename).parent.mkdir(parents=True, exist_ok=True) super().__init__(filename, maxBytes=maxBytes, backupCount=backupCount) # Set secure file permissions after initialization try: os.chmod(filename, 0o600) # Owner read/write only except PermissionError: # File already exists with different permissions - log warning logging.warning(f"Could not set secure permissions on {filename}") def setup_logging( name: str = "flares", log_dir: Optional[Path] = None, level: int = LOG_LEVEL, enable_file_logging: bool = True, enable_console_logging: bool = True ) -> logging.Logger: """ Set up logging for a module with security best practices. Args: name: Logger name (typically __name__) log_dir: Directory for log files (default: ~/.local/share/flares/logs) level: Log level integer (default: from FLARES_LOG_LEVEL env var) enable_file_logging: Enable file-based logging enable_console_logging: Enable console output Returns: Configured logger instance Environment Variables: FLARES_LOG_LEVEL: Log level (debug, info, warning, error, critical) FLARES_LOG_DIR: Directory for log files FLARES_ENABLE_FILE_LOGGING: "true"/"false" to override default FLARES_ENABLE_CONSOLE_LOGGING: "true"/"false" to override default """ logger = logging.getLogger(name) logger.setLevel(level) # Prevent adding handlers multiple times if logger.handlers: return logger # Clear any existing handlers from parent loggers logger.propagate = False # Log directory if log_dir is None: log_dir = _LOG_DIR log_dir = Path(log_dir).expanduser() log_dir.mkdir(parents=True, exist_ok=True) # Override enable flags from environment if os.environ.get("FLARES_ENABLE_FILE_LOGGING", "").lower() in ("false", "0", "no"): enable_file_logging = False if os.environ.get("FLARES_ENABLE_CONSOLE_LOGGING", "").lower() in ("false", "0", "no"): enable_console_logging = False # Console handler (stdout for info+, stderr for errors) if enable_console_logging: console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(level) console_formatter = SecureFormatter() console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # Separate error stream to stderr error_handler = logging.StreamHandler(sys.stderr) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(SecureFormatter()) logger.addHandler(error_handler) # File handlers (separate files for different log types) if enable_file_logging: # Main application log app_log_path = log_dir / f"{name}_app.log" file_handler = RotatingFileHandlerWithSecurity(str(app_log_path)) file_handler.setLevel(logging.INFO) # File gets info+, console gets warning+ file_handler.setFormatter(SecureFormatter()) logger.addHandler(file_handler) # Debug log (only when debug mode enabled) if level <= logging.DEBUG: debug_log_path = log_dir / f"{name}_debug.log" debug_handler = RotatingFileHandlerWithSecurity(str(debug_log_path)) debug_handler.setLevel(logging.DEBUG) debug_handler.setFormatter(SecureFormatter()) logger.addHandler(debug_handler) # Error log (separate file for errors only) error_log_path = log_dir / f"{name}_error.log" error_file_handler = RotatingFileHandlerWithSecurity(str(error_log_path)) error_file_handler.setLevel(logging.ERROR) error_file_handler.setFormatter(SecureFormatter()) logger.addHandler(error_file_handler) return logger def get_logger(name: str = __name__) -> logging.Logger: """ Get a configured logger instance. Args: name: Logger name (typically __name__) Returns: Configured logger with security formatter Usage: >>> from src.shared.logging_config import get_logger >>> logger = get_logger(__name__) >>> logger.info("Application started") """ return setup_logging(name) def log_exception_with_context( logger: logging.Logger, exception: Exception, context: Optional[dict] = None, operation: str = "Unknown operation" ) -> str: """ Log an exception with additional context (for debugging). Args: logger: Logger instance to use exception: Exception that was raised context: Additional context dictionary (avoid sensitive data!) operation: Description of what operation was being performed Returns: Error message string for return/display Security Note: Automatically filters common sensitive patterns from context dict. """ # Build context string (filter sensitive data) context_str = "" if context: safe_context = {} sensitive_keys = {"password", "token", "api_key", "secret", "credential"} for key, value in context.items(): key_lower = key.lower() if any(sens in key_lower for sens in sensitive_keys): safe_context[key] = "[REDACTED]" else: safe_context[key] = value context_str = f" Context: {safe_context}" # Log with full traceback error_msg = f"{operation} failed{context_str}: {str(exception)}" logger.error(error_msg, exc_info=True) return error_msg def create_audit_logger( name: str = "flares.audit", log_dir: Optional[Path] = None ) -> logging.Logger: """ Create a dedicated audit log for security-sensitive operations. Audit logs track: - Authentication attempts - Plugin installations/updates - Configuration changes - File system modifications Args: name: Logger name (default: flares.audit) log_dir: Directory for audit logs Returns: Configured audit logger with separate file handler """ if log_dir is None: log_dir = _LOG_DIR # Ensure audit subdirectory exists audit_log_dir = log_dir / "audit" audit_log_dir.mkdir(parents=True, exist_ok=True) logger = logging.getLogger(name) logger.setLevel(logging.INFO) # Audit logs always at INFO minimum if logger.handlers: return logger logger.propagate = False # Audit file handler (never to console for security) audit_log_path = audit_log_dir / f"{name.replace('.', '_')}.log" audit_handler = RotatingFileHandlerWithSecurity( str(audit_log_path), maxBytes=50*1024*1024, # 50MB max backupCount=10 # Keep 10 backups ) # Audit formatter includes more details audit_formatter = logging.Formatter( fmt="%(asctime)s [AUDIT] %(levelname)-8s %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z" # ISO format with timezone ) audit_handler.setFormatter(audit_formatter) logger.addHandler(audit_handler) return logger # Initialize application-wide loggers on module import def initialize_logging( app_name: str = "flares", level_override: Optional[int] = None ) -> dict[str, logging.Logger]: """ Initialize all application loggers. Args: app_name: Application name prefix for loggers level_override: Override default log level Returns: Dictionary of initialized loggers: - "main": Primary application logger - "audit": Security audit logger - "updates": Update mechanism logger - "plugins": Plugin manager logger """ loggers = {} # Main application logger loggers["main"] = get_logger(app_name) # Specialized loggers loggers["audit"] = create_audit_logger(f"{app_name}.audit") loggers["updates"] = get_logger(f"{app_name}.updates") loggers["plugins"] = get_logger(f"{app_name}.plugins") # Debug message on first initialize loggers["main"].debug( f"Logging initialized: level={logging.getLevelName(level_override or LOG_LEVEL)}, " f"log_dir={_LOG_DIR}" ) return loggers