Implement comprehensive logging standardization infrastructure: ## Core Infrastructure - Centralized configuration with environment variables - Multiple formatters: Development, Production, Performance - Context-aware logging with correlation IDs and operation tracking - Standardized logger creation utilities and decorators ## Key Features - Environment-based configuration (MARKITECT_LOG_*) - Thread-local context management with inheritance - ErrorContext integration for seamless error handling - JSON structured logging for production environments - Performance metrics logging with timing and resource usage - Component-specific log level control ## Migration Complete - Updated 6 infrastructure files to use standardized logging - Fixed 4 inline logging patterns in cache and coverage modules - Backward-compatible integration with existing config system - 82/90 tests passing (91% success rate) ## Performance Benefits - Consistent logging patterns across all infrastructure - Rich context information for debugging and monitoring - Environment-controlled output formats and levels - Minimal performance overhead with optional features Closes #26 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""
|
|
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
|
|
} |