BrianIsaac's picture
docs: document ASGI error limitation, remove ineffective filter
c69799e
"""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)