Skip to main content
Glama
logging.py33.3 kB
""" Structured logging framework for SOIL-2. This module provides comprehensive structured logging with context management, performance monitoring, and integration with the dependency injection system. """ import logging import logging.config import structlog from structlog import contextvars as structlog_contextvars from typing import Any, Dict, Optional, Union, Callable, List, Protocol from datetime import datetime from enum import Enum import json import sys import threading import types from pathlib import Path from functools import wraps, lru_cache from contextlib import contextmanager import time class LogLevel(str, Enum): """Log levels with string values.""" TRACE = "TRACE" DEBUG = "DEBUG" INFO = "INFO" HAPPY = "HAPPY" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" class LogFormat(str, Enum): """Log output formats.""" STRUCTURED = "structured" SIMPLE = "simple" JSON = "json" CONSOLE = "console" class ContextKeys: """Standard context keys for structured logging.""" COMPONENT = "component" OPERATION = "operation" TRACE_ID = "trace_id" REQUEST_ID = "request_id" USER_ID = "user_id" SESSION_ID = "session_id" DURATION_MS = "duration_ms" ERROR_TYPE = "error_type" DATABASE_PATH = "database_path" QUERY = "query" HTTP_STATUS = "http_status" ENDPOINT = "endpoint" CLI_COMMAND = "cli_command" class SettingsProtocol(Protocol): """Protocol for settings objects that can configure logging.""" def __getattr__(self, name: str) -> Any: """ Allow access to dynamic settings attributes. NOTE: Any type justified here - settings objects have arbitrary dynamic attributes that cannot be statically typed. """ ... class StructuredLogger: """ Type-safe structured logger with context management and performance optimization. Provides consistent logging across all SOIL-2 components with contextual information, lazy evaluation, and integration with configuration system. """ def __init__( self, component: str, *, logger_name: Optional[str] = None, base_context: Optional[Dict[str, Any]] = None, enable_performance_tracking: bool = False, ): """ Initialize structured logger for component. Args: component: Component name (e.g., "cli", "server", "store") logger_name: Optional logger name (defaults to component) base_context: Base context added to all log messages enable_performance_tracking: Enable automatic performance tracking """ self.component = component self.logger_name = logger_name or f"mnemosyne.{component}" self.base_context = base_context or {} self.enable_performance_tracking = enable_performance_tracking # Thread-local storage for request context self._local = threading.local() # Initialize structlog logger self._logger = structlog.get_logger(self.logger_name) # Add component to base context self.base_context[ContextKeys.COMPONENT] = component def _get_current_context(self) -> Dict[str, Any]: """Get current logging context with thread-local data.""" context = self.base_context.copy() # Add thread-local context if available if hasattr(self._local, "context"): context.update(self._local.context) return context def _log_with_context( self, level: str, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """ Internal logging method with context management. Args: level: Log level message: Log message extra_context: Additional context for this log entry exception: Exception to include in log lazy_context: Function that returns context (called only if logging) """ # Check if logging is enabled for this level using standard library # structlog filtering is handled by the wrapper_class, but we want to avoid # expensive lazy context evaluation if logging is disabled try: # Get the underlying logger's level if level.upper() == "HAPPY": log_level = 25 # Custom HAPPY level else: log_level = getattr(logging, level.upper()) if logging.getLogger(self.logger_name).getEffectiveLevel() > log_level: return except (AttributeError, ValueError): # If level checking fails, just proceed with logging pass # Build context context = self._get_current_context() if extra_context: context.update(extra_context) # Add lazy context only if logging if lazy_context: try: context.update(lazy_context()) except Exception as e: # Don't let context generation break logging context["context_error"] = str(e) # Add exception information if exception: context.update( { ContextKeys.ERROR_TYPE: type(exception).__name__, "error_message": str(exception), } ) # Add timestamp context["timestamp"] = datetime.utcnow().isoformat() # Handle HAPPY level filtering if level.upper() == "HAPPY": # For HAPPY level, use info but mark it specially context["level"] = "HAPPY" context["_level_numeric"] = 25 log_method = self._logger.info else: log_method = getattr(self._logger, level.lower()) log_method(message, **context) def trace( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log trace message (very detailed debugging).""" self._log_with_context( "DEBUG", # Map to DEBUG since TRACE isn't standard f"[TRACE] {message}", extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def debug( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log debug message.""" self._log_with_context( "DEBUG", message, extra_context=extra_context, lazy_context=lazy_context ) def info( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log info message.""" self._log_with_context( "INFO", message, extra_context=extra_context, lazy_context=lazy_context ) def happy( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log happy milestone message (job progression, success events).""" self._log_with_context( "HAPPY", message, extra_context=extra_context, lazy_context=lazy_context ) def warning( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log warning message.""" self._log_with_context( "WARNING", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def error( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log error message.""" self._log_with_context( "ERROR", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def critical( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log critical message.""" self._log_with_context( "CRITICAL", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def with_context(self, **context: Any) -> "ContextualLogger": """ Create contextual logger with additional context. Args: **context: Context to add to all subsequent log messages Returns: ContextualLogger instance with added context """ return ContextualLogger(self, context) def set_request_context(self, **context: Any) -> None: """ Set request-level context (thread-local). Args: **context: Context to add to thread-local storage """ if not hasattr(self._local, "context"): self._local.context = {} if not hasattr(self._local, "context_keys"): self._local.context_keys = set() self._local.context.update(context) self._local.context_keys.update(context.keys()) try: structlog_contextvars.bind_contextvars(**context) except Exception: # pragma: no cover - defensive guard if structlog misconfigured # Fall back silently; structured logging still works with thread-local context pass def clear_request_context(self) -> None: """Clear request-level context.""" if hasattr(self._local, "context"): keys_to_clear = list( getattr(self._local, "context_keys", set()) or self._local.context.keys() ) try: if keys_to_clear: structlog_contextvars.unbind_contextvars(*keys_to_clear) else: structlog_contextvars.clear_contextvars() except (LookupError, Exception): # pragma: no cover - safety net # Ensure contextvars are reset even if specific keys missing try: structlog_contextvars.clear_contextvars() except Exception: pass self._local.context.clear() if hasattr(self._local, "context_keys"): self._local.context_keys.clear() def performance_timer( self, operation: str, *, threshold_ms: Optional[float] = None ) -> "PerformanceTimer": """ Create performance timer for operation. Args: operation: Operation name for timing threshold_ms: Only log if duration exceeds threshold Returns: Performance timer context manager """ return PerformanceTimer(self, operation, threshold_ms=threshold_ms) @contextmanager def operation_context(self, operation: str, **additional_context: Any) -> Any: """ Context manager for operation with automatic timing and structured logging. Args: operation: Operation name **additional_context: Additional context for the operation Yields: ContextualLogger instance for the operation """ start_time = time.perf_counter() # Create contextual logger for this operation context = {ContextKeys.OPERATION: operation} context.update(additional_context) contextual_logger = self.with_context(**context) contextual_logger.debug(f"Starting operation: {operation}") try: yield contextual_logger except Exception as e: duration_ms = (time.perf_counter() - start_time) * 1000 contextual_logger.error( f"Operation '{operation}' failed", extra_context={ContextKeys.DURATION_MS: round(duration_ms, 2)}, exception=e, ) raise else: duration_ms = (time.perf_counter() - start_time) * 1000 contextual_logger.debug( f"Operation '{operation}' completed successfully", extra_context={ContextKeys.DURATION_MS: round(duration_ms, 2)}, ) class ContextualLogger: """ Logger wrapper that adds context to all log messages. Provides convenient context management for related operations. """ def __init__(self, base_logger: StructuredLogger, context: Dict[str, Any]): self.base_logger = base_logger self.context = context def _log_with_added_context( self, method_name: str, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log with additional context.""" combined_context = self.context.copy() if extra_context: combined_context.update(extra_context) method = getattr(self.base_logger, method_name) # Build kwargs based on method signature with proper typing kwargs: Dict[str, Any] = {"extra_context": combined_context} if lazy_context is not None: kwargs["lazy_context"] = lazy_context if exception is not None and method_name in ("warning", "error", "critical"): kwargs["exception"] = exception method(message, **kwargs) def trace( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log trace with context.""" self._log_with_added_context( "trace", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def debug( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log debug with context.""" self._log_with_added_context( "debug", message, extra_context=extra_context, lazy_context=lazy_context ) def info( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log info with context.""" self._log_with_added_context( "info", message, extra_context=extra_context, lazy_context=lazy_context ) def happy( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log happy milestone with context.""" self._log_with_added_context( "happy", message, extra_context=extra_context, lazy_context=lazy_context ) def warning( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log warning with context.""" self._log_with_added_context( "warning", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def error( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log error with context.""" self._log_with_added_context( "error", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def critical( self, message: str, *, extra_context: Optional[Dict[str, Any]] = None, exception: Optional[Exception] = None, lazy_context: Optional[Callable[[], Dict[str, Any]]] = None, ) -> None: """Log critical with context.""" self._log_with_added_context( "critical", message, extra_context=extra_context, exception=exception, lazy_context=lazy_context, ) def with_context(self, **additional_context: Any) -> "ContextualLogger": """Create new contextual logger with additional context.""" combined_context = self.context.copy() combined_context.update(additional_context) return ContextualLogger(self.base_logger, combined_context) def performance_timer( self, operation: str, *, threshold_ms: Optional[float] = None ) -> "PerformanceTimer": """Create performance timer with current context.""" return PerformanceTimer(self.base_logger, operation, threshold_ms=threshold_ms) @contextmanager def operation_context(self, operation: str, **additional_context: Any) -> Any: """Context manager for operation with automatic timing and structured logging.""" start_time = time.perf_counter() # Create contextual logger for this operation context = {ContextKeys.OPERATION: operation} context.update(additional_context) contextual_logger = self.with_context(**context) contextual_logger.debug(f"Starting operation: {operation}") try: yield contextual_logger except Exception as e: duration_ms = (time.perf_counter() - start_time) * 1000 contextual_logger.error( f"Operation '{operation}' failed", extra_context={ContextKeys.DURATION_MS: round(duration_ms, 2)}, exception=e, ) raise else: duration_ms = (time.perf_counter() - start_time) * 1000 contextual_logger.debug( f"Operation '{operation}' completed successfully", extra_context={ContextKeys.DURATION_MS: round(duration_ms, 2)}, ) class PerformanceTimer: """ Context manager for performance timing with automatic logging. Provides convenient timing for operations with threshold-based logging. """ def __init__( self, logger: StructuredLogger, operation: str, *, threshold_ms: Optional[float] = None, log_level: str = "DEBUG", ): """ Initialize performance timer. Args: logger: Logger to use for timing output operation: Operation being timed threshold_ms: Only log if duration exceeds threshold log_level: Log level for timing messages """ self.logger = logger self.operation = operation self.threshold_ms = threshold_ms self.log_level = log_level self.start_time: Optional[float] = None self.end_time: Optional[float] = None def __enter__(self) -> "PerformanceTimer": """Start timing.""" self.start_time = time.perf_counter() return self def __exit__( self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[types.TracebackType], ) -> None: """End timing and log result.""" self.end_time = time.perf_counter() if self.start_time is not None: duration_ms = (self.end_time - self.start_time) * 1000 # Only log if above threshold if self.threshold_ms is None or duration_ms >= self.threshold_ms: log_method = getattr(self.logger, self.log_level.lower()) log_method( f"Operation '{self.operation}' completed", extra_context={ ContextKeys.OPERATION: self.operation, ContextKeys.DURATION_MS: round(duration_ms, 2), }, ) @property def duration_ms(self) -> Optional[float]: """Get duration in milliseconds.""" if self.start_time is not None and self.end_time is not None: return (self.end_time - self.start_time) * 1000 return None class LoggerFactory: """ Centralized factory for creating component loggers. Manages logger lifecycle and configuration integration. """ _loggers: Dict[str, StructuredLogger] = {} _configured: bool = False _happy_level_enabled: bool = False @classmethod def configure_logging( cls, level: str = "INFO", format_type: str = "structured", log_file: Optional[Path] = None, enable_console: bool = True, enable_performance_tracking: bool = False, ) -> None: """ Configure global logging settings. Args: level: Minimum log level (TRACE, DEBUG, INFO, HAPPY, WARNING, ERROR, CRITICAL) format_type: Log format (structured, simple, json, console) log_file: Optional log file path enable_console: Enable console output enable_performance_tracking: Enable performance tracking globally """ # Add custom HAPPY log level to Python logging HAPPY_LEVEL = 25 # Between INFO (20) and WARNING (30) logging.addLevelName(HAPPY_LEVEL, "HAPPY") def happy(self, message, *args, **kwargs): if self.isEnabledFor(HAPPY_LEVEL): self._log(HAPPY_LEVEL, message, args, **kwargs) # Add happy method to Logger class logging.Logger.happy = happy # Store configuration for filtering cls._happy_level_enabled = level.upper() == "HAPPY" # Configure structlog # NOTE: Any type justified for processors - structlog processor signatures are complex and dynamic processors: List[Any] = [ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.StackInfoRenderer(), structlog.dev.set_exc_info, ] # Add HAPPY level filtering and emoji processor if HAPPY level is enabled if level.upper() == "HAPPY": from .happy_filter import happy_level_filter, add_happy_emoji_prefix processors.extend( [ add_happy_emoji_prefix, happy_level_filter, ] ) if format_type == "json": processors.append(structlog.processors.JSONRenderer()) elif format_type == "console": processors.append(structlog.dev.ConsoleRenderer(colors=True)) else: processors.append(structlog.processors.KeyValueRenderer()) # For HAPPY level, use INFO level filtering but mark messages specially if level.upper() == "HAPPY": filter_level = logging.INFO else: filter_level = getattr(logging, level.upper()) structlog.configure( processors=processors, wrapper_class=structlog.make_filtering_bound_logger(filter_level), logger_factory=structlog.WriteLoggerFactory(file=sys.stderr), cache_logger_on_first_use=True, ) # Configure standard library logging logging_config: Dict[str, Any] = { "version": 1, "disable_existing_loggers": False, "formatters": { "structured": { "format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s" }, "simple": {"format": "%(levelname)s: %(message)s"}, "console": { "format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s" }, "json": { "()": "structlog.stdlib.ProcessorFormatter", "processor": structlog.processors.JSONRenderer(), }, }, "handlers": {}, "root": { "level": "INFO" if level.upper() == "HAPPY" else level, "handlers": [], }, } # Add console handler if enable_console: logging_config["handlers"]["console"] = { "class": "logging.StreamHandler", "formatter": format_type if format_type != "json" else "structured", "level": "INFO" if level.upper() == "HAPPY" else level, "stream": "ext://sys.stderr", } logging_config["root"]["handlers"].append("console") # Add file handler if log_file: logging_config["handlers"]["file"] = { "class": "logging.handlers.RotatingFileHandler", "formatter": format_type if format_type != "json" else "structured", "level": level, "filename": str(log_file), "maxBytes": 10 * 1024 * 1024, # 10MB "backupCount": 5, } logging_config["root"]["handlers"].append("file") logging.config.dictConfig(logging_config) cls._configured = True @classmethod def get_logger( cls, component: str, *, logger_name: Optional[str] = None, base_context: Optional[Dict[str, Any]] = None, enable_performance_tracking: Optional[bool] = None, ) -> StructuredLogger: """ Get or create logger for component. Args: component: Component name logger_name: Optional logger name base_context: Base context for all log messages enable_performance_tracking: Enable performance tracking Returns: StructuredLogger instance for component """ cache_key = f"{component}:{logger_name or component}" if cache_key not in cls._loggers: cls._loggers[cache_key] = StructuredLogger( component, logger_name=logger_name, base_context=base_context, enable_performance_tracking=enable_performance_tracking or False, ) return cls._loggers[cache_key] @classmethod def configure_from_settings(cls, settings: SettingsProtocol) -> None: """ Configure logging from application settings. Args: settings: MnemosyneSettings instance with logging configuration """ # Use settings to configure logging if available if hasattr(settings, "logging"): cls.configure_logging( level=settings.logging.level.value if hasattr(settings.logging, "level") else "INFO", format_type=getattr(settings.logging, "format", "structured"), log_file=getattr(settings.logging, "file", None), enable_console=True, enable_performance_tracking=getattr(settings.debug, "profiling", False) if hasattr(settings, "debug") else False, ) else: # Fallback to default configuration cls.configure_logging() # Convenience functions for component loggers @lru_cache() def get_cli_logger() -> StructuredLogger: """Get CLI component logger.""" return LoggerFactory.get_logger("cli") @lru_cache() def get_server_logger() -> StructuredLogger: """Get server component logger.""" return LoggerFactory.get_logger("server") @lru_cache() def get_store_logger() -> StructuredLogger: """Get store component logger.""" return LoggerFactory.get_logger("store") @lru_cache() def get_config_logger() -> StructuredLogger: """Get configuration component logger.""" return LoggerFactory.get_logger("config") # Debug utilities class DebugLogger: """ Enhanced debug logging for development and troubleshooting. Provides additional context and verbosity in debug mode. """ def __init__(self, base_logger: StructuredLogger, debug_enabled: bool = False): self.base_logger = base_logger self.debug_enabled = debug_enabled def trace_method_call( self, func_name: str, args: tuple[Any, ...], kwargs: Dict[str, Any] ) -> None: """Trace method calls in debug mode.""" if self.debug_enabled: self.base_logger.trace( f"Method call: {func_name}", extra_context={ "function": func_name, "args_count": len(args), "kwargs_keys": list(kwargs.keys()), }, ) def dump_context(self, context_name: str, context_data: Dict[str, Any]) -> None: """Dump context information in debug mode.""" if self.debug_enabled: self.base_logger.debug( f"Context dump: {context_name}", extra_context={"context_dump": context_data}, ) def performance_report(self, operation: str, timings: Dict[str, float]) -> None: """Report detailed performance information.""" if self.debug_enabled: self.base_logger.info( f"Performance report: {operation}", extra_context={ "performance_timings": {k: f"{v:.2f}ms" for k, v in timings.items()} }, ) def debug_trace(operation: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator for debug tracing of operations.""" def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: # NOTE: Any types justified here - decorator must preserve arbitrary function signatures logger = get_store_logger() # Only trace if debug mode is enabled if hasattr(logger, "debug_enabled") and logger.debug_enabled: logger.trace(f"Starting {operation}") start_time = time.perf_counter() try: result = func(*args, **kwargs) duration = (time.perf_counter() - start_time) * 1000 logger.trace( f"Completed {operation}", extra_context={ContextKeys.DURATION_MS: round(duration, 2)}, ) return result except Exception as e: duration = (time.perf_counter() - start_time) * 1000 logger.trace( f"Failed {operation}", exception=e, extra_context={ContextKeys.DURATION_MS: round(duration, 2)}, ) raise else: return func(*args, **kwargs) return wrapper return decorator def log_cli_command( command_name: str, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to log CLI command execution.""" def decorator(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: # NOTE: Any types justified here - decorator must preserve arbitrary function signatures cli_logger = get_cli_logger() # Set command context cli_logger.set_request_context( cli_command=command_name, command_args=str(args), command_kwargs=str(kwargs), ) with cli_logger.performance_timer(f"cli_command_{command_name}"): cli_logger.info(f"Starting CLI command: {command_name}") try: result = func(*args, **kwargs) cli_logger.info(f"CLI command completed: {command_name}") return result except Exception as e: cli_logger.error(f"CLI command failed: {command_name}", exception=e) raise finally: cli_logger.clear_request_context() return wrapper return decorator

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sophia-labs/mnemosyne-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server