feat: Add telemetry interop mode for FastMCP (#4046)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Bill Easton 2026-07-27 15:05:29 -05:00 committed by GitHub
commit 75b9f92504
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 494 additions and 45 deletions

View file

@ -77,7 +77,7 @@ These control how the server listens when running with an HTTP transport.
| Environment Variable | Type | Default | Description |
|---|---|---|---|
| `FASTMCP_ENABLE_TELEMETRY` | `bool` | `true` | Whether FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry) is active. Enabled by default; FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry SDK and exporter are configured. Set to `false` to turn instrumentation off entirely, in which case no FastMCP spans are created even when an SDK is configured. |
| `FASTMCP_TELEMETRY_MODE` | `Literal["native", "propagation_only", "off"]` | `native` | Controls FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry). `native` emits FastMCP's MCP spans and propagates trace context; because FastMCP uses only the OpenTelemetry API, this costs almost nothing unless an SDK and exporter are configured. `propagation_only` keeps `_meta` trace propagation and still parents downstream spans from the incoming context, but emits none of FastMCP's own spans, so another instrumentation layer can own the MCP span hierarchy. `off` is a full pass-through: no spans, and no trace context extracted or attached. |
## Tasks (Docket)

View file

@ -21,11 +21,21 @@ FastMCP uses the OpenTelemetry API for instrumentation. This means:
Because FastMCP only depends on the OpenTelemetry API, span creation is a no-op until you configure an SDK and exporter — so being on by default costs nothing until you opt into collection.
### Turning Telemetry Off
### Telemetry Modes
<VersionBadge version="4.0.0" />
To disable FastMCP's instrumentation entirely, set `FASTMCP_ENABLE_TELEMETRY=false` (or `fastmcp.settings.enable_telemetry = False`). When disabled, FastMCP creates no spans even if an SDK is configured.
`FASTMCP_TELEMETRY_MODE` (or `fastmcp.settings.telemetry_mode`) controls how much of the instrumentation is active:
| Mode | FastMCP spans | Trace context |
|---|---|---|
| `native` (default) | Emitted | Propagated |
| `propagation_only` | Suppressed | Propagated |
| `off` | Suppressed | Untouched |
Use `off` to disable FastMCP's instrumentation entirely. No spans are created even if an SDK is configured, and FastMCP leaves the surrounding OpenTelemetry context exactly as it found it.
Use `propagation_only` when another instrumentation layer already owns the MCP span hierarchy — see [Interoperability](#interoperability) below.
## Enabling Telemetry
@ -148,6 +158,37 @@ trace.set_tracer_provider(provider)
The name check must happen before `ParentBased` delegates. If the name-based sampler is nested inside `ParentBased`, it is not consulted for child spans whose parent was already sampled.
## Interoperability
<VersionBadge version="4.0.0" />
FastMCP assumes it owns the MCP span hierarchy. When something else already owns it — an MCP-aware OpenTelemetry instrumentation library, or a service mesh that understands the protocol — FastMCP's spans duplicate what that layer already emits, and the same request shows up twice in your traces.
Setting `propagation_only` resolves the duplication in FastMCP's favor of the other layer:
```bash
export FASTMCP_TELEMETRY_MODE=propagation_only
```
The distinction from `off` matters here. Both emit no FastMCP spans, but `off` is fully transparent, while `propagation_only` still extracts the trace context arriving in `_meta` and attaches it for the duration of the request. Spans created downstream — by your tool handlers, or by the instrumentation layer that owns the hierarchy — are parented to the calling trace rather than starting a new one. Outbound requests still carry `traceparent` and `tracestate` in `_meta`.
### Suppressing spans for a single block
Library authors embedding FastMCP inside their own instrumented stack often want to own the hierarchy for one specific operation rather than process-wide. `suppress_fastmcp_telemetry()` applies `propagation_only` semantics to a block:
```python
from fastmcp import Client
from fastmcp.telemetry import suppress_fastmcp_telemetry
async def search(client: Client, query: str):
with suppress_fastmcp_telemetry():
return await client.call_tool("search", {"query": query})
```
This is narrower than OpenTelemetry's global instrumentation suppression: only FastMCP's spans are skipped, so nested instrumentation for HTTP clients, databases, and everything else keeps emitting normally.
The context manager has no effect when `telemetry_mode` is already `off`. A request to skip FastMCP's spans cannot re-enable the context propagation that `off` deliberately omits.
## Programmatic Configuration
For more control, configure the SDK in your Python code before importing FastMCP: