Refactor: Phase 5: Granular Refactoring
This commit is contained in:
parent
54f21672aa
commit
6c7c4c5d9c
4 changed files with 1216 additions and 15 deletions
406
detector_worker/utils/error_handler.py
Normal file
406
detector_worker/utils/error_handler.py
Normal file
|
@ -0,0 +1,406 @@
|
|||
"""
|
||||
Common error handling and logging patterns.
|
||||
|
||||
This module provides standardized error handling, logging utilities, and
|
||||
common patterns used throughout the detection worker system.
|
||||
"""
|
||||
import logging
|
||||
import traceback
|
||||
import functools
|
||||
import time
|
||||
from typing import Dict, Any, Optional, Callable, Union
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from ..core.exceptions import DetectionError, PipelineError, StreamError, ModelLoadError
|
||||
|
||||
# Setup logging
|
||||
logger = logging.getLogger("detector_worker.error_handler")
|
||||
|
||||
|
||||
class ErrorSeverity(Enum):
|
||||
"""Error severity levels."""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorContext:
|
||||
"""Context information for error reporting."""
|
||||
component: str = "unknown"
|
||||
operation: str = "unknown"
|
||||
camera_id: Optional[str] = None
|
||||
model_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
additional_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"component": self.component,
|
||||
"operation": self.operation,
|
||||
"camera_id": self.camera_id,
|
||||
"model_id": self.model_id,
|
||||
"session_id": self.session_id,
|
||||
"additional_data": self.additional_data or {}
|
||||
}
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
"""
|
||||
Centralized error handling and logging utility.
|
||||
|
||||
Provides consistent error handling patterns, logging formats,
|
||||
and error recovery strategies across the detection worker.
|
||||
"""
|
||||
|
||||
def __init__(self, component_name: str = "detector_worker"):
|
||||
"""
|
||||
Initialize error handler.
|
||||
|
||||
Args:
|
||||
component_name: Name of the component using this handler
|
||||
"""
|
||||
self.component_name = component_name
|
||||
self.error_counts: Dict[str, int] = {}
|
||||
|
||||
def handle_error(
|
||||
self,
|
||||
error: Exception,
|
||||
context: ErrorContext,
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
||||
reraise: bool = True,
|
||||
log_traceback: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Handle an error with consistent logging and optional re-raising.
|
||||
|
||||
Args:
|
||||
error: Exception that occurred
|
||||
context: Error context information
|
||||
severity: Error severity level
|
||||
reraise: Whether to re-raise the exception
|
||||
log_traceback: Whether to log the full traceback
|
||||
"""
|
||||
# Generate error key for counting
|
||||
error_key = f"{context.component}:{context.operation}:{type(error).__name__}"
|
||||
self.error_counts[error_key] = self.error_counts.get(error_key, 0) + 1
|
||||
|
||||
# Create error message
|
||||
error_msg = self._format_error_message(error, context, severity)
|
||||
|
||||
# Log based on severity
|
||||
if severity == ErrorSeverity.CRITICAL:
|
||||
logger.critical(error_msg)
|
||||
elif severity == ErrorSeverity.HIGH:
|
||||
logger.error(error_msg)
|
||||
elif severity == ErrorSeverity.MEDIUM:
|
||||
logger.warning(error_msg)
|
||||
else:
|
||||
logger.info(error_msg)
|
||||
|
||||
# Log traceback if requested and severity is medium or higher
|
||||
if log_traceback and severity in [ErrorSeverity.MEDIUM, ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
|
||||
logger.error(f"Traceback for {context.component}:{context.operation}:")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# Re-raise if requested
|
||||
if reraise:
|
||||
# Convert to appropriate custom exception
|
||||
custom_error = self._convert_to_custom_exception(error, context)
|
||||
raise custom_error
|
||||
|
||||
def _format_error_message(
|
||||
self,
|
||||
error: Exception,
|
||||
context: ErrorContext,
|
||||
severity: ErrorSeverity
|
||||
) -> str:
|
||||
"""Format error message with context."""
|
||||
severity_emoji = {
|
||||
ErrorSeverity.LOW: "ℹ️",
|
||||
ErrorSeverity.MEDIUM: "⚠️",
|
||||
ErrorSeverity.HIGH: "❌",
|
||||
ErrorSeverity.CRITICAL: "🚨"
|
||||
}
|
||||
|
||||
emoji = severity_emoji.get(severity, "❓")
|
||||
|
||||
parts = [
|
||||
f"{emoji} {severity.value.upper()} ERROR in {context.component}",
|
||||
f"Operation: {context.operation}",
|
||||
f"Error: {type(error).__name__}: {error}"
|
||||
]
|
||||
|
||||
if context.camera_id:
|
||||
parts.append(f"Camera: {context.camera_id}")
|
||||
if context.model_id:
|
||||
parts.append(f"Model: {context.model_id}")
|
||||
if context.session_id:
|
||||
parts.append(f"Session: {context.session_id}")
|
||||
|
||||
error_key = f"{context.component}:{context.operation}:{type(error).__name__}"
|
||||
count = self.error_counts.get(error_key, 1)
|
||||
if count > 1:
|
||||
parts.append(f"Count: {count}")
|
||||
|
||||
return " | ".join(parts)
|
||||
|
||||
def _convert_to_custom_exception(
|
||||
self,
|
||||
error: Exception,
|
||||
context: ErrorContext
|
||||
) -> Exception:
|
||||
"""Convert generic exception to appropriate custom exception."""
|
||||
error_msg = f"{context.operation} failed: {error}"
|
||||
|
||||
if context.component in ["yolo_detector", "tracking_manager", "stability_validator"]:
|
||||
return DetectionError(error_msg, details=context.to_dict())
|
||||
elif context.component in ["pipeline_executor", "action_executor", "field_mapper"]:
|
||||
return PipelineError(error_msg, details=context.to_dict())
|
||||
elif context.component in ["stream_manager", "frame_reader", "camera_monitor"]:
|
||||
return StreamError(error_msg, details=context.to_dict())
|
||||
elif context.component in ["model_manager", "pipeline_loader"]:
|
||||
return ModelLoadError(error_msg, details=context.to_dict())
|
||||
else:
|
||||
return error
|
||||
|
||||
def get_error_stats(self) -> Dict[str, Any]:
|
||||
"""Get error statistics."""
|
||||
total_errors = sum(self.error_counts.values())
|
||||
return {
|
||||
"total_errors": total_errors,
|
||||
"error_breakdown": dict(self.error_counts),
|
||||
"unique_error_types": len(self.error_counts)
|
||||
}
|
||||
|
||||
def reset_error_counts(self) -> None:
|
||||
"""Reset error counts."""
|
||||
self.error_counts.clear()
|
||||
|
||||
|
||||
def with_error_handling(
|
||||
component: str,
|
||||
operation: str,
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
||||
reraise: bool = True,
|
||||
default_return: Any = None
|
||||
):
|
||||
"""
|
||||
Decorator to add consistent error handling to functions.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
operation: Operation name
|
||||
severity: Error severity level
|
||||
reraise: Whether to re-raise exceptions
|
||||
default_return: Default return value if error occurs and not re-raising
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
error_handler = ErrorHandler(component)
|
||||
context = ErrorContext(
|
||||
component=component,
|
||||
operation=operation,
|
||||
additional_data={"args": len(args), "kwargs": list(kwargs.keys())}
|
||||
)
|
||||
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
error_handler.handle_error(
|
||||
e, context, severity=severity, reraise=reraise
|
||||
)
|
||||
return default_return
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
@contextmanager
|
||||
def error_context(
|
||||
component: str,
|
||||
operation: str,
|
||||
camera_id: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM
|
||||
):
|
||||
"""
|
||||
Context manager for error handling.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
operation: Operation name
|
||||
camera_id: Optional camera ID
|
||||
model_id: Optional model ID
|
||||
severity: Error severity level
|
||||
"""
|
||||
error_handler = ErrorHandler(component)
|
||||
context = ErrorContext(
|
||||
component=component,
|
||||
operation=operation,
|
||||
camera_id=camera_id,
|
||||
model_id=model_id
|
||||
)
|
||||
|
||||
try:
|
||||
yield context
|
||||
except Exception as e:
|
||||
error_handler.handle_error(e, context, severity=severity, reraise=True)
|
||||
|
||||
|
||||
class PerformanceTimer:
|
||||
"""
|
||||
Performance timing utility with automatic logging.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation: str,
|
||||
component: str = "detector_worker",
|
||||
log_threshold: float = 1.0,
|
||||
auto_log: bool = True
|
||||
):
|
||||
"""
|
||||
Initialize performance timer.
|
||||
|
||||
Args:
|
||||
operation: Operation being timed
|
||||
component: Component name
|
||||
log_threshold: Log if operation takes longer than this (seconds)
|
||||
auto_log: Whether to automatically log timing
|
||||
"""
|
||||
self.operation = operation
|
||||
self.component = component
|
||||
self.log_threshold = log_threshold
|
||||
self.auto_log = auto_log
|
||||
self.start_time: Optional[float] = None
|
||||
self.end_time: Optional[float] = None
|
||||
|
||||
def __enter__(self) -> 'PerformanceTimer':
|
||||
"""Start timing."""
|
||||
self.start_time = time.time()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""End timing and optionally log."""
|
||||
self.end_time = time.time()
|
||||
|
||||
if self.auto_log:
|
||||
self.log_timing()
|
||||
|
||||
def get_duration(self) -> float:
|
||||
"""Get duration in seconds."""
|
||||
if self.start_time is None:
|
||||
return 0.0
|
||||
end = self.end_time or time.time()
|
||||
return end - self.start_time
|
||||
|
||||
def log_timing(self) -> None:
|
||||
"""Log timing information."""
|
||||
duration = self.get_duration()
|
||||
|
||||
if duration >= self.log_threshold:
|
||||
level = logging.WARNING if duration >= (self.log_threshold * 2) else logging.INFO
|
||||
logger.log(
|
||||
level,
|
||||
f"⏱️ {self.component}:{self.operation} took {duration:.3f}s"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"⏱️ {self.component}:{self.operation} took {duration:.3f}s")
|
||||
|
||||
|
||||
def log_function_entry_exit(component: str = "detector_worker"):
|
||||
"""
|
||||
Decorator to log function entry and exit.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
func_name = f"{func.__module__}.{func.__qualname__}"
|
||||
logger.debug(f"📍 ENTER {component}:{func_name}")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
logger.debug(f"📍 EXIT {component}:{func_name} -> {type(result).__name__}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.debug(f"📍 ERROR {component}:{func_name} -> {type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def create_logger(
|
||||
name: str,
|
||||
level: int = logging.INFO,
|
||||
format_string: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
Create a standardized logger.
|
||||
|
||||
Args:
|
||||
name: Logger name
|
||||
level: Logging level
|
||||
format_string: Custom format string
|
||||
|
||||
Returns:
|
||||
Configured logger
|
||||
"""
|
||||
if format_string is None:
|
||||
format_string = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# Avoid adding handlers multiple times
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(level)
|
||||
formatter = logging.Formatter(format_string)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# Global error handler instance
|
||||
_global_error_handler = ErrorHandler("detector_worker")
|
||||
|
||||
|
||||
# Convenience functions
|
||||
def handle_error(
|
||||
error: Exception,
|
||||
component: str,
|
||||
operation: str,
|
||||
camera_id: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
||||
reraise: bool = True
|
||||
) -> None:
|
||||
"""Global error handling function."""
|
||||
context = ErrorContext(
|
||||
component=component,
|
||||
operation=operation,
|
||||
camera_id=camera_id,
|
||||
model_id=model_id
|
||||
)
|
||||
_global_error_handler.handle_error(error, context, severity, reraise)
|
||||
|
||||
|
||||
def get_global_error_stats() -> Dict[str, Any]:
|
||||
"""Get global error statistics."""
|
||||
return _global_error_handler.get_error_stats()
|
||||
|
||||
|
||||
def reset_global_error_stats() -> None:
|
||||
"""Reset global error statistics."""
|
||||
_global_error_handler.reset_error_counts()
|
Loading…
Add table
Add a link
Reference in a new issue