diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index c783510c9..5a8ca5793 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -195,6 +195,18 @@ class Settings(BaseSettings): docket: DocketSettings = DocketSettings() + enable_rich_logging: Annotated[ + bool, + Field( + description=inspect.cleandoc( + """ + If True, will use rich formatting for log output. If False, + will use standard Python logging without rich formatting. + """ + ) + ), + ] = True + enable_rich_tracebacks: Annotated[ bool, Field( diff --git a/src/fastmcp/utilities/logging.py b/src/fastmcp/utilities/logging.py index 476bdff23..0c2907b48 100644 --- a/src/fastmcp/utilities/logging.py +++ b/src/fastmcp/utilities/logging.py @@ -57,6 +57,18 @@ def configure_logging( logger.propagate = False logger.setLevel(level) + # Remove any existing handlers to avoid duplicates on reconfiguration + for hdlr in logger.handlers[:]: + logger.removeHandler(hdlr) + + # Use standard logging handlers if rich logging is disabled + if not fastmcp.settings.enable_rich_logging: + # Create a standard StreamHandler for stderr + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger.addHandler(handler) + return + # Configure the handler for normal logs handler = RichHandler( console=Console(stderr=True), @@ -91,10 +103,6 @@ def configure_logging( traceback_handler.addFilter(lambda record: record.exc_info is not None) - # Remove any existing handlers to avoid duplicates on reconfiguration - for hdlr in logger.handlers[:]: - logger.removeHandler(hdlr) - logger.addHandler(handler) logger.addHandler(traceback_handler) diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index 58e53eb12..d15b9c355 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -1,5 +1,6 @@ import logging +import fastmcp from fastmcp.utilities.logging import configure_logging, get_logger @@ -54,3 +55,46 @@ def test_configure_logging_traceback_defaults_can_be_overridden(): assert logger.handlers # The traceback handler should have been created with custom values # We can't directly inspect RichHandler internals easily, but we verified no error was raised + + +def test_configure_logging_with_rich_disabled(): + """Test that disabling rich logging uses standard StreamHandler.""" + original_enable_rich = fastmcp.settings.enable_rich_logging + try: + fastmcp.settings.enable_rich_logging = False + configure_logging() + + logger = logging.getLogger("fastmcp") + assert logger.handlers + # Should only have one handler when rich logging is disabled + assert len(logger.handlers) == 1 + # Should be a StreamHandler, not RichHandler + assert isinstance(logger.handlers[0], logging.StreamHandler) + assert not hasattr( + logger.handlers[0], "console" + ) # RichHandler has console attribute + finally: + fastmcp.settings.enable_rich_logging = original_enable_rich + # Reconfigure to restore state + configure_logging() + + +def test_configure_logging_with_rich_enabled(): + """Test that enabling rich logging uses RichHandler.""" + original_enable_rich = fastmcp.settings.enable_rich_logging + try: + fastmcp.settings.enable_rich_logging = True + configure_logging() + + logger = logging.getLogger("fastmcp") + assert logger.handlers + # Should have two handlers when rich logging is enabled (normal + traceback) + assert len(logger.handlers) == 2 + # Both should be RichHandler instances + from rich.logging import RichHandler + + assert all(isinstance(h, RichHandler) for h in logger.handlers) + finally: + fastmcp.settings.enable_rich_logging = original_enable_rich + # Reconfigure to restore state + configure_logging()