mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-17 17:56:46 +00:00
330 lines
12 KiB
Python
330 lines
12 KiB
Python
"""
|
|
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
|
|
|
|
|
|
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 (Path.home() / ".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': '[✗]',
|
|
'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}[✓] {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) |