mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-17 17:56:46 +00:00
Major reorganization of SuperClaude V4 Beta directories: - Moved SuperClaude-Lite content to Framework-Hooks/ - Renamed SuperClaude/ directories to Framework/ for clarity - Created separate Framework-Lite/ for lightweight variant - Consolidated hooks system under Framework-Hooks/ This restructuring aligns with the V4 Beta architecture: - Framework/: Full framework with all features - Framework-Lite/: Lightweight variant - Framework-Hooks/: Hooks system implementation Part of SuperClaude V4 Beta development roadmap. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
275 lines
9.4 KiB
Python
275 lines
9.4 KiB
Python
"""
|
|
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) |