--- 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 ``` OpenTelemetry supports Python 3.9 and higher. Tracing and metrics are stable, while logging is in active development. ## 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/) 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.