""" Logging utilities for MarkiTect. Provides standardized logger creation, performance logging, and integration helpers for consistent logging across the application. """ import logging import time import functools from typing import Callable, Any, Optional, Dict from contextlib import contextmanager from .context import LogContext, with_log_context, log_performance_metrics from infrastructure.exceptions import ErrorContext, OperationType def get_logger(name: str) -> logging.Logger: """ Get a standardized logger instance. This is the standard way to create loggers throughout the application. It ensures consistent configuration and proper hierarchy. Args: name: Logger name, typically __name__ for module-level loggers Returns: Configured logger instance Usage: logger = get_logger(__name__) logger.info("This is a log message") """ return logging.getLogger(name) def get_component_logger(component: str, subcomponent: Optional[str] = None) -> logging.Logger: """ Get a logger for a specific component or subcomponent. Args: component: Main component name (e.g., 'infrastructure', 'domain', 'application') subcomponent: Optional subcomponent name (e.g., 'repositories', 'services') Returns: Configured logger instance Usage: logger = get_component_logger('infrastructure', 'repositories') logger.info("Repository operation completed") """ if subcomponent: name = f"{component}.{subcomponent}" else: name = component return logging.getLogger(name) def log_function_call(logger: Optional[logging.Logger] = None, level: str = 'DEBUG', include_args: bool = False, include_result: bool = False, performance: bool = False) -> Callable: """ Decorator to log function calls with optional arguments and results. Args: logger: Logger to use (defaults to function's module logger) level: Log level for the messages include_args: Whether to include function arguments in logs include_result: Whether to include function result in logs performance: Whether to log performance metrics Usage: @log_function_call(performance=True) def my_function(arg1, arg2): return arg1 + arg2 """ def decorator(func: Callable) -> Callable: @functools.wraps(func) def wrapper(*args, **kwargs): # Get logger func_logger = logger or get_logger(func.__module__) log_level = getattr(logging, level.upper()) # Build function identifier func_name = f"{func.__module__}.{func.__qualname__}" # Log function entry entry_msg = f"Entering {func_name}" if include_args and (args or kwargs): args_str = ", ".join([repr(arg) for arg in args]) kwargs_str = ", ".join([f"{k}={repr(v)}" for k, v in kwargs.items()]) all_args = [s for s in [args_str, kwargs_str] if s] entry_msg += f"({', '.join(all_args)})" func_logger.log(log_level, entry_msg) # Execute function with timing start_time = time.perf_counter() try: result = func(*args, **kwargs) # Log function exit duration_ms = (time.perf_counter() - start_time) * 1000 exit_msg = f"Exiting {func_name}" if include_result: exit_msg += f" -> {repr(result)}" if performance: exit_msg += f" [{duration_ms:.2f}ms]" # Also log to performance logger log_performance_metrics(func_name, duration_ms) func_logger.log(log_level, exit_msg) return result except Exception as e: duration_ms = (time.perf_counter() - start_time) * 1000 error_msg = f"Exception in {func_name} after {duration_ms:.2f}ms: {e}" func_logger.error(error_msg, exc_info=True) raise return wrapper return decorator @contextmanager def log_operation(operation_id: str, operation_type: OperationType, logger: Optional[logging.Logger] = None, level: str = 'INFO', **context_fields): """ Context manager for logging complete operations with start/end and timing. Args: operation_id: Unique identifier for the operation operation_type: Type of operation being performed logger: Logger to use (defaults to infrastructure logger) level: Log level for operation messages **context_fields: Additional context fields Usage: with log_operation("create_issue", OperationType.WRITE, issue_id=123): # Logs operation start create_issue_logic() # Logs operation end with timing """ if logger is None: logger = get_logger('infrastructure.operations') log_level = getattr(logging, level.upper()) # Create operation context context = LogContext( operation_id=operation_id, operation_type=operation_type.value, custom_fields=context_fields ) start_time = time.perf_counter() with with_log_context(context): # Log operation start logger.log(log_level, f"Starting operation: {operation_id}") try: yield context # Log successful completion duration_ms = (time.perf_counter() - start_time) * 1000 logger.log(log_level, f"Operation {operation_id} completed successfully [{duration_ms:.2f}ms]") # Log performance metrics log_performance_metrics(operation_id, duration_ms, **context_fields) except Exception as e: # Log failure duration_ms = (time.perf_counter() - start_time) * 1000 logger.error(f"Operation {operation_id} failed after {duration_ms:.2f}ms: {e}", exc_info=True) raise def log_with_context(logger: logging.Logger, level: str, message: str, context: Optional[LogContext] = None, error_context: Optional[ErrorContext] = None, **extra_fields) -> None: """ Log a message with specific context. Args: logger: Logger instance level: Log level message: Log message context: Log context to use error_context: Error context to convert to log context **extra_fields: Additional fields to include in log """ log_level = getattr(logging, level.upper()) # Determine context to use if error_context: use_context = LogContext.from_error_context(error_context) elif context: use_context = context else: use_context = None # Add extra fields to context if provided if extra_fields and use_context: for key, value in extra_fields.items(): use_context = use_context.with_custom_field(key, value) if use_context: with with_log_context(use_context): logger.log(log_level, message, extra=extra_fields) else: logger.log(log_level, message, extra=extra_fields) def setup_logger_for_testing(logger_name: str, level: str = 'WARNING') -> logging.Logger: """ Set up a logger for testing with minimal output. Args: logger_name: Name of the logger level: Log level to set Returns: Configured logger for testing """ logger = logging.getLogger(logger_name) logger.setLevel(getattr(logging, level.upper())) # Remove any existing handlers for handler in logger.handlers[:]: logger.removeHandler(handler) # Add null handler to prevent logging during tests logger.addHandler(logging.NullHandler()) return logger def create_performance_logger(name: str = 'performance') -> logging.Logger: """ Create a specialized logger for performance metrics. Args: name: Logger name Returns: Performance logger instance """ logger = get_logger(name) return logger def log_repository_operation(logger: logging.Logger, operation: str, resource_type: str, resource_id: Optional[str] = None, **details) -> Callable: """ Decorator for logging repository operations consistently. Args: logger: Logger to use operation: Operation name (e.g., 'get', 'create', 'update', 'delete') resource_type: Type of resource (e.g., 'Issue', 'Project') resource_id: Optional resource identifier **details: Additional operation details Usage: @log_repository_operation(logger, 'get', 'Issue') def get_issue(self, issue_id): return self._fetch_issue(issue_id) """ def decorator(func: Callable) -> Callable: @functools.wraps(func) def wrapper(*args, **kwargs): # Extract resource ID from arguments if not provided actual_resource_id = resource_id if not actual_resource_id and args: # Try to get ID from first argument (common pattern) if hasattr(args[1] if len(args) > 1 else None, '__str__'): actual_resource_id = str(args[1]) operation_id = f"{operation}_{resource_type.lower()}" if actual_resource_id: operation_id += f"_{actual_resource_id}" # Determine operation type operation_type_map = { 'get': OperationType.READ, 'list': OperationType.READ, 'create': OperationType.WRITE, 'update': OperationType.WRITE, 'delete': OperationType.DELETE } op_type = operation_type_map.get(operation.lower(), OperationType.READ) with log_operation(operation_id, op_type, logger, resource_type=resource_type, resource_id=actual_resource_id, **details): return func(*args, **kwargs) return wrapper return decorator # Commonly used logger instances infrastructure_logger = get_logger('infrastructure') domain_logger = get_logger('domain') application_logger = get_logger('application') performance_logger = create_performance_logger() def get_default_loggers() -> Dict[str, logging.Logger]: """ Get dictionary of commonly used loggers. Returns: Dictionary mapping logger names to logger instances """ return { 'infrastructure': infrastructure_logger, 'domain': domain_logger, 'application': application_logger, 'performance': performance_logger }