Skip to main content
Glama
test_logging_config.py16.3 kB
# # Copyright (C) 2024 Billy Bryant # Portions copyright (C) 2024 Sergey Parfenyuk (original MIT-licensed author) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. # # MIT License attribution: Portions of this file were originally licensed # under the MIT License by Sergey Parfenyuk (2024). # """Tests for the enhanced logging configuration module.""" import logging from io import StringIO from unittest.mock import MagicMock, patch from rich.console import Console from mcp_foxxy_bridge.utils.logging import ( MCPRichHandler, get_logger, setup_logging, ) # Constants for console width testing DEFAULT_CONSOLE_WIDTH = 120 CUSTOM_CONSOLE_WIDTH = 80 class TestMCPRichHandler: """Test cases for MCPRichHandler class.""" def test_init_default_settings(self) -> None: """Test MCPRichHandler initialization with default settings.""" handler = MCPRichHandler() assert handler.console is not None assert isinstance(handler.console, Console) assert handler.console.stderr # Should use stderr assert handler.console._force_terminal assert handler.console.options.max_width == DEFAULT_CONSOLE_WIDTH def test_init_custom_console(self) -> None: """Test MCPRichHandler initialization with custom console.""" custom_console = Console(width=CUSTOM_CONSOLE_WIDTH) handler = MCPRichHandler(console=custom_console) assert handler.console is custom_console assert handler.console.options.max_width == CUSTOM_CONSOLE_WIDTH def test_init_custom_options(self) -> None: """Test MCPRichHandler initialization with custom options.""" handler = MCPRichHandler( show_time=False, show_level=False, show_path=True, rich_tracebacks=False, tracebacks_show_locals=True, ) # Note: These settings are passed to parent RichHandler # We can't easily test them without complex introspection assert isinstance(handler.console, Console) def test_get_level_text(self) -> None: """Test custom level text formatting.""" handler = MCPRichHandler() # Create a mock log record record = logging.LogRecord( name="test", level=logging.INFO, pathname="test.py", lineno=1, msg="Test message", args=(), exc_info=None, ) level_text = handler.get_level_text(record) assert level_text.plain == " INFO " # Centered in 7 characters # Check that the style contains the logging level style assert len(level_text.spans) > 0 assert level_text.spans[0].style == "logging.level.info" def test_render_message_basic(self) -> None: """Test basic message rendering.""" handler = MCPRichHandler() record = logging.LogRecord( name="test", level=logging.INFO, pathname="test.py", lineno=1, msg="Test message", args=(), exc_info=None, ) message_text = handler.render_message(record, "Test message") assert message_text.plain == "Test message" def test_render_message_server_highlighting(self) -> None: """Test message rendering with server name highlighting.""" handler = MCPRichHandler() # Create record that looks like it's from a server record = logging.LogRecord( name="mcp.server.filesystem.test", level=logging.INFO, pathname="test.py", lineno=1, msg="Server message", args=(), exc_info=None, ) message_text = handler.render_message(record, "Server message") # Should include server name highlighting assert "filesystem" in message_text.markup assert "[bold green]" in message_text.markup def test_render_message_non_server(self) -> None: """Test message rendering for non-server logs.""" handler = MCPRichHandler() record = logging.LogRecord( name="mcp_foxxy_bridge.config_loader", level=logging.INFO, pathname="test.py", lineno=1, msg="Config message", args=(), exc_info=None, ) message_text = handler.render_message(record, "Config message") # Should not include server highlighting assert message_text.plain == "Config message" class TestSetupRichLogging: """Test cases for setup_rich_logging function.""" def teardown_method(self) -> None: """Clean up logging configuration after each test.""" # Reset root logger root_logger = logging.getLogger() for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) root_logger.setLevel(logging.WARNING) # Reset specific loggers for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "mcp", "mcp.server"]: logger = logging.getLogger(logger_name) logger.handlers.clear() logger.setLevel(logging.NOTSET) logger.propagate = True def test_setup_rich_logging_debug_mode(self) -> None: """Test setup_rich_logging in debug mode.""" setup_logging(debug=True) # Check root logger configuration root_logger = logging.getLogger() assert root_logger.level == logging.DEBUG assert len(root_logger.handlers) == 2 # Console + file handler handler_types = [type(h).__name__ for h in root_logger.handlers] assert "MCPRichHandler" in handler_types assert "RotatingFileHandler" in handler_types # Check MCP handler level mcp_handler = next(h for h in root_logger.handlers if isinstance(h, MCPRichHandler)) handler = mcp_handler assert handler.level == logging.DEBUG def test_setup_rich_logging_normal_mode(self) -> None: """Test setup_rich_logging in normal mode.""" setup_logging(debug=False) # Check root logger configuration root_logger = logging.getLogger() assert root_logger.level == logging.INFO assert len(root_logger.handlers) == 2 # Console + file handler handler_types = [type(h).__name__ for h in root_logger.handlers] assert "MCPRichHandler" in handler_types assert "RotatingFileHandler" in handler_types # Check MCP handler level mcp_handler = next(h for h in root_logger.handlers if isinstance(h, MCPRichHandler)) handler = mcp_handler assert handler.level == logging.INFO def test_setup_third_party_loggers(self) -> None: """Test setup of third-party logger configurations.""" setup_logging(debug=True) # Check asyncio logger asyncio_logger = logging.getLogger("asyncio") assert asyncio_logger.level == logging.ERROR # Check uvicorn loggers uvicorn_logger = logging.getLogger("uvicorn") assert uvicorn_logger.level == logging.INFO assert not uvicorn_logger.propagate assert len(uvicorn_logger.handlers) == 1 assert isinstance(uvicorn_logger.handlers[0], MCPRichHandler) uvicorn_access_logger = logging.getLogger("uvicorn.access") assert uvicorn_access_logger.level == logging.INFO assert not uvicorn_access_logger.propagate uvicorn_error_logger = logging.getLogger("uvicorn.error") assert uvicorn_error_logger.level == logging.WARNING assert not uvicorn_error_logger.propagate def test_setup_mcp_loggers_debug_mode(self) -> None: """Test MCP logger setup in debug mode.""" setup_logging(debug=True) # Check MCP loggers mcp_logger = logging.getLogger("mcp") assert mcp_logger.level == logging.WARNING assert not mcp_logger.propagate mcp_server_logger = logging.getLogger("mcp.server") assert mcp_server_logger.level == logging.INFO # Debug mode assert not mcp_server_logger.propagate def test_setup_mcp_loggers_normal_mode(self) -> None: """Test MCP logger setup in normal mode.""" setup_logging(debug=False) # Check MCP server logger in normal mode mcp_server_logger = logging.getLogger("mcp.server") assert mcp_server_logger.level == logging.ERROR # Normal mode assert not mcp_server_logger.propagate def test_handler_replacement(self) -> None: """Test that existing handlers are properly replaced.""" # Add some existing handlers root_logger = logging.getLogger() old_handler = logging.StreamHandler() root_logger.addHandler(old_handler) uvicorn_logger = logging.getLogger("uvicorn") old_uvicorn_handler = logging.StreamHandler() uvicorn_logger.addHandler(old_uvicorn_handler) # Setup rich logging setup_logging(debug=True) # Check that old handlers were removed assert old_handler not in root_logger.handlers assert old_uvicorn_handler not in uvicorn_logger.handlers # Check that new handlers are MCPRichHandler and RotatingFileHandler instances assert len(root_logger.handlers) == 2 # Console + file handler handler_types = [type(h).__name__ for h in root_logger.handlers] assert "MCPRichHandler" in handler_types assert "RotatingFileHandler" in handler_types assert len(uvicorn_logger.handlers) == 1 assert isinstance(uvicorn_logger.handlers[0], MCPRichHandler) def test_uvicorn_access_formatter(self) -> None: """Test custom Uvicorn access log formatter.""" setup_logging(debug=True) uvicorn_access_logger = logging.getLogger("uvicorn.access") handler = uvicorn_access_logger.handlers[0] formatter = handler.formatter assert formatter is not None, "Formatter should not be None" # Create a mock record like uvicorn creates record = logging.LogRecord( name="uvicorn.access", level=logging.INFO, pathname="access.py", lineno=1, msg='%s - "%s" %s', args=("127.0.0.1:12345", "GET /test", "200"), exc_info=None, ) formatted = formatter.format(record) # Should format as: client - "method path" status assert formatted == '127.0.0.1:12345 - "GET /test" 200' def test_uvicorn_access_formatter_insufficient_args(self) -> None: """Test Uvicorn access formatter with insufficient arguments.""" setup_logging(debug=True) uvicorn_access_logger = logging.getLogger("uvicorn.access") handler = uvicorn_access_logger.handlers[0] formatter = handler.formatter assert formatter is not None, "Formatter should not be None" # Create a record with insufficient args record = logging.LogRecord( name="uvicorn.access", level=logging.INFO, pathname="access.py", lineno=1, msg="Incomplete log entry %s", # Proper format string args=("127.0.0.1",), # Only 1 arg exc_info=None, ) formatted = formatter.format(record) # Should fall back to default formatting assert formatted == "Incomplete log entry 127.0.0.1" def test_uvicorn_access_formatter_no_args(self) -> None: """Test Uvicorn access formatter with no arguments.""" setup_logging(debug=True) uvicorn_access_logger = logging.getLogger("uvicorn.access") handler = uvicorn_access_logger.handlers[0] formatter = handler.formatter assert formatter is not None, "Formatter should not be None" # Create a record with no args record = logging.LogRecord( name="uvicorn.access", level=logging.INFO, pathname="access.py", lineno=1, msg="No args log entry", args=(), exc_info=None, ) formatted = formatter.format(record) # Should fall back to default formatting assert formatted == "No args log entry" class TestGetLogger: """Test cases for get_logger function.""" def test_get_logger(self) -> None: """Test get_logger function.""" logger = get_logger("test.module") assert isinstance(logger, logging.Logger) assert logger.name == "test.module" def test_get_logger_hierarchy(self) -> None: """Test logger hierarchy with get_logger.""" parent_logger = get_logger("parent") child_logger = get_logger("parent.child") assert child_logger.parent == parent_logger def test_get_logger_same_instance(self) -> None: """Test that get_logger returns the same instance for the same name.""" logger1 = get_logger("same.name") logger2 = get_logger("same.name") assert logger1 is logger2 class TestLoggingIntegration: """Integration tests for logging configuration.""" def teardown_method(self) -> None: """Clean up logging configuration after each test.""" # Reset root logger root_logger = logging.getLogger() for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) root_logger.setLevel(logging.WARNING) def test_logging_output_capture(self) -> None: """Test that logging output can be captured for testing.""" # Setup logging with a test handler test_handler = logging.StreamHandler(StringIO()) test_handler.setLevel(logging.DEBUG) root_logger = logging.getLogger() root_logger.addHandler(test_handler) root_logger.setLevel(logging.DEBUG) # Create a logger and log a message logger = get_logger("test.output") logger.info("Test message") # Check that message was captured output = test_handler.stream.getvalue() assert "Test message" in output def test_server_logger_formatting(self) -> None: """Test that server loggers format messages correctly.""" setup_logging(debug=True) # Create a server logger server_logger = get_logger("mcp_foxxy_bridge.servers.test_server") # This would be tested more thoroughly in integration tests # where we can capture the actual Rich output assert server_logger.name == "mcp_foxxy_bridge.servers.test_server" @patch("mcp_foxxy_bridge.utils.logging.Console") def test_rich_console_configuration(self, mock_console_class: MagicMock) -> None: """Test Rich console configuration.""" mock_console = MagicMock() mock_console_class.return_value = mock_console MCPRichHandler() # Verify console was configured correctly mock_console_class.assert_called_with( stderr=True, force_terminal=True, width=120, ) def test_multiple_setup_calls(self) -> None: """Test that multiple setup calls work correctly.""" # First setup setup_logging(debug=True) root_handlers_count_1 = len(logging.getLogger().handlers) # Second setup - should replace handlers setup_logging(debug=False) root_handlers_count_2 = len(logging.getLogger().handlers) # Should still have the same number of handlers (console + file) assert root_handlers_count_1 == root_handlers_count_2 == 2 # Root logger level should be updated assert logging.getLogger().level == logging.INFO

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/billyjbryant/mcp-foxxy-bridge'

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