chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Observability Configuration
|
||||
# ===========================
|
||||
|
||||
# Standard OpenTelemetry environment variables
|
||||
# See https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
|
||||
|
||||
# OTLP Endpoint (for Aspire Dashboard, Jaeger, etc.)
|
||||
# Default protocol is gRPC (port 4317), HTTP uses port 4318
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
|
||||
# Optional: Override endpoint for specific signals
|
||||
# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="http://localhost:4317"
|
||||
# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://localhost:4317"
|
||||
# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://localhost:4317"
|
||||
|
||||
# Optional: Specify protocol (grpc or http)
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
|
||||
|
||||
# Optional: Add headers (e.g., for authentication)
|
||||
# OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token,x-api-key=key"
|
||||
|
||||
# Optional: Service identification
|
||||
# OTEL_SERVICE_NAME="my-agent-app"
|
||||
# OTEL_SERVICE_VERSION="1.0.0"
|
||||
# OTEL_RESOURCE_ATTRIBUTES="deployment.environment=dev,host.name=localhost"
|
||||
|
||||
# Agent Framework specific settings
|
||||
# ==================================
|
||||
|
||||
# Observability is enabled by default. Set to "false" to opt out.
|
||||
# ENABLE_INSTRUMENTATION=false
|
||||
|
||||
# Enable sensitive data logging (prompts, responses, etc.)
|
||||
# WARNING: Only enable in dev/test environments
|
||||
ENABLE_SENSITIVE_DATA=true
|
||||
|
||||
# Optional: Enable console exporters for debugging
|
||||
# ENABLE_CONSOLE_EXPORTERS=true
|
||||
|
||||
# OpenAI specific variables
|
||||
# ==========================
|
||||
OPENAI_API_KEY="..."
|
||||
OPENAI_CHAT_MODEL="gpt-4o-2024-08-06"
|
||||
OPENAI_CHAT_COMPLETION_MODEL="gpt-4o-2024-08-06"
|
||||
|
||||
# Azure AI Foundry specific variables
|
||||
# ====================================
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
FOUNDRY_MODEL="gpt-4o-mini"
|
||||
@@ -0,0 +1,483 @@
|
||||
# Agent Framework Observability
|
||||
|
||||
These samples show how to send Agent Framework observability data to the Application Performance Management (APM) backend of your choice, based on the OpenTelemetry standard.
|
||||
|
||||
The samples target [Application Insights](https://learn.microsoft.com/azure/azure-monitor/app/app-insights-overview), the [Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash), and the console, but any OTLP-compatible backend works.
|
||||
|
||||
> **Quick Start**: For local development without Azure setup, use the [Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) (runs locally via Docker), or the built-in tracing module of the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio).
|
||||
|
||||
> Other backends such as [Prometheus](https://prometheus.io/docs/introduction/overview/) are also supported. See the [OpenTelemetry Python exporters](https://opentelemetry.io/docs/languages/python/exporters/) page for the full list.
|
||||
|
||||
For more information, please refer to the following resources:
|
||||
|
||||
1. [Azure Monitor OpenTelemetry Exporter](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry-exporter)
|
||||
2. [Aspire Dashboard for Python Apps](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone-for-python?tabs=flask%2Cwindows)
|
||||
3. [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio)
|
||||
4. [Python Logging](https://docs.python.org/3/library/logging.html)
|
||||
5. [Observability in Python](https://www.cncf.io/blog/2022/04/22/opentelemetry-and-python-a-complete-instrumentation-guide/)
|
||||
|
||||
## What to expect
|
||||
|
||||
The Agent Framework Python SDK is **natively instrumented** to emit logs, traces, and metrics throughout agent/model invocation and tool execution, so you can monitor your AI application's performance and track token consumption. Instrumentation follows the OpenTelemetry [Semantic Conventions for GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/), and workflows emit their own spans for end-to-end visibility.
|
||||
|
||||
Setting up observability is also easy: a single call to `configure_otel_providers()` from the `agent_framework.observability` module wires up the trace, log, and metric providers. It reads the standard OpenTelemetry environment variables to configure exporters automatically.
|
||||
|
||||
### Five patterns for configuring observability
|
||||
|
||||
> Setting up observability has two parts: (1) **instrumentation**, the code that generates telemetry, and (2) **exporter/provider configuration**, which decides where that telemetry is sent. Agent Framework is natively instrumented and **enabled by default**, so you only need to handle the second part.
|
||||
|
||||
There are five common ways to do that, depending on your needs:
|
||||
|
||||
**1. Standard otel environment variables, configured for you**
|
||||
|
||||
The simplest approach - configure everything via environment variables:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
# Reads OTEL_EXPORTER_OTLP_* environment variables automatically
|
||||
configure_otel_providers()
|
||||
```
|
||||
|
||||
Or if you just want console exporters:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
configure_otel_providers(enable_console_exporters=True)
|
||||
# It is also possible to set ENABLE_CONSOLE_EXPORTERS=true in environment
|
||||
# variables instead of calling `configure_otel_providers()` with the parameter.
|
||||
# The framework will automatically read that and set up console exporters.
|
||||
```
|
||||
|
||||
This is the **recommended approach** for getting started.
|
||||
|
||||
**2. Custom Exporters**
|
||||
|
||||
For more control, construct exporters yourself and pass them to `configure_otel_providers()`. The framework still creates the providers for you:
|
||||
|
||||
```python
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
||||
from opentelemetry.exporter.otlp.proto.grpc.exporter import Compression
|
||||
from agent_framework.observability import configure_otel_providers
|
||||
|
||||
# Create custom exporters with specific configuration
|
||||
exporters = [
|
||||
OTLPSpanExporter(endpoint="http://localhost:4317", compression=Compression.Gzip),
|
||||
OTLPLogExporter(endpoint="http://localhost:4317"),
|
||||
OTLPMetricExporter(endpoint="http://localhost:4317"),
|
||||
]
|
||||
|
||||
# These are added alongside any exporters configured from environment variables
|
||||
configure_otel_providers(exporters=exporters)
|
||||
```
|
||||
|
||||
**3. Third-party setup**
|
||||
|
||||
Many third-party OTel packages ship their own setup helpers (for example, Azure Monitor's `configure_azure_monitor()`). You can use those directly — Agent Framework instrumentation is on by default, so no extra wiring is needed. To also capture sensitive data, call `enable_sensitive_telemetry()` from `agent_framework.observability`.
|
||||
|
||||
The [Microsoft OpenTelemetry Distro](https://pypi.org/project/microsoft-opentelemetry/) bundles this pattern into a single call. Install it with `pip install microsoft-opentelemetry`, then call `use_microsoft_opentelemetry()`, which wires up the OpenTelemetry providers/exporters (optionally including Azure Monitor) and enables Agent Framework instrumentation:
|
||||
|
||||
```python
|
||||
from microsoft.opentelemetry import use_microsoft_opentelemetry
|
||||
|
||||
# Sets up OpenTelemetry providers/exporters and enables Agent Framework instrumentation.
|
||||
# Pass enable_azure_monitor=True to also configure the Azure Monitor exporter.
|
||||
use_microsoft_opentelemetry(enable_azure_monitor=True)
|
||||
```
|
||||
|
||||
```python
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
from agent_framework.observability import create_resource, enable_sensitive_telemetry
|
||||
|
||||
# Configure Azure Monitor first
|
||||
configure_azure_monitor(
|
||||
connection_string="InstrumentationKey=...",
|
||||
resource=create_resource(), # Uses OTEL_SERVICE_NAME, etc.
|
||||
enable_live_metrics=True,
|
||||
)
|
||||
|
||||
# Optional: opt in to capturing sensitive data
|
||||
enable_sensitive_telemetry()
|
||||
```
|
||||
|
||||
For Microsoft Foundry projects, use `client.configure_azure_monitor()` which retrieves the connection string from the project and configures everything:
|
||||
|
||||
```python
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
client = FoundryChatClient(
|
||||
project_endpoint="https://your-project.services.ai.azure.com",
|
||||
model="gpt-4o",
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Automatically configures Azure Monitor with connection string from project
|
||||
await client.configure_azure_monitor(enable_sensitive_data=True)
|
||||
```
|
||||
|
||||
Or with [Langfuse](https://langfuse.com/integrations/frameworks/microsoft-agent-framework):
|
||||
|
||||
```python
|
||||
# environment should be setup correctly, with langfuse urls and keys
|
||||
from agent_framework.observability import enable_sensitive_telemetry
|
||||
from langfuse import get_client
|
||||
|
||||
langfuse = get_client()
|
||||
|
||||
# Verify connection
|
||||
if langfuse.auth_check():
|
||||
print("Langfuse client is authenticated and ready!")
|
||||
else:
|
||||
print("Authentication failed. Please check your credentials and host.")
|
||||
|
||||
# Agent Framework instrumentation is on by default.
|
||||
# Optional: opt in to capturing sensitive data
|
||||
enable_sensitive_telemetry()
|
||||
```
|
||||
|
||||
Or with [Comet Opik](https://www.comet.com/docs/opik/integrations/microsoft-agent-framework):
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from agent_framework.observability import enable_sensitive_telemetry
|
||||
|
||||
# Use Opik OTLP settings from your project settings
|
||||
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "<opik_otlp_endpoint>"
|
||||
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "<opik_otlp_headers>"
|
||||
|
||||
# Agent Framework instrumentation is on by default.
|
||||
# Optional: opt in to capturing sensitive data
|
||||
enable_sensitive_telemetry()
|
||||
```
|
||||
|
||||
**4. Manual setup**
|
||||
|
||||
For full control, set up providers and exporters yourself. See [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a complete example that sends traces, logs, and metrics to the console. The `create_resource()` helper in `agent_framework.observability` can build a resource with the appropriate service name and version from environment variables (or sensible defaults), although the sample does not use it.
|
||||
|
||||
**5. Zero-code provider/exporter configuration**
|
||||
|
||||
Because Agent Framework is **natively instrumented** with OpenTelemetry, you do not need to auto-instrument the framework itself. You can, however, use the [`opentelemetry-instrument`](https://opentelemetry.io/docs/zero-code/python/) CLI wrapper to configure the global tracer/meter providers and exporters from environment variables (or CLI flags) at process startup. Your application code then does not need to call `configure_otel_providers()` — the native spans and metrics from Agent Framework are picked up by the globally configured pipeline. See [advanced_zero_code.py](./advanced_zero_code.py) for an example.
|
||||
|
||||
### MCP trace propagation
|
||||
|
||||
Whenever there is an active OpenTelemetry span context, Agent Framework automatically propagates trace context to MCP servers via the `params._meta` field of `tools/call` requests. It uses the globally configured OpenTelemetry propagator(s) — W3C Trace Context by default (producing `traceparent` and `tracestate`) — so custom propagators (B3, Jaeger, etc.) are also supported. This enables distributed tracing across agent-to-MCP-server boundaries, compliant with the [MCP `_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta).
|
||||
|
||||
**Scope:** automatic `_meta` injection applies only to MCP sessions that the agent process itself opens — `MCPStreamableHTTPTool`, `MCPStdioTool`, and `MCPWebsocketTool` (or any other client-opened `MCPTool` subclass). It does **not** apply to hosted or provider-managed MCP tool configurations such as `FoundryChatClient.get_mcp_tool(...)`, `OpenAIChatClient.get_mcp_tool(...)`, `AnthropicClient.get_mcp_tool(...)`, `GeminiChatClient.get_mcp_tool(...)`, or toolbox-fetched tools (e.g. `toolbox = await client.get_toolbox(...)` then `Agent(tools=toolbox.tools)`). In those cases the `tools/call` message is issued by the provider service runtime rather than by the agent process, so propagating `traceparent`/`tracestate` across that boundary is the service runtime's responsibility. If you need end-to-end distributed tracing to the downstream MCP server, use a client-opened MCP transport instead of a hosted connector.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Dependencies
|
||||
|
||||
Agent Framework's core depends on **`opentelemetry-api`** only — the API package is enough for the instrumentation hooks (spans, meters, log records) to emit telemetry, and it has no runtime side effects when no provider is configured.
|
||||
|
||||
If you want the framework to set up providers / exporters for you via `configure_otel_providers()` (or to use the `create_resource()` / `create_metric_views()` helpers), you also need the OpenTelemetry SDK:
|
||||
|
||||
```bash
|
||||
pip install opentelemetry-sdk
|
||||
```
|
||||
|
||||
If `opentelemetry-sdk` is missing, those helper functions raise a clear `ImportError` telling you to install it. Day-to-day instrumentation still works without the SDK as long as some other component (e.g. `azure-monitor-opentelemetry`, your application bootstrap, an APM agent) has configured the global OpenTelemetry providers.
|
||||
|
||||
Exporters are **not** installed by default — install only what you need:
|
||||
- **Application Insights**: `azure-monitor-opentelemetry`
|
||||
- **Aspire Dashboard or other OTLP/gRPC backends**: `opentelemetry-exporter-otlp-proto-grpc`
|
||||
- **OTLP over HTTP**: `opentelemetry-exporter-otlp-proto-http`
|
||||
|
||||
For other backends, refer to the documentation of the specific exporter.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Agent Framework reads the following environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `ENABLE_INSTRUMENTATION` | `true` | Set to `false` to disable native instrumentation. See [Disabling instrumentation](#disabling-instrumentation) for the programmatic alternative with sticky semantics. |
|
||||
| `ENABLE_SENSITIVE_DATA` | `false` | Set to `true` to emit sensitive data (prompts, responses, etc.). |
|
||||
| `ENABLE_CONSOLE_EXPORTERS` | `false` | Set to `true` to add console exporters. Only used by `configure_otel_providers()`. |
|
||||
| `VS_CODE_EXTENSION_PORT` | unset | Port used by the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) tracing integration. Only used by `configure_otel_providers()`. |
|
||||
|
||||
You can also call `enable_sensitive_telemetry()` from `agent_framework.observability` to opt in to sensitive-data capture programmatically.
|
||||
|
||||
> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production.
|
||||
|
||||
### Disabling instrumentation
|
||||
|
||||
There are two ways to turn Agent Framework's native instrumentation off, and they have **different scopes**:
|
||||
|
||||
| Approach | Scope | Sticky? | When framework code calls `enable_instrumentation()` later, what happens? |
|
||||
|----------|-------|---------|---------------------------------------------------------------------------|
|
||||
| `ENABLE_INSTRUMENTATION=false` in the environment | Initial settings only | No | Instrumentation flips back **on**. |
|
||||
| `disable_instrumentation()` called from code | Process-wide, sticky | Yes | Instrumentation **stays off** — the user-disable intent wins. |
|
||||
|
||||
If you want telemetry off **and want it to stay off**, use `disable_instrumentation()`.
|
||||
|
||||
#### Sticky semantics — why this matters
|
||||
|
||||
Framework integrations and third-party libraries can call `enable_instrumentation()`, `enable_sensitive_telemetry()`, or `configure_otel_providers()` as part of their own setup. For example, `FoundryChatClient.configure_azure_monitor()` calls `enable_instrumentation()` after wiring up Azure Monitor. That's normally what you want — but if **you** have explicitly opted out, you don't want any of those calls to silently re-enable telemetry.
|
||||
|
||||
`disable_instrumentation()` solves this by setting a **sticky** flag on `OBSERVABILITY_SETTINGS` that remains in effect until you explicitly clear it. While the flag is set:
|
||||
|
||||
1. `OBSERVABILITY_SETTINGS.enable_instrumentation` and `enable_sensitive_data` **read as `False`** regardless of the stored value.
|
||||
2. `enable_instrumentation()` and `enable_sensitive_telemetry()` are **no-ops** and log an info-level message.
|
||||
3. `configure_otel_providers()` still configures providers / exporters / views (so a later force-enable can use them), but does not flip instrumentation on.
|
||||
4. Direct attribute writes like `OBSERVABILITY_SETTINGS.enable_instrumentation = True` from any code are **silently dropped** (defense in depth).
|
||||
5. Integrations that consult `OBSERVABILITY_SETTINGS.is_user_disabled` (e.g. `FoundryChatClient.configure_azure_monitor()`, `FoundryAgent.configure_azure_monitor()`) **skip their setup entirely**, so global Azure Monitor providers aren't installed unnecessarily.
|
||||
|
||||
```python
|
||||
from agent_framework.observability import disable_instrumentation
|
||||
|
||||
# After this call, Agent Framework expresses your intent to opt out of telemetry.
|
||||
# Library and framework code is expected to honor that intent and not flip
|
||||
# instrumentation back on (e.g. by calling `enable_instrumentation()`,
|
||||
# `enable_sensitive_telemetry()`, or writing to public attributes on
|
||||
# `OBSERVABILITY_SETTINGS`). The framework actively short-circuits the public
|
||||
# enable paths so the user's intent stays leading. A determined caller can still
|
||||
# pass `force=True` or mutate private (`_`-prefixed) attributes to bypass it,
|
||||
# but those are out-of-contract escape hatches that should not be used by
|
||||
# integrations on the user's behalf.
|
||||
disable_instrumentation()
|
||||
```
|
||||
|
||||
#### Forcing re-enablement after a disable
|
||||
|
||||
To intentionally re-enable telemetry after `disable_instrumentation()`, pass `force=True` to either of the two public enable helpers. This is the only way to clear the sticky disable, so the user's opt-out can only be reversed by a deliberate user opt-in:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import (
|
||||
disable_instrumentation,
|
||||
enable_instrumentation,
|
||||
enable_sensitive_telemetry,
|
||||
)
|
||||
|
||||
disable_instrumentation()
|
||||
|
||||
# Without force=True, these are no-ops while the disable is sticky:
|
||||
enable_instrumentation() # logs info, does nothing
|
||||
enable_sensitive_telemetry() # logs info, does nothing
|
||||
|
||||
# With force=True, the sticky disable is cleared and the call proceeds:
|
||||
enable_instrumentation(force=True)
|
||||
# or
|
||||
enable_sensitive_telemetry(force=True)
|
||||
|
||||
# After a force-enable you can `disable_instrumentation()` again to re-arm
|
||||
# the sticky disable.
|
||||
```
|
||||
|
||||
#### Checking the disable state from integrations
|
||||
|
||||
If you're writing an integration that performs telemetry setup as a side effect (e.g. provisioning a third-party exporter), consult the public read-only `is_user_disabled` property and early-return when it's set:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import OBSERVABILITY_SETTINGS
|
||||
|
||||
if OBSERVABILITY_SETTINGS.is_user_disabled:
|
||||
logger.info(
|
||||
"Skipping telemetry setup because the user called disable_instrumentation()."
|
||||
)
|
||||
return
|
||||
```
|
||||
|
||||
This is what the built-in `FoundryChatClient.configure_azure_monitor()` and `FoundryAgent.configure_azure_monitor()` do — so calling `disable_instrumentation()` reliably prevents Azure Monitor's global providers from being installed by those helpers.
|
||||
|
||||
#### What `disable_instrumentation()` does **not** do
|
||||
|
||||
- It does not tear down OpenTelemetry providers, exporters, or in-flight spans that were already set up before the disable call. It only gates **future** captures by Agent Framework code paths.
|
||||
- It does not stop telemetry from third-party instrumentations (e.g. `azure-monitor-opentelemetry`'s system metrics) that are wired up outside Agent Framework. Configure those separately if needed.
|
||||
- It does not persist across processes. Each Python process starts with the disable flag cleared; if you always want telemetry off in a given environment, set `ENABLE_INSTRUMENTATION=false` as an environment variable in addition to (or instead of) the programmatic call.
|
||||
|
||||
#### Environment variables for `configure_otel_providers()`
|
||||
|
||||
The `configure_otel_providers()` function automatically reads **standard OpenTelemetry environment variables** to configure exporters:
|
||||
|
||||
**OTLP Configuration** (for Aspire Dashboard, Jaeger, etc.):
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT` - Base endpoint for all signals (e.g., `http://localhost:4317`)
|
||||
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - Traces-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` - Metrics-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - Logs-specific endpoint (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_PROTOCOL` - Protocol to use (`grpc` or `http`, default: `grpc`)
|
||||
- `OTEL_EXPORTER_OTLP_HEADERS` - Headers for all signals (e.g., `key1=value1,key2=value2`)
|
||||
- `OTEL_EXPORTER_OTLP_TRACES_HEADERS` - Traces-specific headers (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_METRICS_HEADERS` - Metrics-specific headers (overrides base)
|
||||
- `OTEL_EXPORTER_OTLP_LOGS_HEADERS` - Logs-specific headers (overrides base)
|
||||
|
||||
**Service Identification**:
|
||||
- `OTEL_SERVICE_NAME` - Service name (default: `agent_framework`)
|
||||
- `OTEL_SERVICE_VERSION` - Service version (default: package version)
|
||||
- `OTEL_RESOURCE_ATTRIBUTES` - Additional resource attributes (e.g., `key1=value1,key2=value2`)
|
||||
|
||||
> **Note**: These are standard OpenTelemetry environment variables. See the [OpenTelemetry spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for more details.
|
||||
|
||||
#### Logging
|
||||
|
||||
Use standard Python logging configuration to align logs with telemetry output:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s - %(pathname)s:%(lineno)d - %(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
```
|
||||
|
||||
To control which logs are exported, adjust the root logger level — other loggers inherit from it by default:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logging.getLogger().setLevel(logging.NOTSET)
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
This folder contains different samples demonstrating how to use telemetry in various scenarios.
|
||||
|
||||
| Sample | Description |
|
||||
|--------|-------------|
|
||||
| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | **Recommended starting point**: configure telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). |
|
||||
| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | Create custom exporters with specific configuration and pass them to `configure_otel_providers()`. |
|
||||
| [agent_observability.py](./agent_observability.py) | Telemetry collection for an agentic application with tool calls. |
|
||||
| [foundry_tracing.py](./foundry_tracing.py) | Azure Monitor integration with Microsoft Foundry. |
|
||||
| [microsoft_opentelemetry_distro.py](./microsoft_opentelemetry_distro.py) | One-call setup with the Microsoft OpenTelemetry Distro (`use_microsoft_opentelemetry()`), optionally enabling Azure Monitor. |
|
||||
| [workflow_observability.py](./workflow_observability.py) | Telemetry collection for a workflow with multiple executors and message passing. |
|
||||
| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: manual setup of exporters and providers with console output — useful for understanding how observability works under the hood. |
|
||||
| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: zero-code provider/exporter setup using the `opentelemetry-instrument` CLI wrapper. |
|
||||
|
||||
### Running the samples
|
||||
|
||||
1. Open a terminal in this folder (`python/samples/02-agents/observability/`) so that `.env` is found.
|
||||
2. Create a `.env` file if you don't already have one. See [.env.example](./.env.example).
|
||||
> Instrumentation is on by default. Set `OTEL_EXPORTER_OTLP_ENDPOINT` (or other configuration) as needed. With no exporters configured, set `ENABLE_CONSOLE_EXPORTERS=true` for console output.
|
||||
3. Pick an environment-loading approach:
|
||||
- **A. Sample-managed:** run from this folder so the sample's `load_dotenv()` call can find `.env`.
|
||||
- **B. Shell/IDE-managed:** export environment variables, or use an IDE run configuration that injects them.
|
||||
- **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")`.
|
||||
- **D. CLI-managed:** run with `uv` and pass the file explicitly, e.g. `uv run --env-file=.env python configure_otel_providers_with_env_var.py`.
|
||||
4. Activate your virtual environment, then run a sample (e.g. `python configure_otel_providers_with_env_var.py`).
|
||||
|
||||
> If you set up providers manually (e.g. Azure Monitor), Agent Framework instrumentation is still on by default. Call `enable_sensitive_telemetry()` if you also want to capture sensitive data. To have Agent Framework configure exporters and providers for you, call `configure_otel_providers(...)`.
|
||||
|
||||
> Each sample prints its Operation/Trace ID, which you can use to filter logs and traces in Application Insights or the Aspire Dashboard.
|
||||
|
||||
# Appendix
|
||||
|
||||
## Azure Monitor Queries
|
||||
|
||||
For an overall view of a span in Azure Monitor, run this query in the Logs section:
|
||||
|
||||
```kusto
|
||||
dependencies
|
||||
| where operation_Id in (dependencies
|
||||
| project operation_Id, timestamp
|
||||
| order by timestamp desc
|
||||
| summarize operations = make_set(operation_Id), timestamp = max(timestamp) by operation_Id
|
||||
| order by timestamp desc
|
||||
| project operation_Id
|
||||
| take 2)
|
||||
| evaluate bag_unpack(customDimensions)
|
||||
| extend tool_call_id = tostring(["gen_ai.tool.call.id"])
|
||||
| join kind=leftouter (customMetrics
|
||||
| extend tool_call_id = tostring(customDimensions['gen_ai.tool.call.id'])
|
||||
| where isnotempty(tool_call_id)
|
||||
| project tool_call_duration = value, tool_call_id)
|
||||
on tool_call_id
|
||||
| project-keep timestamp, target, operation_Id, tool_call_duration, duration, gen_ai*
|
||||
| order by timestamp asc
|
||||
```
|
||||
|
||||
### Grafana dashboards with Application Insights data
|
||||
|
||||
In addition to the native Application Insights UI, you can use Grafana to visualize the same telemetry data. Two tailored dashboards are available to get you started:
|
||||
|
||||
#### Agent Overview dashboard
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-agent>
|
||||

|
||||
|
||||
#### Workflow Overview dashboard
|
||||
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
|
||||

|
||||
|
||||
## Migration Guide
|
||||
|
||||
Instrumentation is now **enabled by default** (you no longer have to opt in by calling `enable_instrumentation()` at startup), and the way you opt in to capturing sensitive payloads has its own dedicated function.
|
||||
|
||||
If your code previously did:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
|
||||
enable_instrumentation(enable_sensitive_data=True)
|
||||
```
|
||||
|
||||
replace it with:
|
||||
|
||||
```python
|
||||
from agent_framework.observability import enable_sensitive_telemetry
|
||||
|
||||
enable_sensitive_telemetry()
|
||||
```
|
||||
|
||||
`enable_sensitive_telemetry()` ensures that instrumentation is on and turns sensitive-event capture on in one call. `enable_instrumentation()` still exists for the rare case where you want to programmatically force instrumentation on without enabling sensitive data (e.g. to override `ENABLE_INSTRUMENTATION=false`), and it now also accepts `force=True` to clear a previous `disable_instrumentation()` — see [Disabling instrumentation](#disabling-instrumentation).
|
||||
|
||||
> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production.
|
||||
|
||||
## Aspire Dashboard
|
||||
|
||||
The [Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/standalone) is a local telemetry viewing tool that provides an excellent experience for viewing OpenTelemetry data without requiring Azure setup.
|
||||
|
||||
### Setting up Aspire Dashboard with Docker
|
||||
|
||||
The easiest way to run the Aspire Dashboard locally is using Docker:
|
||||
|
||||
```bash
|
||||
# Pull and run the Aspire Dashboard container
|
||||
docker run --rm -it -d \
|
||||
-p 18888:18888 \
|
||||
-p 4317:18889 \
|
||||
--name aspire-dashboard \
|
||||
mcr.microsoft.com/dotnet/aspire-dashboard:latest
|
||||
```
|
||||
|
||||
This will start the dashboard with:
|
||||
|
||||
- **Web UI**: Available at <http://localhost:18888>
|
||||
- **OTLP endpoint**: Available at `http://localhost:4317` for your applications to send telemetry data
|
||||
|
||||
### Configuring your application
|
||||
|
||||
Make sure your `.env` file includes the OTLP endpoint:
|
||||
|
||||
```bash
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
```
|
||||
|
||||
Or set it as an environment variable when running your samples:
|
||||
|
||||
```bash
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py
|
||||
```
|
||||
|
||||
### Viewing telemetry data
|
||||
|
||||
> Make sure you have the dashboard running to receive telemetry data.
|
||||
|
||||
Once your sample finishes running, navigate to <http://localhost:18888> in a web browser to see the telemetry data. Follow the [Aspire Dashboard exploration guide](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/explore) to authenticate to the dashboard and start exploring your traces, logs, and metrics!
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Agent Framework emits telemetry via the standard OpenTelemetry APIs — it does not itself
|
||||
contact any external system. Where that telemetry is sent (a local collector, a hosted
|
||||
observability backend, the VS Code extension port, etc.) is entirely determined by the
|
||||
exporters and pipeline the developer configures. By default, emitted telemetry is limited to
|
||||
metadata (e.g. token counts, operation names, durations) and does not include message content.
|
||||
Enabling sensitive-data capture — via `enable_sensitive_telemetry()` or the
|
||||
`ENABLE_SENSITIVE_DATA` environment variable — is an explicit, separate opt-in that
|
||||
additionally emits raw chat message content, function-call arguments, and function-call
|
||||
results — treat that data as sensitive and only send it to a telemetry backend
|
||||
you have secured appropriately.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import enable_sensitive_telemetry
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
from opentelemetry.metrics import set_meter_provider
|
||||
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, ConsoleLogRecordExporter
|
||||
from opentelemetry.sdk.metrics import MeterProvider
|
||||
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
||||
from opentelemetry.semconv._incubating.attributes.service_attributes import SERVICE_NAME
|
||||
from opentelemetry.trace import set_tracer_provider
|
||||
from pydantic import Field
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
This sample shows how to manually configure to send traces, logs, and metrics to the console,
|
||||
without using the `configure_otel_providers` helper function.
|
||||
"""
|
||||
|
||||
resource = Resource.create({SERVICE_NAME: "ManualSetup"})
|
||||
|
||||
|
||||
def setup_logging():
|
||||
# Create and set a global logger provider for the application.
|
||||
logger_provider = LoggerProvider(resource=resource)
|
||||
# Log processors are initialized with an exporter which is responsible
|
||||
logger_provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogRecordExporter()))
|
||||
# Sets the global default logger provider
|
||||
set_logger_provider(logger_provider)
|
||||
# Create a logging handler to write logging records, in OTLP format, to the exporter.
|
||||
handler = LoggingHandler()
|
||||
# Attach the handler to the root logger. `getLogger()` with no arguments returns the root logger.
|
||||
# Events from all child loggers will be processed by this handler.
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(handler)
|
||||
# Set the logging level to NOTSET to allow all records to be processed by the handler.
|
||||
logger.setLevel(logging.NOTSET)
|
||||
|
||||
|
||||
def setup_tracing():
|
||||
# Initialize a trace provider for the application. This is a factory for creating tracers.
|
||||
tracer_provider = TracerProvider(resource=resource)
|
||||
# Span processors are initialized with an exporter which is responsible
|
||||
# for sending the telemetry data to a particular backend.
|
||||
tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
||||
# Sets the global default tracer provider
|
||||
set_tracer_provider(tracer_provider)
|
||||
|
||||
|
||||
def setup_metrics():
|
||||
# Initialize a metric provider for the application. This is a factory for creating meters.
|
||||
meter_provider = MeterProvider(
|
||||
metric_readers=[PeriodicExportingMetricReader(ConsoleMetricExporter(), export_interval_millis=5000)],
|
||||
resource=resource,
|
||||
)
|
||||
# Sets the global default meter provider
|
||||
set_meter_provider(meter_provider)
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def run_chat_client() -> None:
|
||||
"""Run an AI service.
|
||||
|
||||
This function runs an AI service and prints the output.
|
||||
Telemetry will be collected for the service execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI service execution.
|
||||
|
||||
Args:
|
||||
stream: Whether to use streaming for the plugin
|
||||
|
||||
Remarks:
|
||||
By default, the built-in non-`Raw...Client` chat clients already compose
|
||||
the layers in this order:
|
||||
`FunctionInvocationLayer -> ChatMiddlewareLayer -> ChatTelemetryLayer -> Raw/Base client`.
|
||||
|
||||
When `FunctionInvocationLayer` is outside `ChatTelemetryLayer`,
|
||||
each call to the model is handled as a separate span.
|
||||
Keep `ChatMiddlewareLayer` outside telemetry
|
||||
so middleware latency does not skew those timings.
|
||||
By contrast, when telemetry is placed outside the function loop,
|
||||
a single span can cover one or more rounds of function calling.
|
||||
|
||||
So for the scenario below, you should see the following:
|
||||
|
||||
2 spans with gen_ai.operation.name=chat
|
||||
The first has finish_reason "tool_calls"
|
||||
The second has finish_reason "stop"
|
||||
2 spans with gen_ai.operation.name=execute_tool
|
||||
|
||||
"""
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
print(f"User: {message}")
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
stream=True,
|
||||
options={"tools": [get_weather]},
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the selected scenario(s)."""
|
||||
setup_logging()
|
||||
setup_tracing()
|
||||
setup_metrics()
|
||||
# Instrumentation is enabled by default; call this to also capture sensitive data.
|
||||
enable_sensitive_telemetry()
|
||||
|
||||
await run_chat_client()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import SupportsChatGetResponse
|
||||
|
||||
|
||||
"""
|
||||
This sample shows how you can configure observability of an application with zero code changes.
|
||||
|
||||
Agent Framework is natively instrumented with OpenTelemetry, so no auto-instrumentation of the
|
||||
framework itself is required. Running the `opentelemetry-instrument` CLI wrapper simply configures
|
||||
the global tracer/meter providers and exporters from environment variables (or CLI flags) at
|
||||
process startup, so the application code does not need to set them up explicitly. The native
|
||||
spans/metrics emitted by Agent Framework are then picked up by that globally configured pipeline.
|
||||
|
||||
See: https://opentelemetry.io/docs/zero-code/python/
|
||||
|
||||
Install the OpenTelemetry CLI tool following the guidance above (when using `uv` there are some
|
||||
additional steps, so follow the instructions carefully).
|
||||
|
||||
Then setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update
|
||||
the endpoint below).
|
||||
|
||||
Then you can run:
|
||||
```bash
|
||||
opentelemetry-instrument \
|
||||
--traces_exporter otlp \
|
||||
--metrics_exporter otlp \
|
||||
--service_name agent_framework \
|
||||
--exporter_otlp_endpoint http://localhost:4317 \
|
||||
python python/samples/02-agents/observability/advanced_zero_code.py
|
||||
```
|
||||
(or use uv run in front when you've done the install within your uv virtual environment)
|
||||
|
||||
You can also set the environment variables instead of passing them as CLI arguments.
|
||||
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None:
|
||||
"""Run an AI service.
|
||||
|
||||
This function runs an AI service and prints the output.
|
||||
Telemetry will be collected for the service execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI service execution.
|
||||
|
||||
Args:
|
||||
stream: Whether to use streaming for the plugin
|
||||
|
||||
Remarks:
|
||||
When `FunctionInvocationLayer` is outside `ChatTelemetryLayer`,
|
||||
each call to the model is handled as a separate span.
|
||||
If `ChatMiddlewareLayer` is present, keep it outside telemetry
|
||||
so middleware latency does not skew those timings.
|
||||
By contrast, when telemetry is placed outside the function loop,
|
||||
a single span can cover one or more rounds of function calling.
|
||||
|
||||
So for the scenario below, you should see the following:
|
||||
|
||||
2 spans with gen_ai.operation.name=chat
|
||||
The first has finish_reason "tool_calls"
|
||||
The second has finish_reason "stop"
|
||||
2 spans with gen_ai.operation.name=execute_tool
|
||||
|
||||
"""
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
stream=True,
|
||||
options={"tools": [get_weather]},
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
options={"tools": [get_weather]},
|
||||
)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
with get_tracer().start_as_current_span("Zero Code", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
await run_chat_client(client, stream=True)
|
||||
await run_chat_client(client, stream=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
This sample shows how you can observe an agent in Agent Framework by using the
|
||||
same observability setup function.
|
||||
|
||||
Pre-requisites:
|
||||
- A Foundry project
|
||||
- An observability backend to receive traces and metrics (for example, a local or remote
|
||||
OpenTelemetry Collector, another OTLP-compatible backend, or console exporters enabled
|
||||
via environment variables).
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
|
||||
# See:
|
||||
# samples/02-agents/tools/function_tool_with_approval.py
|
||||
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main():
|
||||
# calling `configure_otel_providers` will *enable* tracing and create the necessary tracing, logging
|
||||
# and metrics providers based on environment variables.
|
||||
# See the .env.example file for the available configuration options.
|
||||
configure_otel_providers(enable_sensitive_data=True)
|
||||
|
||||
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
|
||||
|
||||
with get_tracer().start_as_current_span("Scenario: Agent Chat", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(credential=AzureCliCredential()),
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="weather-agent",
|
||||
)
|
||||
session = agent.create_session()
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(question, session=session, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import SupportsChatGetResponse
|
||||
|
||||
"""
|
||||
This sample shows how you can configure observability of an application via the
|
||||
`configure_otel_providers` function with environment variables.
|
||||
|
||||
When you run this sample with an OTLP endpoint or an Application Insights connection string,
|
||||
you should see traces, logs, and metrics in the configured backend.
|
||||
|
||||
Pre-requisites:
|
||||
- A Foundry project
|
||||
- A local OpenTelemetry Collector instance to receive the traces and metrics.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Define the scenarios that can be run to show the telemetry data collected by the SDK
|
||||
SCENARIOS = ["client", "client_stream", "tool", "all"]
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None:
|
||||
"""Run an AI service.
|
||||
|
||||
This function runs an AI service and prints the output.
|
||||
Telemetry will be collected for the service execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI service execution.
|
||||
|
||||
Args:
|
||||
client: The chat client to use.
|
||||
stream: Whether to use streaming for the response
|
||||
|
||||
Remarks:
|
||||
For the scenario below, you should see the following:
|
||||
1 Client span, with 4 children:
|
||||
2 Internal span with gen_ai.operation.name=chat
|
||||
The first has finish_reason "tool_calls"
|
||||
The second has finish_reason "stop"
|
||||
2 Internal span with gen_ai.operation.name=execute_tool
|
||||
|
||||
"""
|
||||
scenario_name = "Chat Client Stream" if stream else "Chat Client"
|
||||
with get_tracer().start_as_current_span(name=f"Scenario: {scenario_name}", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario:", scenario_name)
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
stream=True,
|
||||
options={"tools": [get_weather]},
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
options={"tools": [get_weather]},
|
||||
)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
async def run_tool() -> None:
|
||||
"""Run a AI function.
|
||||
|
||||
This function runs a AI function and prints the output.
|
||||
Telemetry will be collected for the function execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI function execution
|
||||
and the AI service execution.
|
||||
"""
|
||||
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario: AI Function")
|
||||
weather = await get_weather.invoke(location="Amsterdam")
|
||||
print(f"Weather in Amsterdam:\n{weather[-1]}")
|
||||
|
||||
|
||||
async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "all"):
|
||||
"""Run the selected scenario(s)."""
|
||||
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on environment variables. See the .env.example file for the available configuration options.
|
||||
configure_otel_providers()
|
||||
|
||||
with get_tracer().start_as_current_span("Sample Scenarios", kind=trace.SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Scenarios where telemetry is collected in the SDK, from the most basic to the most complex.
|
||||
if scenario == "tool" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_tool()
|
||||
if scenario == "client_stream" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_chat_client(client, stream=True)
|
||||
if scenario == "client" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_chat_client(client, stream=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
|
||||
arg_parser.add_argument(
|
||||
"--scenario",
|
||||
type=str,
|
||||
choices=SCENARIOS,
|
||||
default="all",
|
||||
help="The scenario to run. Default is all.",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
asyncio.run(main(args.scenario))
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from random import randint
|
||||
from typing import TYPE_CHECKING, Annotated, Literal
|
||||
|
||||
from agent_framework import Message, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import SupportsChatGetResponse
|
||||
|
||||
"""
|
||||
This sample shows how you can configure observability with custom exporters passed directly
|
||||
to the `configure_otel_providers()` function.
|
||||
|
||||
This approach gives you full control over exporter configuration (endpoints, headers, compression, etc.)
|
||||
and allows you to add multiple exporters programmatically.
|
||||
|
||||
For standard OTLP setup, it's recommended to use environment variables (see configure_otel_providers_with_env_var.py).
|
||||
Use this approach when you need custom exporter configuration beyond what environment variables provide.
|
||||
|
||||
Pre-requisites:
|
||||
- A Foundry project
|
||||
- A local OpenTelemetry Collector instance to receive the traces and metrics.
|
||||
"""
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Define the scenarios that can be run to show the telemetry data collected by the SDK
|
||||
SCENARIOS = ["client", "client_stream", "tool", "all"]
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
|
||||
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = False) -> None:
|
||||
"""Run an AI service.
|
||||
|
||||
This function runs an AI service and prints the output.
|
||||
Telemetry will be collected for the service execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI service execution.
|
||||
|
||||
Args:
|
||||
client: The chat client to use.
|
||||
stream: Whether to use streaming for the response
|
||||
|
||||
Remarks:
|
||||
For the scenario below, you should see the following:
|
||||
1 Client span, with 4 children:
|
||||
2 Internal span with gen_ai.operation.name=chat
|
||||
The first has finish_reason "tool_calls"
|
||||
The second has finish_reason "stop"
|
||||
2 Internal span with gen_ai.operation.name=execute_tool
|
||||
|
||||
"""
|
||||
scenario_name = "Chat Client Stream" if stream else "Chat Client"
|
||||
with get_tracer().start_as_current_span(name=f"Scenario: {scenario_name}", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario:", scenario_name)
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
stream=True,
|
||||
options={"tools": [get_weather]},
|
||||
):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
[Message(role="user", contents=[message])],
|
||||
options={"tools": [get_weather]},
|
||||
)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
async def run_tool() -> None:
|
||||
"""Run a AI function.
|
||||
|
||||
This function runs a AI function and prints the output.
|
||||
Telemetry will be collected for the function execution behind the scenes,
|
||||
and the traces will be sent to the configured telemetry backend.
|
||||
|
||||
The telemetry will include information about the AI function execution
|
||||
and the AI service execution.
|
||||
"""
|
||||
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
|
||||
print("Running scenario: AI Function")
|
||||
weather = await get_weather.invoke(location="Amsterdam")
|
||||
print(f"Weather in Amsterdam:\n{weather[-1]}")
|
||||
|
||||
|
||||
async def main(scenario: Literal["client", "client_stream", "tool", "all"] = "all"):
|
||||
"""Run the selected scenario(s)."""
|
||||
|
||||
# Setup the logging with the more complete format
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s - %(pathname)s:%(lineno)d - %(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
# Create custom OTLP exporters with specific configuration
|
||||
# Note: You need to install opentelemetry-exporter-otlp-proto-grpc or -http separately
|
||||
try:
|
||||
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # pyright: ignore[reportMissingImports]
|
||||
OTLPLogExporter,
|
||||
)
|
||||
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( # pyright: ignore[reportMissingImports]
|
||||
OTLPMetricExporter,
|
||||
)
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # pyright: ignore[reportMissingImports]
|
||||
OTLPSpanExporter,
|
||||
)
|
||||
|
||||
# Create exporters with custom configuration
|
||||
# These will be added to any exporters configured via environment variables
|
||||
custom_exporters = [
|
||||
OTLPSpanExporter(endpoint="http://localhost:4317"),
|
||||
OTLPMetricExporter(endpoint="http://localhost:4317"),
|
||||
OTLPLogExporter(endpoint="http://localhost:4317"),
|
||||
]
|
||||
except ImportError:
|
||||
print(
|
||||
"Warning: opentelemetry-exporter-otlp-proto-grpc not installed. "
|
||||
"Install with: pip install opentelemetry-exporter-otlp-proto-grpc"
|
||||
)
|
||||
print("Continuing without custom exporters...\n")
|
||||
custom_exporters = []
|
||||
|
||||
# Setup observability with custom exporters and sensitive data enabled
|
||||
# The exporters parameter allows you to add custom exporters alongside
|
||||
# those configured via environment variables (OTEL_EXPORTER_OTLP_*)
|
||||
configure_otel_providers(
|
||||
enable_sensitive_data=True,
|
||||
exporters=custom_exporters,
|
||||
)
|
||||
|
||||
with get_tracer().start_as_current_span("Sample Scenarios", kind=trace.SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Scenarios where telemetry is collected in the SDK, from the most basic to the most complex.
|
||||
if scenario == "tool" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_tool()
|
||||
if scenario == "client_stream" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_chat_client(client, stream=True)
|
||||
if scenario == "client" or scenario == "all":
|
||||
with suppress(Exception):
|
||||
await run_chat_client(client, stream=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
|
||||
arg_parser.add_argument(
|
||||
"--scenario",
|
||||
type=str,
|
||||
choices=SCENARIOS,
|
||||
default="all",
|
||||
help="The scenario to run. Default is all.",
|
||||
)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
asyncio.run(main(args.scenario))
|
||||
@@ -0,0 +1,95 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-foundry",
|
||||
# "azure-monitor-opentelemetry",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run python/samples/02-agents/observability/foundry_tracing.py
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
This sample shows how to setup telemetry in Microsoft Foundry for a custom agent
|
||||
using ``FoundryChatClient.configure_azure_monitor()``.
|
||||
|
||||
First ensure you have a Foundry workspace with Application Insights enabled.
|
||||
And use the Operate tab to Register an Agent.
|
||||
Set the OpenTelemetry agent ID to the value used below in the Agent creation: ``weather-agent``
|
||||
(or change both).
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Microsoft Foundry project endpoint
|
||||
FOUNDRY_MODEL — Model deployment name (e.g. gpt-4o)
|
||||
"""
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity.
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# configure_azure_monitor() retrieves the Application Insights connection string
|
||||
# from the project client and sets up tracing automatically.
|
||||
await client.configure_azure_monitor(
|
||||
enable_sensitive_data=True,
|
||||
enable_live_metrics=True,
|
||||
)
|
||||
print("Observability is set up. Starting Weather Agent...")
|
||||
|
||||
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
|
||||
|
||||
with get_tracer().start_as_current_span("Weather Agent Chat", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
tools=[get_weather],
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="weather-agent",
|
||||
)
|
||||
session = agent.create_session()
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(question, session=session, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-foundry",
|
||||
# "microsoft-opentelemetry",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run python/samples/02-agents/observability/microsoft_opentelemetry_distro.py
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.observability import get_tracer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from microsoft.opentelemetry import use_microsoft_opentelemetry
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from pydantic import Field
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
await asyncio.sleep(randint(0, 10) / 10.0) # Simulate a network call
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main():
|
||||
# Set up Azure monitor exporters for telemetry
|
||||
# This will automatically enable instrumentation for Agent Framework
|
||||
# Install the Microsoft OpenTelemetry Distro package to enable this functionality:
|
||||
# pip install microsoft-opentelemetry
|
||||
# Requires the following environment variables to be set:
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey...
|
||||
use_microsoft_opentelemetry(enable_azure_monitor=True)
|
||||
|
||||
questions = [
|
||||
"What's the weather in Amsterdam?",
|
||||
"and in Paris, and which is better?",
|
||||
"Why is the sky blue?",
|
||||
]
|
||||
|
||||
with get_tracer().start_as_current_span(
|
||||
"Scenario: Agent Chat", kind=SpanKind.CLIENT
|
||||
) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(credential=AzureCliCredential()),
|
||||
tools=get_weather,
|
||||
name="WeatherAgent",
|
||||
instructions="You are a weather assistant.",
|
||||
id="weather-agent",
|
||||
)
|
||||
session = agent.create_session()
|
||||
for question in questions:
|
||||
print(f"\nUser: {question}")
|
||||
print(f"{agent.name}: ", end="")
|
||||
async for update in agent.run(question, session=session, stream=True):
|
||||
if update.text:
|
||||
print(update.text, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
Executor,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.observability import configure_otel_providers, get_tracer
|
||||
from opentelemetry.trace import SpanKind
|
||||
from opentelemetry.trace.span import format_trace_id
|
||||
from typing_extensions import Never
|
||||
|
||||
"""
|
||||
This sample shows the telemetry collected when running a Agent Framework workflow.
|
||||
|
||||
This simple workflow consists of two executors arranged sequentially:
|
||||
1. An executor that converts input text to uppercase.
|
||||
2. An executor that reverses the uppercase text.
|
||||
|
||||
The workflow receives an initial string message, processes it through the two executors,
|
||||
and yields the final result.
|
||||
|
||||
Telemetry data that the workflow system emits includes:
|
||||
- Overall workflow build & execution spans
|
||||
- workflow.build (events: build.started, build.validation_completed, build.completed, edge_group.process)
|
||||
- workflow.run (events: workflow.started, workflow.completed or workflow.error)
|
||||
- Individual executor processing spans
|
||||
- executor.process (for each executor invocation)
|
||||
- Message publishing between executors
|
||||
- message.send (for each outbound message)
|
||||
|
||||
Prerequisites:
|
||||
- Basic understanding of workflow executors, edges, and messages.
|
||||
- Basic understanding of OpenTelemetry concepts like spans and traces.
|
||||
"""
|
||||
|
||||
|
||||
# Executors for sequential workflow
|
||||
class UpperCaseExecutor(Executor):
|
||||
"""An executor that converts text to uppercase."""
|
||||
|
||||
@handler
|
||||
async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
|
||||
"""Execute the task by converting the input string to uppercase."""
|
||||
print(f"UpperCaseExecutor: Processing '{text}'")
|
||||
result = text.upper()
|
||||
print(f"UpperCaseExecutor: Result '{result}'")
|
||||
|
||||
# Send the result to the next executor in the workflow.
|
||||
await ctx.send_message(result)
|
||||
|
||||
|
||||
class ReverseTextExecutor(Executor):
|
||||
"""An executor that reverses text."""
|
||||
|
||||
@handler
|
||||
async def reverse_text(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
|
||||
"""Execute the task by reversing the input string."""
|
||||
print(f"ReverseTextExecutor: Processing '{text}'")
|
||||
result = text[::-1]
|
||||
print(f"ReverseTextExecutor: Result '{result}'")
|
||||
|
||||
# Yield the output.
|
||||
await ctx.yield_output(result)
|
||||
|
||||
|
||||
async def run_sequential_workflow() -> None:
|
||||
"""Run a simple sequential workflow demonstrating telemetry collection.
|
||||
|
||||
This workflow processes a string through two executors in sequence:
|
||||
1. UpperCaseExecutor converts the input to uppercase
|
||||
2. ReverseTextExecutor reverses the string and completes the workflow
|
||||
"""
|
||||
# Step 1: Create the executors.
|
||||
upper_case_executor = UpperCaseExecutor(id="upper_case_executor")
|
||||
reverse_text_executor = ReverseTextExecutor(id="reverse_text_executor")
|
||||
|
||||
# Step 2: Build the workflow with the defined edges.
|
||||
workflow = (
|
||||
WorkflowBuilder(start_executor=upper_case_executor).add_edge(upper_case_executor, reverse_text_executor).build()
|
||||
)
|
||||
|
||||
# Step 3: Run the workflow with an initial message.
|
||||
input_text = "hello world"
|
||||
print(f"Starting workflow with input: '{input_text}'")
|
||||
|
||||
output_event = None
|
||||
async for event in workflow.run("Hello world", stream=True):
|
||||
if event.type == "output":
|
||||
# The WorkflowOutputEvent contains the final result.
|
||||
output_event = event
|
||||
|
||||
if output_event:
|
||||
print(f"Workflow completed with result: '{output_event.data}'")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the telemetry sample with a simple sequential workflow."""
|
||||
# This will enable tracing and create the necessary tracing, logging and metrics providers
|
||||
# based on environment variables. See the .env.example file for the available configuration options.
|
||||
configure_otel_providers()
|
||||
|
||||
with get_tracer().start_as_current_span("Sequential Workflow Scenario", kind=SpanKind.CLIENT) as current_span:
|
||||
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
|
||||
|
||||
# Run the sequential workflow scenario
|
||||
await run_sequential_workflow()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user