Repository navigation
Record HTTP request and tool call metrics for FastMCP in Prometheus - #155
personage-hub wants to merge 2 commits into
Conversation
The FastMCP Prometheus instrument only exposed the registry on the metrics route, so the endpoint had nothing but process_* and python_* series. Every ASGI application returned by http_app() is now instrumented with prometheus-fastapi-instrumentator, the same library the FastAPI bootstrapper uses: requests are counted by route template (handler="/mcp", handler="/health/"), unknown paths are grouped into handler="none" and the metrics route itself is excluded. FastMcpPrometheusConfig gets the FastAPI-style prometheus_instrumentator_params, prometheus_instrument_params and prometheus_custom_labels; prometheus_registry is used for the request metrics as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tool calls are the unit of work of an MCP server, so FastMcpPrometheusMiddleware records
fastmcp_tool_calls_total{tool, status} and fastmcp_tool_call_duration_seconds{tool}, sharing the
registry and custom labels of the request metrics. Enabled by default, prometheus_tool_metrics=False
turns it off.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5d24612 to
48f0051
Compare
|
Reproduction with the same calls against one registry: registry = prometheus_client.CollectorRegistry()
for _ in range(2):
app = Starlette(routes=[Route("/ping", lambda _: PlainTextResponse("ok"))])
Instrumentator(registry=registry).add(instrumentator_metrics.default(registry=registry)).instrument(app)
with TestClient(app) as client:
client.get("/ping")
# http_requests_total{handler="/ping"} stays at 1.0 after the second app's requestA fix that works for us in modern-python/lite-bootstrap#281: build the default instrumentation once per instrument, on the first |
Summary
FastMcpPrometheusInstrumentonly exposed the registry on the metrics route, so a FastMCP service had nothing in/metricsbutprocess_*andpython_*, unlike the Litestar, FastAPI and FastStream bootstrappers, which also record request metrics. Two commits, the second one can be dropped if you prefer to keep the library to HTTP metrics:http_app()is instrumented via theadd_http_application_postprocessorhook from Add OpenTelemetry instrument for FastMCP #152, so requests are counted by route template (handler="/mcp",handler="/health/"), unknown paths are grouped intohandler="none", the metrics route itself is excluded.FastMcpPrometheusConfiggets the FastAPI-styleprometheus_instrumentator_params,prometheus_instrument_paramsandprometheus_custom_labels;prometheus_registryis reused for the request metrics. Thefastmcpextra now depends onprometheus-fastapi-instrumentator>=7.1(it only requiresstarletteandprometheus-client).FastMcpPrometheusMiddleware(a FastMCP middleware next toFastMcpLoggingMiddleware) recordsfastmcp_tool_calls_total{tool, status}andfastmcp_tool_call_duration_seconds{tool}. Enabled by default,prometheus_tool_metrics=Falseturns it off.No library for MCP-level metrics exists, so the tool call part is hand-written; the HTTP part reuses an existing dependency instead of adding a custom ASGI middleware.
Tests
test_fastmcp_prometheus_counts_http_requests_by_route_template: health route counted twice as2xx, unknown path asnone,POST /mcpby the mount path, metrics route not counted, custom label present.test_fastmcp_prometheus_instrumentator_params_are_passed:excluded_handlersandshould_group_status_codesoverrides.test_fastmcp_prometheus_counts_tool_calls/..._can_be_disabledwith the in-memoryfastmcp.Client.just test: 346 passed.Note:
uv run mypy .reports two pre-existing errors inmicrobootstrap/helpers.py:29with the locked mypy on Python 3.14; untouched here.🤖 Generated with Claude Code