Skip to content

Commit 3139347

Browse files
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
1 parent 3c37971 commit 3139347

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+5852
-4644
lines changed

python/.env.example

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ ANTHROPIC_MODEL=""
3333
OLLAMA_ENDPOINT=""
3434
OLLAMA_MODEL=""
3535
# Observability
36-
ENABLE_OTEL=true
36+
ENABLE_INSTRUMENTATION=true
3737
ENABLE_SENSITIVE_DATA=true
38-
OTLP_ENDPOINT="http://localhost:4317/"
39-
# APPLICATIONINSIGHTS_CONNECTION_STRING="..."
38+
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/"

python/packages/a2a/agent_framework_a2a/_agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
import uuid
77
from collections.abc import AsyncIterable, Sequence
8-
from typing import Any, cast
8+
from typing import Any, Final, cast
99

1010
import httpx
1111
from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card
@@ -38,6 +38,7 @@
3838
UriContent,
3939
prepend_agent_framework_to_user_agent,
4040
)
41+
from agent_framework.observability import use_agent_instrumentation
4142

4243
__all__ = ["A2AAgent"]
4344

@@ -58,6 +59,7 @@ def _get_uri_data(uri: str) -> str:
5859
return match.group("base64_data")
5960

6061

62+
@use_agent_instrumentation
6163
class A2AAgent(BaseAgent):
6264
"""Agent2Agent (A2A) protocol implementation.
6365
@@ -69,6 +71,8 @@ class A2AAgent(BaseAgent):
6971
Can be initialized with a URL, AgentCard, or existing A2A Client instance.
7072
"""
7173

