Repository navigation
Default opentelemetry_exclude_spans to ["receive", "send"] and support it on FastMCP #309
Description
opentelemetry_exclude_spans defaults to an empty list on FastAPI and Litestar (lite_bootstrap/bootstrappers/fastapi_bootstrapper.py:65, litestar_bootstrapper.py:127), so every request records three spans: the server span plus one each for the ASGI receive and send events. benchmarks/README.md section 4a measured ["receive", "send"] at -34.0 µs per request on FastAPI. The event spans are started with start_as_current_span even when the trace is unsampled, so the context attach and detach run either way.
Re-measured on 2026-10-24 at 705d8a3 on Linux with 2 CPUs (CPython 3.14.9, opentelemetry 1.44.0 / contrib 0.65b0, 100% sampling, OTLP over HTTP to a stub collector in another process, do-nothing handler):
| default | ["receive", "send"] |
saved | |
|---|---|---|---|
| FastAPI, in-process | +147.9 µs | +101.6 µs | 46 µs |
| Litestar, in-process (two runs) | +145.4 / +131.8 µs | +92.4 / +90.8 µs | 41-53 µs |
Litestar on Granian (1 worker, ab -k -c 16) |
+168.9 µs | +102.7 µs | 66 µs |
FastMCP per tools/call, exclusion passed to the middleware as a prototype |
+272 µs | +181 µs | 91 µs |
The FastMCP suite is noisy at about ±30 µs. At 10% sampling the exclusion still saves 20-25 µs on FastAPI and Litestar (79.0 to 54.0, 66.8 to 46.7). The tables and scripts are to be published with #335.
FastMcpConfig has no such field (fastmcp_bootstrapper.py:192-199), although the OpenTelemetryMiddleware it adds at :235-244 accepts exclude_spans at the 0.49b0 floor (opentelemetry/instrumentation/asgi/__init__.py:551 at contrib tag v0.49b0). FastMCP (3.x and 4.x) also emits its own SERVER span per MCP operation (fastmcp 3.4.6 server/telemetry.py:56-75, telemetry docs), so a tool call over HTTP currently records an HTTP server span, a FastMCP server span and the send spans.
Work
- Test first: flip
test_fastapi_opentelemetry_exclude_spans_defaults_to_recording_them(tests/test_fastapi_bootstrap.py:74-76) andtest_litestar_otel_exclude_spans_defaults_to_recording_them(tests/test_litestar_bootstrap.py:640-642) to expect["receive", "send"], and add a FastMCP test that the middleware receivesexclude_spans=["receive", "send"]by default. All three fail today. - Change the default on
FastAPIConfigandLitestarConfig, and update the comment atfastapi_bootstrapper.py:64, which says only the FastAPI and Litestar instrumentors take it. - Add
opentelemetry_exclude_spanstoFastMcpConfigand pass it toOpenTelemetryMiddlewareinFastMcpOpenTelemetryInstrument._instrument_http_app. The field with an empty default is non-breaking and can ship in a 1.x minor ahead of the default flip, so the 2.0 PR is only the flip. - Optional: declare the field once on a small mixin shared by the three ASGI configs instead of three times. FastStream and Free must not get it.
- Docs:
docs/introduction/configuration.md:178-183(the "For FastAPI and Litestar" paragraph, which also gains FastMCP), the table indocs/introduction/performance.md:49, and the tuned configuration inbenchmarks/README.mdsection 6, which then no longer needs the setting.
Verify: the three tests pass, and a Litestar request under opentelemetry_log_traces=True prints one server span.
Breaking for anyone who reads the per-message receive/send spans. They set opentelemetry_exclude_spans=[] to get them back.
Activity