Skip to content

Commit 4c7147b

Browse files
committed
Add OpenTelemetry + Aspire Dashboard example
- Add agent_otel_aspire.py: tool-calling agent that exports traces, metrics, and structured logs to the .NET Aspire Dashboard via OTLP/gRPC - Telemetry conditionally enabled when OTEL_EXPORTER_OTLP_ENDPOINT is set - Add opentelemetry-exporter-otlp-proto-grpc dependency - Add Aspire Dashboard setup instructions to README
1 parent 391a52b commit 4c7147b

4 files changed

Lines changed: 325 additions & 2 deletions

File tree

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,52 @@ You can run the examples in this repository by executing the scripts in the `exa
173173
| [agent_mcp_local.py](examples/agent_mcp_local.py) | An agent connected to a local MCP server (e.g. for expense logging). |
174174
| [openai_tool_calling.py](examples/openai_tool_calling.py) | Tool calling with the low-level OpenAI SDK, showing manual tool dispatch. |
175175
| [workflow_basic.py](examples/workflow_basic.py) | A workflow-based agent. |
176+
| [agent_otel_aspire.py](examples/agent_otel_aspire.py) | An agent with OpenTelemetry tracing, metrics, and structured logs exported to the [.NET Aspire Dashboard](https://aspire.dev/dashboard/standalone/). |
177+
178+
## Using the Aspire Dashboard for telemetry
179+
180+
The [agent_otel_aspire.py](examples/agent_otel_aspire.py) example can export OpenTelemetry traces, metrics, and structured logs to the [.NET Aspire Dashboard](https://aspire.dev/dashboard/standalone/), a free standalone container for visualizing telemetry.
181+
182+
1. Start the Aspire Dashboard:
183+
184+
```shell
185+
docker run --rm -it -d \
186+
-p 18888:18888 \
187+
-p 4317:18889 \
188+
--name aspire-dashboard \
189+
mcr.microsoft.com/dotnet/aspire-dashboard:latest
190+
```
191+
192+
2. Get the login token from the container logs:
193+
194+
```shell
195+
docker logs aspire-dashboard
196+
```
197+
198+
Look for the line containing `Login to the dashboard at http://localhost:18888/login?t=<TOKEN>`. Copy the token or open the URL directly.
199+
200+
3. Run the example with telemetry export enabled:
201+
202+
```shell
203+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 uv run python examples/agent_otel_aspire.py
204+
```
205+
206+
4. Open the dashboard at <http://localhost:18888> and explore:
207+
208+
* **Traces**: See the full span tree — agent invocation → chat completion → tool execution
209+
* **Metrics**: View token usage and operation duration histograms
210+
* **Structured Logs**: Browse conversation messages (system, user, assistant, tool)
211+
* **GenAI visualizer**: Select a chat completion span to see the rendered conversation
212+
213+
Without `OTEL_EXPORTER_OTLP_ENDPOINT` set, the example runs normally with no telemetry export and no errors.
214+
215+
5. When done, stop the dashboard:
216+
217+
```shell
218+
docker stop aspire-dashboard
219+
```
220+
221+
For the full Python + Aspire guide, see [Use the Aspire dashboard with Python apps](https://aspire.dev/dashboard/standalone-for-python/).
176222
177223
## Resources
178224

examples/agent_otel_aspire.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
OpenTelemetry + Aspire Dashboard example.
3+
4+
Demonstrates a tool-calling agent that exports OpenTelemetry traces, metrics,
5+
and structured logs to the .NET Aspire Dashboard via OTLP/gRPC.
6+
7+
Telemetry is only exported when the OTEL_EXPORTER_OTLP_ENDPOINT environment
8+
variable is set. Without it, the agent runs normally with no telemetry export.
9+
10+
To start the Aspire Dashboard:
11+
12+
docker run --rm -it -d \
13+
-p 18888:18888 \
14+
-p 4317:18889 \
15+
--name aspire-dashboard \
16+
mcr.microsoft.com/dotnet/aspire-dashboard:latest
17+
18+
The dashboard UI is at http://localhost:18888.
19+
Get the login token from the container logs:
20+
21+
docker logs aspire-dashboard
22+
23+
Look for: "Login to the dashboard at http://localhost:18888/login?t=<TOKEN>"
24+
25+
Then run this example with telemetry export enabled:
26+
27+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python examples/agent_otel_aspire.py
28+
29+
In the Aspire Dashboard you will see:
30+
- Traces: agent -> chat completion -> tool execution spans
31+
- Metrics: token usage and operation duration histograms
32+
- Structured Logs: conversation messages (system, user, assistant, tool)
33+
- GenAI telemetry visualizer: full conversation view on chat spans
34+
35+
To stop the dashboard:
36+
37+
docker stop aspire-dashboard
38+
39+
For the full Python + Aspire guide, see:
40+
https://aspire.dev/dashboard/standalone-for-python/
41+
"""
42+
43+
import asyncio
44+
import logging
45+
import os
46+
import random
47+
from datetime import datetime, timezone
48+
from typing import Annotated
49+
50+
from agent_framework import ChatAgent
51+
from agent_framework.observability import configure_otel_providers
52+
from agent_framework.openai import OpenAIChatClient
53+
from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider
54+
from dotenv import load_dotenv
55+
from pydantic import Field
56+
from rich import print
57+
from rich.logging import RichHandler
58+
59+
# Setup logging
60+
handler = RichHandler(show_path=False, rich_tracebacks=True, show_level=False)
61+
logging.basicConfig(level=logging.WARNING, handlers=[handler], force=True, format="%(message)s")
62+
logger = logging.getLogger(__name__)
63+
logger.setLevel(logging.INFO)
64+
65+
# Configure OpenTelemetry export to the Aspire Dashboard (if endpoint is set)
66+
otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
67+
if otlp_endpoint:
68+
os.environ.setdefault("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
69+
os.environ.setdefault("OTEL_SERVICE_NAME", "agent-framework-demo")
70+
configure_otel_providers(enable_sensitive_data=True)
71+
logger.info(f"OpenTelemetry export enabled — sending to {otlp_endpoint}")
72+
else:
73+
logger.info("Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 to export telemetry to the Aspire Dashboard")
74+
75+
# Configure OpenAI client based on environment
76+
load_dotenv(override=True)
77+
API_HOST = os.getenv("API_HOST", "github")
78+
79+
async_credential = None
80+
if API_HOST == "azure":
81+
async_credential = DefaultAzureCredential()
82+
token_provider = get_bearer_token_provider(async_credential, "https://cognitiveservices.azure.com/.default")
83+
client = OpenAIChatClient(
84+
base_url=f"{os.environ['AZURE_OPENAI_ENDPOINT']}/openai/v1/",
85+
api_key=token_provider,
86+
model_id=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT"],
87+
)
88+
elif API_HOST == "github":
89+
client = OpenAIChatClient(
90+
base_url="https://models.github.ai/inference",
91+
api_key=os.environ["GITHUB_TOKEN"],
92+
model_id=os.getenv("GITHUB_MODEL", "openai/gpt-5-mini"),
93+
)
94+
else:
95+
client = OpenAIChatClient(
96+
api_key=os.environ["OPENAI_API_KEY"], model_id=os.environ.get("OPENAI_MODEL", "gpt-5-mini")
97+
)
98+
99+
100+
def get_weather(
101+
city: Annotated[str, Field(description="City name, spelled out fully")],
102+
) -> dict:
103+
"""Returns weather data for a given city, a dictionary with temperature and description."""
104+
logger.info(f"Getting weather for {city}")
105+
weather_options = [
106+
{"temperature": 72, "description": "Sunny"},
107+
{"temperature": 60, "description": "Rainy"},
108+
{"temperature": 55, "description": "Cloudy"},
109+
{"temperature": 45, "description": "Windy"},
110+
]
111+
return random.choice(weather_options)
112+
113+
114+
def get_current_time(
115+
timezone_name: Annotated[str, Field(description="Timezone name, e.g. 'US/Eastern', 'Asia/Tokyo', 'UTC'")],
116+
) -> str:
117+
"""Returns the current date and time in UTC (timezone_name is for display context only)."""
118+
logger.info(f"Getting current time for {timezone_name}")
119+
now = datetime.now(timezone.utc)
120+
return f"The current time in {timezone_name} is approximately {now.strftime('%Y-%m-%d %H:%M:%S')} UTC"
121+
122+
123+
agent = ChatAgent(
124+
name="weather-time-agent",
125+
chat_client=client,
126+
instructions="You are a helpful assistant that can look up weather and time information.",
127+
tools=[get_weather, get_current_time],
128+
)
129+
130+
131+
async def main():
132+
response = await agent.run("What's the weather in Seattle and what time is it in Tokyo?")
133+
print(response.text)
134+
135+
if async_credential:
136+
await async_credential.close()
137+
138+
139+
if __name__ == "__main__":
140+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"aiohttp",
1414
"faker",
1515
"fastmcp",
16+
"opentelemetry-exporter-otlp-proto-grpc",
1617
"agent-framework-core @ git+https://github.com/microsoft/agent-framework.git@98cd72839e4057d661a58092a3b013993264d834#subdirectory=python/packages/core",
1718
"agent-framework-devui @ git+https://github.com/microsoft/agent-framework.git@98cd72839e4057d661a58092a3b013993264d834#subdirectory=python/packages/devui",
1819
]

0 commit comments

Comments
 (0)