import logging import datetime import os from pathlib import Path from typing import Optional, Any import json from threading import Lock import traceback import inspect class ApplicationLogger: _instance = None _lock = Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self): if not hasattr(self, 'initialized'): self.initialized = True self.log_entries = [] self.max_entries = 50000 # Increased for more detailed logging self.interaction_count = 0 # Create logs directory if it doesn't exist self.log_dir = Path("logs") self.log_dir.mkdir(exist_ok=True) # Setup file logging self.log_file = self.log_dir / f"cpm_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.log" # Configure logger self.logger = logging.getLogger('CPM') self.logger.setLevel(logging.DEBUG) # File handler file_handler = logging.FileHandler(self.log_file, encoding='utf-8') file_handler.setLevel(logging.DEBUG) # Console handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # Detailed Formatter formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s', datefmt='%Y-%m-%d %H:%M:%S.%f'[:-3] # Include milliseconds ) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # Add handlers self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) # Custom handler to store in memory self.memory_handler = MemoryHandler(self) self.memory_handler.setFormatter(formatter) self.logger.addHandler(self.memory_handler) self.info("Claude Project Manager - Logger initialized") def debug(self, message: str, extra: Optional[dict] = None): self._log('DEBUG', message, extra) def info(self, message: str, extra: Optional[dict] = None): self._log('INFO', message, extra) def warning(self, message: str, extra: Optional[dict] = None): self._log('WARNING', message, extra) def error(self, message: str, extra: Optional[dict] = None, exc_info=None): self._log('ERROR', message, extra, exc_info) def critical(self, message: str, extra: Optional[dict] = None, exc_info=None): self._log('CRITICAL', message, extra, exc_info) def _log(self, level: str, message: str, extra: Optional[dict] = None, exc_info=None): # Get caller information frame = inspect.currentframe() if frame and frame.f_back and frame.f_back.f_back: caller = frame.f_back.f_back caller_info = f"{caller.f_code.co_filename}:{caller.f_lineno}" else: caller_info = "unknown" log_method = getattr(self.logger, level.lower()) if extra: message = f"{message} | Extra: {json.dumps(extra, default=str)}" if exc_info: log_method(message, exc_info=exc_info) else: log_method(message) def add_entry(self, timestamp: str, level: str, message: str): """Add log entry to memory buffer""" with self._lock: self.log_entries.append({ 'timestamp': timestamp, 'level': level, 'message': message }) # Keep only the latest entries if len(self.log_entries) > self.max_entries: self.log_entries = self.log_entries[-self.max_entries:] def get_log_content(self) -> str: """Get all log entries as formatted string""" with self._lock: lines = [] for entry in self.log_entries: lines.append(f"{entry['timestamp']} - {entry['level']} - {entry['message']}") return '\n'.join(lines) def export_logs(self, filepath: str, include_system_info: bool = True): """Export logs to a file with optional system information""" content = [] if include_system_info: content.append("=" * 80) content.append("CLAUDE PROJECT MANAGER - LOG EXPORT") content.append("=" * 80) content.append(f"Export Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") content.append(f"Log File: {self.log_file}") content.append(f"Total Interactions: {self.interaction_count}") content.append(f"Total Log Entries: {len(self.log_entries)}") content.append("=" * 80) content.append("") content.append(self.get_log_content()) with open(filepath, 'w', encoding='utf-8') as f: f.write('\n'.join(content) if isinstance(content, list) else content) def clear_logs(self): """Clear in-memory log entries""" with self._lock: self.log_entries.clear() self.info("Log entries cleared") class MemoryHandler(logging.Handler): """Custom handler to store logs in memory""" def __init__(self, logger_instance): super().__init__() self.logger_instance = logger_instance def emit(self, record): try: msg = self.format(record) # Extract timestamp, level and message parts = msg.split(' - ', 3) if len(parts) >= 4: timestamp = parts[0] level = parts[2] message = parts[3] self.logger_instance.add_entry(timestamp, level, message) except Exception: self.handleError(record) def log_interaction(self, component: str, action: str, details: Optional[dict] = None): """Log UI interactions""" self.interaction_count += 1 interaction_msg = f"UI_INTERACTION #{self.interaction_count} - Component: {component}, Action: {action}" if details: interaction_msg += f", Details: {json.dumps(details, default=str)}" self.info(interaction_msg) def log_git_operation(self, operation: str, status: str, details: Optional[dict] = None): """Log Git operations with details""" git_msg = f"GIT_OPERATION - Operation: {operation}, Status: {status}" if details: git_msg += f", Details: {json.dumps(details, default=str)}" self.info(git_msg) def log_exception(self, exception: Exception, context: str = ""): """Log exceptions with full traceback""" tb = traceback.format_exc() error_msg = f"EXCEPTION in {context}: {type(exception).__name__}: {str(exception)}\nTraceback:\n{tb}" self.error(error_msg) def log_method_call(self, method_name: str, args: tuple = None, kwargs: dict = None): """Log method calls with arguments""" call_msg = f"METHOD_CALL - {method_name}" if args: call_msg += f", Args: {args}" if kwargs: call_msg += f", Kwargs: {kwargs}" self.debug(call_msg) def log_state_change(self, component: str, old_state: Any, new_state: Any): """Log state changes in the application""" state_msg = f"STATE_CHANGE - Component: {component}, Old: {old_state}, New: {new_state}" self.info(state_msg) def get_full_log_path(self) -> str: """Get the full path to the current log file""" return str(self.log_file.absolute()) def get_all_log_files(self) -> list: """Get all log files in the log directory""" return sorted([f for f in self.log_dir.glob('*.log')], reverse=True) # Global logger instance logger = ApplicationLogger()