Source code for pantheon.utils.log

import sys
import warnings
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Optional

# Reconfigure stdout/stderr to use UTF-8 encoding (fixes Windows GBK issues with emoji)
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
    sys.stderr.reconfigure(encoding='utf-8', errors='replace')

from loguru import logger as loguru_logger

LEVEL_MAP = {
    "DEBUG": 10,
    "INFO": 20,
    "WARNING": 30,
    "ERROR": 40,
    "CRITICAL": 50,
}

# Track file handler ID for management
_file_handler_id: Optional[int] = None


[docs] @contextmanager def temporary_log_level(level: str): """Context manager to temporarily set log level for loguru logger Usage: with temporary_log_level("WARNING"): agent.run() # Only WARNING and ERROR will be logged """ # Use loguru's contextualize to set a context variable # Then the filter checks this variable to decide whether to log with loguru_logger.contextualize(log_level_override=level): yield
# Configure loguru handler with context-aware filter def _context_aware_filter(record): """Filter that respects context-local log level settings""" override_level = record["extra"].get("log_level_override") if override_level is None: return True # No override, allow all logs override_level_num = LEVEL_MAP.get(override_level, 0) record_level_num = record["level"].no return record_level_num >= override_level_num logger = loguru_logger # Track if logging has been explicitly disabled _logging_disabled = False # Apply context-aware filter to all handlers # Remove default handler and add new one with our filter # Use stdout instead of stderr so it works with prompt_toolkit's patch_stdout # Track console handler ID for management _console_handler_id: Optional[int] = None # Apply context-aware filter to all handlers # Remove default handler and add new one with our filter loguru_logger.remove() _console_handler_id = loguru_logger.add(sys.stdout, filter=_context_aware_filter, level="WARNING")
[docs] def set_level(level: str): """Set the logging level for the console handler. This safely replaces only the console handler, preserving other handlers (like file handlers). """ global _logging_disabled, _console_handler_id if _logging_disabled: return # Don't re-enable if disabled # Remove existing console handler if we have its ID if _console_handler_id is not None: try: loguru_logger.remove(_console_handler_id) except ValueError: pass # Handler might have been removed elsewhere # Add new console handler _console_handler_id = loguru_logger.add(sys.stdout, filter=_context_aware_filter, level=level)
[docs] def disable_all(): """Completely disable all logging. Cannot be re-enabled.""" global _logging_disabled _logging_disabled = True loguru_logger.remove() loguru_logger.disable("pantheon")
[docs] def setup_file_logging( log_dir: Optional[Path] = None, level: str = "INFO", session_name: str = "repl", ) -> Path: """Setup file logging to save logs to a file. This is useful in REPL mode where console logging is suppressed, but you still want to capture logs for debugging. The file log level defaults to INFO, which captures INFO, WARNING, and ERROR logs while avoiding verbose DEBUG output. This provides a good balance between having useful diagnostic information and avoiding excessive log size. Args: log_dir: Directory for log files. Defaults to settings.logs_dir (.pantheon/logs) level: Log level for file handler (default: INFO - captures most useful logs) session_name: Prefix for log file name (default: "repl") Returns: Path to the created log file """ global _file_handler_id # Import settings lazily to avoid circular imports if log_dir is None: from pantheon.settings import get_settings log_dir = get_settings().logs_dir # Ensure log directory exists log_dir = Path(log_dir) log_dir.mkdir(parents=True, exist_ok=True) # Generate timestamped log file name timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_file = log_dir / f"{session_name}_{timestamp}.log" # Remove existing file handler if any if _file_handler_id is not None: try: loguru_logger.remove(_file_handler_id) except ValueError: pass # Handler already removed # Add new file handler - captures all logs regardless of console level _file_handler_id = loguru_logger.add( log_file, level=level, format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {name}:{function}:{line} - {message}", rotation="10 MB", retention="7 days", encoding="utf-8", ) return log_file
# ============================================================================= # Warning Suppression # ============================================================================= # Suppress aiohttp "Unclosed client session" warnings. # These warnings are harmless - the OS cleans up connections on process exit. warnings.filterwarnings("ignore", message="Unclosed client session", category=ResourceWarning) warnings.filterwarnings("ignore", message="Unclosed connector", category=ResourceWarning) # Suppress websockets deprecation warnings from uvicorn warnings.filterwarnings("ignore", message="websockets.legacy is deprecated", category=DeprecationWarning) warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated", category=DeprecationWarning)
[docs] def suppress_aiohttp_warnings(loop, context) -> None: """Custom asyncio exception handler to suppress aiohttp cleanup warnings. aiohttp prints warnings via asyncio's exception handler during GC. Use with: loop.set_exception_handler(suppress_aiohttp_warnings) """ message = context.get("message", "") if "Unclosed" in message: return # Suppress aiohttp cleanup warnings # For other exceptions, use default handling loop.default_exception_handler(context)