Avoid propagating logs (#1042)

* Avoid propagating logs

* Set up caplog for non-root loggers

* Update tests
This commit is contained in:
Jeremiah Lowin 2025-07-04 12:22:11 -04:00 committed by GitHub
commit 228425e309
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 65 additions and 30 deletions

View file

@ -50,3 +50,6 @@ def configure_logging(
logger.removeHandler(hdlr)
logger.addHandler(handler)
# Don't propagate to the root logger
logger.propagate = False

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import copy
import logging
import multiprocessing
import socket
import time
@ -129,3 +130,15 @@ def run_server_in_process(
proc.join(timeout=2)
if proc.is_alive():
raise RuntimeError("Server process failed to terminate even after kill")
@contextmanager
def caplog_for_fastmcp(caplog):
"""Context manager to capture logs from FastMCP loggers even when propagation is disabled."""
caplog.clear()
logger = logging.getLogger("FastMCP")
logger.addHandler(caplog.handler)
try:
yield
finally:
logger.removeHandler(caplog.handler)

View file

@ -6,6 +6,7 @@ import pytest
from fastmcp import FastMCP
from fastmcp.settings import Settings
from fastmcp.utilities.tests import caplog_for_fastmcp
# reset deprecation warnings for this module
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
@ -313,7 +314,8 @@ class TestDeprecatedEnvironmentVariables:
try:
os.environ[env_var_name] = "192.168.1.1"
settings = Settings()
with caplog_for_fastmcp(caplog):
settings = Settings()
# Check that a warning was logged
assert any(
@ -341,8 +343,9 @@ class TestDeprecatedSettingsProperty:
"""Test that accessing fastmcp.settings.settings logs a deprecation warning."""
from fastmcp import settings
# Access the deprecated property
deprecated_settings = settings.settings
with caplog_for_fastmcp(caplog):
# Access the deprecated property
deprecated_settings = settings.settings
# Check that a warning was logged
assert any(

View file

@ -7,6 +7,7 @@ from fastmcp.exceptions import NotFoundError, PromptError
from fastmcp.prompts import Prompt
from fastmcp.prompts.prompt import FunctionPrompt, PromptMessage, TextContent
from fastmcp.prompts.prompt_manager import PromptManager
from fastmcp.utilities.tests import caplog_for_fastmcp
class TestPromptManager:
@ -31,7 +32,10 @@ class TestPromptManager:
manager = PromptManager(duplicate_behavior="warn")
prompt = Prompt.from_function(fn)
first = manager.add_prompt(prompt)
second = manager.add_prompt(prompt)
with caplog_for_fastmcp(caplog):
second = manager.add_prompt(prompt)
assert first == second
assert "Prompt already exists" in caplog.text
@ -58,7 +62,9 @@ class TestPromptManager:
prompt = Prompt.from_function(test_fn, name="test_prompt")
manager.add_prompt(prompt)
manager.add_prompt(prompt)
with caplog_for_fastmcp(caplog):
manager.add_prompt(prompt)
assert "Prompt already exists: test_prompt" in caplog.text
# Should have the prompt

View file

@ -11,6 +11,7 @@ from fastmcp.resources import (
ResourceTemplate,
)
from fastmcp.resources.resource import FunctionResource
from fastmcp.utilities.tests import caplog_for_fastmcp
@pytest.fixture
@ -78,7 +79,9 @@ class TestResourceManager:
)
manager.add_resource(resource)
manager.add_resource(resource)
with caplog_for_fastmcp(caplog):
manager.add_resource(resource)
assert "Resource already exists" in caplog.text
# Should have the resource
@ -181,7 +184,9 @@ class TestResourceManager:
)
manager.add_template(template)
manager.add_template(template)
with caplog_for_fastmcp(caplog):
manager.add_template(template)
assert "Template already exists" in caplog.text
# Should have the template

View file

@ -8,6 +8,7 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport, SSETransport
from fastmcp.server.proxy import FastMCPProxy
from fastmcp.utilities.tests import caplog_for_fastmcp
class TestBasicMount:
@ -290,25 +291,26 @@ class TestMultipleServerMount:
main_app.mount(unreachable_proxy, "unreachable")
# All object types should work from working server despite unreachable proxy
async with Client(main_app) as client:
# Test tools
tools = await client.list_tools()
tool_names = [tool.name for tool in tools]
assert "working_working_tool" in tool_names
with caplog_for_fastmcp(caplog):
async with Client(main_app) as client:
# Test tools
tools = await client.list_tools()
tool_names = [tool.name for tool in tools]
assert "working_working_tool" in tool_names
# Test calling a tool
result = await client.call_tool("working_working_tool", {})
assert result.data == "Working tool"
# Test calling a tool
result = await client.call_tool("working_working_tool", {})
assert result.data == "Working tool"
# Test resources
resources = await client.list_resources()
resource_uris = [str(resource.uri) for resource in resources]
assert "working://working/data" in resource_uris
# Test resources
resources = await client.list_resources()
resource_uris = [str(resource.uri) for resource in resources]
assert "working://working/data" in resource_uris
# Test prompts
prompts = await client.list_prompts()
prompt_names = [prompt.name for prompt in prompts]
assert "working_working_prompt" in prompt_names
# Test prompts
prompts = await client.list_prompts()
prompt_names = [prompt.name for prompt in prompts]
assert "working_working_prompt" in prompt_names
# Verify that warnings were logged for the unreachable server
warning_messages = [

View file

@ -15,6 +15,7 @@ from typing_extensions import TypedDict
from fastmcp.tools.tool import Tool, _convert_to_content
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.tests import caplog_for_fastmcp
from fastmcp.utilities.types import Audio, File, Image
@ -1007,12 +1008,11 @@ class TestConvertResultToContent:
def test_custom_serializer_error_fallback(self, caplog):
"""Test that if a custom serializer fails, it falls back to the default."""
import logging
def custom_serializer_that_fails(data):
raise ValueError("Serialization failed")
with caplog.at_level(logging.WARNING):
with caplog_for_fastmcp(caplog):
result = _convert_to_content(
{"a": 1}, serializer=custom_serializer_that_fails
)

View file

@ -12,6 +12,7 @@ from fastmcp import Context, FastMCP
from fastmcp.exceptions import NotFoundError, ToolError
from fastmcp.tools import FunctionTool, ToolManager
from fastmcp.tools.tool import Tool
from fastmcp.utilities.tests import caplog_for_fastmcp
from fastmcp.utilities.types import Image
@ -178,8 +179,10 @@ class TestAddTools:
tool1 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool1)
tool2 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool2)
with caplog_for_fastmcp(caplog):
tool2 = Tool.from_function(test_fn, name="test_tool")
manager.add_tool(tool2)
assert "Tool already exists: test_tool" in caplog.text
# Should have the tool