"""
Logging configuration utility for Zotero MCP.
This module provides centralized logging configuration based on config.json settings.
"""
import json
import logging
import logging.handlers
import os
from pathlib import Path
from typing import Dict, Any, Optional
def configure_logging_from_config(config_path: Optional[str] = None, config: Optional[Dict[str, Any]] = None) -> None:
"""
Configure logging based on configuration file or dictionary.
Args:
config_path: Path to configuration file
config: Configuration dictionary (alternative to config_path)
"""
try:
# Load configuration
if config is None:
if config_path is None:
# Try default config paths
default_paths = [
Path.home() / ".config" / "zotero-mcp" / "config.json",
Path("KILOHINTS/config/config.json"),
Path("config.json")
]
for path in default_paths:
if path.exists():
config_path = str(path)
break
if config_path is None:
# No config found, use basic configuration
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return
with open(config_path, 'r') as f:
full_config = json.load(f)
else:
full_config = config
logging_config = full_config.get("logging", {})
# If no logging config, use basic setup
if not logging_config:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, logging_config.get("level", "INFO").upper()))
# Clear existing handlers
root_logger.handlers.clear()
# Get format
log_format = logging_config.get("format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
formatter = logging.Formatter(log_format)
# Configure handlers
handlers_config = logging_config.get("handlers", {})
# Console handler
console_config = handlers_config.get("console", {})
if console_config.get("enabled", True):
console_handler = logging.StreamHandler()
console_level = console_config.get("level", "INFO").upper()
console_handler.setLevel(getattr(logging, console_level))
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# File handler
file_config = handlers_config.get("file", {})
if file_config.get("enabled", False):
filename = file_config.get("filename", "zotero-mcp.log")
# Create log directory if needed
log_path = Path(filename)
log_path.parent.mkdir(parents=True, exist_ok=True)
# Use RotatingFileHandler if size limits are specified
max_bytes = file_config.get("max_bytes", 0)
backup_count = file_config.get("backup_count", 0)
if max_bytes > 0:
file_handler = logging.handlers.RotatingFileHandler(
filename=filename,
maxBytes=max_bytes,
backupCount=backup_count
)
else:
file_handler = logging.FileHandler(filename)
file_level = file_config.get("level", "DEBUG").upper()
file_handler.setLevel(getattr(logging, file_level))
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Configure specific loggers
loggers_config = logging_config.get("loggers", {})
for logger_name, logger_level in loggers_config.items():
if logger_name == "root":
continue # Already configured above
logger = logging.getLogger(logger_name)
logger.setLevel(getattr(logging, logger_level.upper()))
# Log configuration success
logger = logging.getLogger(__name__)
logger.info(f"Logging configured from config file: {config_path or 'provided config'}")
# Log active handlers
active_handlers = []
for handler in root_logger.handlers:
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler):
active_handlers.append(f"console({logging.getLevelName(handler.level)})")
elif isinstance(handler, logging.handlers.RotatingFileHandler):
active_handlers.append(f"rotating_file({logging.getLevelName(handler.level)})")
elif isinstance(handler, logging.FileHandler):
active_handlers.append(f"file({logging.getLevelName(handler.level)})")
logger.info(f"Active handlers: {', '.join(active_handlers)}")
except Exception as e:
# Fallback to basic logging if configuration fails
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger.error(f"Error configuring logging from config: {e}")
logger.info("Using basic logging configuration as fallback")
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance with the given name.
Args:
name: Logger name (typically __name__)
Returns:
Logger instance
"""
return logging.getLogger(name)
def setup_environment_logging() -> None:
"""
Setup logging based on environment variables (backwards compatibility).
"""
log_level = os.getenv("ZOTERO_LOG_LEVEL", "INFO").upper()
if log_level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
logging.basicConfig(
level=getattr(logging, log_level),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger.info(f"Logging configured from environment variable ZOTERO_LOG_LEVEL={log_level}")
def ensure_logging_configured(config_path: Optional[str] = None) -> None:
"""
Ensure logging is configured, using config file if available, environment variable otherwise.
Args:
config_path: Optional path to configuration file
"""
# Check if logging is already configured
root_logger = logging.getLogger()
if root_logger.handlers:
return # Already configured
# Try config file first
try:
configure_logging_from_config(config_path)
return
except Exception:
pass
# Fallback to environment variable
try:
setup_environment_logging()
return
except Exception:
pass
# Final fallback to basic config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)