""" Simple logger for SuperClaude-Lite hooks. Provides structured logging of hook events for later analysis. Focuses on capturing hook lifecycle, decisions, and errors in a structured format without any analysis or complex features. """ import json import logging import os import time from datetime import datetime, timezone, timedelta from pathlib import Path from typing import Optional, Dict, Any import uuid import glob # Import configuration loader try: from .yaml_loader import UnifiedConfigLoader except ImportError: # Fallback if yaml_loader is not available UnifiedConfigLoader = None class HookLogger: """Simple logger for SuperClaude-Lite hooks.""" def __init__(self, log_dir: str = None, retention_days: int = None): """ Initialize the logger. Args: log_dir: Directory to store log files. Defaults to cache/logs/ retention_days: Number of days to keep log files. Defaults to 30. """ # Load configuration self.config = self._load_config() # Check if logging is enabled if not self.config.get('logging', {}).get('enabled', True): self.enabled = False return self.enabled = True # Set up log directory if log_dir is None: # Get SuperClaude-Lite root directory (2 levels up from shared/) root_dir = Path(__file__).parent.parent.parent log_dir_config = self.config.get('logging', {}).get('file_settings', {}).get('log_directory', 'cache/logs') log_dir = root_dir / log_dir_config self.log_dir = Path(log_dir) self.log_dir.mkdir(parents=True, exist_ok=True) # Log retention settings if retention_days is None: retention_days = self.config.get('logging', {}).get('file_settings', {}).get('retention_days', 30) self.retention_days = retention_days # Session ID for correlating events self.session_id = str(uuid.uuid4())[:8] # Set up Python logger self._setup_logger() # Clean up old logs on initialization self._cleanup_old_logs() def _load_config(self) -> Dict[str, Any]: """Load logging configuration from YAML file.""" if UnifiedConfigLoader is None: # Return default configuration if loader not available return { 'logging': { 'enabled': True, 'level': 'INFO', 'file_settings': { 'log_directory': 'cache/logs', 'retention_days': 30 } } } try: # Get project root root_dir = Path(__file__).parent.parent.parent loader = UnifiedConfigLoader(root_dir) # Load logging configuration config = loader.load_yaml('logging') return config or {} except Exception: # Return default configuration on error return { 'logging': { 'enabled': True, 'level': 'INFO', 'file_settings': { 'log_directory': 'cache/logs', 'retention_days': 30 } } } def _setup_logger(self): """Set up the Python logger with JSON formatting.""" self.logger = logging.getLogger("superclaude_lite_hooks") # Set log level from configuration log_level_str = self.config.get('logging', {}).get('level', 'INFO').upper() log_level = getattr(logging, log_level_str, logging.INFO) self.logger.setLevel(log_level) # Remove existing handlers to avoid duplicates self.logger.handlers.clear() # Create daily log file today = datetime.now().strftime("%Y-%m-%d") log_file = self.log_dir / f"superclaude-lite-{today}.log" # File handler handler = logging.FileHandler(log_file, mode='a', encoding='utf-8') handler.setLevel(logging.INFO) # Simple formatter - just output the message (which is already JSON) formatter = logging.Formatter('%(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) def _create_event(self, event_type: str, hook_name: str, data: Dict[str, Any] = None) -> Dict[str, Any]: """Create a structured event.""" event = { "timestamp": datetime.now(timezone.utc).isoformat(), "session": self.session_id, "hook": hook_name, "event": event_type } if data: event["data"] = data return event def _should_log_event(self, hook_name: str, event_type: str) -> bool: """Check if this event should be logged based on configuration.""" if not self.enabled: return False # Check hook-specific configuration hook_config = self.config.get('hook_configuration', {}).get(hook_name, {}) if not hook_config.get('enabled', True): return False # Check event type configuration hook_logging = self.config.get('logging', {}).get('hook_logging', {}) event_mapping = { 'start': 'log_lifecycle', 'end': 'log_lifecycle', 'decision': 'log_decisions', 'error': 'log_errors' } config_key = event_mapping.get(event_type, 'log_lifecycle') return hook_logging.get(config_key, True) def log_hook_start(self, hook_name: str, context: Optional[Dict[str, Any]] = None): """Log the start of a hook execution.""" if not self._should_log_event(hook_name, 'start'): return event = self._create_event("start", hook_name, context) self.logger.info(json.dumps(event)) def log_hook_end(self, hook_name: str, duration_ms: int, success: bool, result: Optional[Dict[str, Any]] = None): """Log the end of a hook execution.""" if not self._should_log_event(hook_name, 'end'): return data = { "duration_ms": duration_ms, "success": success } if result: data["result"] = result event = self._create_event("end", hook_name, data) self.logger.info(json.dumps(event)) def log_decision(self, hook_name: str, decision_type: str, choice: str, reason: str): """Log a decision made by a hook.""" if not self._should_log_event(hook_name, 'decision'): return data = { "type": decision_type, "choice": choice, "reason": reason } event = self._create_event("decision", hook_name, data) self.logger.info(json.dumps(event)) def log_error(self, hook_name: str, error: str, context: Optional[Dict[str, Any]] = None): """Log an error that occurred in a hook.""" if not self._should_log_event(hook_name, 'error'): return data = { "error": error } if context: data["context"] = context event = self._create_event("error", hook_name, data) self.logger.info(json.dumps(event)) def _cleanup_old_logs(self): """Remove log files older than retention_days.""" if self.retention_days <= 0: return cutoff_date = datetime.now() - timedelta(days=self.retention_days) # Find all log files log_pattern = self.log_dir / "superclaude-lite-*.log" for log_file in glob.glob(str(log_pattern)): try: # Extract date from filename filename = os.path.basename(log_file) date_str = filename.replace("superclaude-lite-", "").replace(".log", "") file_date = datetime.strptime(date_str, "%Y-%m-%d") # Remove if older than cutoff if file_date < cutoff_date: os.remove(log_file) except (ValueError, OSError): # Skip files that don't match expected format or can't be removed continue # Global logger instance _logger = None def get_logger() -> HookLogger: """Get the global logger instance.""" global _logger if _logger is None: _logger = HookLogger() return _logger # Convenience functions for easy hook integration def log_hook_start(hook_name: str, context: Optional[Dict[str, Any]] = None): """Log the start of a hook execution.""" get_logger().log_hook_start(hook_name, context) def log_hook_end(hook_name: str, duration_ms: int, success: bool, result: Optional[Dict[str, Any]] = None): """Log the end of a hook execution.""" get_logger().log_hook_end(hook_name, duration_ms, success, result) def log_decision(hook_name: str, decision_type: str, choice: str, reason: str): """Log a decision made by a hook.""" get_logger().log_decision(hook_name, decision_type, choice, reason) def log_error(hook_name: str, error: str, context: Optional[Dict[str, Any]] = None): """Log an error that occurred in a hook.""" get_logger().log_error(hook_name, error, context)