Skip to main content
Glama
logging.py8.36 kB
"""Structured logging with correlation IDs and metrics.""" import json import logging import uuid from contextvars import ContextVar from datetime import datetime from functools import wraps from typing import Any, Callable, Dict, Optional from rich.console import Console from rich.logging import RichHandler # Context variables for request tracing correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") operation_context: ContextVar[Dict[str, Any]] = ContextVar( "operation_context", default={} ) class StructuredFormatter(logging.Formatter): """JSON formatter for structured logging.""" def format(self, record: logging.LogRecord) -> str: # Create base log entry log_entry = { "timestamp": datetime.utcnow().isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "module": record.module, "function": record.funcName, "line": record.lineno, } # Add correlation ID if available if correlation_id.get(): log_entry["correlation_id"] = correlation_id.get() # Add operation context if available context = operation_context.get() if context: log_entry["context"] = context # Add any extra fields from the log record if hasattr(record, "extra_fields"): log_entry.update(record.extra_fields) # Add exception info if present if record.exc_info: log_entry["exception"] = self.formatException(record.exc_info) return json.dumps(log_entry, default=str) class StructuredLogger: """Enhanced logger with structured output and correlation IDs.""" def __init__(self, name: str, use_rich: bool = True): self.logger = logging.getLogger(f"LightfastMCP.{name}") self.use_rich = use_rich self._setup_formatter() def _setup_formatter(self): """Setup structured formatter or rich handler.""" # Remove existing handlers to avoid duplicates for handler in self.logger.handlers[:]: self.logger.removeHandler(handler) if self.use_rich: # Use Rich handler for development/interactive use handler = RichHandler( console=Console(stderr=True), rich_tracebacks=True, show_path=False, show_time=True, ) handler.setFormatter(logging.Formatter("%(message)s")) else: # Use JSON formatter for production/structured logging handler = logging.StreamHandler() handler.setFormatter(StructuredFormatter()) self.logger.addHandler(handler) self.logger.setLevel(logging.INFO) self.logger.propagate = False def _log_with_context(self, level: int, message: str, **kwargs): """Log with correlation ID and context.""" extra_fields = { "correlation_id": correlation_id.get(), "context": operation_context.get(), **kwargs, } # Create a log record with extra fields extra = {"extra_fields": extra_fields} self.logger.log(level, message, extra=extra) def debug(self, message: str, **kwargs): """Log debug message with context.""" self._log_with_context(logging.DEBUG, message, **kwargs) def info(self, message: str, **kwargs): """Log info message with context.""" self._log_with_context(logging.INFO, message, **kwargs) def warning(self, message: str, **kwargs): """Log warning message with context.""" self._log_with_context(logging.WARNING, message, **kwargs) def error(self, message: str, error: Optional[Exception] = None, **kwargs): """Log error message with context and optional exception.""" if error: kwargs["error_type"] = type(error).__name__ kwargs["error_details"] = str(error) if hasattr(error, "error_code"): kwargs["error_code"] = error.error_code if hasattr(error, "details"): kwargs["error_context"] = error.details self._log_with_context(logging.ERROR, message, **kwargs) # Also log the exception traceback if provided if error: self.logger.exception("Exception details:", exc_info=error) def critical(self, message: str, error: Optional[Exception] = None, **kwargs): """Log critical message with context.""" if error: kwargs["error_type"] = type(error).__name__ kwargs["error_details"] = str(error) self._log_with_context(logging.CRITICAL, message, **kwargs) def get_logger(name: str, use_rich: bool = True) -> StructuredLogger: """Get a structured logger instance. Args: name: Logger name (will be prefixed with 'LightfastMCP.') use_rich: Whether to use Rich formatting (True) or JSON (False) """ return StructuredLogger(name, use_rich=use_rich) def with_correlation_id(func: Optional[Callable] = None, *, generate_new: bool = True): """Decorator to add correlation ID to operations. Args: func: Function to decorate generate_new: Whether to generate a new correlation ID if one doesn't exist """ def decorator(f: Callable) -> Callable: @wraps(f) async def async_wrapper(*args, **kwargs): if generate_new and not correlation_id.get(): correlation_id.set(str(uuid.uuid4())) return await f(*args, **kwargs) @wraps(f) def sync_wrapper(*args, **kwargs): if generate_new and not correlation_id.get(): correlation_id.set(str(uuid.uuid4())) return f(*args, **kwargs) # Return appropriate wrapper based on function type import asyncio if asyncio.iscoroutinefunction(f): return async_wrapper else: return sync_wrapper if func is None: return decorator else: return decorator(func) def with_operation_context(**context_kwargs): """Decorator to add operation context to logs. Args: **context_kwargs: Context key-value pairs to add """ def decorator(func: Callable) -> Callable: @wraps(func) async def async_wrapper(*args, **kwargs): # Merge with existing context current_context = operation_context.get() new_context = {**current_context, **context_kwargs} operation_context.set(new_context) try: return await func(*args, **kwargs) finally: # Restore previous context operation_context.set(current_context) @wraps(func) def sync_wrapper(*args, **kwargs): current_context = operation_context.get() new_context = {**current_context, **context_kwargs} operation_context.set(new_context) try: return func(*args, **kwargs) finally: operation_context.set(current_context) import asyncio if asyncio.iscoroutinefunction(func): return async_wrapper else: return sync_wrapper return decorator def configure_logging( level: str = "INFO", use_rich: bool = True, logger_name: str = "LightfastMCP" ) -> None: """Configure logging for the entire application. Args: level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) use_rich: Whether to use Rich formatting logger_name: Root logger name """ root_logger = logging.getLogger(logger_name) root_logger.setLevel(getattr(logging, level.upper())) # Remove existing handlers for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) if use_rich: handler = RichHandler( console=Console(stderr=True), rich_tracebacks=True, show_path=False, show_time=True, ) handler.setFormatter(logging.Formatter("%(message)s")) else: handler = logging.StreamHandler() handler.setFormatter(StructuredFormatter()) root_logger.addHandler(handler) root_logger.propagate = False

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/lightfastai/lightfast-mcp'

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