"""Centralised logging configuration for Portfolio Intelligence Platform. This module provides unified logging control via environment variables: - LOG_LEVEL: Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL, OFF) - LOGGING_ENABLED: Enable/disable all logging (true/false) Usage: from backend.logging_config import configure_logging # Call early in app startup (before other imports) configure_logging() """ import os import logging import sys from typing import Optional # Mapping of string levels to logging constants LOG_LEVELS = { "DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "WARN": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL, "OFF": logging.CRITICAL + 10, # Higher than any level to disable all "NONE": logging.CRITICAL + 10, } # Third-party loggers to suppress in production THIRD_PARTY_LOGGERS = [ "gradio", "gradio_client", "httpx", "uvicorn", "uvicorn.access", "uvicorn.error", "matplotlib", "matplotlib.font_manager", "PIL", "httpcore", "websockets", "asyncio", "multipart", "charset_normalizer", ] class NullHandler(logging.Handler): """A handler that does nothing - used to completely disable logging.""" def emit(self, record: logging.LogRecord) -> None: pass def get_log_level() -> int: """Get the configured log level from environment. Reads LOG_LEVEL environment variable and returns corresponding logging constant. Defaults to INFO if not set or invalid. Returns: Logging level constant (e.g., logging.INFO) """ level_str = os.getenv("LOG_LEVEL", "INFO").upper().strip() return LOG_LEVELS.get(level_str, logging.INFO) def is_logging_enabled() -> bool: """Check if logging is enabled via environment variable. Reads LOGGING_ENABLED environment variable. Defaults to True if not set. Accepts 'true', '1', 'yes', 'on' as truthy values. Returns: True if logging is enabled, False otherwise """ value = os.getenv("LOGGING_ENABLED", "true").lower().strip() return value in ("true", "1", "yes", "on") def is_production() -> bool: """Check if running in production environment. Detects HuggingFace Spaces via SPACE_ID environment variable or ENVIRONMENT=production. Returns: True if in production environment """ return bool(os.getenv("SPACE_ID")) or os.getenv("ENVIRONMENT") == "production" def configure_logging( level: Optional[int] = None, enabled: Optional[bool] = None, suppress_third_party: bool = True, format_string: Optional[str] = None, ) -> None: """Configure logging for the entire application. This function should be called early in application startup, before importing other modules that create loggers. Args: level: Override log level (uses LOG_LEVEL env var if None) enabled: Override enabled state (uses LOGGING_ENABLED env var if None) suppress_third_party: Whether to suppress noisy third-party loggers format_string: Custom format string (uses default if None) Environment Variables: LOG_LEVEL: DEBUG, INFO, WARNING, ERROR, CRITICAL, OFF LOGGING_ENABLED: true/false to enable/disable all logging Examples: # Use environment variables configure_logging() # Override with specific level configure_logging(level=logging.DEBUG) # Completely disable logging configure_logging(enabled=False) """ # Determine final settings log_level = level if level is not None else get_log_level() logging_enabled = enabled if enabled is not None else is_logging_enabled() # Get root logger root_logger = logging.getLogger() # Clear any existing handlers root_logger.handlers.clear() if not logging_enabled or log_level >= LOG_LEVELS["OFF"]: # Completely disable logging root_logger.addHandler(NullHandler()) root_logger.setLevel(LOG_LEVELS["OFF"]) return # Set up console handler handler = logging.StreamHandler(sys.stderr) handler.setLevel(log_level) # Configure format if format_string is None: if is_production(): # Compact format for production format_string = "%(levelname)s:%(name)s:%(message)s" else: # Detailed format for development format_string = "%(asctime)s - %(levelname)s - %(name)s - %(message)s" formatter = logging.Formatter(format_string) handler.setFormatter(formatter) # Configure root logger root_logger.addHandler(handler) root_logger.setLevel(log_level) # Suppress third-party loggers in production if suppress_third_party and is_production(): for logger_name in THIRD_PARTY_LOGGERS: logging.getLogger(logger_name).setLevel(logging.WARNING) # Always suppress matplotlib font manager (very noisy) logging.getLogger("matplotlib.font_manager").setLevel(logging.WARNING) # Note: Starlette BaseHTTPMiddleware ASGI assertion errors (triggered by # Gradio's "Use via API" button) cannot be suppressed via logging filters # because uvicorn prints them directly to stderr. This is a known # Gradio/Starlette limitation - the app works correctly despite the errors. # Log the configuration (if not suppressed) if log_level <= logging.INFO: config_logger = logging.getLogger(__name__) config_logger.info( f"Logging configured: level={logging.getLevelName(log_level)}, " f"production={is_production()}" ) def get_logger(name: str) -> logging.Logger: """Get a logger with the specified name. Convenience function that ensures logging is configured before returning the logger. Args: name: Logger name (typically __name__) Returns: Configured logger instance """ return logging.getLogger(name)