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
@@ -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"]