EU-Utility/core/debug_toolbar.py

462 lines
14 KiB
Python

"""
EU-Utility - Debug Toolbar
Provides debugging utilities, logging, and diagnostics for development.
Part of the developer experience improvements.
"""
import sys
import time
import logging
import functools
import traceback
from pathlib import Path
from typing import Dict, List, Optional, Callable, Any
from datetime import datetime
from dataclasses import dataclass, field, asdict
import json
import threading
@dataclass
class DebugEvent:
"""A single debug event entry."""
timestamp: str
level: str
source: str
message: str
details: Dict[str, Any] = field(default_factory=dict)
traceback: Optional[str] = None
class DebugLogger:
"""
Structured logging system for EU-Utility.
Features:
- Multiple log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- Source tracking (which plugin/component)
- Structured data attachments
- File and console output
- Log rotation
"""
_instance = None
_lock = threading.Lock()
LEVELS = {
'DEBUG': 10,
'INFO': 20,
'WARNING': 30,
'ERROR': 40,
'CRITICAL': 50
}
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, log_dir: Path = None, min_level: str = 'INFO'):
if self._initialized:
return
self._initialized = True
self._log_dir = log_dir or Path('logs')
self._log_dir.mkdir(parents=True, exist_ok=True)
self._min_level = self.LEVELS.get(min_level, 20)
self._events: List[DebugEvent] = []
self._max_events = 1000
self._listeners: List[Callable[[DebugEvent], None]] = []
self._lock = threading.Lock()
# Setup file logging
self._setup_file_handler()
# Console output
self._console_output = True
def _setup_file_handler(self):
"""Setup file handler for persistent logging."""
timestamp = datetime.now().strftime('%Y%m%d')
self._log_file = self._log_dir / f'eu-utility-{timestamp}.log'
# Create file with header
if not self._log_file.exists():
with open(self._log_file, 'w') as f:
f.write(f"# EU-Utility Debug Log - {datetime.now().isoformat()}\n")
f.write("#" * 60 + "\n\n")
def _should_log(self, level: str) -> bool:
"""Check if level should be logged based on min_level."""
return self.LEVELS.get(level, 20) >= self._min_level
def _format_message(self, event: DebugEvent) -> str:
"""Format event for console/file output."""
timestamp = event.timestamp.split('T')[1].split('.')[0]
prefix = f"[{timestamp}] [{event.level:8}] [{event.source:20}]"
if event.details:
details_str = json.dumps(event.details, default=str)
return f"{prefix} {event.message} | {details_str}"
return f"{prefix} {event.message}"
def _write_to_file(self, event: DebugEvent):
"""Write event to log file."""
try:
with open(self._log_file, 'a') as f:
f.write(self._format_message(event) + '\n')
if event.traceback:
f.write(f"TRACEBACK:\n{event.traceback}\n")
f.write("-" * 60 + "\n")
except Exception as e:
print(f"[DebugLogger] Failed to write to file: {e}")
def _notify_listeners(self, event: DebugEvent):
"""Notify all registered listeners."""
for listener in self._listeners:
try:
listener(event)
except Exception as e:
print(f"[DebugLogger] Listener error: {e}")
def log(self, level: str, source: str, message: str, **details):
"""
Log a message with structured data.
Args:
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
source: Source component (plugin name, module, etc.)
message: Log message
**details: Additional structured data
"""
if not self._should_log(level):
return
event = DebugEvent(
timestamp=datetime.now().isoformat(),
level=level,
source=source,
message=message,
details=details
)
with self._lock:
self._events.append(event)
if len(self._events) > self._max_events:
self._events.pop(0)
# Console output
if self._console_output:
color = self._get_color(level)
reset = '\033[0m'
print(f"{color}{self._format_message(event)}{reset}")
# File output
self._write_to_file(event)
# Notify listeners
self._notify_listeners(event)
def _get_color(self, level: str) -> str:
"""Get ANSI color code for log level."""
colors = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
}
return colors.get(level, '\033[0m')
# Convenience methods
def debug(self, source: str, message: str, **details):
"""Log debug message."""
self.log('DEBUG', source, message, **details)
def info(self, source: str, message: str, **details):
"""Log info message."""
self.log('INFO', source, message, **details)
def warning(self, source: str, message: str, **details):
"""Log warning message."""
self.log('WARNING', source, message, **details)
def error(self, source: str, message: str, exception: Exception = None, **details):
"""Log error message with optional exception."""
tb = None
if exception:
tb = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))
event = DebugEvent(
timestamp=datetime.now().isoformat(),
level='ERROR',
source=source,
message=message,
details=details,
traceback=tb
)
with self._lock:
self._events.append(event)
if self._console_output:
print(f"\033[31m{self._format_message(event)}\033[0m")
self._write_to_file(event)
self._notify_listeners(event)
def critical(self, source: str, message: str, exception: Exception = None, **details):
"""Log critical message."""
tb = None
if exception:
tb = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))
event = DebugEvent(
timestamp=datetime.now().isoformat(),
level='CRITICAL',
source=source,
message=message,
details=details,
traceback=tb
)
with self._lock:
self._events.append(event)
if self._console_output:
print(f"\033[35m{self._format_message(event)}\033[0m")
self._write_to_file(event)
self._notify_listeners(event)
def get_events(self, level: str = None, source: str = None, limit: int = None) -> List[DebugEvent]:
"""
Get logged events with optional filtering.
Args:
level: Filter by log level
source: Filter by source
limit: Maximum number of events
"""
with self._lock:
events = self._events.copy()
if level:
events = [e for e in events if e.level == level]
if source:
events = [e for e in events if e.source == source]
if limit:
events = events[-limit:]
return events
def add_listener(self, callback: Callable[[DebugEvent], None]):
"""Add a listener for real-time log events."""
self._listeners.append(callback)
def remove_listener(self, callback: Callable[[DebugEvent], None]):
"""Remove a listener."""
if callback in self._listeners:
self._listeners.remove(callback)
def set_min_level(self, level: str):
"""Set minimum log level."""
self._min_level = self.LEVELS.get(level, 20)
def clear(self):
"""Clear in-memory events."""
with self._lock:
self._events.clear()
def export_json(self, path: Path = None) -> Path:
"""Export events to JSON file."""
path = path or self._log_dir / f'export-{datetime.now().strftime("%Y%m%d-%H%M%S")}.json'
with self._lock:
data = [asdict(e) for e in self._events]
with open(path, 'w') as f:
json.dump(data, f, indent=2)
return path
# Global logger instance
def get_logger() -> DebugLogger:
"""Get the global debug logger instance."""
return DebugLogger()
class DebugToolbar:
"""
Debug toolbar for EU-Utility development.
Features:
- Performance profiling
- Plugin diagnostics
- System information
- Hot reload monitoring
"""
def __init__(self):
self.logger = get_logger()
self._profiling_data: Dict[str, Dict] = {}
self._start_times: Dict[str, float] = {}
def profile(self, name: str):
"""Context manager for profiling code blocks."""
return ProfileContext(self, name)
def start_timer(self, name: str):
"""Start a named timer."""
self._start_times[name] = time.perf_counter()
def end_timer(self, name: str) -> float:
"""End a named timer and return elapsed time."""
if name not in self._start_times:
self.logger.warning('DebugToolbar', f'Timer "{name}" not started')
return 0.0
elapsed = time.perf_counter() - self._start_times[name]
del self._start_times[name]
# Store profiling data
if name not in self._profiling_data:
self._profiling_data[name] = {'count': 0, 'total': 0, 'min': float('inf'), 'max': 0}
data = self._profiling_data[name]
data['count'] += 1
data['total'] += elapsed
data['min'] = min(data['min'], elapsed)
data['max'] = max(data['max'], elapsed)
data['avg'] = data['total'] / data['count']
self.logger.debug('DebugToolbar', f'Profile: {name}', elapsed_ms=round(elapsed * 1000, 2))
return elapsed
def get_profile_summary(self) -> Dict[str, Dict]:
"""Get profiling summary."""
return self._profiling_data.copy()
def get_system_info(self) -> Dict[str, Any]:
"""Get system information."""
import platform
return {
'python_version': platform.python_version(),
'platform': platform.platform(),
'processor': platform.processor(),
'machine': platform.machine(),
'node': platform.node(),
}
def print_summary(self):
"""Print debug summary to console."""
print("\n" + "=" * 60)
print("DEBUG TOOLBAR SUMMARY")
print("=" * 60)
# System info
print("\n📊 System Information:")
for key, value in self.get_system_info().items():
print(f" {key}: {value}")
# Profiling data
if self._profiling_data:
print("\n⏱️ Performance Profile:")
print(f" {'Name':<30} {'Count':>8} {'Avg (ms)':>10} {'Min (ms)':>10} {'Max (ms)':>10}")
print(" " + "-" * 70)
for name, data in sorted(self._profiling_data.items()):
avg_ms = data.get('avg', 0) * 1000
min_ms = data.get('min', 0) * 1000
max_ms = data.get('max', 0) * 1000
print(f" {name:<30} {data['count']:>8} {avg_ms:>10.2f} {min_ms:>10.2f} {max_ms:>10.2f}")
# Recent errors
errors = self.logger.get_events(level='ERROR', limit=5)
if errors:
print("\n❌ Recent Errors:")
for event in errors:
print(f" [{event.timestamp}] {event.source}: {event.message}")
print("\n" + "=" * 60)
class ProfileContext:
"""Context manager for profiling."""
def __init__(self, toolbar: DebugToolbar, name: str):
self.toolbar = toolbar
self.name = name
def __enter__(self):
self.toolbar.start_timer(self.name)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = self.toolbar.end_timer(self.name)
if exc_val:
self.toolbar.logger.error(
'DebugToolbar',
f'Exception in profiled block "{self.name}"',
exception=exc_val
)
def log_call(func: Callable) -> Callable:
"""Decorator to log function calls."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger = get_logger()
source = func.__module__
logger.debug(source, f'Calling {func.__name__}', args=len(args), kwargs=len(kwargs))
try:
result = func(*args, **kwargs)
logger.debug(source, f'{func.__name__} completed successfully')
return result
except Exception as e:
logger.error(source, f'{func.__name__} failed', exception=e)
raise
return wrapper
def debug_on_error(func: Callable) -> Callable:
"""Decorator that enters pdb on exception."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger = get_logger()
logger.error('debug_on_error', f'Exception in {func.__name__}', exception=e)
import pdb
pdb.post_mortem()
raise
return wrapper
# Convenience functions
def debug_print(message: str, **data):
"""Quick debug print with structured data."""
get_logger().debug('debug_print', message, **data)
def info_print(message: str, **data):
"""Quick info print with structured data."""
get_logger().info('info_print', message, **data)
def error_print(message: str, exception: Exception = None, **data):
"""Quick error print with structured data."""
get_logger().error('error_print', message, exception=exception, **data)