Skip to content

Commit 197d291

Browse files
authored
Merge pull request #17 from madebygps/add-aspire-standalone
Add aspire standalone
2 parents 1f69dee + 25b5f3b commit 197d291

File tree

6 files changed

+1084
-989
lines changed

6 files changed

+1084
-989
lines changed

.env-sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ OLLAMA_API_KEY=ollama
1919
# OpenAI Configuration (default if API_HOST not set)
2020
OPENAI_MODEL=gpt-4o-mini
2121
OPENAI_API_KEY=your_openai_api_key_here
22+
23+
# OpenTelemetry Configuration (for Aspire Dashboard)
24+
# Uncomment to enable tracing, metrics, and logs export
25+
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A demonstration project showcasing Model Context Protocol (MCP) implementations
1212
- [Use with GitHub Copilot](#use-with-github-copilot)
1313
- [Debug with VS Code](#debug-with-vs-code)
1414
- [Inspect with MCP inspector](#inspect-with-mcp-inspector)
15+
- [View traces with Aspire Dashboard](#view-traces-with-aspire-dashboard)
1516
- [Run local Agents <-> MCP](#run-local-agents---mcp)
1617
- [Deploy to Azure](#deploy-to-azure)
1718
- [Deploy to Azure with private networking](#deploy-to-azure-with-private-networking)
@@ -156,6 +157,42 @@ The inspector provides a web interface to:
156157
- Inspect server responses and errors
157158
- Debug server communication
158159

160+
### View traces with Aspire Dashboard
161+
162+
You can use the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) to view OpenTelemetry traces, metrics, and logs from the MCP server.
163+
164+
> **Note:** Aspire Dashboard integration is only configured for the HTTP server (`basic_mcp_http.py`).
165+
166+
1. Start the Aspire Dashboard:
167+
168+
```bash
169+
docker run --rm -d -p 18888:18888 -p 4317:18889 --name aspire-dashboard \
170+
mcr.microsoft.com/dotnet/aspire-dashboard:latest
171+
```
172+
173+
> The Aspire Dashboard exposes its OTLP endpoint on container port 18889. The mapping `-p 4317:18889` makes it available on the host's standard OTLP port 4317.
174+
175+
Get the dashboard URL and login token from the container logs:
176+
177+
```bash
178+
docker logs aspire-dashboard 2>&1 | grep "Login to the dashboard"
179+
```
180+
181+
2. Enable OpenTelemetry by adding this to your `.env` file:
182+
183+
```bash
184+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
185+
```
186+
187+
3. Start the HTTP server:
188+
189+
```bash
190+
uv run servers/basic_mcp_http.py
191+
```
192+
193+
194+
4. View the dashboard at: http://localhost:18888
195+
159196
---
160197

161198
## Run local Agents <-> MCP

pyproject.toml

Lines changed: 2 additions & 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
"opentelemetry-instrumentation-starlette>=0.49b0",
21+
"opentelemetry-exporter-otlp-proto-grpc>=1.28.0",
2122
"logfire>=4.15.1",
2223
"azure-core-tracing-opentelemetry>=1.0.0b12"
2324
]
@@ -33,3 +34,4 @@ line-length = 120
3334
target-version = "py310"
3435
lint.select = ["E", "F", "I", "UP"]
3536
lint.ignore = ["D203"]
37+
lint.isort.known-first-party = ["opentelemetry_middleware"]

servers/basic_mcp_http.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
import csv
22
import logging
3+
import os
34
from datetime import date
45
from enum import Enum
56
from pathlib import Path
67
from typing import Annotated
78

9+
from dotenv import load_dotenv
810
from fastmcp import FastMCP
11+
from fastmcp.server.middleware import Middleware
12+
13+
from opentelemetry_middleware import OpenTelemetryMiddleware, configure_aspire_dashboard
14+
15+
load_dotenv(override=True)
916

1017
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
1118
logger = logging.getLogger("ExpensesMCP")
1219

20+
middleware: list[Middleware] = []
21+
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
22+
logger.info("Setting up Aspire Dashboard instrumentation (OTLP)")
23+
configure_aspire_dashboard(service_name="expenses-mcp")
24+
middleware = [OpenTelemetryMiddleware(tracer_name="expenses.mcp")]
25+
1326

1427
SCRIPT_DIR = Path(__file__).parent
1528
EXPENSES_FILE = SCRIPT_DIR / "expenses.csv"
1629

1730

18-
mcp = FastMCP("Expenses Tracker")
31+
mcp = FastMCP("Expenses Tracker", middleware=middleware)
1932

2033

2134
class PaymentMethod(Enum):

servers/opentelemetry_middleware.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,61 @@
1+
import logging
2+
import os
3+
14
from fastmcp.server.middleware import Middleware, MiddlewareContext
2-
from opentelemetry import trace
5+
from opentelemetry import metrics, trace
6+
from opentelemetry._logs import set_logger_provider
7+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
8+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
9+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
10+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler # _logs is "experimental", not "private"
11+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
12+
from opentelemetry.sdk.metrics import MeterProvider
13+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
14+
from opentelemetry.sdk.resources import Resource
15+
from opentelemetry.sdk.trace import TracerProvider
16+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
317
from opentelemetry.trace import Status, StatusCode
418

519

20+
def configure_aspire_dashboard(service_name: str = "expenses-mcp"):
21+
"""Configure OpenTelemetry to send telemetry to the Aspire standalone dashboard.
22+
23+
Requires the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to be set.
24+
"""
25+
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
26+
if not otlp_endpoint:
27+
raise ValueError("OTEL_EXPORTER_OTLP_ENDPOINT environment variable must be set to configure telemetry export.")
28+
29+
# Create resource with service name
30+
resource = Resource.create({"service.name": service_name})
31+
32+
# Configure Tracing
33+
tracer_provider = TracerProvider(resource=resource)
34+
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint)))
35+
trace.set_tracer_provider(tracer_provider)
36+
37+
# Configure Metrics
38+
metric_reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=otlp_endpoint))
39+
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
40+
metrics.set_meter_provider(meter_provider)
41+
42+
# Configure Logging
43+
logger_provider = LoggerProvider(resource=resource)
44+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter(endpoint=otlp_endpoint)))
45+
set_logger_provider(logger_provider)
46+
47+
# Add logging handler to send Python logs to OTLP
48+
root_logger = logging.getLogger()
49+
handler_exists = any(
50+
isinstance(existing, LoggingHandler) and getattr(existing, "logger_provider", None) is logger_provider
51+
for existing in root_logger.handlers
52+
)
53+
54+
if not handler_exists:
55+
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
56+
root_logger.addHandler(handler)
57+
58+
659
class OpenTelemetryMiddleware(Middleware):
760
"""Middleware that creates OpenTelemetry spans for MCP operations."""
861

0 commit comments

Comments
 (0)