""" Centralized logging configuration for MarkiTect. Provides environment-based configuration, structured logging setup, and integration with the existing configuration system. """ import os import logging import logging.config import logging.handlers from typing import Dict, Any, Optional from dataclasses import dataclass from enum import Enum from .formatters import DevelopmentFormatter, ProductionFormatter class LogLevel(Enum): """Supported log levels.""" DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" class LogFormat(Enum): """Supported log formats.""" DEVELOPMENT = "development" PRODUCTION = "production" JSON = "json" @dataclass class LoggingConfig: """Logging configuration settings.""" level: LogLevel = LogLevel.INFO format_type: LogFormat = LogFormat.DEVELOPMENT enable_console: bool = True enable_file: bool = False file_path: Optional[str] = None max_file_size: int = 10 * 1024 * 1024 # 10MB backup_count: int = 5 enable_context: bool = True enable_performance: bool = False # Component-specific levels component_levels: Dict[str, LogLevel] = None def __post_init__(self): if self.component_levels is None: self.component_levels = {} def get_logging_config() -> LoggingConfig: """ Get logging configuration from environment variables. Environment Variables: MARKITECT_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) MARKITECT_LOG_FORMAT: Log format (development, production, json) MARKITECT_LOG_CONSOLE: Enable console logging (true/false) MARKITECT_LOG_FILE: Enable file logging (true/false) MARKITECT_LOG_FILE_PATH: File path for file logging MARKITECT_LOG_FILE_SIZE: Maximum file size in bytes MARKITECT_LOG_BACKUP_COUNT: Number of backup files to keep MARKITECT_LOG_CONTEXT: Enable context logging (true/false) MARKITECT_LOG_PERFORMANCE: Enable performance logging (true/false) # Component-specific levels MARKITECT_LOG_LEVEL_INFRASTRUCTURE: Log level for infrastructure components MARKITECT_LOG_LEVEL_DOMAIN: Log level for domain components MARKITECT_LOG_LEVEL_APPLICATION: Log level for application components """ config = LoggingConfig() # Main log level level_str = os.getenv('MARKITECT_LOG_LEVEL', config.level.value) try: config.level = LogLevel(level_str.upper()) except ValueError: config.level = LogLevel.INFO # Log format format_str = os.getenv('MARKITECT_LOG_FORMAT', config.format_type.value) try: config.format_type = LogFormat(format_str.lower()) except ValueError: config.format_type = LogFormat.DEVELOPMENT # Console and file logging config.enable_console = _parse_bool(os.getenv('MARKITECT_LOG_CONSOLE', 'true')) config.enable_file = _parse_bool(os.getenv('MARKITECT_LOG_FILE', 'false')) config.file_path = os.getenv('MARKITECT_LOG_FILE_PATH') # File rotation settings try: config.max_file_size = int(os.getenv('MARKITECT_LOG_FILE_SIZE', str(config.max_file_size))) except ValueError: pass try: config.backup_count = int(os.getenv('MARKITECT_LOG_BACKUP_COUNT', str(config.backup_count))) except ValueError: pass # Context and performance config.enable_context = _parse_bool(os.getenv('MARKITECT_LOG_CONTEXT', 'true')) config.enable_performance = _parse_bool(os.getenv('MARKITECT_LOG_PERFORMANCE', 'false')) # Component-specific levels component_prefixes = ['INFRASTRUCTURE', 'DOMAIN', 'APPLICATION'] for prefix in component_prefixes: env_var = f'MARKITECT_LOG_LEVEL_{prefix}' level_str = os.getenv(env_var) if level_str: try: config.component_levels[prefix.lower()] = LogLevel(level_str.upper()) except ValueError: pass return config def setup_logging(config: Optional[LoggingConfig] = None) -> None: """ Set up logging configuration for the entire application. Args: config: Optional logging configuration. If None, loads from environment. """ if config is None: config = get_logging_config() # Create logging dictionary configuration log_config = _create_logging_dict_config(config) # Apply the configuration logging.config.dictConfig(log_config) # Set component-specific levels _configure_component_loggers(config) # Log the configuration setup logger = logging.getLogger('infrastructure.logging.config') logger.info(f"Logging configured with level={config.level.value}, format={config.format_type.value}") def _create_logging_dict_config(config: LoggingConfig) -> Dict[str, Any]: """Create logging dictionary configuration.""" log_config = { 'version': 1, 'disable_existing_loggers': False, 'formatters': {}, 'handlers': {}, 'loggers': {}, 'root': { 'level': config.level.value, 'handlers': [] } } # Configure formatters if config.format_type in (LogFormat.DEVELOPMENT, LogFormat.PRODUCTION): formatter_class = DevelopmentFormatter if config.format_type == LogFormat.DEVELOPMENT else ProductionFormatter log_config['formatters']['standard'] = { '()': f'{formatter_class.__module__}.{formatter_class.__name__}', 'enable_context': config.enable_context } else: # JSON format log_config['formatters']['standard'] = { 'format': '%(message)s', 'class': 'pythonjsonlogger.jsonlogger.JsonFormatter' } # Configure console handler if config.enable_console: log_config['handlers']['console'] = { 'class': 'logging.StreamHandler', 'level': config.level.value, 'formatter': 'standard', 'stream': 'ext://sys.stdout' } log_config['root']['handlers'].append('console') # Configure file handler if config.enable_file and config.file_path: log_config['handlers']['file'] = { 'class': 'logging.handlers.RotatingFileHandler', 'level': config.level.value, 'formatter': 'standard', 'filename': config.file_path, 'maxBytes': config.max_file_size, 'backupCount': config.backup_count, 'encoding': 'utf-8' } log_config['root']['handlers'].append('file') return log_config def _configure_component_loggers(config: LoggingConfig) -> None: """Configure component-specific logger levels.""" component_mappings = { 'infrastructure': [ 'infrastructure', 'infrastructure.repositories', 'infrastructure.connection_manager', 'infrastructure.config', 'infrastructure.logging' ], 'domain': [ 'domain', 'domain.issues', 'domain.projects', 'domain.services' ], 'application': [ 'application', 'tddai', 'markitect' ] } for component, level in config.component_levels.items(): logger_names = component_mappings.get(component, []) for logger_name in logger_names: logger = logging.getLogger(logger_name) logger.setLevel(level.value) def _parse_bool(value: str) -> bool: """Parse boolean value from string.""" return value.lower() in ('true', '1', 'yes', 'on', 'enabled') def validate_logging_config(config: LoggingConfig) -> tuple[bool, list[str]]: """ Validate logging configuration. Returns: Tuple of (is_valid, error_messages) """ errors = [] # Validate file path if file logging is enabled if config.enable_file: if not config.file_path: errors.append("File logging enabled but no file path specified") else: # Check if directory exists and is writable import os from pathlib import Path file_path = Path(config.file_path) parent_dir = file_path.parent if not parent_dir.exists(): try: parent_dir.mkdir(parents=True, exist_ok=True) except OSError as e: errors.append(f"Cannot create log directory {parent_dir}: {e}") if parent_dir.exists() and not os.access(parent_dir, os.W_OK): errors.append(f"Log directory {parent_dir} is not writable") # Validate file size and backup count if config.max_file_size <= 0: errors.append("Maximum file size must be positive") if config.backup_count < 0: errors.append("Backup count must be non-negative") # Validate at least one output is enabled if not config.enable_console and not config.enable_file: errors.append("At least one output (console or file) must be enabled") return len(errors) == 0, errors # Default configurations for different environments DEFAULT_DEVELOPMENT_CONFIG = LoggingConfig( level=LogLevel.DEBUG, format_type=LogFormat.DEVELOPMENT, enable_console=True, enable_file=False, enable_context=True, enable_performance=True ) DEFAULT_PRODUCTION_CONFIG = LoggingConfig( level=LogLevel.INFO, format_type=LogFormat.PRODUCTION, enable_console=True, enable_file=True, file_path='logs/markitect.log', enable_context=True, enable_performance=False ) DEFAULT_TESTING_CONFIG = LoggingConfig( level=LogLevel.WARNING, format_type=LogFormat.DEVELOPMENT, enable_console=False, enable_file=False, enable_context=False, enable_performance=False )