| | """ |
| | ShortSmith v2 - Centralized Logging Module |
| | |
| | Provides consistent logging across all components with: |
| | - File and console handlers |
| | - Different log levels per module |
| | - Timing decorators for performance tracking |
| | - Structured log formatting |
| | """ |
| |
|
| | import logging |
| | import sys |
| | import time |
| | import functools |
| | from pathlib import Path |
| | from typing import Optional, Callable, Any |
| | from datetime import datetime |
| | from contextlib import contextmanager |
| |
|
| |
|
| | |
| | LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s" |
| | LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" |
| |
|
| | |
| | MODULE_LOG_LEVELS = { |
| | "shortsmith": logging.INFO, |
| | "shortsmith.models": logging.INFO, |
| | "shortsmith.core": logging.INFO, |
| | "shortsmith.pipeline": logging.INFO, |
| | "shortsmith.scoring": logging.DEBUG, |
| | } |
| |
|
| | |
| | _logging_initialized = False |
| |
|
| |
|
| | class ColoredFormatter(logging.Formatter): |
| | """Formatter that adds colors to log levels for console output.""" |
| |
|
| | COLORS = { |
| | logging.DEBUG: "\033[36m", |
| | logging.INFO: "\033[32m", |
| | logging.WARNING: "\033[33m", |
| | logging.ERROR: "\033[31m", |
| | logging.CRITICAL: "\033[35m", |
| | } |
| | RESET = "\033[0m" |
| |
|
| | def format(self, record: logging.LogRecord) -> str: |
| | """Format log record with colors.""" |
| | |
| | color = self.COLORS.get(record.levelno, "") |
| | record.levelname = f"{color}{record.levelname}{self.RESET}" |
| | return super().format(record) |
| |
|
| |
|
| | def setup_logging( |
| | log_level: str = "INFO", |
| | log_file: Optional[str] = None, |
| | log_to_console: bool = True, |
| | use_colors: bool = True, |
| | ) -> None: |
| | """ |
| | Initialize the logging system. |
| | |
| | Args: |
| | log_level: Default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
| | log_file: Path to log file (None to disable file logging) |
| | log_to_console: Whether to log to console |
| | use_colors: Whether to use colored output in console |
| | |
| | Raises: |
| | ValueError: If invalid log level provided |
| | """ |
| | global _logging_initialized |
| |
|
| | if _logging_initialized: |
| | return |
| |
|
| | |
| | numeric_level = getattr(logging, log_level.upper(), None) |
| | if not isinstance(numeric_level, int): |
| | raise ValueError(f"Invalid log level: {log_level}") |
| |
|
| | |
| | root_logger = logging.getLogger("shortsmith") |
| | root_logger.setLevel(logging.DEBUG) |
| |
|
| | |
| | root_logger.handlers.clear() |
| |
|
| | |
| | if log_to_console: |
| | console_handler = logging.StreamHandler(sys.stdout) |
| | console_handler.setLevel(numeric_level) |
| |
|
| | if use_colors and sys.stdout.isatty(): |
| | console_formatter = ColoredFormatter(LOG_FORMAT, LOG_DATE_FORMAT) |
| | else: |
| | console_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT) |
| |
|
| | console_handler.setFormatter(console_formatter) |
| | root_logger.addHandler(console_handler) |
| |
|
| | |
| | if log_file: |
| | try: |
| | log_path = Path(log_file) |
| | log_path.parent.mkdir(parents=True, exist_ok=True) |
| |
|
| | file_handler = logging.FileHandler(log_file, encoding="utf-8") |
| | file_handler.setLevel(logging.DEBUG) |
| | file_formatter = logging.Formatter(LOG_FORMAT, LOG_DATE_FORMAT) |
| | file_handler.setFormatter(file_formatter) |
| | root_logger.addHandler(file_handler) |
| | except (OSError, PermissionError) as e: |
| | |
| | if log_to_console: |
| | root_logger.warning(f"Could not create log file {log_file}: {e}") |
| |
|
| | |
| | for module, level in MODULE_LOG_LEVELS.items(): |
| | logging.getLogger(module).setLevel(level) |
| |
|
| | _logging_initialized = True |
| | root_logger.info(f"Logging initialized at level {log_level}") |
| |
|
| |
|
| | def get_logger(name: str) -> logging.Logger: |
| | """ |
| | Get a logger instance for a specific module. |
| | |
| | Args: |
| | name: Module name (will be prefixed with 'shortsmith.') |
| | |
| | Returns: |
| | Configured logger instance |
| | """ |
| | |
| | if not _logging_initialized: |
| | setup_logging() |
| |
|
| | |
| | if not name.startswith("shortsmith"): |
| | name = f"shortsmith.{name}" |
| |
|
| | return logging.getLogger(name) |
| |
|
| |
|
| | class LogTimer: |
| | """ |
| | Context manager and decorator for timing operations. |
| | |
| | Usage as context manager: |
| | with LogTimer(logger, "Processing video"): |
| | process_video() |
| | |
| | Usage as decorator: |
| | @LogTimer.decorator(logger, "Processing") |
| | def process_video(): |
| | ... |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | logger: logging.Logger, |
| | operation: str, |
| | level: int = logging.INFO, |
| | ): |
| | """ |
| | Initialize timer. |
| | |
| | Args: |
| | logger: Logger to use for output |
| | operation: Description of the operation being timed |
| | level: Log level for timing messages |
| | """ |
| | self.logger = logger |
| | self.operation = operation |
| | self.level = level |
| | self.start_time: Optional[float] = None |
| | self.end_time: Optional[float] = None |
| |
|
| | def __enter__(self) -> "LogTimer": |
| | """Start timing.""" |
| | self.start_time = time.perf_counter() |
| | self.logger.log(self.level, f"Starting: {self.operation}") |
| | return self |
| |
|
| | def __exit__(self, exc_type, exc_val, exc_tb) -> None: |
| | """Stop timing and log duration.""" |
| | self.end_time = time.perf_counter() |
| | duration = self.end_time - self.start_time |
| |
|
| | if exc_type is not None: |
| | self.logger.error( |
| | f"Failed: {self.operation} after {duration:.2f}s - {exc_type.__name__}: {exc_val}" |
| | ) |
| | else: |
| | self.logger.log( |
| | self.level, |
| | f"Completed: {self.operation} in {duration:.2f}s" |
| | ) |
| |
|
| | @property |
| | def elapsed(self) -> float: |
| | """Get elapsed time in seconds.""" |
| | if self.start_time is None: |
| | return 0.0 |
| | end = self.end_time if self.end_time else time.perf_counter() |
| | return end - self.start_time |
| |
|
| | @staticmethod |
| | def decorator( |
| | logger: logging.Logger, |
| | operation: Optional[str] = None, |
| | level: int = logging.INFO, |
| | ) -> Callable: |
| | """ |
| | Create a timing decorator. |
| | |
| | Args: |
| | logger: Logger to use |
| | operation: Operation name (defaults to function name) |
| | level: Log level |
| | |
| | Returns: |
| | Decorator function |
| | """ |
| | def decorator_func(func: Callable) -> Callable: |
| | op_name = operation or func.__name__ |
| |
|
| | @functools.wraps(func) |
| | def wrapper(*args, **kwargs) -> Any: |
| | with LogTimer(logger, op_name, level): |
| | return func(*args, **kwargs) |
| |
|
| | return wrapper |
| | return decorator_func |
| |
|
| |
|
| | @contextmanager |
| | def log_context(logger: logging.Logger, context: str): |
| | """ |
| | Context manager that logs entry and exit of a code block. |
| | |
| | Args: |
| | logger: Logger instance |
| | context: Description of the context |
| | |
| | Yields: |
| | None |
| | """ |
| | logger.debug(f"Entering: {context}") |
| | try: |
| | yield |
| | except Exception as e: |
| | logger.error(f"Error in {context}: {type(e).__name__}: {e}") |
| | raise |
| | finally: |
| | logger.debug(f"Exiting: {context}") |
| |
|
| |
|
| | def log_exception(logger: logging.Logger, message: str = "An error occurred"): |
| | """ |
| | Decorator that logs exceptions with full context. |
| | |
| | Args: |
| | logger: Logger instance |
| | message: Custom error message prefix |
| | |
| | Returns: |
| | Decorator function |
| | """ |
| | def decorator(func: Callable) -> Callable: |
| | @functools.wraps(func) |
| | def wrapper(*args, **kwargs) -> Any: |
| | try: |
| | return func(*args, **kwargs) |
| | except Exception as e: |
| | logger.exception(f"{message} in {func.__name__}: {e}") |
| | raise |
| |
|
| | return wrapper |
| | return decorator |
| |
|
| |
|
| | |
| | __all__ = [ |
| | "setup_logging", |
| | "get_logger", |
| | "LogTimer", |
| | "log_context", |
| | "log_exception", |
| | ] |
| |
|