""" Custom log formatters for MarkiTect. Provides structured formatting for development and production environments with context-aware logging capabilities. """ import json import logging import traceback from datetime import datetime, timezone from typing import Dict, Any, Optional from .context import get_current_log_context class BaseFormatter(logging.Formatter): """Base formatter with common functionality.""" def __init__(self, enable_context: bool = True, *args, **kwargs): super().__init__(*args, **kwargs) self.enable_context = enable_context def format(self, record: logging.LogRecord) -> str: """Format the log record with context information.""" # Add context information if enabled if self.enable_context: self._add_context_to_record(record) # Add standard fields self._add_standard_fields(record) return super().format(record) def _add_context_to_record(self, record: logging.LogRecord) -> None: """Add context information to log record.""" context = get_current_log_context() if context: record.correlation_id = context.correlation_id record.operation_id = context.operation_id record.operation_type = context.operation_type record.user_id = context.user_id record.request_id = context.request_id # Add custom fields for key, value in context.custom_fields.items(): setattr(record, f'ctx_{key}', value) else: record.correlation_id = None record.operation_id = None record.operation_type = None record.user_id = None record.request_id = None def _add_standard_fields(self, record: logging.LogRecord) -> None: """Add standard fields to log record.""" record.timestamp = datetime.now(timezone.utc).isoformat() + 'Z' record.logger_name = record.name record.level_name = record.levelname record.thread_name = record.threadName record.process_id = record.process # Add exception information if present if record.exc_info and record.exc_info != (None, None, None): if isinstance(record.exc_info, tuple) and len(record.exc_info) == 3: record.exception_type = record.exc_info[0].__name__ if record.exc_info[0] else None record.exception_message = str(record.exc_info[1]) if record.exc_info[1] else None record.stack_trace = traceback.format_exception(*record.exc_info) else: # Handle case where exc_info is True but we need to get current exception import sys exc_info = sys.exc_info() if exc_info[0] is not None: record.exception_type = exc_info[0].__name__ record.exception_message = str(exc_info[1]) record.stack_trace = traceback.format_exception(*exc_info) else: record.exception_type = None record.exception_message = None record.stack_trace = None else: record.exception_type = None record.exception_message = None record.stack_trace = None class DevelopmentFormatter(BaseFormatter): """ Human-readable formatter for development environment. Provides colored output and structured information for easy debugging. """ # Color codes for different log levels COLORS = { 'DEBUG': '\033[36m', # Cyan 'INFO': '\033[32m', # Green 'WARNING': '\033[33m', # Yellow 'ERROR': '\033[31m', # Red 'CRITICAL': '\033[41m', # Red background 'RESET': '\033[0m' # Reset } def __init__(self, enable_context: bool = True, enable_colors: bool = True, *args, **kwargs): super().__init__(enable_context, *args, **kwargs) self.enable_colors = enable_colors and self._supports_color() def format(self, record: logging.LogRecord) -> str: """Format record for development environment.""" super().format(record) # Build the message parts = [] # Timestamp timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] parts.append(f"[{timestamp}]") # Log level with color level = record.levelname if self.enable_colors and level in self.COLORS: level = f"{self.COLORS[level]}{level:<8}{self.COLORS['RESET']}" else: level = f"{level:<8}" parts.append(level) # Logger name (shortened) logger_name = self._shorten_logger_name(record.name) parts.append(f"[{logger_name}]") # Context information if self.enable_context and hasattr(record, 'correlation_id') and record.correlation_id: context_parts = [] if record.correlation_id: context_parts.append(f"cid:{record.correlation_id[:8]}") if record.operation_id: context_parts.append(f"op:{record.operation_id}") if context_parts: parts.append(f"({' '.join(context_parts)})") # Main message parts.append(record.getMessage()) # Exception information if record.exc_info: parts.append(f"\n{self.formatException(record.exc_info)}") # Performance information if hasattr(record, 'duration_ms'): parts.append(f"[{record.duration_ms:.2f}ms]") return " ".join(parts) def _shorten_logger_name(self, name: str) -> str: """Shorten logger name for compact display.""" parts = name.split('.') if len(parts) <= 2: return name # Keep first and last part, abbreviate middle parts first = parts[0] last = parts[-1] middle = '.'.join(part[0] for part in parts[1:-1]) return f"{first}.{middle}.{last}" if middle else f"{first}.{last}" def _supports_color(self) -> bool: """Check if terminal supports color output.""" import os import sys # Check if we're in a terminal if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): return False # Check environment variables if os.getenv('NO_COLOR'): return False if os.getenv('FORCE_COLOR'): return True # Check terminal type term = os.getenv('TERM', '') return 'color' in term or term in ('xterm', 'xterm-256color', 'screen') class ProductionFormatter(BaseFormatter): """ Structured formatter for production environment. Provides JSON-like structured output optimized for log aggregation systems. """ def format(self, record: logging.LogRecord) -> str: """Format record for production environment.""" super().format(record) # Build structured log entry log_entry = { 'timestamp': record.timestamp, 'level': record.levelname, 'logger': record.name, 'message': record.getMessage(), 'thread': record.thread_name, 'process': record.process_id } # Add context information if self.enable_context: context_info = {} if hasattr(record, 'correlation_id') and record.correlation_id: context_info['correlation_id'] = record.correlation_id if hasattr(record, 'operation_id') and record.operation_id: context_info['operation_id'] = record.operation_id if hasattr(record, 'operation_type') and record.operation_type: context_info['operation_type'] = record.operation_type if hasattr(record, 'user_id') and record.user_id: context_info['user_id'] = record.user_id if hasattr(record, 'request_id') and record.request_id: context_info['request_id'] = record.request_id if context_info: log_entry['context'] = context_info # Add custom context fields custom_fields = {} for attr_name in dir(record): if attr_name.startswith('ctx_'): field_name = attr_name[4:] # Remove 'ctx_' prefix custom_fields[field_name] = getattr(record, attr_name) if custom_fields: log_entry['custom'] = custom_fields # Add exception information if record.exc_info: log_entry['exception'] = { 'type': record.exception_type, 'message': record.exception_message, 'traceback': record.stack_trace } # Add performance information if hasattr(record, 'duration_ms'): log_entry['performance'] = { 'duration_ms': record.duration_ms } # Add location information log_entry['source'] = { 'file': record.pathname, 'line': record.lineno, 'function': record.funcName } return self._format_structured_entry(log_entry) def _format_structured_entry(self, entry: Dict[str, Any]) -> str: """Format structured entry as string.""" # Use compact JSON format return json.dumps(entry, separators=(',', ':'), ensure_ascii=False, default=str) class PerformanceFormatter(BaseFormatter): """ Specialized formatter for performance logging. Optimized for capturing timing, metrics, and performance data. """ def format(self, record: logging.LogRecord) -> str: """Format record for performance logging.""" super().format(record) # Performance-focused format parts = [ record.timestamp, record.levelname, record.name ] # Context for performance tracking if self.enable_context and hasattr(record, 'operation_id') and record.operation_id: parts.append(f"op:{record.operation_id}") # Main message parts.append(record.getMessage()) # Performance metrics metrics = [] if hasattr(record, 'duration_ms'): metrics.append(f"duration:{record.duration_ms:.2f}ms") if hasattr(record, 'memory_mb'): metrics.append(f"memory:{record.memory_mb:.2f}MB") if hasattr(record, 'cpu_percent'): metrics.append(f"cpu:{record.cpu_percent:.1f}%") if metrics: parts.append(f"[{', '.join(metrics)}]") return " | ".join(parts)