fastmcp/docs/servers/telemetry.mdx
Bill Easton 7184a4ca21
OTEL: Fix attribute compliance with MCP semantic conventions (#3889)
* OTEL: Fix attribute compliance and improve telemetry helpers

Attribute compliance:
- Remove rpc.system/service/method (MCP is not traditional RPC)
- Add gen_ai.tool.name on tools/call spans
- Add gen_ai.prompt.name on prompts/get spans
- Fix session_id check (truthy -> is not None)

Telemetry helper improvements:
- Add is_recording() guards to skip work on non-recording spans
- Add error.type attribute with __qualname__ on error spans
- Use isinstance check for ToolError to set "tool_error" error type
- Include exception message in span status description
- Add tool_name/prompt_name params to server_span and client_span

Client call_tool enrichment:
- Reflect tool-level errors (result.isError) on client span status
  so callers see ERROR even though the MCP protocol call succeeded

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove resource URI from span names to avoid high-cardinality

Per MCP semantic conventions, resource URIs SHOULD NOT be included in
span names by default since they can be unbounded (especially with
templates like users://{id}/profile). The URI remains available via
the mcp.resource.uri attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing gen_ai/mcp attributes to proxy and delegate spans

- Proxy tool spans: add gen_ai.tool.name
- Proxy prompt spans: add gen_ai.prompt.name
- All delegate spans: add mcp.method.name
- Docs: remove rpc.* references, update span names and attributes table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hoist ToolError imports to module level, add rpc.* migration note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:28:47 -04:00

278 lines
9 KiB
Text

---
title: OpenTelemetry
sidebarTitle: Telemetry
description: Native OpenTelemetry instrumentation for distributed tracing.
icon: chart-line
tag: NEW
---
FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, and resource template operations, providing visibility into server behavior, request handling, and provider delegation chains.
## How It Works
FastMCP uses the OpenTelemetry API for instrumentation. This means:
- **Zero configuration required** - Instrumentation is always active
- **No overhead when unused** - Without an SDK, all operations are no-ops
- **Bring your own SDK** - You control collection, export, and sampling
- **Works with any OTEL backend** - Jaeger, Zipkin, Datadog, New Relic, etc.
## Enabling Telemetry
The easiest way to export traces is using `opentelemetry-instrument`, which configures the SDK automatically:
```bash
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
```
Then run your server with tracing enabled:
```bash
opentelemetry-instrument \
--service_name my-fastmcp-server \
--exporter_otlp_endpoint http://localhost:4317 \
fastmcp run server.py
```
Or configure via environment variables:
```bash
export OTEL_SERVICE_NAME=my-fastmcp-server
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
opentelemetry-instrument fastmcp run server.py
```
This works with any OTLP-compatible backend (Jaeger, Zipkin, Grafana Tempo, Datadog, etc.) and requires no changes to your FastMCP code.
<Card title="OpenTelemetry Python Documentation" icon="book" href="https://opentelemetry.io/docs/languages/python/">
Learn more about the OpenTelemetry Python SDK, auto-instrumentation, and available exporters.
</Card>
## Tracing
FastMCP creates spans for all MCP operations, providing end-to-end visibility into request handling.
### Server Spans
The server creates spans for each operation using [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
| Span Name | Description |
|-----------|-------------|
| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |
| `resources/read` | Resource read (URI in `mcp.resource.uri` attribute, not span name) |
| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |
For mounted servers, an additional `delegate {name}` span shows the delegation to the child server.
### Client Spans
The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read`, `prompts/get {name}`).
### Span Hierarchy
Spans form a hierarchy showing the request flow. For mounted servers:
```
tools/call weather_forecast (CLIENT)
└── tools/call weather_forecast (SERVER, provider=FastMCPProvider)
└── delegate get_weather (INTERNAL)
└── tools/call get_weather (SERVER, provider=LocalProvider)
```
For proxy providers connecting to remote servers:
```
tools/call remote_search (CLIENT)
└── tools/call remote_search (SERVER, provider=ProxyProvider)
└── [remote server spans via trace context propagation]
```
## Programmatic Configuration
For more control, configure the SDK in your Python code before importing FastMCP:
```python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Configure the SDK with OTLP exporter
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# Now import and use FastMCP - traces will be exported automatically
from fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def greet(name: str) -> str:
return f"Hello, {name}!"
```
<Tip>
The SDK must be configured **before** importing FastMCP to ensure the tracer provider is set when FastMCP initializes.
</Tip>
### Local Development
For quick local trace visualization, [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer) is a lightweight single-binary tool:
```bash
# macOS
brew install nico-barbas/brew/otel-desktop-viewer
# Or download from GitHub releases
```
Run it alongside your server:
```bash
# Terminal 1: Start the viewer (UI at http://localhost:8000, OTLP on :4317)
otel-desktop-viewer
# Terminal 2: Run your server with tracing
opentelemetry-instrument fastmcp run server.py
```
For more features, use [Jaeger](https://www.jaegertracing.io/):
```bash
docker run -d --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest
```
Then view traces at http://localhost:16686
## Custom Spans
You can add your own spans using the FastMCP tracer:
```python
from fastmcp import FastMCP
from fastmcp.telemetry import get_tracer
mcp = FastMCP("custom-spans")
@mcp.tool()
async def complex_operation(input: str) -> str:
tracer = get_tracer()
with tracer.start_as_current_span("parse_input") as span:
span.set_attribute("input.length", len(input))
parsed = parse(input)
with tracer.start_as_current_span("process_data") as span:
span.set_attribute("data.count", len(parsed))
result = process(parsed)
return result
```
## Error Handling
When errors occur, spans are automatically marked with error status and the exception is recorded:
```python
@mcp.tool()
def risky_operation() -> str:
raise ValueError("Something went wrong")
# The span will have:
# - status = ERROR with exception message as description
# - error.type = "tool_error" (or exception class name for non-tool errors)
# - exception event with stack trace
```
## Attributes Reference
<Warning>
**Migrating from v3.1 or earlier:** The `rpc.system`, `rpc.service`, and `rpc.method` span attributes were removed in favor of the [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) listed below. If you have dashboards or alerts keyed on those `rpc.*` attributes, update them to use `mcp.method.name` and the `fastmcp.*` attributes instead.
</Warning>
### MCP Semantic Conventions
FastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
| Attribute | Description |
|-----------|-------------|
| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |
| `mcp.session.id` | Session identifier for the MCP connection |
| `mcp.resource.uri` | The resource URI (for resource operations) |
| `gen_ai.tool.name` | Tool name (on `tools/call` spans) |
| `gen_ai.prompt.name` | Prompt name (on `prompts/get` spans) |
| `error.type` | Error classification (`tool_error` for ToolError, otherwise exception class name) |
### Auth Attributes
Standard [identity attributes](https://opentelemetry.io/docs/specs/semconv/attributes-registry/enduser/):
| Attribute | Description |
|-----------|-------------|
| `enduser.id` | Client ID from access token (when authenticated) |
| `enduser.scope` | Space-separated OAuth scopes (when authenticated) |
### FastMCP Custom Attributes
All custom attributes use the `fastmcp.` prefix for features unique to FastMCP:
| Attribute | Description |
|-----------|-------------|
| `fastmcp.server.name` | Server name |
| `fastmcp.component.type` | `tool`, `resource`, `prompt`, or `resource_template` |
| `fastmcp.component.key` | Full component identifier (e.g., `tool:greet`) |
| `fastmcp.provider.type` | Provider class (`LocalProvider`, `FastMCPProvider`, `ProxyProvider`) |
Provider-specific attributes for delegation context:
| Attribute | Description |
|-----------|-------------|
| `fastmcp.delegate.original_name` | Original tool/prompt name before namespacing |
| `fastmcp.delegate.original_uri` | Original resource URI before namespacing |
| `fastmcp.proxy.backend_name` | Remote server tool/prompt name |
| `fastmcp.proxy.backend_uri` | Remote server resource URI |
## Testing with Telemetry
For testing, use the in-memory exporter:
```python
import pytest
from collections.abc import Generator
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from fastmcp import FastMCP
@pytest.fixture
def trace_exporter() -> Generator[InMemorySpanExporter, None, None]:
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
original_provider = trace.get_tracer_provider()
trace.set_tracer_provider(provider)
yield exporter
exporter.clear()
trace.set_tracer_provider(original_provider)
async def test_tool_creates_span(trace_exporter: InMemorySpanExporter) -> None:
mcp = FastMCP("test")
@mcp.tool()
def hello() -> str:
return "world"
await mcp.call_tool("hello", {})
spans = trace_exporter.get_finished_spans()
assert any(s.name == "tools/call hello" for s in spans)
```