Skip to content

Commit fbc7092

Browse files
committed
Use custom OTelMiddleware with either Azure or Logfire
1 parent 4e9c5ba commit fbc7092

File tree

5 files changed

+95
-20
lines changed

5 files changed

+95
-20
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
"azure-cosmos>=4.9.0",
1919
"azure-monitor-opentelemetry>=1.6.4",
2020
"logfire>=3.11.0",
21+
"azure-core-tracing-opentelemetry>=1.0.0b12"
2122
]
2223

2324
[dependency-groups]

servers/__init__.py

Whitespace-only changes.

servers/deployed_mcp.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,31 @@
66
from typing import Annotated
77

88
import logfire
9+
from azure.core.settings import settings
910
from azure.cosmos.aio import CosmosClient
1011
from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential
1112
from azure.monitor.opentelemetry import configure_azure_monitor
12-
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
1313
from dotenv import load_dotenv
1414
from fastmcp import FastMCP
15-
from opentelemetry import trace
16-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
15+
from opentelemetry_middleware import OpenTelemetryMiddleware
1716

1817
RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true"
1918

2019
if not RUNNING_IN_PRODUCTION:
2120
load_dotenv(override=True)
2221

23-
logging.basicConfig(level=logging.WARNING, format="%(asctime)s - %(message)s")
22+
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(message)s")
2423
logger = logging.getLogger("ExpensesMCP")
2524
logger.setLevel(logging.INFO)
2625

2726
# Configure OpenTelemetry tracing
28-
APPLICATIONINSIGHTS_CONNECTION_STRING = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
29-
if APPLICATIONINSIGHTS_CONNECTION_STRING:
27+
if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"):
3028
logger.info("Setting up Azure Monitor instrumentation")
3129
configure_azure_monitor()
32-
33-
# Use Logfire to instrument MCP tool calls
34-
logfire.configure(
35-
service_name="expenses-mcp",
36-
send_to_logfire=False, # Send spans to Application Insights, not Logfire backend
37-
)
38-
logfire.instrument_mcp()
39-
40-
if APPLICATIONINSIGHTS_CONNECTION_STRING:
41-
logger.info("Adding Azure Monitor Trace Exporter to TracerProvider")
42-
tracer_provider = trace.get_tracer_provider()
43-
exporter = AzureMonitorTraceExporter(connection_string=APPLICATIONINSIGHTS_CONNECTION_STRING)
44-
span_processor = BatchSpanProcessor(exporter)
45-
tracer_provider.add_span_processor(span_processor)
30+
if os.getenv("LOGFIRE_PROJECT_NAME"):
31+
logger.info("Setting up Logfire instrumentation")
32+
settings.tracing_implementation = "opentelemetry" # Send Azure Monitor traces via OpenTelemetry
33+
logfire.configure(service_name="expenses-mcp", send_to_logfire=True)
4634

4735
# Cosmos DB configuration from environment variables
4836
AZURE_COSMOSDB_ACCOUNT = os.environ["AZURE_COSMOSDB_ACCOUNT"]
@@ -65,6 +53,7 @@
6553
logger.info(f"Connected to Cosmos DB: {AZURE_COSMOSDB_ACCOUNT}")
6654

6755
mcp = FastMCP("Expenses Tracker")
56+
mcp.add_middleware(OpenTelemetryMiddleware("ExpensesMCP"))
6857

6958

7059
class PaymentMethod(Enum):
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from fastmcp.server.middleware import Middleware, MiddlewareContext
2+
from opentelemetry import trace
3+
from opentelemetry.trace import Status, StatusCode
4+
5+
6+
class OpenTelemetryMiddleware(Middleware):
7+
"""Middleware that creates OpenTelemetry spans for MCP operations."""
8+
9+
def __init__(self, tracer_name: str):
10+
self.tracer = trace.get_tracer(tracer_name)
11+
12+
async def on_call_tool(self, context: MiddlewareContext, call_next):
13+
"""Create a span for each tool call with detailed attributes."""
14+
tool_name = context.message.name
15+
16+
with self.tracer.start_as_current_span(
17+
f"tool.{tool_name}",
18+
attributes={
19+
"mcp.method": context.method,
20+
"mcp.source": context.source,
21+
"mcp.tool.name": tool_name,
22+
"mcp.tool.arguments": str(context.message.arguments),
23+
},
24+
) as span:
25+
try:
26+
result = await call_next(context)
27+
span.set_attribute("mcp.tool.success", True)
28+
span.set_status(Status(StatusCode.OK))
29+
return result
30+
except Exception as e:
31+
span.set_attribute("mcp.tool.success", False)
32+
span.set_attribute("mcp.tool.error", str(e))
33+
span.set_status(Status(StatusCode.ERROR, str(e)))
34+
span.record_exception(e)
35+
raise
36+
37+
async def on_read_resource(self, context: MiddlewareContext, call_next):
38+
"""Create a span for each resource read."""
39+
resource_uri = str(getattr(context.message, "uri", "unknown"))
40+
41+
with self.tracer.start_as_current_span(
42+
f"resource.{resource_uri}",
43+
attributes={
44+
"mcp.method": context.method,
45+
"mcp.source": context.source,
46+
"mcp.resource.uri": resource_uri,
47+
},
48+
) as span:
49+
try:
50+
result = await call_next(context)
51+
span.set_attribute("mcp.resource.success", True)
52+
span.set_status(Status(StatusCode.OK))
53+
return result
54+
except Exception as e:
55+
span.set_attribute("mcp.resource.success", False)
56+
span.set_attribute("mcp.resource.error", str(e))
57+
span.set_status(Status(StatusCode.ERROR, str(e)))
58+
span.record_exception(e)
59+
raise
60+
61+
async def on_get_prompt(self, context: MiddlewareContext, call_next):
62+
"""Create a span for each prompt retrieval."""
63+
prompt_name = getattr(context.message, "name", "unknown")
64+
65+
with self.tracer.start_as_current_span(
66+
f"prompt.{prompt_name}",
67+
attributes={
68+
"mcp.method": context.method,
69+
"mcp.source": context.source,
70+
"mcp.prompt.name": prompt_name,
71+
},
72+
) as span:
73+
try:
74+
result = await call_next(context)
75+
span.set_attribute("mcp.prompt.success", True)
76+
span.set_status(Status(StatusCode.OK))
77+
return result
78+
except Exception as e:
79+
span.set_attribute("mcp.prompt.success", False)
80+
span.set_attribute("mcp.prompt.error", str(e))
81+
span.set_status(Status(StatusCode.ERROR, str(e)))
82+
span.record_exception(e)
83+
raise

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)