74+
AGENT_PROVIDER_NAME: Final[str] = "A2A"
75+
7276
def __init__(
7377
self,
7478
*,

python/packages/ag-ui/agent_framework_ag_ui/_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from agent_framework._middleware import use_chat_middleware
2424
from agent_framework._tools import use_function_invocation
2525
from agent_framework._types import BaseContent, Contents
26-
from agent_framework.observability import use_observability
26+
from agent_framework.observability import use_instrumentation
2727

2828
from ._event_converters import AGUIEventConverter
2929
from ._http_service import AGUIHttpService
@@ -89,7 +89,7 @@ async def response_wrapper(self, *args: Any, **kwargs: Any) -> ChatResponse:
8989

9090
@_apply_server_function_call_unwrap
9191
@use_function_invocation
92-
@use_observability
92+
@use_instrumentation
9393
@use_chat_middleware
9494
class AGUIChatClient(BaseChatClient):
9595
"""Chat client for communicating with AG-UI compliant servers.

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
)
3636
from agent_framework._pydantic import AFBaseSettings
3737
from agent_framework.exceptions import ServiceInitializationError
38-
from agent_framework.observability import use_observability
38+
from agent_framework.observability import use_instrumentation
3939
from anthropic import AsyncAnthropic
4040
from anthropic.types.beta import (
4141
BetaContentBlock,
@@ -110,7 +110,7 @@ class AnthropicSettings(AFBaseSettings):
110110

111111

112112
@use_function_invocation
113-
@use_observability
113+
@use_instrumentation
114114
@use_chat_middleware
115115
class AnthropicClient(BaseChatClient):
116116
"""Anthropic Chat client."""

python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
use_function_invocation,
4444
)
4545
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
46-
from agent_framework.observability import use_observability
46+
from agent_framework.observability import use_instrumentation
4747
from azure.ai.agents.aio import AgentsClient
4848
from azure.ai.agents.models import (
4949
Agent,
@@ -107,7 +107,7 @@
107107

108108

109109
@use_function_invocation
110-
@use_observability
110+
@use_instrumentation
111111
@use_chat_middleware
112112
class AzureAIAgentClient(BaseChatClient):
113113
"""Azure AI Agent Chat client."""

python/packages/azure-ai/agent_framework_azure_ai/_client.py

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use_function_invocation,
1616
)
1717
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
18-
from agent_framework.observability import use_observability
18+
from agent_framework.observability import use_instrumentation
1919
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
2020
from azure.ai.projects.aio import AIProjectClient
2121
from azure.ai.projects.models import (
@@ -49,7 +49,7 @@
4949

5050

5151
@use_function_invocation
52-
@use_observability
52+
@use_instrumentation
5353
@use_chat_middleware
5454
class AzureAIClient(OpenAIBaseResponsesClient):
5555
"""Azure AI Agent client."""
@@ -164,27 +164,94 @@ def __init__(
164164
# Track whether we should close client connection
165165
self._should_close_client = should_close_client
166166

167-
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
168-
"""Use this method to setup tracing in your Azure AI Project.
167+
async def configure_azure_monitor(
168+
self,
169+
enable_sensitive_data: bool = False,
170+
**kwargs: Any,
171+
) -> None:
172+
"""Setup observability with Azure Monitor (Azure AI Foundry integration).
173+
174+
This method configures Azure Monitor for telemetry collection using the
175+
connection string from the Azure AI project client.
169176
170-
This will take the connection string from the project project_client.
171-
It will override any connection string that is set in the environment variables.
172-
It will disable any OTLP endpoint that might have been set.
177+
Args:
178+
enable_sensitive_data: Enable sensitive data logging (prompts, responses).
179+
Should only be enabled in development/test environments. Default is False.
180+
**kwargs: Additional arguments passed to configure_azure_monitor().
181+
Common options include:
182+
- enable_live_metrics (bool): Enable Azure Monitor Live Metrics
183+
- credential (TokenCredential): Azure credential for Entra ID auth
184+
- resource (Resource): Custom OpenTelemetry resource
185+
See https://learn.microsoft.com/python/api/azure-monitor-opentelemetry/azure.monitor.opentelemetry.configure_azure_monitor
186+
for full list of options.
187+
188+
Raises:
189+
ImportError: If azure-monitor-opentelemetry-exporter is not installed.
190+
191+
Examples:
192+
.. code-block:: python
193+
194+
from agent_framework.azure import AzureAIClient
195+
from azure.ai.projects.aio import AIProjectClient
196+
from azure.identity.aio import DefaultAzureCredential
197+
198+
async with (
199+
DefaultAzureCredential() as credential,
200+
AIProjectClient(
201+
endpoint="https://your-project.api.azureml.ms", credential=credential
202+
) as project_client,
203+
AzureAIClient(project_client=project_client) as client,
204+
):
205+
# Setup observability with defaults
206+
await client.configure_azure_monitor()
207+
208+
# With live metrics enabled
209+
await client.configure_azure_monitor(enable_live_metrics=True)
210+
211+
# With sensitive data logging (dev/test only)
212+
await client.configure_azure_monitor(enable_sensitive_data=True)
213+
214+
Note:
215+
This method retrieves the Application Insights connection string from the
216+
Azure AI project client automatically. You must have Application Insights
217+
configured in your Azure AI project for this to work.
173218
"""
219+
# Get connection string from project client
174220
try:
175221
conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
176222
except ResourceNotFoundError:
177223
logger.warning(
178-
"No Application Insights connection string found for the Azure AI Project, "
179-
"please call setup_observability() manually."
224+
"No Application Insights connection string found for the Azure AI Project. "
225+
"Please ensure Application Insights is configured in your Azure AI project, "
226+
"or call configure_otel_providers() manually with custom exporters."
180227
)
181228
return
182-
from agent_framework.observability import setup_observability
183229

184-
setup_observability(
185-
applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
230+
# Import Azure Monitor with proper error handling
231+
try:
232+
from azure.monitor.opentelemetry import configure_azure_monitor
233+
except ImportError as exc:
234+
raise ImportError(
235+
"azure-monitor-opentelemetry is required for Azure Monitor integration. "
236+
"Install it with: pip install azure-monitor-opentelemetry"
237+
) from exc
238+
239+
from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation
240+
241+
# Create resource if not provided in kwargs
242+
if "resource" not in kwargs:
243+
kwargs["resource"] = create_resource()
244+
245+
# Configure Azure Monitor with connection string and kwargs
246+
configure_azure_monitor(
247+
connection_string=conn_string,
248+
views=create_metric_views(),
249+
**kwargs,
186250
)
187251

252+
# Complete setup with core observability
253+
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
254+
188255
async def __aenter__(self) -> "Self":
189256
"""Async context manager entry."""
190257
return self

python/packages/core/agent_framework/_agents.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
ToolMode,
3535
)
3636
from .exceptions import AgentExecutionException, AgentInitializationError
37-
from .observability import use_agent_observability
37+
from .observability import use_agent_instrumentation
3838

3939
if sys.version_info >= (3, 12):
4040
from typing import override # type: ignore # pragma: no cover
@@ -516,8 +516,8 @@ def _prepare_context_providers(
516516

517517

518518
@use_agent_middleware
519-
@use_agent_observability
520-
class ChatAgent(BaseAgent):
519+
@use_agent_instrumentation(capture_usage=False) # type: ignore[arg-type,misc]
520+
class ChatAgent(BaseAgent): # type: ignore[misc]
521521
"""A Chat Client Agent.
522522
523523
This is the primary agent implementation that uses a chat client to interact
@@ -583,7 +583,7 @@ def get_weather(location: str) -> str:
583583
print(update.text, end="")
584584
"""
585585

586-
AGENT_SYSTEM_NAME: ClassVar[str] = "microsoft.agent_framework"
586+
AGENT_PROVIDER_NAME: ClassVar[str] = "microsoft.agent_framework"
587587

588588
def __init__(
589589
self,

python/packages/core/agent_framework/_clients.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from pydantic import BaseModel
99

1010
from ._logging import get_logger
11-
from ._mcp import MCPTool
1211
from ._memory import AggregateContextProvider, ContextProvider
1312
from ._middleware import (
1413
ChatMiddleware,
@@ -426,6 +425,8 @@ async def _normalize_tools(
426425
else [tools]
427426
)
428427
for tool in tools_list: # type: ignore[reportUnknownType]
428+
from ._mcp import MCPTool
429+
429430
if isinstance(tool, MCPTool):
430431
if not tool.is_connected:
431432
await tool.connect()

python/packages/core/agent_framework/_memory.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
from collections.abc import MutableSequence, Sequence
77
from contextlib import AsyncExitStack
88
from types import TracebackType
9-
from typing import Any, Final, cast
9+
from typing import TYPE_CHECKING, Any, Final, cast
1010

11-
from ._tools import ToolProtocol
1211
from ._types import ChatMessage
1312

13+
if TYPE_CHECKING:
14+
from ._tools import ToolProtocol
15+
1416
if sys.version_info >= (3, 12):
1517
from typing import override # type: ignore # pragma: no cover
1618
else:
@@ -54,7 +56,7 @@ def __init__(
5456
self,
5557
instructions: str | None = None,
5658
messages: Sequence[ChatMessage] | None = None,
57-
tools: Sequence[ToolProtocol] | None = None,
59+
tools: Sequence["ToolProtocol"] | None = None,
5860
):
5961
"""Create a new Context object.
6062
@@ -65,7 +67,7 @@ def __init__(
6567
"""
6668
self.instructions = instructions
6769
self.messages: Sequence[ChatMessage] = messages or []
68-
self.tools: Sequence[ToolProtocol] = tools or []
70+
self.tools: Sequence["ToolProtocol"] = tools or []
6971

7072

7173
# region ContextProvider
@@ -247,7 +249,7 @@ async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], *
247249
contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers])
248250
instructions: str = ""
249251
return_messages: list[ChatMessage] = []
250-
tools: list[ToolProtocol] = []
252+
tools: list["ToolProtocol"] = []
251253
for ctx in contexts:
252254
if ctx.instructions:
253255
instructions += ctx.instructions

python/packages/core/agent_framework/_serialization.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,17 @@ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True)
339339
continue
340340
# Handle dicts containing SerializationProtocol values
341341
if isinstance(value, dict):
342+
from datetime import date, datetime, time
343+
342344
serialized_dict: dict[str, Any] = {}
343345
for k, v in value.items():
344346
if isinstance(v, SerializationProtocol):
345347
serialized_dict[k] = v.to_dict(exclude=exclude, exclude_none=exclude_none)
346348
continue
349+
# Convert datetime objects to strings
350+
if isinstance(v, (datetime, date, time)):
351+
serialized_dict[k] = str(v)
352+
continue
347353
# Check if the value is JSON serializable
348354
if is_serializable(v):
349355
serialized_dict[k] = v

0 commit comments

Comments
 (0)