mcp-browser/mcp_browser/logging_config.py

147 lines
4.8 KiB
Python

"""
Logging configuration for MCP Browser.
"""
import logging
import sys
import os
from pathlib import Path
from typing import Optional
from datetime import datetime
class ServerNameAdapter(logging.LoggerAdapter):
"""Add server name context to log messages."""
def process(self, msg, kwargs):
server = self.extra.get('server', 'main')
return f"[{server}] {msg}", kwargs
def setup_logging(debug: bool = False, log_file: Optional[Path] = None,
log_level: Optional[str] = None, use_syslog: bool = False):
"""
Configure logging for MCP Browser.
Args:
debug: Enable debug logging
log_file: Optional file to write logs to
log_level: Override log level (DEBUG, INFO, WARNING, ERROR)
use_syslog: Use syslog instead of console/file (for server mode)
"""
# Determine log level
if log_level:
level = getattr(logging, log_level.upper(), logging.INFO)
elif debug:
level = logging.DEBUG
else:
level = logging.INFO
# Create formatter
if level == logging.DEBUG:
# Include timestamp and module for debug
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
else:
# Simpler format for normal use
formatter = logging.Formatter('%(levelname)s: %(message)s')
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
# Remove existing handlers
root_logger.handlers.clear()
if use_syslog:
# Use syslog for server mode - NEVER write to stdout/stderr
from logging.handlers import SysLogHandler
try:
# Try Unix socket first (most common on Linux)
syslog_handler = SysLogHandler(
facility=SysLogHandler.LOG_DAEMON,
address='/dev/log'
)
except (OSError, FileNotFoundError):
# Fall back to UDP socket (for macOS, some Linux distros)
try:
syslog_handler = SysLogHandler(
facility=SysLogHandler.LOG_DAEMON,
address=('localhost', 514)
)
except Exception:
# If syslog is not available, log to a file instead
import tempfile
fallback_log = Path(tempfile.gettempdir()) / 'mcp-browser-server.log'
file_handler = logging.FileHandler(fallback_log, mode='a')
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
return
# Syslog has its own format, but we can prepend our app name
syslog_formatter = logging.Formatter('mcp-browser[%(process)d]: %(name)s - %(levelname)s - %(message)s')
syslog_handler.setFormatter(syslog_formatter)
root_logger.addHandler(syslog_handler)
elif log_file:
# File handler if requested
file_handler = logging.FileHandler(log_file, mode='a')
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Only add console handler if not logging to /dev/null
if str(log_file) != '/dev/null':
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
else:
# Console handler (stderr) only if no file specified
console_handler = logging.StreamHandler(sys.stderr)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# Set levels for specific loggers
# Suppress some noisy libraries unless in debug mode
if level > logging.DEBUG:
logging.getLogger('asyncio').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
def get_logger(name: str, server: Optional[str] = None) -> logging.Logger:
"""
Get a logger with optional server context.
Args:
name: Logger name (usually __name__)
server: Optional server name for context
Returns:
Logger or LoggerAdapter with server context
"""
logger = logging.getLogger(name)
if server:
return ServerNameAdapter(logger, {'server': server})
return logger
class RawIOFilter(logging.Filter):
"""Filter to only show raw I/O at TRACE level (5)."""
def filter(self, record):
# Only show raw I/O messages at TRACE level
return record.levelno <= 5 or not record.msg.startswith(('>>> ', '<<< '))
# Add custom TRACE level for raw I/O
TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
def trace(self, message, *args, **kwargs):
if self.isEnabledFor(TRACE):
self._log(TRACE, message, args, **kwargs)
# Add trace method to Logger class
logging.Logger.trace = trace