mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
3c379718e9
commit
3139347526
@@ -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."""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user