Source code for logeverything.external.external
"""
External logger integration for the LogEverything library.
This module provides utilities for integrating third-party loggers with LogEverything,
using Python's standard logging mechanisms rather than complex wrapper classes.
"""
import importlib
import logging
import sys
from typing import Any, Dict, List, Optional, Tuple, Union
from ..core import _config, get_logger
from ..handlers.handlers import ConsoleHandler, PrettyFormatter
def _safe_log_level(val: Any, default: int = logging.INFO) -> int:
"""Safely convert a value to a valid log level."""
if isinstance(val, int):
return val
if isinstance(val, str):
level: Any = getattr(logging, val.upper(), None)
if level is not None and isinstance(level, int):
return int(level) # Explicit cast to int
return default
[docs]
def check_dependency(module_name: str, package_name: Optional[str] = None) -> Tuple[bool, str]:
"""
Check if an optional dependency is installed.
Args:
module_name: The name of the module to import.
package_name: The name of the package to mention in error messages
(if different from module_name).
Returns:
Tuple of (is_available, message). ``is_available`` is ``True`` if
the dependency is available; ``message`` is empty when available or
an error string when not.
"""
package = package_name or module_name
try:
# First check if the module is already imported
if module_name in sys.modules:
return True, ""
# Try to import the module
importlib.import_module(module_name)
return True, ""
except ImportError:
return False, (
f"Optional dependency '{package}' not installed. "
f"Install it with: pip install logeverything[{package.lower()}] "
f"or pip install {package}"
)
except Exception as e:
# Handle other import errors (like circular imports)
# Still consider the module available if it's a non-ImportError
# This handles cases like MLflow's circular import issues
if module_name in sys.modules:
return True, ""
return False, (
f"Optional dependency '{package}' could not be imported due to error: {e}. "
f"Install it with: pip install logeverything[{package.lower()}] "
f"or pip install {package}"
)
# Module-level state for stdlib interception
_original_root_handlers = None
_intercept_handler = None
class _LogEverythingInterceptHandler(logging.Handler):
"""Handler that routes all stdlib log records through LogEverything formatting."""
def __init__(self, level: int = logging.NOTSET, use_pretty_formatter: bool = True):
super().__init__(level)
self.use_pretty_formatter = use_pretty_formatter
if use_pretty_formatter:
self.setFormatter(PrettyFormatter())
def emit(self, record: logging.LogRecord) -> None:
try:
msg = self.format(record)
stream = sys.stderr if record.levelno >= logging.WARNING else sys.stdout
try:
stream.write(msg + "\n")
except UnicodeEncodeError:
# Fall back to ASCII-safe output on terminals that can't handle Unicode
stream.write(msg.encode("ascii", errors="replace").decode("ascii") + "\n")
stream.flush()
except Exception:
self.handleError(record)
[docs]
def intercept_stdlib(level: Optional[int] = None, use_pretty_formatter: bool = True) -> Any:
"""
Install a LogEverything handler on the root stdlib logger.
All stdlib loggers will now use LogEverything's formatting.
Args:
level: Logging level (default: uses LogEverything config or INFO)
use_pretty_formatter: Whether to use PrettyFormatter (default: True)
Returns:
The intercept handler (for later removal if needed)
"""
global _original_root_handlers, _intercept_handler
root = logging.getLogger()
# Save original state for restore_stdlib()
_original_root_handlers = list(root.handlers)
# Remove existing root handlers
for h in root.handlers[:]:
root.removeHandler(h)
# Install our handler
_intercept_handler = _LogEverythingInterceptHandler(
level=level or logging.NOTSET,
use_pretty_formatter=use_pretty_formatter,
)
root.addHandler(_intercept_handler)
# Set root level
effective_level: Any = level
if effective_level is None:
effective_level = _safe_log_level(_config.get("level", logging.INFO))
if isinstance(effective_level, str):
effective_level = getattr(logging, effective_level.upper(), logging.INFO)
root.setLevel(effective_level)
return _intercept_handler
[docs]
def restore_stdlib() -> None:
"""Restore the root logger to its state before intercept_stdlib()."""
global _original_root_handlers, _intercept_handler
if _original_root_handlers is None:
return
root = logging.getLogger()
# Remove our handler
for h in root.handlers[:]:
root.removeHandler(h)
# Restore originals
for h in _original_root_handlers:
root.addHandler(h)
_original_root_handlers = None
_intercept_handler = None
[docs]
def configure_external_logger(
logger_name: str,
level: Optional[Union[int, str]] = None,
use_pretty_formatter: bool = True,
propagate: bool = False,
) -> logging.Logger:
"""
Configure a third-party logger to use LogEverything's formatting and handling.
This function efficiently integrates external loggers with LogEverything's
configuration without creating complex wrappers or interceptors. It directly
configures the logger using standard Python logging mechanisms.
Args:
logger_name: The name of the logger to configure (e.g., 'langchain', 'mlflow')
level: The log level to set, or None to use LogEverything's default level
use_pretty_formatter: Whether to apply LogEverything's pretty formatting
propagate: Whether to allow the logger to propagate messages to parent loggers
Returns:
logging.Logger: The configured logger
Examples:
>>> # Configure a third-party logger
>>> configure_external_logger('langchain', level='DEBUG')
>>>
>>> # Configure a logger with propagation to parent loggers
>>> configure_external_logger('uvicorn.access', propagate=True)
"""
# Get the external logger
try:
external_logger = logging.getLogger(logger_name)
except Exception as e:
# This shouldn't happen, but just in case
get_logger(__name__).error(f"Failed to get logger '{logger_name}': {e}")
# Create a new logger as a fallback
external_logger = logging.Logger(logger_name)
# Determine the level to use - priority: explicit level > external_logger_level > global level
level_to_use = None
if level is not None:
if isinstance(level, str):
try:
level_to_use = getattr(logging, level.upper(), None)
if level_to_use is None:
get_logger(__name__).warning(f"Invalid log level '{level}', using INFO instead")
level_to_use = logging.INFO
except Exception:
get_logger(__name__).warning(
f"Failed to parse log level '{level}', using INFO instead"
)
level_to_use = logging.INFO
else:
level_to_use = level
elif _config.get("external_logger_level") is not None:
level_to_use = _safe_log_level(_config.get("external_logger_level"))
elif _config.get("level") is not None:
level_to_use = _safe_log_level(_config.get("level"))
# Set the level if determined
if level_to_use is not None:
external_logger.setLevel(level_to_use)
# Set propagation
external_logger.propagate = propagate
# Only manage handlers if not propagating
# When propagating, we let the parent logger handle the output
if not propagate:
# Store existing handlers before removing them (for debugging/auditing)
existing_handlers = list(external_logger.handlers)
# Clear existing handlers
for handler in existing_handlers:
external_logger.removeHandler(handler)
# Add appropriate handler with proper formatting
try:
if use_pretty_formatter:
handler = ConsoleHandler()
handler.setFormatter(PrettyFormatter())
external_logger.addHandler(handler)
else:
# Use a simple console handler
handler = logging.StreamHandler()
formatter = logging.Formatter(str(_config.get("format", "%(message)s")))
handler.setFormatter(formatter)
external_logger.addHandler(handler)
except Exception as e:
# If something goes wrong with our handlers, add a basic one to ensure logging works
get_logger(__name__).warning(f"Error setting up handlers for '{logger_name}': {e}")
fallback_handler = logging.StreamHandler()
fallback_handler.setFormatter(
logging.Formatter("%(name)s - %(levelname)s - %(message)s")
)
external_logger.addHandler(fallback_handler)
return external_logger
[docs]
def harmonize_logger_levels(
level: Optional[Union[int, str]] = None,
include_root: bool = True,
include_patterns: Optional[List[str]] = None,
exclude_patterns: Optional[List[str]] = None,
) -> Dict[str, int]:
"""
Set the same logging level for all detected loggers in the application.
This function helps ensure consistent logging levels across all loggers,
which is especially useful when integrating with third-party libraries
that may have their own logging settings.
Args:
level: The log level to set for all loggers (None uses LogEverything's level)
include_root: Whether to include the root logger (default: True)
include_patterns: List of logger name patterns to include (e.g., ['langchain.*', 'fastapi'])
exclude_patterns: List of logger name patterns to exclude (e.g., ['werkzeug.*'])
Returns:
Dict[str, int]: Dictionary mapping logger names to their previous levels
Examples:
>>> # Set all loggers to INFO level
>>> harmonize_logger_levels('INFO')
>>>
>>> # Set all loggers except the root logger to DEBUG
>>> harmonize_logger_levels('DEBUG', include_root=False)
>>>
>>> # Harmonize only specific loggers
>>> harmonize_logger_levels('WARNING', include_patterns=['uvicorn.*', 'fastapi'])
"""
# Determine the level value to use
level_value = None
if level is not None:
if isinstance(level, str):
try:
level_value = getattr(logging, level.upper(), None)
if level_value is None:
get_logger(__name__).warning(f"Invalid log level '{level}', using INFO instead")
level_value = logging.INFO
except Exception as e:
get_logger(__name__).warning(
f"Failed to parse log level '{level}', using INFO: {e}"
)
level_value = logging.INFO
else:
level_value = level
else:
# No level specified, use LogEverything's config
level_value = _safe_log_level(_config.get("level", logging.INFO))
# Store original levels
original_levels = {}
# Handle the root logger
if include_root:
root = logging.getLogger()
original_levels[root.name] = root.level
root.setLevel(level_value)
# Process include/exclude patterns
def should_process_logger(name: str) -> bool:
# If no include patterns, include everything by default
should_include = include_patterns is None
# Check include patterns
if include_patterns:
for pattern in include_patterns:
# Simple wildcard handling
if pattern.endswith(".*"):
prefix = pattern[:-2]
if name == prefix or name.startswith(prefix + "."):
should_include = True
break
elif pattern == name:
should_include = True
break
# Check exclude patterns
if exclude_patterns:
for pattern in exclude_patterns:
if pattern.endswith(".*"):
prefix = pattern[:-2]
if name == prefix or name.startswith(prefix + "."):
return False
elif pattern == name:
return False
return should_include
# Get all loggers from the manager
logger_names = list(logging.root.manager.loggerDict.keys())
# Process each logger
errors = []
for name in logger_names:
try:
if should_process_logger(name):
logger = logging.getLogger(name)
original_levels[name] = logger.level
logger.setLevel(level_value)
except Exception as e:
errors.append(f"Failed to set level for '{name}': {e}")
# Report any errors
if errors and len(errors) > 0:
for error in errors:
get_logger(__name__).warning(error)
return original_levels
[docs]
def configure_common_loggers(
additional_loggers: Optional[List[Union[str, Tuple[str, str]]]] = None,
exclude_loggers: Optional[List[str]] = None,
level: Optional[Union[int, str]] = None,
use_pretty_formatter: bool = True,
propagate: bool = False,
show_warnings: bool = True,
) -> List[str]:
"""
Automatically configure common third-party loggers.
Detects and configures common libraries' loggers to use
LogEverything's formatting for consistent output.
Args:
additional_loggers: Additional loggers to configure beyond the built-in list
exclude_loggers: Loggers to exclude from configuration
level: The log level to set for all loggers, or None to use the external_logger_level
use_pretty_formatter: Whether to apply LogEverything's pretty formatting
propagate: Whether to allow loggers to propagate messages to parent loggers
show_warnings: Whether to show warnings for missing optional dependencies
Returns:
List[str]: The names of the loggers that were configured
Examples:
>>> # Configure all detected common loggers
>>> configure_common_loggers()
>>>
>>> # Configure common loggers plus custom ones
>>> configure_common_loggers(additional_loggers=['my_custom_lib'])
>>>
>>> # Configure common loggers except specific ones
>>> configure_common_loggers(exclude_loggers=['sqlalchemy'])
"""
# Define common loggers with their associated module checks
logger_dependencies = [
# ML/AI Libraries
("langchain", "langchain"),
("mlflow", "mlflow"),
("transformers", "transformers", "transformers"),
("pytorch_lightning", "pytorch_lightning", "pytorch-lightning"),
("tensorflow", "tensorflow"),
("torch", "torch"),
("huggingface_hub", "huggingface_hub"),
# Web Frameworks
("fastapi", "fastapi"),
("uvicorn", "uvicorn"),
("uvicorn.access", "uvicorn"),
("flask", "flask"),
("django", "django"),
("starlette", "starlette"),
# Database
("sqlalchemy", "sqlalchemy"),
("alembic", "alembic"),
("pymongo", "pymongo"),
# Cloud/APIs
("boto3", "boto3"),
("azure", "azure"),
("google", "google"),
("requests", "requests"),
("urllib3", "urllib3"),
# Other common libraries
("pandas", "pandas"),
("numpy", "numpy"),
("matplotlib", "matplotlib"),
("plotly", "plotly"),
("jupyter", "jupyter"),
]
# Add any additional loggers (as tuples for consistency)
if additional_loggers:
for name in additional_loggers:
if isinstance(name, str):
logger_dependencies.append((name, name))
else:
logger_dependencies.append(name) # Apply exclusions
if exclude_loggers:
logger_dependencies = [
item for item in logger_dependencies if item[0] not in exclude_loggers
]
configured = []
dependency_warnings = []
# Determine the log level to use
# Priority: explicit level > external_logger_level > global level
level_to_use = level
if level_to_use is None and _config.get("external_logger_level") is not None:
external_level = _config.get("external_logger_level")
if isinstance(external_level, (int, str)):
level_to_use = external_level
# Track any errors for debugging
errors = []
# Get logger for this module
local_logger = get_logger(__name__)
for logger_item in logger_dependencies:
logger_name = logger_item[0]
module_name = logger_item[1]
package_name = logger_item[2] if len(logger_item) > 2 else module_name
# Check if dependency is available
is_available, warning = check_dependency(module_name, package_name)
if not is_available:
if show_warnings:
dependency_warnings.append(warning)
continue
try:
# Check if the library is imported (has a logger in the logger dict)
if logger_name in logging.Logger.manager.loggerDict:
# Configure with the external_logger_level if specified
configure_external_logger(
logger_name,
level=level_to_use,
use_pretty_formatter=use_pretty_formatter,
propagate=propagate,
)
configured.append(logger_name)
except Exception as e:
errors.append(f"Failed to configure logger '{logger_name}': {e}")
# If there were any errors, log them
if errors and len(errors) > 0:
for error in errors:
local_logger.warning(error)
# Show dependency warnings if any
if dependency_warnings and show_warnings:
for warning in dependency_warnings:
local_logger.info(f"Optional dependency warning: {warning}")
return configured