mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-09-01 12:03:19 +02:00
Enable distributed tracing across protocols that don't support HTTP headers (like SSE) by propagating W3C Trace Context through MCP _meta fields. - Add propagate_context parameter (default: True) to OpenTelemetryMiddleware - Implement _extract_trace_context() to read traceparent/tracestate from request metadata - Implement _inject_trace_context() to write trace context to response metadata - Update all operation handlers to extract parent context and inject into results - Add comprehensive test coverage for context propagation - Update documentation with examples and configuration details This enables trace continuity across MCP calls, allowing clients to link server spans to their traces and propagate context downstream. Co-authored-by: William Easton <strawgate@users.noreply.github.com>
473 lines
16 KiB
Text
473 lines
16 KiB
Text
---
|
|
title: OpenTelemetry Integration
|
|
description: Instrument your FastMCP server with OpenTelemetry for distributed tracing and observability
|
|
icon: chart-line
|
|
---
|
|
|
|
import { VersionBadge } from "/snippets/version-badge.mdx"
|
|
|
|
FastMCP includes built-in OpenTelemetry instrumentation that automatically creates spans for all MCP operations. The integration provides comprehensive observability through distributed tracing, logging, and metrics with zero configuration required.
|
|
|
|
## Why OpenTelemetry?
|
|
|
|
OpenTelemetry is the industry-standard observability framework that provides:
|
|
|
|
- **Distributed Tracing**: Track MCP operations across your system with spans
|
|
- **Structured Logging**: Export FastMCP logs to observability backends
|
|
- **Metrics Collection**: Monitor performance and usage patterns
|
|
- **Vendor Agnostic**: Works with Jaeger, Zipkin, Grafana, Datadog, and more
|
|
- **Production Ready**: Battle-tested with stable APIs for tracing and metrics
|
|
|
|
## Quick Start
|
|
|
|
FastMCP includes OpenTelemetry middleware out of the box. Simply add the middleware to your server:
|
|
|
|
```python
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware
|
|
|
|
mcp = FastMCP("My Server")
|
|
mcp.add_middleware(OpenTelemetryMiddleware())
|
|
|
|
@mcp.tool()
|
|
def greet(name: str) -> str:
|
|
return f"Hello, {name}!"
|
|
```
|
|
|
|
If you don't have OpenTelemetry installed, the middleware gracefully becomes a no-op. To enable full instrumentation:
|
|
|
|
```bash
|
|
pip install fastmcp[opentelemetry]
|
|
```
|
|
|
|
Or install the packages directly:
|
|
|
|
```bash
|
|
pip install opentelemetry-api opentelemetry-sdk
|
|
```
|
|
|
|
For production deployments with OTLP export:
|
|
|
|
```bash
|
|
pip install opentelemetry-exporter-otlp-proto-grpc
|
|
```
|
|
|
|
<Note>
|
|
OpenTelemetry supports Python 3.9 and higher. Tracing and metrics are stable, while logging is in active development.
|
|
</Note>
|
|
|
|
## Logging Integration
|
|
|
|
FastMCP uses Python's standard `logging` module, which OpenTelemetry can instrument directly using `LoggingHandler`. This sends your FastMCP logs to any OpenTelemetry-compatible backend.
|
|
|
|
### Basic Setup
|
|
|
|
```python
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.resources import Resource
|
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
from opentelemetry._logs import set_logger_provider
|
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.utilities.logging import get_logger
|
|
|
|
# Configure OpenTelemetry
|
|
resource = Resource(attributes={
|
|
"service.name": "my-fastmcp-server",
|
|
"service.version": "1.0.0",
|
|
})
|
|
|
|
# Set up tracing
|
|
trace_provider = TracerProvider(resource=resource)
|
|
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
trace.set_tracer_provider(trace_provider)
|
|
|
|
# Set up logging
|
|
logger_provider = LoggerProvider(resource=resource)
|
|
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
|
|
set_logger_provider(logger_provider)
|
|
|
|
# Attach OpenTelemetry to FastMCP's logger
|
|
fastmcp_logger = get_logger("my_server")
|
|
fastmcp_logger.addHandler(LoggingHandler(logger_provider=logger_provider))
|
|
|
|
# Create your FastMCP server
|
|
mcp = FastMCP("My Server")
|
|
|
|
@mcp.tool()
|
|
def greet(name: str) -> str:
|
|
"""Greet someone by name."""
|
|
fastmcp_logger.info(f"Greeting {name}")
|
|
return f"Hello, {name}!"
|
|
```
|
|
|
|
### Production OTLP Export
|
|
|
|
For production environments, replace console exporters with OTLP exporters:
|
|
|
|
```python
|
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
|
|
|
# Configure OTLP endpoint (e.g., Grafana, Jaeger, or any OTLP collector)
|
|
otlp_endpoint = "http://localhost:4317"
|
|
|
|
# Tracing
|
|
trace_provider = TracerProvider(resource=resource)
|
|
trace_provider.add_span_processor(
|
|
BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint))
|
|
)
|
|
trace.set_tracer_provider(trace_provider)
|
|
|
|
# Logging
|
|
logger_provider = LoggerProvider(resource=resource)
|
|
logger_provider.add_log_record_processor(
|
|
BatchLogRecordProcessor(OTLPLogExporter(endpoint=otlp_endpoint))
|
|
)
|
|
set_logger_provider(logger_provider)
|
|
```
|
|
|
|
### Structured Logging with OpenTelemetry
|
|
|
|
FastMCP's `StructuredLoggingMiddleware` outputs JSON logs that OpenTelemetry collectors can parse and enrich:
|
|
|
|
```python
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.middleware.logging import StructuredLoggingMiddleware
|
|
|
|
mcp = FastMCP("Structured Server")
|
|
|
|
# Add structured logging middleware
|
|
mcp.add_middleware(StructuredLoggingMiddleware(
|
|
include_payloads=True,
|
|
max_payload_length=1000
|
|
))
|
|
|
|
# OpenTelemetry will capture these structured logs
|
|
```
|
|
|
|
The structured logs include metadata like request timestamps, method names, token estimates, and payload sizes - perfect for observability platforms.
|
|
|
|
## Built-in Tracing Middleware
|
|
|
|
FastMCP includes `OpenTelemetryMiddleware` that automatically creates spans for all MCP operations including tools, resources, prompts, and list operations.
|
|
|
|
### Configuration Options
|
|
|
|
The middleware supports several configuration options:
|
|
|
|
```python
|
|
from fastmcp import FastMCP
|
|
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware
|
|
|
|
mcp = FastMCP("My Server")
|
|
|
|
# Default configuration (recommended)
|
|
mcp.add_middleware(OpenTelemetryMiddleware())
|
|
|
|
# Custom configuration
|
|
mcp.add_middleware(OpenTelemetryMiddleware(
|
|
tracer_name="my-custom-tracer", # Custom tracer name
|
|
enabled=True, # Explicitly enable/disable
|
|
include_arguments=False, # Don't include arguments for privacy
|
|
max_argument_length=1000, # Limit argument string length in spans
|
|
propagate_context=True # Enable trace context propagation (default)
|
|
))
|
|
```
|
|
|
|
### Trace Context Propagation
|
|
|
|
The middleware automatically propagates trace context across MCP calls using the `_meta` field. This enables distributed tracing even when using protocols like SSE that don't support standard HTTP headers.
|
|
|
|
**How it works:**
|
|
|
|
1. **Incoming requests**: The middleware extracts trace context (W3C `traceparent` and `tracestate`) from the request's `_meta` field
|
|
2. **Span creation**: New spans are created as children of the incoming trace context
|
|
3. **Outgoing responses**: The middleware injects the current trace context into the response's `_meta` field
|
|
|
|
This means that if a client includes trace context in their request metadata, your server's spans will be linked to the client's trace. Similarly, if your server calls another MCP server, you can propagate context downstream.
|
|
|
|
**Example: Client propagating context**
|
|
|
|
```python
|
|
# Client code - sending trace context
|
|
result = await client.call_tool(
|
|
"my_tool",
|
|
{"arg": "value"},
|
|
_meta={"traceparent": "00-trace_id-span_id-01", "tracestate": "vendor=value"}
|
|
)
|
|
|
|
# The server will create spans as children of this trace
|
|
# And the response will include updated trace context in result.meta
|
|
if result.meta:
|
|
downstream_traceparent = result.meta.get("traceparent")
|
|
```
|
|
|
|
To disable context propagation:
|
|
|
|
```python
|
|
mcp.add_middleware(OpenTelemetryMiddleware(propagate_context=False))
|
|
```
|
|
|
|
### What Gets Traced
|
|
|
|
The middleware automatically creates spans for:
|
|
|
|
- **Tool Calls** (`tool.{name}`): Includes tool name, arguments, success status
|
|
- **Resource Reads** (`resource.read`): Includes resource URI
|
|
- **Prompt Retrievals** (`prompt.{name}`): Includes prompt name and arguments
|
|
- **List Operations**: Includes count of items returned
|
|
- `tools.list`
|
|
- `resources.list`
|
|
- `resource_templates.list`
|
|
- `prompts.list`
|
|
|
|
All spans include:
|
|
- MCP method name
|
|
- Source (client/server)
|
|
- Message type (request/notification)
|
|
- Success/error status
|
|
- Exception details on failure
|
|
|
|
### Custom Tracing Middleware
|
|
|
|
If you need additional custom spans beyond what the built-in middleware provides, you can extend the `Middleware` base class:
|
|
|
|
```python
|
|
from opentelemetry import trace
|
|
from opentelemetry.trace import Status, StatusCode
|
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
|
|
class CustomTracingMiddleware(Middleware):
|
|
"""Add custom spans for specific business logic."""
|
|
|
|
def __init__(self):
|
|
self.tracer = trace.get_tracer("my-custom-tracer")
|
|
|
|
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
"""Add custom spans around tool calls."""
|
|
tool_name = context.message.name
|
|
|
|
# Create a child span with custom attributes
|
|
with self.tracer.start_as_current_span(
|
|
f"custom.{tool_name}",
|
|
attributes={"custom.attribute": "value"}
|
|
) as span:
|
|
result = await call_next(context)
|
|
# Add custom business logic attributes
|
|
span.set_attribute("custom.result_type", type(result).__name__)
|
|
return result
|
|
|
|
# Stack middleware - built-in first, then custom
|
|
mcp.add_middleware(OpenTelemetryMiddleware()) # Built-in tracing
|
|
mcp.add_middleware(CustomTracingMiddleware()) # Your custom spans
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
Here's a production-ready example combining logging and tracing:
|
|
|
|
```python
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.resources import Resource
|
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
from opentelemetry._logs import set_logger_provider
|
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.utilities.logging import get_logger
|
|
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware
|
|
|
|
# Configure OpenTelemetry
|
|
resource = Resource(attributes={
|
|
"service.name": "weather-mcp-server",
|
|
"service.version": "1.0.0",
|
|
"deployment.environment": "production",
|
|
})
|
|
|
|
# Tracing setup
|
|
trace_provider = TracerProvider(resource=resource)
|
|
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
trace.set_tracer_provider(trace_provider)
|
|
|
|
# Logging setup
|
|
logger_provider = LoggerProvider(resource=resource)
|
|
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
|
|
set_logger_provider(logger_provider)
|
|
|
|
# Create FastMCP server
|
|
mcp = FastMCP("Weather Server")
|
|
|
|
# Attach OpenTelemetry to FastMCP logger
|
|
logger = get_logger("weather")
|
|
logger.addHandler(LoggingHandler(logger_provider=logger_provider))
|
|
|
|
# Add built-in tracing middleware
|
|
mcp.add_middleware(OpenTelemetryMiddleware())
|
|
|
|
@mcp.tool()
|
|
def get_weather(city: str) -> dict:
|
|
"""Get weather for a city."""
|
|
logger.info(f"Fetching weather for {city}")
|
|
return {"city": city, "temp": 72, "condition": "sunny"}
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|
|
```
|
|
|
|
## Exporting to Observability Backends
|
|
|
|
### Console Exporter (Development)
|
|
|
|
The console exporter is perfect for local development and testing:
|
|
|
|
```python
|
|
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
|
from opentelemetry.sdk._logs.export import ConsoleLogExporter
|
|
|
|
# Already shown in examples above
|
|
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
|
|
```
|
|
|
|
### OTLP Exporter (Production)
|
|
|
|
OTLP (OpenTelemetry Protocol) works with most modern observability platforms:
|
|
|
|
```python
|
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
|
|
|
# Configure for your backend
|
|
otlp_endpoint = "http://your-collector:4317"
|
|
|
|
trace_provider.add_span_processor(
|
|
BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint))
|
|
)
|
|
logger_provider.add_log_record_processor(
|
|
BatchLogRecordProcessor(OTLPLogExporter(endpoint=otlp_endpoint))
|
|
)
|
|
```
|
|
|
|
Supported backends include:
|
|
- **Grafana** with Tempo and Loki
|
|
- **Jaeger** for distributed tracing
|
|
- **Zipkin** for trace visualization
|
|
- **Datadog**, **New Relic**, **Honeycomb** (commercial platforms)
|
|
- **Self-hosted** OpenTelemetry Collector
|
|
|
|
### Environment Variables
|
|
|
|
OpenTelemetry exporters can be configured via environment variables:
|
|
|
|
```bash
|
|
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
|
export OTEL_SERVICE_NAME="my-fastmcp-server"
|
|
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
|
|
```
|
|
|
|
Then in your code:
|
|
|
|
```python
|
|
# OpenTelemetry will automatically use environment variables
|
|
trace_provider = TracerProvider()
|
|
trace_provider.add_span_processor(
|
|
BatchSpanProcessor(OTLPSpanExporter()) # Uses OTEL_EXPORTER_OTLP_ENDPOINT
|
|
)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### When to Use Logging vs Spans
|
|
|
|
- **Logging**: Discrete events, errors, diagnostic messages
|
|
- **Spans**: Operations with duration, distributed tracing across services
|
|
|
|
For FastMCP servers:
|
|
- Use **spans** for tool calls, resource reads, prompt executions
|
|
- Use **logging** for validation errors, configuration issues, business logic events
|
|
|
|
### Performance Considerations
|
|
|
|
OpenTelemetry is designed for production, but follow these guidelines:
|
|
|
|
1. **Use BatchProcessors**: Always use `BatchSpanProcessor` and `BatchLogRecordProcessor` rather than synchronous exporters
|
|
2. **Sampling**: For high-volume servers, configure sampling to reduce overhead:
|
|
|
|
```python
|
|
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
|
|
# Sample 10% of traces
|
|
trace_provider = TracerProvider(
|
|
resource=resource,
|
|
sampler=TraceIdRatioBased(0.1)
|
|
)
|
|
```
|
|
|
|
3. **Attribute Limits**: Avoid adding large payloads as span attributes. Use `max_payload_length` in middleware:
|
|
|
|
```python
|
|
# Good - limit attribute size
|
|
span.set_attribute("tool.arguments", str(args)[:500])
|
|
|
|
# Bad - unbounded attribute size
|
|
span.set_attribute("tool.arguments", str(args)) # Could be huge!
|
|
```
|
|
|
|
### Security: Avoiding Sensitive Data
|
|
|
|
Never log sensitive information in traces or logs:
|
|
|
|
```python
|
|
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
tool_name = context.message.name
|
|
|
|
# Redact sensitive arguments
|
|
safe_args = {
|
|
k: v if k not in ["password", "api_key", "token"] else "***REDACTED***"
|
|
for k, v in context.message.arguments.items()
|
|
}
|
|
|
|
with self.tracer.start_as_current_span(
|
|
f"tool.{tool_name}",
|
|
attributes={"tool.arguments": str(safe_args)}
|
|
) as span:
|
|
return await call_next(context)
|
|
```
|
|
|
|
### Integration with Other Middleware
|
|
|
|
OpenTelemetry middleware works seamlessly with FastMCP's other built-in middleware:
|
|
|
|
```python
|
|
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware
|
|
from fastmcp.server.middleware.timing import TimingMiddleware
|
|
from fastmcp.server.middleware.logging import LoggingMiddleware
|
|
|
|
# Order matters for middleware execution
|
|
mcp.add_middleware(OpenTelemetryMiddleware()) # Tracing first for complete lifecycle
|
|
mcp.add_middleware(TimingMiddleware()) # Timing within traces
|
|
mcp.add_middleware(LoggingMiddleware()) # Logging captures everything
|
|
```
|
|
|
|
The execution order ensures:
|
|
1. OpenTelemetry captures the complete request lifecycle including timing and logging
|
|
2. Timing data is included within trace spans
|
|
3. Logs are correlated with active traces
|
|
4. Everything is properly instrumented for observability
|
|
|
|
## Additional Resources
|
|
|
|
- [OpenTelemetry Python Documentation](https://opentelemetry.io/docs/languages/python/)
|
|
- [FastMCP Middleware Guide](/servers/middleware)
|
|
- [FastMCP Logging Guide](/servers/logging)
|
|
- [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/)
|
|
|
|
<Tip>
|
|
For examples and sample code, see [`examples/opentelemetry_example.py`](https://github.com/jlowin/fastmcp/tree/main/examples/opentelemetry_example.py) in the FastMCP repository.
|
|
</Tip>
|