""" Logging system for SuperClaude installation suite """ import logging import sys from datetime import datetime from pathlib import Path from typing import Optional, Dict, Any from enum import Enum from .ui import Colors from .symbols import symbols from .paths import get_home_directory class LogLevel(Enum): """Log levels""" DEBUG = logging.DEBUG INFO = logging.INFO WARNING = logging.WARNING ERROR = logging.ERROR CRITICAL = logging.CRITICAL class Logger: """Enhanced logger with console and file output""" def __init__(self, name: str = "superclaude", log_dir: Optional[Path] = None, console_level: LogLevel = LogLevel.INFO, file_level: LogLevel = LogLevel.DEBUG): """ Initialize logger Args: name: Logger name log_dir: Directory for log files (defaults to ~/.claude/logs) console_level: Minimum level for console output file_level: Minimum level for file output """ self.name = name self.log_dir = log_dir or (get_home_directory() / ".claude" / "logs") self.console_level = console_level self.file_level = file_level self.session_start = datetime.now() # Create logger self.logger = logging.getLogger(name) self.logger.setLevel(logging.DEBUG) # Accept all levels, handlers will filter # Remove existing handlers to avoid duplicates self.logger.handlers.clear() # Setup handlers self._setup_console_handler() self._setup_file_handler() self.log_counts: Dict[str, int] = { 'debug': 0, 'info': 0, 'warning': 0, 'error': 0, 'critical': 0 } def _setup_console_handler(self) -> None: """Setup colorized console handler""" handler = logging.StreamHandler(sys.stdout) handler.setLevel(self.console_level.value) # Custom formatter with colors class ColorFormatter(logging.Formatter): def format(self, record): # Color mapping colors = { 'DEBUG': Colors.WHITE, 'INFO': Colors.BLUE, 'WARNING': Colors.YELLOW, 'ERROR': Colors.RED, 'CRITICAL': Colors.RED + Colors.BRIGHT } # Prefix mapping prefixes = { 'DEBUG': '[DEBUG]', 'INFO': '[INFO]', 'WARNING': '[!]', 'ERROR': f'[{symbols.crossmark}]', 'CRITICAL': '[CRITICAL]' } color = colors.get(record.levelname, Colors.WHITE) prefix = prefixes.get(record.levelname, '[LOG]') return f"{color}{prefix} {record.getMessage()}{Colors.RESET}" handler.setFormatter(ColorFormatter()) self.logger.addHandler(handler) def _setup_file_handler(self) -> None: """Setup file handler with rotation""" try: # Ensure log directory exists self.log_dir.mkdir(parents=True, exist_ok=True) # Create timestamped log file timestamp = self.session_start.strftime("%Y%m%d_%H%M%S") log_file = self.log_dir / f"{self.name}_{timestamp}.log" handler = logging.FileHandler(log_file, encoding='utf-8') handler.setLevel(self.file_level.value) # Detailed formatter for files formatter = logging.Formatter( '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) handler.setFormatter(formatter) self.logger.addHandler(handler) self.log_file = log_file # Clean up old log files (keep last 10) self._cleanup_old_logs() except Exception as e: # If file logging fails, continue with console only print(f"{Colors.YELLOW}[!] Could not setup file logging: {e}{Colors.RESET}") self.log_file = None def _cleanup_old_logs(self, keep_count: int = 10) -> None: """Clean up old log files""" try: # Get all log files for this logger log_files = list(self.log_dir.glob(f"{self.name}_*.log")) # Sort by modification time, newest first log_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) # Remove old files for old_file in log_files[keep_count:]: try: old_file.unlink() except OSError: pass # Ignore errors when cleaning up except Exception: pass # Ignore cleanup errors def debug(self, message: str, **kwargs) -> None: """Log debug message""" self.logger.debug(message, **kwargs) self.log_counts['debug'] += 1 def info(self, message: str, **kwargs) -> None: """Log info message""" self.logger.info(message, **kwargs) self.log_counts['info'] += 1 def warning(self, message: str, **kwargs) -> None: """Log warning message""" self.logger.warning(message, **kwargs) self.log_counts['warning'] += 1 def error(self, message: str, **kwargs) -> None: """Log error message""" self.logger.error(message, **kwargs) self.log_counts['error'] += 1 def critical(self, message: str, **kwargs) -> None: """Log critical message""" self.logger.critical(message, **kwargs) self.log_counts['critical'] += 1 def success(self, message: str, **kwargs) -> None: """Log success message (info level with special formatting)""" # Use a custom success formatter for console if self.logger.handlers: console_handler = self.logger.handlers[0] if hasattr(console_handler, 'formatter'): original_format = console_handler.formatter.format def success_format(record): return f"{Colors.GREEN}[{symbols.checkmark}] {record.getMessage()}{Colors.RESET}" console_handler.formatter.format = success_format self.logger.info(message, **kwargs) console_handler.formatter.format = original_format else: self.logger.info(f"SUCCESS: {message}", **kwargs) else: self.logger.info(f"SUCCESS: {message}", **kwargs) self.log_counts['info'] += 1 def step(self, step: int, total: int, message: str, **kwargs) -> None: """Log step progress""" step_msg = f"[{step}/{total}] {message}" self.info(step_msg, **kwargs) def section(self, title: str, **kwargs) -> None: """Log section header""" separator = "=" * min(50, len(title) + 4) self.info(separator, **kwargs) self.info(f" {title}", **kwargs) self.info(separator, **kwargs) def exception(self, message: str, exc_info: bool = True, **kwargs) -> None: """Log exception with traceback""" self.logger.error(message, exc_info=exc_info, **kwargs) self.log_counts['error'] += 1 def log_system_info(self, info: Dict[str, Any]) -> None: """Log system information""" self.section("System Information") for key, value in info.items(): self.info(f"{key}: {value}") def log_operation_start(self, operation: str, details: Optional[Dict[str, Any]] = None) -> None: """Log start of operation""" self.section(f"Starting: {operation}") if details: for key, value in details.items(): self.info(f"{key}: {value}") def log_operation_end(self, operation: str, success: bool, duration: float, details: Optional[Dict[str, Any]] = None) -> None: """Log end of operation""" status = "SUCCESS" if success else "FAILED" self.info(f"Operation {operation} completed: {status} (Duration: {duration:.2f}s)") if details: for key, value in details.items(): self.info(f"{key}: {value}") def get_statistics(self) -> Dict[str, Any]: """Get logging statistics""" runtime = datetime.now() - self.session_start return { 'session_start': self.session_start.isoformat(), 'runtime_seconds': runtime.total_seconds(), 'log_counts': self.log_counts.copy(), 'total_messages': sum(self.log_counts.values()), 'log_file': str(self.log_file) if hasattr(self, 'log_file') and self.log_file else None, 'has_errors': self.log_counts['error'] + self.log_counts['critical'] > 0 } def set_console_level(self, level: LogLevel) -> None: """Change console logging level""" self.console_level = level if self.logger.handlers: self.logger.handlers[0].setLevel(level.value) def set_file_level(self, level: LogLevel) -> None: """Change file logging level""" self.file_level = level if len(self.logger.handlers) > 1: self.logger.handlers[1].setLevel(level.value) def flush(self) -> None: """Flush all handlers""" for handler in self.logger.handlers: if hasattr(handler, 'flush'): handler.flush() def close(self) -> None: """Close logger and handlers""" self.section("Installation Session Complete") stats = self.get_statistics() self.info(f"Total runtime: {stats['runtime_seconds']:.1f} seconds") self.info(f"Messages logged: {stats['total_messages']}") if stats['has_errors']: self.warning(f"Errors/warnings: {stats['log_counts']['error'] + stats['log_counts']['warning']}") if stats['log_file']: self.info(f"Full log saved to: {stats['log_file']}") # Close all handlers for handler in self.logger.handlers[:]: handler.close() self.logger.removeHandler(handler) # Global logger instance _global_logger: Optional[Logger] = None def get_logger(name: str = "superclaude") -> Logger: """Get or create global logger instance""" global _global_logger if _global_logger is None or _global_logger.name != name: _global_logger = Logger(name) return _global_logger def setup_logging(name: str = "superclaude", log_dir: Optional[Path] = None, console_level: LogLevel = LogLevel.INFO, file_level: LogLevel = LogLevel.DEBUG) -> Logger: """Setup logging with specified configuration""" global _global_logger _global_logger = Logger(name, log_dir, console_level, file_level) return _global_logger # Convenience functions using global logger def debug(message: str, **kwargs) -> None: """Log debug message using global logger""" get_logger().debug(message, **kwargs) def info(message: str, **kwargs) -> None: """Log info message using global logger""" get_logger().info(message, **kwargs) def warning(message: str, **kwargs) -> None: """Log warning message using global logger""" get_logger().warning(message, **kwargs) def error(message: str, **kwargs) -> None: """Log error message using global logger""" get_logger().error(message, **kwargs) def critical(message: str, **kwargs) -> None: """Log critical message using global logger""" get_logger().critical(message, **kwargs) def success(message: str, **kwargs) -> None: """Log success message using global logger""" get_logger().success(message, **kwargs)