Python: [BREAKING] Observability updates (#2782)

* fixes Python: Add env_file_path parameter to setup_observability() similar to AzureOpenAIChatClient
Fixes #2186

* WIP on updates using configure_azure_monitor

* improved setup and clarity

* fixed root .env.example

* revert changes

* updated files

* updated sample

* updated zero code

* test fixes and fixed links

* fix devui

* removed planning docs

* added enable method and updated readme and samples

* clarified docstring

* add return annotation

* updated naming

* update capatilized version

* updated readme and some fixes

* updated decorator name inline with the rest

* feedback from comments addressed
This commit is contained in:
Eduard van Valkenburg
2025-12-16 07:56:30 +01:00
committed by GitHub
Unverified
parent 3c379718e9
commit 3139347526
46 changed files with 5823 additions and 4615 deletions
+2 -3
View File
@@ -33,7 +33,6 @@ ANTHROPIC_MODEL=""
OLLAMA_ENDPOINT=""
OLLAMA_MODEL=""
# Observability
ENABLE_OTEL=true
ENABLE_INSTRUMENTATION=true
ENABLE_SENSITIVE_DATA=true
OTLP_ENDPOINT="http://localhost:4317/"
# APPLICATIONINSIGHTS_CONNECTION_STRING="..."
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/"
@@ -5,7 +5,7 @@ import json
import re
import uuid
from collections.abc import AsyncIterable, Sequence
from typing import Any, cast
from typing import Any, Final, cast
import httpx
from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card
@@ -38,6 +38,7 @@ from agent_framework import (
UriContent,
prepend_agent_framework_to_user_agent,
)
from agent_framework.observability import use_agent_instrumentation
__all__ = ["A2AAgent"]
@@ -58,6 +59,7 @@ def _get_uri_data(uri: str) -> str:
return match.group("base64_data")
@use_agent_instrumentation
class A2AAgent(BaseAgent):
"""Agent2Agent (A2A) protocol implementation.
@@ -69,6 +71,8 @@ class A2AAgent(BaseAgent):
Can be initialized with a URL, AgentCard, or existing A2A Client instance.
"""
AGENT_PROVIDER_NAME: Final[str] = "A2A"
def __init__(
self,
*,
@@ -23,7 +23,7 @@ from agent_framework import (
from agent_framework._middleware import use_chat_middleware
from agent_framework._tools import use_function_invocation
from agent_framework._types import BaseContent, Contents
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from ._event_converters import AGUIEventConverter
from ._http_service import AGUIHttpService
@@ -89,7 +89,7 @@ def _apply_server_function_call_unwrap(chat_client: TBaseChatClient) -> TBaseCha
@_apply_server_function_call_unwrap
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AGUIChatClient(BaseChatClient):
"""Chat client for communicating with AG-UI compliant servers.
@@ -35,7 +35,7 @@ from agent_framework import (
)
from agent_framework._pydantic import AFBaseSettings
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from anthropic import AsyncAnthropic
from anthropic.types.beta import (
BetaContentBlock,
@@ -110,7 +110,7 @@ TAnthropicClient = TypeVar("TAnthropicClient", bound="AnthropicClient")
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AnthropicClient(BaseChatClient):
"""Anthropic Chat client."""
@@ -43,7 +43,7 @@ from agent_framework import (
use_function_invocation,
)
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import (
Agent,
@@ -107,7 +107,7 @@ TAzureAIAgentClient = TypeVar("TAzureAIAgentClient", bound="AzureAIAgentClient")
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AzureAIAgentClient(BaseChatClient):
"""Azure AI Agent Chat client."""
@@ -15,7 +15,7 @@ from agent_framework import (
use_function_invocation,
)
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
@@ -49,7 +49,7 @@ TAzureAIClient = TypeVar("TAzureAIClient", bound="AzureAIClient")
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AzureAIClient(OpenAIBaseResponsesClient):
"""Azure AI Agent client."""
@@ -164,27 +164,94 @@ class AzureAIClient(OpenAIBaseResponsesClient):
# Track whether we should close client connection
self._should_close_client = should_close_client
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
"""Use this method to setup tracing in your Azure AI Project.
async def configure_azure_monitor(
self,
enable_sensitive_data: bool = False,
**kwargs: Any,
) -> None:
"""Setup observability with Azure Monitor (Azure AI Foundry integration).
This will take the connection string from the project project_client.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
This method configures Azure Monitor for telemetry collection using the
connection string from the Azure AI project client.
Args:
enable_sensitive_data: Enable sensitive data logging (prompts, responses).
Should only be enabled in development/test environments. Default is False.
**kwargs: Additional arguments passed to configure_azure_monitor().
Common options include:
- enable_live_metrics (bool): Enable Azure Monitor Live Metrics
- credential (TokenCredential): Azure credential for Entra ID auth
- resource (Resource): Custom OpenTelemetry resource
See https://learn.microsoft.com/python/api/azure-monitor-opentelemetry/azure.monitor.opentelemetry.configure_azure_monitor
for full list of options.
Raises:
ImportError: If azure-monitor-opentelemetry-exporter is not installed.
Examples:
.. code-block:: python
from agent_framework.azure import AzureAIClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import DefaultAzureCredential
async with (
DefaultAzureCredential() as credential,
AIProjectClient(
endpoint="https://your-project.api.azureml.ms", credential=credential
) as project_client,
AzureAIClient(project_client=project_client) as client,
):
# Setup observability with defaults
await client.configure_azure_monitor()
# With live metrics enabled
await client.configure_azure_monitor(enable_live_metrics=True)
# With sensitive data logging (dev/test only)
await client.configure_azure_monitor(enable_sensitive_data=True)
Note:
This method retrieves the Application Insights connection string from the
Azure AI project client automatically. You must have Application Insights
configured in your Azure AI project for this to work.
"""
# Get connection string from project client
try:
conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
logger.warning(
"No Application Insights connection string found for the Azure AI Project, "
"please call setup_observability() manually."
"No Application Insights connection string found for the Azure AI Project. "
"Please ensure Application Insights is configured in your Azure AI project, "
"or call configure_otel_providers() manually with custom exporters."
)
return
from agent_framework.observability import setup_observability
setup_observability(
applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
# Import Azure Monitor with proper error handling
try:
from azure.monitor.opentelemetry import configure_azure_monitor
except ImportError as exc:
raise ImportError(
"azure-monitor-opentelemetry is required for Azure Monitor integration. "
"Install it with: pip install azure-monitor-opentelemetry"
) from exc
from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation
# Create resource if not provided in kwargs
if "resource" not in kwargs:
kwargs["resource"] = create_resource()
# Configure Azure Monitor with connection string and kwargs
configure_azure_monitor(
connection_string=conn_string,
views=create_metric_views(),
**kwargs,
)
# Complete setup with core observability
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
async def __aenter__(self) -> "Self":
"""Async context manager entry."""
return self
@@ -34,7 +34,7 @@ from ._types import (
ToolMode,
)
from .exceptions import AgentExecutionException, AgentInitializationError
from .observability import use_agent_observability
from .observability import use_agent_instrumentation
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
@@ -516,8 +516,8 @@ class BaseAgent(SerializationMixin):
@use_agent_middleware
@use_agent_observability
class ChatAgent(BaseAgent):
@use_agent_instrumentation(capture_usage=False) # type: ignore[arg-type,misc]
class ChatAgent(BaseAgent): # type: ignore[misc]
"""A Chat Client Agent.
This is the primary agent implementation that uses a chat client to interact
@@ -583,7 +583,7 @@ class ChatAgent(BaseAgent):
print(update.text, end="")
"""
AGENT_SYSTEM_NAME: ClassVar[str] = "microsoft.agent_framework"
AGENT_PROVIDER_NAME: ClassVar[str] = "microsoft.agent_framework"
def __init__(
self,
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, TypeVar, run
from pydantic import BaseModel
from ._logging import get_logger
from ._mcp import MCPTool
from ._memory import AggregateContextProvider, ContextProvider
from ._middleware import (
ChatMiddleware,
@@ -426,6 +425,8 @@ class BaseChatClient(SerializationMixin, ABC):
else [tools]
)
for tool in tools_list: # type: ignore[reportUnknownType]
from ._mcp import MCPTool
if isinstance(tool, MCPTool):
if not tool.is_connected:
await tool.connect()
@@ -6,11 +6,13 @@ from abc import ABC, abstractmethod
from collections.abc import MutableSequence, Sequence
from contextlib import AsyncExitStack
from types import TracebackType
from typing import Any, Final, cast
from typing import TYPE_CHECKING, Any, Final, cast
from ._tools import ToolProtocol
from ._types import ChatMessage
if TYPE_CHECKING:
from ._tools import ToolProtocol
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
@@ -54,7 +56,7 @@ class Context:
self,
instructions: str | None = None,
messages: Sequence[ChatMessage] | None = None,
tools: Sequence[ToolProtocol] | None = None,
tools: Sequence["ToolProtocol"] | None = None,
):
"""Create a new Context object.
@@ -65,7 +67,7 @@ class Context:
"""
self.instructions = instructions
self.messages: Sequence[ChatMessage] = messages or []
self.tools: Sequence[ToolProtocol] = tools or []
self.tools: Sequence["ToolProtocol"] = tools or []
# region ContextProvider
@@ -247,7 +249,7 @@ class AggregateContextProvider(ContextProvider):
contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers])
instructions: str = ""
return_messages: list[ChatMessage] = []
tools: list[ToolProtocol] = []
tools: list["ToolProtocol"] = []
for ctx in contexts:
if ctx.instructions:
instructions += ctx.instructions
@@ -339,11 +339,17 @@ class SerializationMixin:
continue
# Handle dicts containing SerializationProtocol values
if isinstance(value, dict):
from datetime import date, datetime, time
serialized_dict: dict[str, Any] = {}
for k, v in value.items():
if isinstance(v, SerializationProtocol):
serialized_dict[k] = v.to_dict(exclude=exclude, exclude_none=exclude_none)
continue
# Convert datetime objects to strings
if isinstance(v, (datetime, date, time)):
serialized_dict[k] = str(v)
continue
# Check if the value is JSON serializable
if is_serializable(v):
serialized_dict[k] = v
@@ -1816,13 +1816,14 @@ def prepare_function_call_results(content: Contents | Any | list[Contents | Any]
"""Prepare the values of the function call results."""
if isinstance(content, Contents):
# For BaseContent objects, use to_dict and serialize to JSON
return json.dumps(content.to_dict(exclude={"raw_representation", "additional_properties"}))
# Use default=str to handle datetime and other non-JSON-serializable objects
return json.dumps(content.to_dict(exclude={"raw_representation", "additional_properties"}), default=str)
dumpable = _prepare_function_call_results_as_dumpable(content)
if isinstance(dumpable, str):
return dumpable
# fallback
return json.dumps(dumpable)
# fallback - use default=str to handle datetime and other non-JSON-serializable objects
return json.dumps(dumpable, default=str)
# region Chat Response constants
@@ -21,7 +21,7 @@ from agent_framework import (
use_function_invocation,
)
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from agent_framework.openai._chat_client import OpenAIBaseChatClient
from ._shared import (
@@ -41,7 +41,7 @@ TAzureOpenAIChatClient = TypeVar("TAzureOpenAIChatClient", bound="AzureOpenAICha
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AzureOpenAIChatClient(AzureOpenAIConfigMixin, OpenAIBaseChatClient):
"""Azure OpenAI Chat completion class."""
@@ -10,7 +10,7 @@ from pydantic import ValidationError
from agent_framework import use_chat_middleware, use_function_invocation
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import use_observability
from agent_framework.observability import use_instrumentation
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
from ._shared import (
@@ -22,7 +22,7 @@ TAzureOpenAIResponsesClient = TypeVar("TAzureOpenAIResponsesClient", bound="Azur
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class AzureOpenAIResponsesClient(AzureOpenAIConfigMixin, OpenAIBaseResponsesClient):
"""Azure Responses completion class."""
File diff suppressed because it is too large Load Diff
@@ -40,7 +40,7 @@ from .._types import (
prepare_function_call_results,
)
from ..exceptions import ServiceInitializationError
from ..observability import use_observability
from ..observability import use_instrumentation
from ._shared import OpenAIConfigMixin, OpenAISettings
if sys.version_info >= (3, 11):
@@ -53,7 +53,7 @@ __all__ = ["OpenAIAssistantsClient"]
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
"""OpenAI Assistants client."""
@@ -44,7 +44,7 @@ from ..exceptions import (
ServiceInvalidRequestError,
ServiceResponseException,
)
from ..observability import use_observability
from ..observability import use_instrumentation
from ._exceptions import OpenAIContentFilterException
from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings
@@ -467,7 +467,7 @@ TOpenAIChatClient = TypeVar("TOpenAIChatClient", bound="OpenAIChatClient")
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class OpenAIChatClient(OpenAIConfigMixin, OpenAIBaseChatClient):
"""OpenAI Chat completion class."""
@@ -64,7 +64,7 @@ from ..exceptions import (
ServiceInvalidRequestError,
ServiceResponseException,
)
from ..observability import use_observability
from ..observability import use_instrumentation
from ._exceptions import OpenAIContentFilterException
from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings
@@ -1127,7 +1127,7 @@ TOpenAIResponsesClient = TypeVar("TOpenAIResponsesClient", bound="OpenAIResponse
@use_function_invocation
@use_observability
@use_instrumentation
@use_chat_middleware
class OpenAIResponsesClient(OpenAIConfigMixin, OpenAIBaseResponsesClient):
"""OpenAI Responses client class."""
-1
View File
@@ -30,7 +30,6 @@ dependencies = [
# telemetry
"opentelemetry-api>=1.39.0",
"opentelemetry-sdk>=1.39.0",
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",
"opentelemetry-semantic-conventions-ai>=0.4.13",
# connectors and functions
"openai>=1.99.0",
+28 -10
View File
@@ -10,7 +10,7 @@ from pytest import fixture
@fixture
def enable_otel(request: Any) -> bool:
def enable_instrumentation(request: Any) -> bool:
"""Fixture that returns a boolean indicating if Otel is enabled."""
return request.param if hasattr(request, "param") else True
@@ -22,20 +22,31 @@ def enable_sensitive_data(request: Any) -> bool:
@fixture
def span_exporter(monkeypatch, enable_otel: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]:
def span_exporter(monkeypatch, enable_instrumentation: bool, enable_sensitive_data: bool) -> Generator[SpanExporter]:
"""Fixture to remove environment variables for ObservabilitySettings."""
env_vars = [
"ENABLE_OTEL",
"ENABLE_INSTRUMENTATION",
"ENABLE_SENSITIVE_DATA",
"OTLP_ENDPOINT",
"APPLICATIONINSIGHTS_CONNECTION_STRING",
"ENABLE_CONSOLE_EXPORTERS",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
"OTEL_SERVICE_NAME",
"OTEL_SERVICE_VERSION",
"OTEL_RESOURCE_ATTRIBUTES",
]
for key in env_vars:
monkeypatch.delenv(key, raising=False) # type: ignore
monkeypatch.setenv("ENABLE_OTEL", str(enable_otel)) # type: ignore
if not enable_otel:
monkeypatch.setenv("ENABLE_INSTRUMENTATION", str(enable_instrumentation)) # type: ignore
if not enable_instrumentation:
# we overwrite sensitive data for tests
enable_sensitive_data = False
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", str(enable_sensitive_data)) # type: ignore
@@ -51,15 +62,22 @@ def span_exporter(monkeypatch, enable_otel: bool, enable_sensitive_data: bool) -
# recreate observability settings with values from above and no file.
observability_settings = observability.ObservabilitySettings(env_file_path="test.env")
observability_settings._configure() # pyright: ignore[reportPrivateUsage]
# Configure providers manually without calling _configure() to avoid OTLP imports
if enable_instrumentation or enable_sensitive_data:
from opentelemetry.sdk.trace import TracerProvider
tracer_provider = TracerProvider(resource=observability_settings._resource)
trace.set_tracer_provider(tracer_provider)
monkeypatch.setattr(observability, "OBSERVABILITY_SETTINGS", observability_settings, raising=False) # type: ignore
with (
patch("agent_framework.observability.OBSERVABILITY_SETTINGS", observability_settings),
patch("agent_framework.observability.setup_observability"),
patch("agent_framework.observability.configure_otel_providers"),
):
exporter = InMemorySpanExporter()
if enable_otel or enable_sensitive_data:
if enable_instrumentation or enable_sensitive_data:
tracer_provider = trace.get_tracer_provider()
if not hasattr(tracer_provider, "add_span_processor"):
raise RuntimeError("Tracer provider does not support adding span processors.")
@@ -33,8 +33,8 @@ from agent_framework.observability import (
ChatMessageListTimestampFilter,
OtelAttr,
get_function_span,
use_agent_observability,
use_observability,
use_agent_instrumentation,
use_instrumentation,
)
# region Test constants
@@ -157,7 +157,7 @@ def test_start_span_with_tool_call_id(span_exporter: InMemorySpanExporter):
assert span.attributes[OtelAttr.TOOL_TYPE] == "function"
# region Test use_observability decorator
# region Test use_instrumentation decorator
def test_decorator_with_valid_class():
@@ -175,7 +175,7 @@ def test_decorator_with_valid_class():
return gen()
# Apply the decorator
decorated_class = use_observability(MockChatClient)
decorated_class = use_instrumentation(MockChatClient)
assert hasattr(decorated_class, OPEN_TELEMETRY_CHAT_CLIENT_MARKER)
@@ -187,7 +187,7 @@ def test_decorator_with_missing_methods():
# Apply the decorator - should not raise an error
with pytest.raises(ChatClientInitializationError):
use_observability(MockChatClient)
use_instrumentation(MockChatClient)
def test_decorator_with_partial_methods():
@@ -200,7 +200,7 @@ def test_decorator_with_partial_methods():
return Mock()
with pytest.raises(ChatClientInitializationError):
use_observability(MockChatClient)
use_instrumentation(MockChatClient)
# region Test telemetry decorator with mock client
@@ -235,7 +235,7 @@ def mock_chat_client():
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_chat_client_observability(mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data):
"""Test that when diagnostics are enabled, telemetry is applied."""
client = use_observability(mock_chat_client)()
client = use_instrumentation(mock_chat_client)()
messages = [ChatMessage(role=Role.USER, text="Test message")]
span_exporter.clear()
@@ -258,8 +258,8 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo
async def test_chat_client_streaming_observability(
mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""Test streaming telemetry through the use_observability decorator."""
client = use_observability(mock_chat_client)()
"""Test streaming telemetry through the use_instrumentation decorator."""
client = use_instrumentation(mock_chat_client)()
messages = [ChatMessage(role=Role.USER, text="Test")]
span_exporter.clear()
# Collect all yielded updates
@@ -282,7 +282,7 @@ async def test_chat_client_streaming_observability(
async def test_chat_client_without_model_id_observability(mock_chat_client, span_exporter: InMemorySpanExporter):
"""Test telemetry shouldn't fail when the model_id is not provided for unknown reason."""
client = use_observability(mock_chat_client)()
client = use_instrumentation(mock_chat_client)()
messages = [ChatMessage(role=Role.USER, text="Test")]
span_exporter.clear()
response = await client.get_response(messages=messages)
@@ -301,7 +301,7 @@ async def test_chat_client_streaming_without_model_id_observability(
mock_chat_client, span_exporter: InMemorySpanExporter
):
"""Test streaming telemetry shouldn't fail when the model_id is not provided for unknown reason."""
client = use_observability(mock_chat_client)()
client = use_instrumentation(mock_chat_client)()
messages = [ChatMessage(role=Role.USER, text="Test")]
span_exporter.clear()
# Collect all yielded updates
@@ -329,7 +329,7 @@ def test_prepend_user_agent_with_none_value():
assert AGENT_FRAMEWORK_USER_AGENT in str(result["User-Agent"])
# region Test use_agent_observability decorator
# region Test use_agent_instrumentation decorator
def test_agent_decorator_with_valid_class():
@@ -337,7 +337,7 @@ def test_agent_decorator_with_valid_class():
# Create a mock class with the required methods
class MockChatClientAgent:
AGENT_SYSTEM_NAME = "test_agent_system"
AGENT_PROVIDER_NAME = "test_agent_system"
def __init__(self):
self.id = "test_agent_id"
@@ -358,7 +358,7 @@ def test_agent_decorator_with_valid_class():
return AgentThread()
# Apply the decorator
decorated_class = use_agent_observability(MockChatClientAgent)
decorated_class = use_agent_instrumentation(MockChatClientAgent)
assert hasattr(decorated_class, OPEN_TELEMETRY_AGENT_MARKER)
@@ -367,19 +367,19 @@ def test_agent_decorator_with_missing_methods():
"""Test that agent decorator handles classes missing required methods gracefully."""
class MockAgent:
AGENT_SYSTEM_NAME = "test_agent_system"
AGENT_PROVIDER_NAME = "test_agent_system"
# Apply the decorator - should not raise an error
with pytest.raises(AgentInitializationError):
use_agent_observability(MockAgent)
use_agent_instrumentation(MockAgent)
def test_agent_decorator_with_partial_methods():
"""Test agent decorator when only one method is present."""
from agent_framework.observability import use_agent_observability
from agent_framework.observability import use_agent_instrumentation
class MockAgent:
AGENT_SYSTEM_NAME = "test_agent_system"
AGENT_PROVIDER_NAME = "test_agent_system"
def __init__(self):
self.id = "test_agent_id"
@@ -390,7 +390,7 @@ def test_agent_decorator_with_partial_methods():
return Mock()
with pytest.raises(AgentInitializationError):
use_agent_observability(MockAgent)
use_agent_instrumentation(MockAgent)
# region Test agent telemetry decorator with mock agent
@@ -401,7 +401,7 @@ def mock_chat_agent():
"""Create a mock chat client agent for testing."""
class MockChatClientAgent:
AGENT_SYSTEM_NAME = "test_agent_system"
AGENT_PROVIDER_NAME = "test_agent_system"
def __init__(self):
self.id = "test_agent_id"
@@ -433,7 +433,7 @@ async def test_agent_instrumentation_enabled(
):
"""Test that when agent diagnostics are enabled, telemetry is applied."""
agent = use_agent_observability(mock_chat_agent)()
agent = use_agent_instrumentation(mock_chat_agent)()
span_exporter.clear()
response = await agent.run("Test message")
@@ -457,8 +457,8 @@ async def test_agent_instrumentation_enabled(
async def test_agent_streaming_response_with_diagnostics_enabled_via_decorator(
mock_chat_agent: AgentProtocol, span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""Test agent streaming telemetry through the use_agent_observability decorator."""
agent = use_agent_observability(mock_chat_agent)()
"""Test agent streaming telemetry through the use_agent_instrumentation decorator."""
agent = use_agent_instrumentation(mock_chat_agent)()
span_exporter.clear()
updates = []
async for update in agent.run_stream("Test message"):
@@ -522,3 +522,393 @@ async def test_function_call_with_error_handling(span_exporter: InMemorySpanExpo
exception_message = exception_event.attributes["exception.message"]
assert isinstance(exception_message, str)
assert "Function execution failed" in exception_message
# region Test OTEL environment variable parsing
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_with_grpc_endpoint(monkeypatch):
"""Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_ENDPOINT (gRPC)."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
exporters = _get_exporters_from_env()
# Should return 3 exporters (trace, metrics, logs)
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_with_http_endpoint(monkeypatch):
"""Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_ENDPOINT (HTTP)."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http")
exporters = _get_exporters_from_env()
# Should return 3 exporters (trace, metrics, logs)
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_with_individual_endpoints(monkeypatch):
"""Test _get_exporters_from_env with individual signal endpoints."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4317")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "http://localhost:4319")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
exporters = _get_exporters_from_env()
# Should return 3 exporters (trace, metrics, logs)
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_with_headers(monkeypatch):
"""Test _get_exporters_from_env with OTEL_EXPORTER_OTLP_HEADERS."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "key1=value1,key2=value2")
exporters = _get_exporters_from_env()
# Should return 3 exporters with headers
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_with_signal_specific_headers(monkeypatch):
"""Test _get_exporters_from_env with signal-specific headers."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4317")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", "trace-key=trace-value")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
exporters = _get_exporters_from_env()
# Should have at least the traces exporter
assert len(exporters) >= 1
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_without_env_vars(monkeypatch):
"""Test _get_exporters_from_env returns empty list when no env vars set."""
from agent_framework.observability import _get_exporters_from_env
# Clear all OTEL env vars
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
exporters = _get_exporters_from_env()
# Should return empty list
assert len(exporters) == 0
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_get_exporters_from_env_missing_grpc_dependency(monkeypatch):
"""Test _get_exporters_from_env raises ImportError when gRPC exporters not installed."""
from agent_framework.observability import _get_exporters_from_env
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc")
# Mock the import to raise ImportError
original_import = __builtins__.__import__
def mock_import(name, *args, **kwargs):
if "opentelemetry.exporter.otlp.proto.grpc" in name:
raise ImportError("No module named 'opentelemetry.exporter.otlp.proto.grpc'")
return original_import(name, *args, **kwargs)
monkeypatch.setattr(__builtins__, "__import__", mock_import)
with pytest.raises(ImportError, match="opentelemetry-exporter-otlp-proto-grpc"):
_get_exporters_from_env()
# region Test create_resource
def test_create_resource_from_env(monkeypatch):
"""Test create_resource reads OTEL environment variables."""
from agent_framework.observability import create_resource
monkeypatch.setenv("OTEL_SERVICE_NAME", "test-service")
monkeypatch.setenv("OTEL_SERVICE_VERSION", "1.0.0")
monkeypatch.setenv("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=production,host.name=server1")
resource = create_resource()
assert resource.attributes["service.name"] == "test-service"
assert resource.attributes["service.version"] == "1.0.0"
assert resource.attributes["deployment.environment"] == "production"
assert resource.attributes["host.name"] == "server1"
def test_create_resource_with_parameters_override_env(monkeypatch):
"""Test create_resource parameters override environment variables."""
from agent_framework.observability import create_resource
monkeypatch.setenv("OTEL_SERVICE_NAME", "env-service")
monkeypatch.setenv("OTEL_SERVICE_VERSION", "0.1.0")
resource = create_resource(service_name="param-service", service_version="2.0.0")
# Parameters should override env vars
assert resource.attributes["service.name"] == "param-service"
assert resource.attributes["service.version"] == "2.0.0"
def test_create_resource_with_custom_attributes(monkeypatch):
"""Test create_resource accepts custom attributes."""
from agent_framework.observability import create_resource
resource = create_resource(custom_attr="custom_value", another_attr=123)
assert resource.attributes["custom_attr"] == "custom_value"
assert resource.attributes["another_attr"] == 123
# region Test _create_otlp_exporters
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_create_otlp_exporters_grpc_with_single_endpoint():
"""Test _create_otlp_exporters creates gRPC exporters with single endpoint."""
from agent_framework.observability import _create_otlp_exporters
exporters = _create_otlp_exporters(endpoint="http://localhost:4317", protocol="grpc")
# Should return 3 exporters (trace, metrics, logs)
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_create_otlp_exporters_http_with_single_endpoint():
"""Test _create_otlp_exporters creates HTTP exporters with single endpoint."""
from agent_framework.observability import _create_otlp_exporters
exporters = _create_otlp_exporters(endpoint="http://localhost:4318", protocol="http")
# Should return 3 exporters (trace, metrics, logs)
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_create_otlp_exporters_with_individual_endpoints():
"""Test _create_otlp_exporters with individual signal endpoints."""
from agent_framework.observability import _create_otlp_exporters
exporters = _create_otlp_exporters(
protocol="grpc",
traces_endpoint="http://localhost:4317",
metrics_endpoint="http://localhost:4318",
logs_endpoint="http://localhost:4319",
)
# Should return 3 exporters
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_create_otlp_exporters_with_headers():
"""Test _create_otlp_exporters with headers."""
from agent_framework.observability import _create_otlp_exporters
exporters = _create_otlp_exporters(
endpoint="http://localhost:4317", protocol="grpc", headers={"Authorization": "Bearer token"}
)
# Should return 3 exporters with headers
assert len(exporters) == 3
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_create_otlp_exporters_grpc_missing_dependency():
"""Test _create_otlp_exporters raises ImportError when gRPC exporters not installed."""
import sys
from unittest.mock import patch
from agent_framework.observability import _create_otlp_exporters
# Mock the import to raise ImportError
with (
patch.dict(sys.modules, {"opentelemetry.exporter.otlp.proto.grpc.trace_exporter": None}),
pytest.raises(ImportError, match="opentelemetry-exporter-otlp-proto-grpc"),
):
_create_otlp_exporters(endpoint="http://localhost:4317", protocol="grpc")
# region Test configure_otel_providers with views
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_configure_otel_providers_with_views(monkeypatch):
"""Test configure_otel_providers accepts views parameter."""
from opentelemetry.sdk.metrics import View
from opentelemetry.sdk.metrics.view import DropAggregation
from agent_framework.observability import configure_otel_providers
# Clear all OTEL env vars
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
# Create a view that drops all metrics
views = [View(instrument_name="*", aggregation=DropAggregation())]
# Should not raise an error
configure_otel_providers(views=views)
@pytest.mark.skipif(
True,
reason="Skipping OTLP exporter tests - optional dependency not installed by default",
)
def test_configure_otel_providers_without_views(monkeypatch):
"""Test configure_otel_providers works without views parameter."""
from agent_framework.observability import configure_otel_providers
# Clear all OTEL env vars
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
# Should not raise an error with default empty views
configure_otel_providers()
# region Test console exporters opt-in
def test_console_exporters_opt_in_false(monkeypatch):
"""Test console exporters are not added when ENABLE_CONSOLE_EXPORTERS is false."""
from agent_framework.observability import ObservabilitySettings
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "false")
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
settings = ObservabilitySettings(env_file_path="test.env")
assert settings.enable_console_exporters is False
def test_console_exporters_opt_in_true(monkeypatch):
"""Test console exporters are added when ENABLE_CONSOLE_EXPORTERS is true."""
from agent_framework.observability import ObservabilitySettings
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")
settings = ObservabilitySettings(env_file_path="test.env")
assert settings.enable_console_exporters is True
def test_console_exporters_default_false(monkeypatch):
"""Test console exporters default to False when not set."""
from agent_framework.observability import ObservabilitySettings
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
settings = ObservabilitySettings(env_file_path="test.env")
assert settings.enable_console_exporters is False
# region Test _parse_headers helper
def test_parse_headers_valid():
"""Test _parse_headers with valid header string."""
from agent_framework.observability import _parse_headers
headers = _parse_headers("key1=value1,key2=value2")
assert headers == {"key1": "value1", "key2": "value2"}
def test_parse_headers_with_spaces():
"""Test _parse_headers handles spaces around keys and values."""
from agent_framework.observability import _parse_headers
headers = _parse_headers("key1 = value1 , key2 = value2 ")
assert headers == {"key1": "value1", "key2": "value2"}
def test_parse_headers_empty_string():
"""Test _parse_headers with empty string."""
from agent_framework.observability import _parse_headers
headers = _parse_headers("")
assert headers == {}
def test_parse_headers_invalid_format():
"""Test _parse_headers ignores invalid pairs."""
from agent_framework.observability import _parse_headers
headers = _parse_headers("key1=value1,invalid,key2=value2")
# Should only include valid pairs
assert headers == {"key1": "value1", "key2": "value2"}
@@ -832,7 +832,7 @@ def test_create_streaming_response_content_with_mcp_approval_request() -> None:
assert fa.function_call.name == "do_stream_action"
@pytest.mark.parametrize("enable_otel", [False], indirect=True)
@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True)
@pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True)
async def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
"""End-to-end mocked test:
@@ -22,5 +22,5 @@ def test_datetime_in_tool_results() -> None:
result = _to_otel_part(content)
parsed = json.loads(result["response"])
# Datetime should be converted to string
assert isinstance(parsed["timestamp"], str)
# Datetime should be converted to string in the result field
assert isinstance(parsed["result"]["timestamp"], str)
@@ -229,8 +229,10 @@ async def test_trace_context_handling(span_exporter: InMemorySpanExporter) -> No
assert processing_span.attributes.get("message.payload_type") == "str"
@pytest.mark.parametrize("enable_otel", [False], indirect=True)
async def test_trace_context_disabled_when_tracing_disabled(enable_otel, span_exporter: InMemorySpanExporter) -> None:
@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True)
async def test_trace_context_disabled_when_tracing_disabled(
enable_instrumentation, span_exporter: InMemorySpanExporter
) -> None:
"""Test that no trace context is added when tracing is disabled."""
# Tracing should be disabled by default
executor = MockExecutor("test-executor")
@@ -433,7 +435,7 @@ async def test_workflow_error_handling_in_tracing(span_exporter: InMemorySpanExp
assert workflow_span.status.status_code.name == "ERROR"
@pytest.mark.parametrize("enable_otel", [False], indirect=True)
@pytest.mark.parametrize("enable_instrumentation", [False], indirect=True)
async def test_message_trace_context_serialization(span_exporter: InMemorySpanExporter) -> None:
"""Test that message trace context is properly serialized/deserialized."""
ctx = InProcRunnerContext(InMemoryCheckpointStorage())
@@ -177,9 +177,9 @@ def serve(
import os
# Only set if not already configured by user
if not os.environ.get("ENABLE_OTEL"):
os.environ["ENABLE_OTEL"] = "true"
logger.info("Set ENABLE_OTEL=true for tracing")
if not os.environ.get("ENABLE_INSTRUMENTATION"):
os.environ["ENABLE_INSTRUMENTATION"] = "true"
logger.info("Set ENABLE_INSTRUMENTATION=true for tracing")
if not os.environ.get("ENABLE_SENSITIVE_DATA"):
os.environ["ENABLE_SENSITIVE_DATA"] = "true"
@@ -82,27 +82,23 @@ class AgentFrameworkExecutor:
def _setup_agent_framework_tracing(self) -> None:
"""Set up Agent Framework's built-in tracing."""
# Configure Agent Framework tracing only if ENABLE_OTEL is set
if os.environ.get("ENABLE_OTEL"):
# Configure Agent Framework tracing only if ENABLE_INSTRUMENTATION is set
if os.environ.get("ENABLE_INSTRUMENTATION"):
try:
from agent_framework.observability import OBSERVABILITY_SETTINGS, setup_observability
from agent_framework.observability import OBSERVABILITY_SETTINGS, configure_otel_providers
# Only configure if not already executed
if not OBSERVABILITY_SETTINGS._executed_setup:
# Get OTLP endpoint from either custom or standard env var
# This handles the case where env vars are set after ObservabilitySettings was imported
otlp_endpoint = os.environ.get("OTLP_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
# Pass the endpoint explicitly to setup_observability
# Run the configure_otel_providers
# This ensures OTLP exporters are created even if env vars were set late
setup_observability(enable_sensitive_data=True, otlp_endpoint=otlp_endpoint)
configure_otel_providers(enable_sensitive_data=True)
logger.info("Enabled Agent Framework observability")
else:
logger.debug("Agent Framework observability already configured")
except Exception as e:
logger.warning(f"Failed to enable Agent Framework observability: {e}")
else:
logger.debug("ENABLE_OTEL not set, skipping observability setup")
logger.debug("ENABLE_INSTRUMENTATION not set, skipping observability setup")
async def discover_entities(self) -> list[EntityInfo]:
"""Discover all available entities.
@@ -407,7 +407,7 @@ class DevServer:
framework="agent_framework",
runtime="python", # Python DevUI backend
capabilities={
"tracing": os.getenv("ENABLE_OTEL") == "true",
"tracing": os.getenv("ENABLE_INSTRUMENTATION") == "true",
"openai_proxy": openai_executor.is_configured,
"deployment": True, # Deployment feature is available
},
File diff suppressed because one or more lines are too long
@@ -321,8 +321,7 @@ function processEventsForDisplay(
} else {
// Shouldn't happen if output_item.added was emitted first
console.warn(
`Received argument delta for unknown call with item_id: ${
"item_id" in event ? event.item_id : "unknown"
`Received argument delta for unknown call with item_id: ${"item_id" in event ? event.item_id : "unknown"
}`
);
}
@@ -416,17 +415,15 @@ function getEventSummary(event: ExtendedResponseStreamEvent): string {
? data.arguments.slice(0, 30)
: JSON.stringify(data.arguments).slice(0, 30)
: "";
return `Calling ${functionName}(${argsStr}${
argsStr.length >= 30 ? "..." : ""
})`;
return `Calling ${functionName}(${argsStr}${argsStr.length >= 30 ? "..." : ""
})`;
}
return "Function call";
case "response.function_call_arguments.delta":
if ("delta" in event && event.delta) {
return `Function arg delta: ${event.delta.slice(0, 30)}${
event.delta.length > 30 ? "..." : ""
}`;
return `Function arg delta: ${event.delta.slice(0, 30)}${event.delta.length > 30 ? "..." : ""
}`;
}
return "Function arguments...";
@@ -434,9 +431,8 @@ function getEventSummary(event: ExtendedResponseStreamEvent): string {
const resultEvent =
event as import("@/types").ResponseFunctionResultComplete;
const truncated = resultEvent.output.slice(0, 40);
return `Function result: ${truncated}${
truncated.length >= 40 ? "..." : ""
}`;
return `Function result: ${truncated}${truncated.length >= 40 ? "..." : ""
}`;
}
case "response.output_item.added": {
@@ -595,9 +591,8 @@ function EventItem({ event }: EventItemProps) {
<div className="text-sm">
<div
className={`flex items-center gap-2 ${
hasExpandableContent ? "cursor-pointer" : ""
}`}
className={`flex items-center gap-2 ${hasExpandableContent ? "cursor-pointer" : ""
}`}
onClick={() => hasExpandableContent && setIsExpanded(!isExpanded)}
>
{hasExpandableContent && (
@@ -757,11 +752,10 @@ function EventExpandedContent({
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${
resultEvent.status === "completed"
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${resultEvent.status === "completed"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
}`}
>
{resultEvent.status}
</span>
@@ -802,11 +796,10 @@ function EventExpandedContent({
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${
result.status === "completed"
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${result.status === "completed"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
}`}
>
{result.status}
</span>
@@ -936,11 +929,10 @@ function EventExpandedContent({
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${
data.status === "StatusCode.UNSET" || data.status === "OK"
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${data.status === "StatusCode.UNSET" || data.status === "OK"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
}`}
>
{data.status || "unknown"}
</span>
@@ -1191,7 +1183,7 @@ function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
<Info className="inline h-4 w-4 mr-1 " />
You may have to set the environment variable{" "}
<span className="font-mono bg-accent/10 px-1 rounded">
ENABLE_OTEL=true
ENABLE_INSTRUMENTATION=true
</span>{" "}
or restart devui with the tracing flag{" "}
<div className="font-mono bg-accent/10 px-1 rounded">
@@ -1351,12 +1343,11 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${
data.status === "StatusCode.UNSET" ||
data.status === "OK"
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${data.status === "StatusCode.UNSET" ||
data.status === "OK"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
}`}
>
{data.status || "unknown"}
</span>
@@ -249,7 +249,7 @@ services:
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\${AZURE_OPENAI_CHAT_DEPLOYMENT_NAME}
# Optional: Enable tracing
- ENABLE_OTEL=\${ENABLE_OTEL:-false}
- ENABLE_INSTRUMENTATION=\${ENABLE_INSTRUMENTATION:-false}
ports:
- "8080:8080"
restart: unless-stopped
@@ -282,11 +282,10 @@ openai>=1.0.0
<div className="flex border-b px-6">
<button
onClick={() => setActiveTab("docker")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "docker"
className={`px-4 py-2 text-sm font-medium transition-colors relative ${activeTab === "docker"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
}`}
>
<Container className="h-4 w-4 mr-2 inline" />
Docker
@@ -297,11 +296,10 @@ openai>=1.0.0
{deploymentSupported && (
<button
onClick={() => setActiveTab("azure")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "azure"
className={`px-4 py-2 text-sm font-medium transition-colors relative ${activeTab === "azure"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
}`}
>
<Cloud className="h-4 w-4 mr-2 inline" />
Azure
@@ -536,8 +534,8 @@ openai>=1.0.0
<div className="mt-2 p-2 bg-blue-100 dark:bg-blue-900 rounded text-xs">
<p className="mb-1">Run these commands once per subscription:</p>
<code className="block font-mono">
az provider register -n Microsoft.App --wait<br/>
az provider register -n Microsoft.ContainerRegistry --wait<br/>
az provider register -n Microsoft.App --wait<br />
az provider register -n Microsoft.ContainerRegistry --wait<br />
az provider register -n Microsoft.OperationalInsights --wait
</code>
</div>
@@ -564,9 +562,8 @@ openai>=1.0.0
<label className="text-sm font-medium">App Name</label>
<input
type="text"
className={`w-full mt-1 px-3 py-2 border rounded-md text-sm ${
appNameError ? "border-red-500" : ""
}`}
className={`w-full mt-1 px-3 py-2 border rounded-md text-sm ${appNameError ? "border-red-500" : ""
}`}
placeholder="my-agent-app"
value={appName}
onChange={(e) => {
@@ -732,73 +729,73 @@ openai>=1.0.0
<div className="space-y-3">
<h4 className="font-medium text-sm">Deployment Steps</h4>
<div className="space-y-3">
{/* Step 1 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
1
</div>
<h5 className="font-medium text-sm">
Create Azure Container Registry
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Create resource group
<div className="space-y-3">
{/* Step 1 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
1
</div>
<h5 className="font-medium text-sm">
Create Azure Container Registry
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Create resource group
az group create --name myResourceGroup --location eastus
# Create container registry
az acr create --resource-group myResourceGroup \\
--name myregistry --sku Basic`}
</pre>
</div>
{/* Step 2 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
2
</pre>
</div>
<h5 className="font-medium text-sm">
Build and Push Docker Image
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Build and push in one command
{/* Step 2 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
2
</div>
<h5 className="font-medium text-sm">
Build and Push Docker Image
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Build and push in one command
az acr build --registry myregistry \\
--image ${agentName.toLowerCase()}-agent:latest .`}
</pre>
</div>
{/* Step 3 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
3
</pre>
</div>
<h5 className="font-medium text-sm">
Create Container Apps Environment
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp env create --name myEnvironment \\
{/* Step 3 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
3
</div>
<h5 className="font-medium text-sm">
Create Container Apps Environment
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp env create --name myEnvironment \\
--resource-group myResourceGroup \\
--location eastus`}
</pre>
</div>
{/* Step 4 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
4
</pre>
</div>
<h5 className="font-medium text-sm">
Deploy Container App
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp create --name ${agentName.toLowerCase()}-app \\
{/* Step 4 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
4
</div>
<h5 className="font-medium text-sm">
Deploy Container App
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp create --name ${agentName.toLowerCase()}-app \\
--resource-group myResourceGroup \\
--environment myEnvironment \\
--image myregistry.azurecr.io/${agentName.toLowerCase()}-agent:latest \\
@@ -806,51 +803,51 @@ az acr build --registry myregistry \\
--ingress 'external' \\
--registry-server myregistry.azurecr.io \\
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_MODEL_ID=gpt-4o-mini`}
</pre>
</div>
{/* Step 5 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
5
</pre>
</div>
<h5 className="font-medium text-sm">
Get Application URL
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp show --name ${agentName.toLowerCase()}-app \\
{/* Step 5 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
5
</div>
<h5 className="font-medium text-sm">
Get Application URL
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp show --name ${agentName.toLowerCase()}-app \\
--resource-group myResourceGroup \\
--query properties.configuration.ingress.fqdn`}
</pre>
</pre>
</div>
</div>
</div>
</div>
</div>
{/* Learn More */}
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<h4 className="text-sm font-semibold mb-2">Learn More</h4>
<p className="text-xs text-muted-foreground mb-3">
Explore Azure Container Apps documentation for advanced
features like scaling, monitoring, and CI/CD integration.
</p>
<Button
size="sm"
variant="outline"
className="w-full"
asChild
>
<a
href="https://learn.microsoft.com/azure/container-apps/"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Azure Container Apps Documentation
</a>
</Button>
</div>
{/* Learn More */}
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<h4 className="text-sm font-semibold mb-2">Learn More</h4>
<p className="text-xs text-muted-foreground mb-3">
Explore Azure Container Apps documentation for advanced
features like scaling, monitoring, and CI/CD integration.
</p>
<Button
size="sm"
variant="outline"
className="w-full"
asChild
>
<a
href="https://learn.microsoft.com/azure/container-apps/"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Azure Container Apps Documentation
</a>
</Button>
</div>
</>
)}
</div>
@@ -30,7 +30,6 @@ class GAIATelemetryConfig:
self,
enable_tracing: bool = False,
otlp_endpoint: str | None = None,
applicationinsights_connection_string: str | None = None,
trace_to_file: bool = False,
file_path: str | None = None,
):
@@ -39,24 +38,27 @@ class GAIATelemetryConfig:
Args:
enable_tracing: Whether to enable OpenTelemetry tracing
otlp_endpoint: OTLP endpoint for trace export
applicationinsights_connection_string: Azure Monitor connection string
trace_to_file: Whether to export traces to local file
file_path: Path for local file export (defaults to gaia_traces.json)
Note:
For Azure Monitor integration, configure using environment variables
(OTEL_EXPORTER_OTLP_ENDPOINT, etc.) or use AzureAIClient.configure_azure_monitor()
before creating the GAIA instance.
"""
self.enable_tracing = enable_tracing
self.otlp_endpoint = otlp_endpoint
self.applicationinsights_connection_string = applicationinsights_connection_string
self.trace_to_file = trace_to_file
self.file_path = file_path or "gaia_traces.json"
def setup_observability(self) -> None:
def configure_otel_providers(self) -> None:
"""Set up OpenTelemetry based on configuration."""
if not self.enable_tracing:
return
# If only file tracing is requested (no OTLP or Application Insights),
# skip the default setup_observability which adds console exporter
if self.trace_to_file and not self.otlp_endpoint and not self.applicationinsights_connection_string:
# If only file tracing is requested (no OTLP),
# skip the default configure_otel_providers which adds console exporter
if self.trace_to_file and not self.otlp_endpoint:
# Set up minimal tracing with only file export
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.trace import set_tracer_provider
@@ -65,13 +67,17 @@ class GAIATelemetryConfig:
set_tracer_provider(tracer_provider)
self._setup_file_export()
else:
# Use full observability setup for OTLP/AppInsights
from agent_framework.observability import setup_observability
# Use full observability setup for OTLP
from agent_framework.observability import configure_otel_providers
setup_observability(
# Set OTLP endpoint env var if provided
if self.otlp_endpoint:
import os
os.environ.setdefault("OTEL_EXPORTER_OTLP_ENDPOINT", self.otlp_endpoint)
configure_otel_providers(
enable_sensitive_data=True, # Enable for detailed task traces
otlp_endpoint=self.otlp_endpoint,
applicationinsights_connection_string=self.applicationinsights_connection_string,
)
# Set up local file export if requested
@@ -333,7 +339,7 @@ class GAIA:
self.telemetry_config = telemetry_config or GAIATelemetryConfig()
# Set up telemetry
self.telemetry_config.setup_observability()
self.telemetry_config.configure_otel_providers()
# Initialize tracer
if self.telemetry_config.enable_tracing:
@@ -22,13 +22,13 @@ class AgentFrameworkTracer(AgentOpsTracer): # type: ignore
def init(self) -> None:
"""Initialize the agent-framework-lab-lightning for training."""
OBSERVABILITY_SETTINGS.enable_otel = True
OBSERVABILITY_SETTINGS.enable_instrumentation = True
super().init()
def teardown(self) -> None:
"""Teardown the agent-framework-lab-lightning for training."""
super().teardown()
OBSERVABILITY_SETTINGS.enable_otel = False
OBSERVABILITY_SETTINGS.enable_instrumentation = False
__all__: list[str] = ["AgentFrameworkTracer"]
+3 -3
View File
@@ -241,10 +241,10 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
| [`getting_started/observability/advanced_manual_setup_console_output.py`](./getting_started/observability/advanced_manual_setup_console_output.py) | Advanced manual observability setup with console output |
| [`getting_started/observability/advanced_zero_code.py`](./getting_started/observability/advanced_zero_code.py) | Zero-code observability setup example |
| [`getting_started/observability/agent_observability.py`](./getting_started/observability/agent_observability.py) | Agent observability example |
| [`getting_started/observability/agent_with_foundry_tracing.py`](./getting_started/observability/agent_with_foundry_tracing.py) | Any chat client setup with Azure Foundry Observability |
| [`getting_started/observability/azure_ai_agent_observability.py`](./getting_started/observability/azure_ai_agent_observability.py) | Azure AI agent observability example |
| [`getting_started/observability/azure_ai_chat_client_with_observability.py`](./getting_started/observability/azure_ai_chat_client_with_observability.py) | Azure AI chat client with observability example |
| [`getting_started/observability/setup_observability_with_env_var.py`](./getting_started/observability/setup_observability_with_env_var.py) | Setup observability using environment variables |
| [`getting_started/observability/setup_observability_with_parameters.py`](./getting_started/observability/setup_observability_with_parameters.py) | Setup observability using parameters |
| [`getting_started/observability/configure_otel_providers_with_env_var.py`](./getting_started/observability/configure_otel_providers_with_env_var.py) | Setup observability using environment variables |
| [`getting_started/observability/configure_otel_providers_with_parameters.py`](./getting_started/observability/configure_otel_providers_with_parameters.py) | Setup observability using parameters |
| [`getting_started/observability/workflow_observability.py`](./getting_started/observability/workflow_observability.py) | Workflow observability example |
## Threads
File diff suppressed because one or more lines are too long
@@ -1,14 +1,49 @@
APPLICATIONINSIGHTS_CONNECTION_STRING="..."
OTLP_ENDPOINT="http://localhost:4317/"
# 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
# ==================================
# Enable sensitive data logging (prompts, responses, etc.)
# WARNING: Only enable in dev/test environments
ENABLE_SENSITIVE_DATA=true
# This is not required if you run `setup_observability()` in your code
ENABLE_OTEL=true
# Optional: Enable console exporters for debugging
# ENABLE_CONSOLE_EXPORTERS=true
# Optional: Enable observability (automatically enabled if env vars are set or configure_otel_providers() is called)
# ENABLE_INSTRUMENTATION=true
# OpenAI specific variables
# ==========================
OPENAI_API_KEY="..."
OPENAI_RESPONSES_MODEL_ID="gpt-4o-2024-08-06"
OPENAI_CHAT_MODEL_ID="gpt-4o-2024-08-06"
# Foundry specific variables
# Azure AI Foundry specific variables
# ====================================
AZURE_AI_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o-mini"
@@ -20,80 +20,162 @@ For more information, please refer to the following resources:
The Agent Framework Python SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of agent/model invocation and tool execution. This allows you to effectively monitor your AI application's performance and accurately track token consumption. It does so based on the Semantic Conventions for GenAI defined by OpenTelemetry, and the workflows emit their own spans to provide end-to-end visibility.
Next to what happens in the code when you run, we also make setting up observability as easy as possible. By calling a single function `configure_otel_providers()` from the `agent_framework.observability` module, you can enable telemetry for traces, logs, and metrics. The function automatically reads standard OpenTelemetry environment variables to configure exporters and providers, making it simple to get started.
### Five patterns for configuring observability
We've identified multiple ways to configure observability in your application, 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
# Enable console exporters via environment variable
configure_otel_providers(enable_console_exporters=True)
```
This is the **recommended approach** for getting started.
**2. Custom Exporters**
One level more control over the exporters that are created is to do that yourself, and then pass them to `configure_otel_providers()`. We will still create the providers for you, but you can customize the exporters as needed:
```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 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 will be added alongside any exporters from environment variables
configure_otel_providers(exporters=exporters, enable_sensitive_data=True)
```
**3. Third party setup**
A lot of third party specific otel package, have their own easy setup methods, for example Azure Monitor has `configure_azure_monitor()`. You can use those methods to setup the third party first, and then call `enable_instrumentation()` from the `agent_framework.observability` module to activate the Agent Framework telemetry code paths. In all these cases, if you already setup observability via environment variables, you don't need to call `enable_instrumentation()` as it will be enabled automatically.
```python
from azure.monitor.opentelemetry import configure_azure_monitor
from agent_framework.observability import create_resource, enable_instrumentation
# Configure Azure Monitor first
configure_azure_monitor(
connection_string="InstrumentationKey=...",
resource=create_resource(), # Uses OTEL_SERVICE_NAME, etc.
enable_live_metrics=True,
)
# Then activate Agent Framework's telemetry code paths
# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars
enable_instrumentation(enable_sensitive_data=False)
```
For Azure AI projects, use the `client.configure_azure_monitor()` method which wraps the calls to `configure_azure_monitor()` and `enable_instrumentation()`:
```python
from agent_framework.azure import AzureAIClient
from azure.ai.projects.aio import AIProjectClient
async with (
AIProjectClient(...) as project_client,
AzureAIClient(project_client=project_client) as client,
):
# Automatically configures Azure Monitor with connection string from project
await client.configure_azure_monitor(enable_live_metrics=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_instrumentation
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.")
# Then activate Agent Framework's telemetry code paths
# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars
enable_instrumentation(enable_sensitive_data=False)
```
**4. Manual setup**
Of course you can also do a complete manual setup of exporters, providers, and instrumentation. Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. This gives you full control over which exporters and providers to use. We do have a helper function `create_resource()` in the `agent_framework.observability` module that you can use to create a resource with the appropriate service name and version based on environment variables or standard defaults for Agent Framework, this is not used in the sample.
**5. Auto-instrumentation (zero-code)**
You can also use the [OpenTelemetry CLI tool](https://opentelemetry.io/docs/instrumentation/python/getting-started/#automatic-instrumentation) to automatically instrument your application without changing any code. Please refer to sample [advanced_zero_code.py](./advanced_zero_code.py) for an example of how to use the CLI tool to enable instrumentation for Agent Framework applications.
## Configuration
### Required resources
1. OpenAI or [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal)
2. An [Azure AI project](https://ai.azure.com/doc/azure/ai-foundry/what-is-azure-ai-foundry)
### Optional resources
The following resources are needed if you want to send telemetry data to them:
1. [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource)
2. [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone-for-python?tabs=flask%2Cwindows#start-the-aspire-dashboard)
### Dependencies
No additional dependencies are required to enable telemetry. The necessary packages are included as part of the `agent-framework` package. Unless you want to use a different APM vendor, in which case you will need to install the appropriate OpenTelemetry exporter package.
As part of Agent Framework we use the following OpenTelemetry packages:
- `opentelemetry-api`
- `opentelemetry-sdk`
- `opentelemetry-semantic-conventions-ai`
We do not install exporters by default, so you will need to add those yourself, this prevents us from installing unnecessary dependencies. For Application Insights, you will need to install `azure-monitor-opentelemetry`. For Aspire Dashboard or other OTLP compatible backends, you will need to install `opentelemetry-exporter-otlp-proto-grpc`. For HTTP protocol support, you will also need to install `opentelemetry-exporter-otlp-proto-http`.
And for many others, different packages are used, so refer to the documentation of the specific exporter you want to use.
### Environment variables
The following environment variables are used to turn on/off observability of the Agent Framework:
- ENABLE_OTEL=true
- ENABLE_SENSITIVE_DATA=true
- `ENABLE_INSTRUMENTATION`
- `ENABLE_SENSITIVE_DATA`
- `ENABLE_CONSOLE_EXPORTERS`
The framework will emit observability data when one of the above environment variables is set to true.
All of these are booleans and default to `false`.
Finally we have `VS_CODE_EXTENSION_PORT` which you can set to a port, which can be used to setup the AI Toolkit for VS Code tracing integration. See [here](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) for more details.
The framework will emit observability data when the `ENABLE_INSTRUMENTATION` environment variable is set to `true`. If both are `true` then it will also emit sensitive information. When these are not set, or set to false, you can use the `enable_instrumentation()` function from the `agent_framework.observability` module to turn on instrumentation programmatically. This is useful when you want to control this via code instead of environment variables.
> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data.
### Configuring exporters and providers
The two other variables, `ENABLE_CONSOLE_EXPORTERS` and `VS_CODE_EXTENSION_PORT`, are used to configure where the observability data is sent. Those are only activated when calling `configure_otel_providers()`.
Turning on observability is just the first step, you also need to configure where to send the observability data (i.e. Console, Application Insights). By default, no exporters or providers are configured.
#### Environment variables for `configure_otel_providers()`
#### Setting up exporters and providers manually
The `configure_otel_providers()` function automatically reads **standard OpenTelemetry environment variables** to configure exporters:
Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console.
**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)
#### Setting up exporters and providers using `setup_observability()`
**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`)
To make it easier for developers to get started, the `agent_framework.observability` module provides a `setup_observability()` function that will setup exporters and providers for traces, logs, and metrics based on environment variables. You can call this function at the start of your application to enable telemetry.
```python
from agent_framework.observability import setup_observability
setup_observability()
```
Agent Framework also has an opinionated logging format, which you can setup using:
```python
from agent_framework import setup_logging
setup_logging()
```
#### Environment variables for `setup_observability()`
The `setup_observability()` function will look for the following environment variables to determine how to setup the exporters and providers:
- OTLP_ENDPOINT="..."
- APPLICATIONINSIGHTS_CONNECTION_STRING="..."
By providing the above environment variables, the `setup_observability()` function will automatically configure the appropriate exporters and providers for you. If no environment variables are provided, the function will not setup any exporters or providers.
You can also pass in a list of exporters directly to the `setup_observability()` function if you want to customize the exporters or add additional ones besides the ones configured via environment variables.
```python
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from agent_framework.observability import setup_observability
exporter = OTLPSpanExporter(endpoint="another-otlp-endpoint")
setup_observability(exporters=[exporter])
```
> Using this method implicitly enables telemetry, so you do not need to set the `ENABLE_OTEL` environment variable. You can still set `ENABLE_SENSITIVE_DATA` to control whether sensitive data is included in the telemetry, or call the `setup_observability()` function with the `enable_sensitive_data` parameter set to `True`.
> **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
Agent Framework has a built-in logging configuration that works well with telemetry. It sets the format to a standard format that includes timestamp, pathname, line number, and log level. You can use that by calling the `setup_logging()` function from the `agent_framework` module.
@@ -119,65 +201,27 @@ This folder contains different samples demonstrating how to use telemetry in var
| Sample | Description |
|--------|-------------|
| [setup_observability_with_parameters.py](./setup_observability_with_parameters.py) | A simple example showing how to setup telemetry by passing in parameters to the `setup_observability()` function. This sample also uses the `setup_logging()` function to configure logging. |
| [setup_observability_with_env_var.py](./setup_observability_with_env_var.py) | A simple example showing how to setup telemetry with the `setup_observability()` function using environment variables. |
| [agent_observability.py](./agent_observability.py) | A simple example showing how to setup telemetry for an agentic application. |
| [azure_ai_agent_observability.py](./azure_ai_agent_observability.py) | A simple example showing how to setup telemetry for an agentic application with an Azure AI project. |
| [azure_ai_chat_client_with_observability.py](./azure_ai_chat_client_with_observability.py) | A simple example showing how to setup telemetry for a chat client with an Azure AI project. |
| [workflow_observability.py](./workflow_observability.py) | A simple example showing how to setup telemetry for a workflow. |
| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | A comprehensive example showing how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. |
| [advanced_zero_code.py](./advanced_zero_code.py) | A comprehensive example showing how to setup telemetry using the `opentelemetry-instrument` lib without modifying any code. |
| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | **Recommended starting point**: Shows how to create custom exporters with specific configuration and pass them to `configure_otel_providers()`. Useful for advanced scenarios. |
| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | Shows how to setup telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). |
| [agent_observability.py](./agent_observability.py) | Shows telemetry collection for an agentic application with tool calls using environment variables. |
| [agent_with_foundry_tracing.py](./agent_with_foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. |
| [azure_ai_agent_observability.py](./azure_ai_agent_observability.py) | Shows Azure Monitor integration for a AzureAIClient. |
| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: Shows 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: Shows zero-code telemetry setup using the `opentelemetry-enable_instrumentation` CLI tool. |
| [workflow_observability.py](./workflow_observability.py) | Shows telemetry collection for a workflow with multiple executors and message passing. |
### Running the samples
1. Open a terminal and navigate to this folder: `python/samples/getting_started/observability/`. This is necessary for the `.env` file to be read correctly.
2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example).
> Note that `APPLICATIONINSIGHTS_CONNECTION_STRING` and `OTLP_ENDPOINT` are optional. If you don't configure them, everything will get outputted to the console.
3. Activate your python virtual environment, and then run `python setup_observability_with_env_vars.py` or others.
> **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output.
3. Activate your python virtual environment, and then run `python configure_otel_providers_with_env_var.py` or others.
> This will also print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
## Application Insights/Azure Monitor
# Appendix
### Authentication
You can connect to your Application Insights instance using a connection string. You can also authenticate using Entra ID by passing a [TokenCredential](https://learn.microsoft.com/en-us/python/api/azure-core/azure.core.credentials.tokencredential?view=azure-python) to the `setup_observability()` function used in the samples above.
```python
from azure.identity import DefaultAzureCredential
# The credential will be for resources specified in the environment variables and the parameters passed in.
setup_observability(..., credential=DefaultAzureCredential())
```
It is recommended to use [DefaultAzureCredential](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential?view=azure-python) for local development and [ManagedIdentityCredential](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.managedidentitycredential?view=azure-python) for production environments.
### Logs and traces
Go to your Application Insights instance, click on _Transaction search_ on the left menu. Use the operation id printed by the program to search for the logs and traces associated with the operation. Click on any of the search result to view the end-to-end transaction details. Read more [here](https://learn.microsoft.com/en-us/azure/azure-monitor/app/transaction-search-and-diagnostics?tabs=transaction-search).
### Metrics
Running the application once will only generate one set of measurements (for each metrics). Run the application a couple times to generate more sets of measurements.
> Note: Make sure not to run the program too frequently. Otherwise, you may get throttled.
Please refer to here on how to analyze metrics in [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/analyze-metrics).
### Adding exporters
You can also create exporters directly and have those added to the tracer_providers, logger_providers and metrics_providers, this is useful if you want to add a different exporter on the fly, or if you want to customize the exporter. Here is an example of how to create an OTLP exporter and add it to the observability setup:
```python
from grpc import Compression
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from agent_framework.observability import setup_observability
exporter = OTLPSpanExporter(endpoint="your-otlp-endpoint", compression=Compression.Gzip)
setup_observability(exporters=[exporter])
```
### Logs
## Azure Monitor Queries
When you are in Azure Monitor and want to have a overall view of the span, use this query in the logs section:
@@ -212,6 +256,118 @@ Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-agent>
Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
![Workflow Overview dashboard](https://github.com/Azure/azure-managed-grafana/raw/main/samples/assets/grafana-af-workflow.gif)
## Migration Guide
We've done a major update to the observability API in Agent Framework Python SDK. The new API simplifies configuration by relying more on standard OpenTelemetry environment variables and have split the instrumentation from the configuration.
If you're updating from a previous version of the Agent Framework, here are the key changes to the observability API:
### Environment Variables
| Old Variable | New Variable | Notes |
|-------------|--------------|-------|
| `OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Standard OpenTelemetry env var |
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | N/A | Use `configure_azure_monitor()` |
| N/A | `ENABLE_CONSOLE_EXPORTERS` | New opt-in flag for console output |
### OTLP Configuration
**Before (Deprecated):**
```python
from agent_framework.observability import setup_observability
# Via parameter
setup_observability(otlp_endpoint="http://localhost:4317")
# Via environment variable
# OTLP_ENDPOINT=http://localhost:4317
setup_observability()
```
**After (Current):**
```python
from agent_framework.observability import configure_otel_providers
# Via standard OTEL environment variable (recommended)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
configure_otel_providers()
# Or via custom exporters
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
configure_otel_providers(exporters=[
OTLPSpanExporter(endpoint="http://localhost:4317"),
OTLPLogExporter(endpoint="http://localhost:4317"),
OTLPMetricExporter(endpoint="http://localhost:4317"),
])
```
### Azure Monitor Configuration
**Before (Deprecated):**
```python
from agent_framework.observability import setup_observability
setup_observability(
applicationinsights_connection_string="InstrumentationKey=...",
applicationinsights_live_metrics=True,
)
```
**After (Current):**
```python
# For Azure AI projects
from agent_framework.azure import AzureAIClient
from azure.ai.projects.aio import AIProjectClient
async with (
AIProjectClient(...) as project_client,
AzureAIClient(project_client=project_client) as client,
):
await client.configure_azure_monitor(enable_live_metrics=True)
# For non-Azure AI projects
from azure.monitor.opentelemetry import configure_azure_monitor
from agent_framework.observability import create_resource, enable_instrumentation
configure_azure_monitor(
connection_string="InstrumentationKey=...",
resource=create_resource(),
enable_live_metrics=True,
)
enable_instrumentation()
```
### Console Output
**Before (Deprecated):**
```python
from agent_framework.observability import setup_observability
# Console was used as automatic fallback
setup_observability() # Would output to console if no exporters configured
```
**After (Current):**
```python
from agent_framework.observability import configure_otel_providers
# Console exporters are now opt-in
# ENABLE_CONSOLE_EXPORTERS=true
configure_otel_providers()
# Or programmatically
configure_otel_providers(enable_console_exporters=True)
```
### Benefits of New API
1. **Standards Compliant**: Uses standard OpenTelemetry environment variables
2. **Simpler**: Less configuration needed, more relies on environment
3. **Flexible**: Easy to add custom exporters alongside environment-based ones
4. **Cleaner Separation**: Azure Monitor setup is in Azure-specific client
5. **Better Compatibility**: Works with any OTEL-compatible tool (Jaeger, Zipkin, Prometheus, etc.)
## Aspire Dashboard
The [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) is a local telemetry viewing tool that provides an excellent experience for viewing OpenTelemetry data without requiring Azure setup.
@@ -239,13 +395,13 @@ This will start the dashboard with:
Make sure your `.env` file includes the OTLP endpoint:
```bash
OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
```
Or set it as an environment variable when running your samples:
```bash
ENABLE_OTEL=true OTLP_ENDPOINT=http://localhost:4317 python 01-zero_code.py
ENABLE_INSTRUMENTATION=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py
```
### Viewing telemetry data
@@ -253,9 +409,3 @@ ENABLE_OTEL=true OTLP_ENDPOINT=http://localhost:4317 python 01-zero_code.py
> 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/en-us/dotnet/aspire/fundamentals/dashboard/explore) to authenticate to the dashboard and start exploring your traces, logs, and metrics!
## Console output
You won't have to deploy an Application Insights resource or install Docker to run Aspire Dashboard if you choose to inspect telemetry data in a console. However, it is difficult to navigate through all the spans and logs produced, so **this method is only recommended when you are just getting started**.
Use the guides from OpenTelemetry to setup exporters for [the console](https://opentelemetry.io/docs/languages/python/getting-started/), or use [advanced_manual_setup_console_output](./advanced_manual_setup_console_output.py) as a reference, just know that there are a lot of options you can setup and this is not a comprehensive example.
@@ -5,6 +5,7 @@ import logging
from random import randint
from typing import Annotated
from agent_framework.observability import enable_instrumentation
from agent_framework.openai import OpenAIChatClient
from opentelemetry._logs import set_logger_provider
from opentelemetry.metrics import set_meter_provider
@@ -21,7 +22,7 @@ from pydantic import Field
"""
This sample shows how to manually configure to send traces, logs, and metrics to the console,
without using the `setup_observability` helper function.
without using the `configure_otel_providers` helper function.
"""
resource = Resource.create({SERVICE_NAME: "ManualSetup"})
@@ -114,6 +115,7 @@ async def main():
setup_logging()
setup_tracing()
setup_metrics()
enable_instrumentation()
await run_chat_client()
@@ -19,12 +19,23 @@ This sample shows how you can configure observability of an application with zer
It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup
is done via environment variables.
This sample requires the `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable to be set.
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool.
Run the sample with the following command:
```
uv run --env-file=.env opentelemetry-instrument python advanced_zero_code.py
And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below).
Then you can run:
```bash
opentelemetry-enable_instrumentation \
--traces_exporter otlp \
--metrics_exporter otlp \
--service_name agent_framework \
--exporter_otlp_endpoint http://localhost:4317 \
python samples/getting_started/observability/advanced_zero_code.py
```
(or use uv run in front when you have did the install within your uv virtual environment)
You can also set the environment variables instead of passing them as CLI arguments.
"""
@@ -5,7 +5,7 @@ from random import randint
from typing import Annotated
from agent_framework import ChatAgent
from agent_framework.observability import get_tracer, setup_observability
from agent_framework.observability import configure_otel_providers, get_tracer
from agent_framework.openai import OpenAIChatClient
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
@@ -27,9 +27,10 @@ async def get_weather(
async def main():
# 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.
setup_observability()
# 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()
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
@@ -41,10 +42,11 @@ async def main():
tools=get_weather,
name="WeatherAgent",
instructions="You are a weather assistant.",
id="weather-agent",
)
thread = agent.get_new_thread()
for question in questions:
print(f"User: {question}")
print(f"\nUser: {question}")
print(f"{agent.display_name}: ", end="")
async for update in agent.run_stream(
question,
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import os
from random import randint
from typing import Annotated
import dotenv
from agent_framework import ChatAgent
from agent_framework.observability import create_resource, enable_instrumentation, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
from pydantic import Field
"""
This sample shows you can can setup telemetry in Microsoft Foundry for a custom agent.
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 ChatAgent creation: `weather-agent` (or change both).
The sample uses the Azure Monitor OpenTelemetry exporter to send traces to Application Insights.
So ensure you have the `azure-monitor-opentelemetry` package installed.
"""
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
dotenv.load_dotenv()
logger = logging.getLogger(__name__)
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():
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
):
# This will enable tracing and configure the application to send telemetry data to the
# Application Insights instance attached to the Azure AI project.
# This will override any existing configuration.
try:
conn_string = await project_client.telemetry.get_application_insights_connection_string()
except Exception:
logger.warning(
"No Application Insights connection string found for the Azure AI Project. "
"Please ensure Application Insights is configured in your Azure AI project, "
"or call configure_otel_providers() manually with custom exporters."
)
return
configure_azure_monitor(
connection_string=conn_string,
enable_live_metrics=True,
resource=create_resource(),
enable_performance_counters=False,
)
# This call is not necessary if you have the environment variable ENABLE_INSTRUMENTATION=true set
# If not or set to false, or if you want to enable or disable sensitive data collection, call this function.
enable_instrumentation(enable_sensitive_data=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 = ChatAgent(
chat_client=OpenAIResponsesClient(),
tools=get_weather,
name="WeatherAgent",
instructions="You are a weather assistant.",
id="weather-agent",
)
thread = agent.get_new_thread()
for question in questions:
print(f"\nUser: {question}")
print(f"{agent.display_name}: ", end="")
async for update in agent.run_stream(
question,
thread=thread,
):
if update.text:
print(update.text, end="")
if __name__ == "__main__":
asyncio.run(main())
@@ -7,11 +7,9 @@ from typing import Annotated
import dotenv
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from agent_framework.azure import AzureAIClient
from agent_framework.observability import get_tracer
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import AzureCliCredential
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
@@ -40,36 +38,16 @@ async def get_weather(
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def setup_azure_ai_observability(
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
) -> None:
"""Use this method to setup tracing in your Azure AI Project.
This will take the connection string from the AIProjectClient.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
"""
try:
conn_string = await project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
print("No Application Insights connection string found for the Azure AI Project.")
return
from agent_framework.observability import setup_observability
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
async def main():
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
AzureAIAgentClient(agents_client=agents_client) as client,
AzureAIClient(project_client=project_client) as client,
):
# This will enable tracing and configure the application to send telemetry data to the
# Application Insights instance attached to the Azure AI project.
# This will override any existing configuration.
await setup_azure_ai_observability(project_client)
await client.configure_azure_monitor(enable_live_metrics=True)
questions = ["What's the weather in Amsterdam?", "and in Paris, and which is better?", "Why is the sky blue?"]
@@ -81,10 +59,11 @@ async def main():
tools=get_weather,
name="WeatherAgent",
instructions="You are a weather assistant.",
id="edvan-weather-agent",
)
thread = agent.get_new_thread()
for question in questions:
print(f"User: {question}")
print(f"\nUser: {question}")
print(f"{agent.display_name}: ", end="")
async for update in agent.run_stream(
question,
@@ -1,114 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated
import dotenv
from agent_framework import HostedCodeInterpreterTool
from agent_framework.azure import AzureAIAgentClient
from agent_framework.observability import get_tracer
from azure.ai.agents.aio import AgentsClient
from azure.ai.projects.aio import AIProjectClient
from azure.core.exceptions import ResourceNotFoundError
from azure.identity.aio import AzureCliCredential
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
from pydantic import Field
"""
This sample, shows you can leverage the built-in telemetry in Azure AI.
It uses the Azure AI client to setup the telemetry, this calls out to
Azure AI for the connection string of the attached Application Insights
instance.
You must add an Application Insights instance to your Azure AI project
for this sample to work.
"""
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
dotenv.load_dotenv()
# ANSI color codes for printing in blue and resetting after each print
BLUE = "\x1b[34m"
RESET = "\x1b[0m"
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 setup_azure_ai_observability(
project_client: AIProjectClient, enable_sensitive_data: bool | None = None
) -> None:
"""Use this method to setup tracing in your Azure AI Project.
This will take the connection string from the AIProjectClient instance.
It will override any connection string that is set in the environment variables.
It will disable any OTLP endpoint that might have been set.
"""
try:
conn_string = await project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
print("No Application Insights connection string found for the Azure AI Project.")
return
from agent_framework.observability import setup_observability
setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data)
async def main() -> 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.
In azure_ai you will also see specific operations happening that are called by the Azure AI implementation,
such as `create_agent`.
"""
questions = [
"What's the weather in Amsterdam and in Paris?",
"Why is the sky blue?",
"Tell me about AI.",
"Can you write a python function that adds two numbers? and use it to add 8483 and 5692?",
]
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
AzureAIAgentClient(agents_client=agents_client) as client,
):
# This will enable tracing and configure the application to send telemetry data to the
# Application Insights instance attached to the Azure AI project.
# This will override any existing configuration.
await setup_azure_ai_observability(project_client)
with get_tracer().start_as_current_span(
name="Foundry Telemetry from Agent Framework", kind=SpanKind.CLIENT
) as current_span:
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
for question in questions:
print(f"{BLUE}User: {question}{RESET}")
print(f"{BLUE}Assistant: {RESET}", end="")
async for chunk in client.get_streaming_response(
question, tools=[get_weather, HostedCodeInterpreterTool()]
):
if str(chunk):
print(f"{BLUE}{str(chunk)}{RESET}", end="")
print(f"{BLUE}{RESET}")
print(f"{BLUE}Done{RESET}")
if __name__ == "__main__":
asyncio.run(main())
@@ -7,7 +7,7 @@ from random import randint
from typing import TYPE_CHECKING, Annotated, Literal
from agent_framework import ai_function
from agent_framework.observability import get_tracer, setup_observability
from agent_framework.observability import configure_otel_providers, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from opentelemetry import trace
from opentelemetry.trace.span import format_trace_id
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
"""
This sample, show how you can configure observability of an application via the
`setup_observability` function with environment variables.
`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.
@@ -100,7 +100,7 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "ai_functi
# 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.
setup_observability()
configure_otel_providers()
with get_tracer().start_as_current_span("Sample Scenario's", kind=trace.SpanKind.CLIENT) as current_span:
print(f"Trace ID: {format_trace_id(current_span.get_span_context().trace_id)}")
@@ -7,7 +7,7 @@ from random import randint
from typing import TYPE_CHECKING, Annotated, Literal
from agent_framework import ai_function, setup_logging
from agent_framework.observability import get_tracer, setup_observability
from agent_framework.observability import configure_otel_providers, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from opentelemetry import trace
from opentelemetry.trace.span import format_trace_id
@@ -17,14 +17,14 @@ if TYPE_CHECKING:
from agent_framework import ChatClientProtocol
"""
This sample, show how you can configure observability of an application via the
`setup_observability` function and inline parameters.
This sample shows how you can configure observability with custom exporters passed directly
to the `configure_otel_providers()` function.
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.
This approach gives you full control over exporter configuration (endpoints, headers, compression, etc.)
and allows you to add multiple exporters programmatically.
If no OTLP endpoint or Application Insights connection string is configured, the sample will
output traces, logs, and metrics to the console.
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.
"""
# Define the scenarios that can be run to show the telemetry data collected by the SDK
@@ -100,14 +100,35 @@ async def main(scenario: Literal["chat_client", "chat_client_stream", "ai_functi
# Setup the logging with the more complete format
setup_logging()
# This will enable tracing and create the necessary tracing, logging and metrics providers
# based on the provided parameters.
setup_observability(
# 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 OTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import 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,
# If you have set the `OTLP_ENDPOINT` environment variable and it'd different from the one below,
# both endpoints will be used to create the OTLP exporter.
# Same applies to the Application Insights connection string.
otlp_endpoint=["http://localhost:4317/"],
exporters=custom_exporters,
)
with get_tracer().start_as_current_span("Sample Scenario's", kind=trace.SpanKind.CLIENT) as current_span:
@@ -9,7 +9,7 @@ from agent_framework import (
WorkflowOutputEvent,
handler,
)
from agent_framework.observability import get_tracer, setup_observability
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
@@ -105,7 +105,7 @@ 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.
setup_observability()
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)}")
@@ -25,7 +25,7 @@ What this example shows:
- ExecutorCompletedEvent.data contains the messages sent via ctx.send_message()
- How to generically observe all executor I/O through workflow streaming events
This approach allows you to instrument any workflow for observability without
This approach allows you to enable_instrumentation any workflow for observability without
changing the executor implementations.
Prerequisites:
+3673 -3593
View File
File diff suppressed because it is too large Load Diff