mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-10 23:59:10 +02:00
Adds opt-in distributed tracing via OpenTelemetry for observability into
FastMCP server and client operations.
Server spans are created for tool calls, resource reads, and prompt
renders with attributes like component key, component type, provider
type, session ID, and auth context. Client spans wrap outgoing calls
with trace context propagation via W3C headers in request meta.
Components provide their own span attributes through a `get_span_attributes()`
method that subclasses override - this lets LocalProvider, FastMCPProvider,
and ProxyProvider each include relevant context (original names, backend URIs).
To enable: configure an OpenTelemetry SDK with a TracerProvider before
importing fastmcp. Traces export to any OTLP-compatible backend.
Closes ENG-2813
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
#!/usr/bin/env python
|
|
"""Client script to exercise all diagnostics server components with tracing.
|
|
|
|
Usage:
|
|
# First, start the diagnostics server with tracing in one terminal:
|
|
uv run examples/run_with_tracing.py examples/diagnostics/server.py --transport sse --port 8001
|
|
|
|
# Then run this client in another terminal:
|
|
uv run examples/diagnostics/client_with_tracing.py
|
|
|
|
# View traces in otel-desktop-viewer (http://localhost:8000):
|
|
otel-desktop-viewer
|
|
|
|
This script exercises all 8 components:
|
|
- 4 successful: ping, diag://status, diag://echo/{message}, greet prompt
|
|
- 4 error: fail_tool, diag://error, diag://error/{code}, fail prompt
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import os
|
|
|
|
# Configure OTEL SDK before importing fastmcp
|
|
from opentelemetry import trace
|
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
from opentelemetry.sdk.resources import Resource
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
|
|
|
|
def setup_tracing():
|
|
"""Set up OpenTelemetry tracing with OTLP export."""
|
|
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
|
|
service_name = os.environ.get("OTEL_SERVICE_NAME", "fastmcp-diagnostics-client")
|
|
|
|
resource = Resource.create({"service.name": service_name})
|
|
provider = TracerProvider(resource=resource)
|
|
exporter = OTLPSpanExporter(endpoint=endpoint, insecure=True)
|
|
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
trace.set_tracer_provider(provider)
|
|
|
|
print(f"Tracing enabled: OTLP {endpoint}")
|
|
print(f"Service: {service_name}")
|
|
print("View traces: otel-desktop-viewer (http://localhost:8000)\n")
|
|
|
|
|
|
async def main():
|
|
setup_tracing()
|
|
|
|
from fastmcp import Client
|
|
|
|
server_url = os.environ.get("DIAGNOSTICS_SERVER_URL", "http://localhost:8001/sse")
|
|
print(f"Connecting to: {server_url}\n")
|
|
|
|
async with Client(server_url) as client:
|
|
# List all available components
|
|
tools = await client.list_tools()
|
|
resources = await client.list_resources()
|
|
prompts = await client.list_prompts()
|
|
|
|
print(f"Found {len(tools)} tools: {[t.name for t in tools]}")
|
|
print(f"Found {len(resources)} resources: {[r.uri for r in resources]}")
|
|
print(f"Found {len(prompts)} prompts: {[p.name for p in prompts]}\n")
|
|
|
|
# === SUCCESSFUL OPERATIONS ===
|
|
print("=" * 60)
|
|
print("SUCCESSFUL OPERATIONS")
|
|
print("=" * 60)
|
|
|
|
# Local successful components
|
|
print("\n--- Local Tools ---")
|
|
result = await client.call_tool("ping", {})
|
|
print(f"ping: {result}")
|
|
|
|
print("\n--- Local Resources ---")
|
|
result = await client.read_resource("diag://status")
|
|
print(f"diag://status: {result}")
|
|
|
|
result = await client.read_resource("diag://echo/hello-world")
|
|
print(f"diag://echo/hello-world: {result}")
|
|
|
|
print("\n--- Local Prompts ---")
|
|
result = await client.get_prompt("greet", {"name": "Diagnostics"})
|
|
print(
|
|
f"greet: {result.messages[0].content.text if result.messages else result}"
|
|
)
|
|
|
|
# Proxied components from echo server
|
|
print("\n--- Proxied Tools ---")
|
|
try:
|
|
result = await client.call_tool(
|
|
"proxied_echo_tool", {"text": "proxied test"}
|
|
)
|
|
print(f"proxied_echo_tool: {result}")
|
|
except Exception as e:
|
|
print(f"proxied_echo_tool: ERROR - {e}")
|
|
|
|
print("\n--- Proxied Resources ---")
|
|
try:
|
|
# Resource: echo://static -> echo://proxied/static
|
|
result = await client.read_resource("echo://proxied/static")
|
|
print(f"echo://proxied/static: {result}")
|
|
except Exception as e:
|
|
print(f"echo://proxied/static: ERROR - {e}")
|
|
|
|
try:
|
|
# Template: echo://{text} -> echo://proxied/{text}
|
|
result = await client.read_resource("echo://proxied/test-message")
|
|
print(f"echo://proxied/test-message: {result}")
|
|
except Exception as e:
|
|
print(f"echo://proxied/test-message: ERROR - {e}")
|
|
|
|
print("\n--- Proxied Prompts ---")
|
|
try:
|
|
result = await client.get_prompt("proxied_echo", {"text": "proxied prompt"})
|
|
print(
|
|
f"proxied_echo: {result.messages[0].content.text if result.messages else result}"
|
|
)
|
|
except Exception as e:
|
|
print(f"proxied_echo: ERROR - {e}")
|
|
|
|
# === ERROR OPERATIONS ===
|
|
print("\n" + "=" * 60)
|
|
print("ERROR OPERATIONS (expected to fail)")
|
|
print("=" * 60)
|
|
|
|
print("\n--- Error Tools ---")
|
|
try:
|
|
await client.call_tool("fail_tool", {})
|
|
print("fail_tool: UNEXPECTED SUCCESS")
|
|
except Exception as e:
|
|
print(f"fail_tool: {type(e).__name__} - {e}")
|
|
|
|
print("\n--- Error Resources ---")
|
|
try:
|
|
await client.read_resource("diag://error")
|
|
print("diag://error: UNEXPECTED SUCCESS")
|
|
except Exception as e:
|
|
print(f"diag://error: {type(e).__name__} - {e}")
|
|
|
|
try:
|
|
await client.read_resource("diag://error/500")
|
|
print("diag://error/500: UNEXPECTED SUCCESS")
|
|
except Exception as e:
|
|
print(f"diag://error/500: {type(e).__name__} - {e}")
|
|
|
|
print("\n--- Error Prompts ---")
|
|
try:
|
|
await client.get_prompt("fail", {})
|
|
print("fail: UNEXPECTED SUCCESS")
|
|
except Exception as e:
|
|
print(f"fail: {type(e).__name__} - {e}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("DONE - Check otel-desktop-viewer for traces")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|