fastmcp/examples/opentelemetry_example.py
claude[bot] 8ecce7a95c Add built-in OpenTelemetry instrumentation middleware
- Create OpenTelemetryMiddleware with automatic span creation for all MCP operations
- Add opentelemetry as optional dependency (pip install fastmcp[opentelemetry])
- Gracefully degrades to no-op when OpenTelemetry not installed
- Configuration options for privacy (include_arguments) and performance (max_argument_length)
- Update documentation to reflect built-in support
- Update example to use built-in middleware
- Add comprehensive tests

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-12-03 22:46:14 +00:00

189 lines
5.8 KiB
Python

"""
OpenTelemetry Integration Example
This example demonstrates how to integrate OpenTelemetry with FastMCP for
comprehensive observability. It shows:
1. Configuring OpenTelemetry tracing and logging
2. Creating custom middleware that emits spans
3. Attaching OpenTelemetry to FastMCP's logger
4. Exporting to console (easily switch to OTLP for production)
To run this example:
uv run examples/opentelemetry_example.py
For production, replace ConsoleSpanExporter/ConsoleLogExporter with:
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
Requirements:
pip install opentelemetry-api opentelemetry-sdk
"""
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from fastmcp import FastMCP
from fastmcp.server.middleware.opentelemetry import OpenTelemetryMiddleware
from fastmcp.utilities.logging import get_logger
# ============================================================================
# OpenTelemetry Configuration
# ============================================================================
# Define service metadata
resource = Resource(
attributes={
"service.name": "fastmcp-weather-server",
"service.version": "1.0.0",
"deployment.environment": "development",
}
)
# Configure tracing
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(trace_provider)
# Configure logging
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
set_logger_provider(logger_provider)
# ============================================================================
# FastMCP Server Setup
# ============================================================================
# Create FastMCP server
mcp = FastMCP("Weather Server")
# Attach OpenTelemetry to FastMCP's logger
logger = get_logger("weather")
logger.addHandler(LoggingHandler(logger_provider=logger_provider))
# Add OpenTelemetry middleware
mcp.add_middleware(OpenTelemetryMiddleware())
# ============================================================================
# Server Tools
# ============================================================================
@mcp.tool()
def get_weather(city: str) -> dict:
"""Get current weather for a city.
Args:
city: Name of the city
Returns:
Weather information including temperature and conditions
"""
logger.info(f"Fetching weather for {city}")
# Simulate weather lookup
weather_data = {
"city": city,
"temperature": 72,
"condition": "sunny",
"humidity": 45,
}
logger.info(
f"Weather retrieved: {weather_data['condition']}, {weather_data['temperature']}°F"
)
return weather_data
@mcp.tool()
def get_forecast(city: str, days: int = 3) -> dict:
"""Get weather forecast for a city.
Args:
city: Name of the city
days: Number of days to forecast (1-7)
Returns:
Forecast data for the specified number of days
"""
logger.info(f"Fetching {days}-day forecast for {city}")
if days < 1 or days > 7:
logger.warning(f"Invalid days parameter: {days}. Must be 1-7.")
raise ValueError("Days must be between 1 and 7")
# Simulate forecast data
forecast = {
"city": city,
"days": days,
"forecast": [
{"day": i + 1, "temp": 70 + i, "condition": "partly cloudy"}
for i in range(days)
],
}
logger.info(f"Forecast retrieved for {days} days")
return forecast
@mcp.tool()
def convert_temperature(temp: float, from_unit: str, to_unit: str) -> dict:
"""Convert temperature between Fahrenheit and Celsius.
Args:
temp: Temperature value to convert
from_unit: Source unit ('F' or 'C')
to_unit: Target unit ('F' or 'C')
Returns:
Converted temperature value
"""
logger.debug(f"Converting {temp}°{from_unit} to °{to_unit}")
# Validate units
if from_unit not in ["F", "C"] or to_unit not in ["F", "C"]:
logger.error(f"Invalid units: {from_unit} or {to_unit}")
raise ValueError("Units must be 'F' or 'C'")
# Perform conversion
if from_unit == to_unit:
result = temp
elif from_unit == "F" and to_unit == "C":
result = (temp - 32) * 5 / 9
else: # from_unit == "C" and to_unit == "F"
result = (temp * 9 / 5) + 32
logger.info(f"Converted {temp}°{from_unit} to {result:.1f}°{to_unit}")
return {
"original": {"value": temp, "unit": from_unit},
"converted": {"value": round(result, 1), "unit": to_unit},
}
# ============================================================================
# Main
# ============================================================================
if __name__ == "__main__":
print("=" * 70)
print("FastMCP + OpenTelemetry Example")
print("=" * 70)
print("\nThis example demonstrates OpenTelemetry integration with FastMCP.")
print("Watch the console for:")
print(" - Trace spans showing tool execution timing")
print(" - Log entries from FastMCP's logger")
print("\nFor production, replace console exporters with OTLP exporters")
print("to send data to Grafana, Jaeger, or other observability platforms.")
print("=" * 70)
print()
# Run the server
mcp.run()