diff --git a/python/.env.example b/python/.env.example index bff78961aa..eab84910b1 100644 --- a/python/.env.example +++ b/python/.env.example @@ -44,7 +44,6 @@ GEMINI_MODEL="" # Ollama OLLAMA_ENDPOINT="" OLLAMA_MODEL="" -# Observability -ENABLE_INSTRUMENTATION=true +# Observability (instrumentation is enabled by default; set "ENABLE_INSTRUMENTATION" to "false" to opt out) ENABLE_SENSITIVE_DATA=true OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/" diff --git a/python/.github/skills/python-development/SKILL.md b/python/.github/skills/python-development/SKILL.md index d3bb38ca4b..4214c08bdb 100644 --- a/python/.github/skills/python-development/SKILL.md +++ b/python/.github/skills/python-development/SKILL.md @@ -72,7 +72,7 @@ def equal(arg1: str, arg2: str) -> bool: from agent_framework import Agent, Message, tool # Components -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry # Connectors (lazy-loaded) from agent_framework.openai import OpenAIChatClient diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 8c73414f3f..a9140ca353 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -186,7 +186,7 @@ The package follows a flat import structure: - **Components**: Import from `agent_framework.` ```python - from agent_framework.observability import enable_instrumentation, configure_otel_providers + from agent_framework.observability import enable_sensitive_telemetry, configure_otel_providers ``` - **Connectors**: Import from `agent_framework.` diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index a6f041ca64..bc175ccc48 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -157,9 +157,7 @@ def __init__( self.client = factory.create(agent_card, interceptors=interceptors) # type: ignore except Exception as transport_error: # Transport negotiation failed - fall back to minimal agent card with JSONRPC - fallback_url = ( - agent_card.supported_interfaces[0].url if agent_card.supported_interfaces else url - ) + fallback_url = agent_card.supported_interfaces[0].url if agent_card.supported_interfaces else url if not fallback_url: raise ValueError( "A2A transport negotiation failed and no fallback URL is available. " diff --git a/python/packages/chatkit/agent_framework_chatkit/_converter.py b/python/packages/chatkit/agent_framework_chatkit/_converter.py index e92583188c..2332d5ee3f 100644 --- a/python/packages/chatkit/agent_framework_chatkit/_converter.py +++ b/python/packages/chatkit/agent_framework_chatkit/_converter.py @@ -5,7 +5,6 @@ from __future__ import annotations import logging -import sys from collections.abc import Awaitable, Callable, Sequence from agent_framework import ( @@ -30,11 +29,6 @@ WorkflowItem, ) -if sys.version_info >= (3, 11): - from typing import assert_never # type:ignore # pragma: no cover -else: - from typing_extensions import assert_never # type:ignore # pragma: no cover - logger = logging.getLogger(__name__) @@ -528,7 +522,10 @@ async def _thread_item_to_input_item( # TODO(evmattso): Implement generated image handling in a future PR return [] case _: - assert_never(item) + # Unknown ThreadItem variant (e.g. types added in newer chatkit versions). + # Skip rather than fail so we remain forward-compatible with chatkit upgrades. + logger.debug("Skipping unsupported ThreadItem of type %s", type(item).__name__) + return [] async def to_agent_input( self, diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index c1d0c77e45..d67eadf571 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -651,9 +651,7 @@ def _validate_compatibility(compatibility: str | None) -> None: ValueError: If the value exceeds the maximum allowed length. """ if compatibility is not None and len(compatibility) > MAX_COMPATIBILITY_LENGTH: - raise ValueError( - f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer." - ) + raise ValueError(f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer.") def _build_skill_content( @@ -733,6 +731,7 @@ class InlineSkill(Skill): instructions="Use this skill for DB tasks.", ) + @skill.resource def get_schema() -> str: return "CREATE TABLE ..." @@ -2522,11 +2521,7 @@ def _validate_and_normalize_directory_names( # Reject absolute paths (check both POSIX and Windows-style roots # so validation is consistent regardless of the host OS) - if ( - os.path.isabs(directory) - or normalized.startswith("/") - or re.match(r"^[A-Za-z]:[/\\]", directory) - ): + if os.path.isabs(directory) or normalized.startswith("/") or re.match(r"^[A-Za-z]:[/\\]", directory): logger.warning( "Skipping directory '%s': absolute paths are not allowed.", directory, diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index d324caa757..022008c05b 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -4,6 +4,8 @@ Commonly used exports: - enable_instrumentation +- disable_instrumentation +- enable_sensitive_telemetry - configure_otel_providers - AgentTelemetryLayer - ChatTelemetryLayer @@ -80,7 +82,9 @@ "configure_otel_providers", "create_metric_views", "create_resource", + "disable_instrumentation", "enable_instrumentation", + "enable_sensitive_telemetry", "get_meter", "get_tracer", ] @@ -643,8 +647,8 @@ class ObservabilitySettings: Sensitive events should only be enabled on test and development environments. Keyword Args: - enable_instrumentation: Enable OpenTelemetry diagnostics. Default is False. - Can be set via environment variable ENABLE_INSTRUMENTATION. + enable_instrumentation: Enable OpenTelemetry diagnostics. Default is True. + Can be disabled by setting environment variable ENABLE_INSTRUMENTATION=false. enable_sensitive_data: Enable OpenTelemetry sensitive events. Default is False. Can be set via environment variable ENABLE_SENSITIVE_DATA. enable_console_exporters: Enable console exporters for traces, logs, and metrics. @@ -659,12 +663,12 @@ class ObservabilitySettings: from agent_framework import ObservabilitySettings # Using environment variables - # Set ENABLE_INSTRUMENTATION=true + # Instrumentation is enabled by default; set ENABLE_INSTRUMENTATION=false to disable. # Set ENABLE_CONSOLE_EXPORTERS=true settings = ObservabilitySettings() # Or passing parameters directly - settings = ObservabilitySettings(enable_instrumentation=True, enable_console_exporters=True) + settings = ObservabilitySettings(enable_console_exporters=True) """ def __init__(self, **kwargs: Any) -> None: @@ -677,14 +681,74 @@ def __init__(self, **kwargs: Any) -> None: env_file_encoding=env_file_encoding, **kwargs, ) - self.enable_instrumentation: bool = data.get("enable_instrumentation") or False - self.enable_sensitive_data: bool = data.get("enable_sensitive_data") or False + # Sticky-disable flag, set by `disable_instrumentation()`. When True, this + # singleton refuses to be re-enabled by any subsequent assignment to the + # `enable_instrumentation` / `enable_sensitive_data` properties (including + # direct third-party writes). It can only be cleared by an explicit + # `enable_instrumentation(force=True)` / `enable_sensitive_telemetry(force=True)` + # call, which is the user re-stating their intent. + self._user_disabled: bool = False + # `enable_instrumentation` is defaulted to True if not set + instrumentation_value = data.get("enable_instrumentation") + self._enable_instrumentation: bool = True if instrumentation_value is None else instrumentation_value + self._enable_sensitive_data: bool = data.get("enable_sensitive_data") or False + if self._enable_sensitive_data and not self._enable_instrumentation: + logger.warning( + "Sensitive data capture is enabled but instrumentation is disabled. " + "Sensitive data will not be captured. Please enable instrumentation to capture sensitive data." + ) + self.enable_console_exporters: bool = data.get("enable_console_exporters") or False self.vs_code_extension_port: int | None = data.get("vs_code_extension_port") self.env_file_path = env_file_path self.env_file_encoding = env_file_encoding self._executed_setup = False + @property + def enable_instrumentation(self) -> bool: + """Whether instrumentation is enabled. + + Always returns False once ``disable_instrumentation()`` has been called, + regardless of the stored value, until ``enable_instrumentation(force=True)`` + clears the sticky disable. + """ + if self._user_disabled: + return False + return self._enable_instrumentation + + @enable_instrumentation.setter + def enable_instrumentation(self, value: bool) -> None: + if self._user_disabled and value: + # Defense in depth: a third-party (or internal) write of True is + # silently dropped while the user-disabled flag is set, so the + # sticky disable cannot be circumvented by direct attribute writes. + logger.debug( + "Ignoring enable_instrumentation=True assignment: instrumentation was explicitly disabled via " + "disable_instrumentation(). Call enable_instrumentation(force=True) to clear the disable." + ) + return + self._enable_instrumentation = value + + @property + def enable_sensitive_data(self) -> bool: + """Whether sensitive-data capture is enabled. + + Always returns False once ``disable_instrumentation()`` has been called. + """ + if self._user_disabled: + return False + return self._enable_sensitive_data + + @enable_sensitive_data.setter + def enable_sensitive_data(self, value: bool) -> None: + if self._user_disabled and value: + logger.debug( + "Ignoring enable_sensitive_data=True assignment: instrumentation was explicitly disabled via " + "disable_instrumentation(). Call enable_sensitive_telemetry(force=True) to clear the disable." + ) + return + self._enable_sensitive_data = value + @property def ENABLED(self) -> bool: """Check if model diagnostics are enabled. @@ -706,6 +770,17 @@ def is_setup(self) -> bool: """Check if the setup has been executed.""" return self._executed_setup + @property + def is_user_disabled(self) -> bool: + """Whether ``disable_instrumentation()`` has been called and the disable is still in effect. + + Integrations that perform telemetry setup as a side-effect (e.g. provisioning Azure Monitor + providers from a Foundry project's connection string) should consult this flag before doing + their setup work, so the user's explicit opt-out is respected end-to-end and not just at the + framework's span-emission boundary. + """ + return self._user_disabled + def _configure( self, *, @@ -951,24 +1026,91 @@ def _read_int_env(name: str, *, default: int | None = None) -> int | None: return default +def enable_sensitive_telemetry(*, force: bool = False) -> None: + """Enable capture of sensitive data in telemetry for your application. + + Instrumentation is enabled by default; this method exists to opt-in to capturing + sensitive event payloads (e.g., chat messages, tool arguments). + + This method does not configure exporters or providers. It also ensures that + instrumentation is enabled (in case it was explicitly disabled via the + ENABLE_INSTRUMENTATION environment variable). + + Keyword Args: + force: When True, clears any sticky disable previously set by + ``disable_instrumentation()`` before enabling. Without it, calls are + no-ops if instrumentation has been explicitly disabled. + + Warning: + Sensitive events should only be enabled on test and development environments. + """ + global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled and not force: # type: ignore[reportPrivateUsage] + logger.info( + "enable_sensitive_telemetry() ignored: instrumentation was explicitly disabled via " + "disable_instrumentation(). Pass force=True to re-enable." + ) + return + if force: + OBSERVABILITY_SETTINGS._user_disabled = False # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS.enable_instrumentation = True + OBSERVABILITY_SETTINGS.enable_sensitive_data = True + + +def disable_instrumentation() -> None: + """Explicitly disable Agent Framework instrumentation for this process. + + The disable is **sticky**: subsequent attempts by framework auto-setup paths, + library integrations, ``enable_instrumentation()``, ``enable_sensitive_telemetry()``, + ``configure_otel_providers()``, or direct writes to + ``OBSERVABILITY_SETTINGS.enable_instrumentation`` are ignored and no spans, metrics, + or logs are emitted by Agent Framework code paths. + + To override the disable later, call ``enable_instrumentation(force=True)`` or + ``enable_sensitive_telemetry(force=True)``. This makes the user's intent to opt out + win against framework code that would otherwise re-enable instrumentation + automatically. + + Note: + Disabling does not tear down already-configured OpenTelemetry providers, + exporters, or in-flight spans; it gates future captures by Agent Framework + instrumentation only. To stop emitting telemetry from third-party + instrumentations as well, configure them separately. + """ + global OBSERVABILITY_SETTINGS + OBSERVABILITY_SETTINGS._user_disabled = True # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS._enable_instrumentation = False # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS._enable_sensitive_data = False # type: ignore[reportPrivateUsage] + + def enable_instrumentation( *, enable_sensitive_data: bool | None = None, + force: bool = False, ) -> None: - """Enable instrumentation for your application. - - Calling this method implies you want to enable observability in your application. + """Enable instrumentation for Microsoft Agent Framework. - This method does not configure exporters or providers. - It only updates the global variables that trigger the instrumentation code. - If you have already set the environment variable ENABLE_INSTRUMENTATION=true, - calling this method has no effect, unless you want to enable or disable sensitive data events. + Note that instrumentation is enabled by default, so this method is only necessary + if you need a programmatic way to enable it (e.g., if you are not sure whether the + environment variable ENABLE_INSTRUMENTATION is set to True or False and want to + ensure it is enabled). Keyword Args: enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides the environment variable ENABLE_SENSITIVE_DATA if set. Default is None. + force: When True, clears any sticky disable previously set by + ``disable_instrumentation()`` before enabling. Without it, calls are + no-ops if instrumentation has been explicitly disabled. """ global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled and not force: # type: ignore[reportPrivateUsage] + logger.info( + "enable_instrumentation() ignored: instrumentation was explicitly disabled via " + "disable_instrumentation(). Pass force=True to re-enable." + ) + return + if force: + OBSERVABILITY_SETTINGS._user_disabled = False # type: ignore[reportPrivateUsage] OBSERVABILITY_SETTINGS.enable_instrumentation = True if enable_sensitive_data is not None: OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data @@ -1008,7 +1150,7 @@ def configure_otel_providers( Since you can only setup one provider per signal type (logs, traces, metrics), you can choose to use this method and take the exporter and provider that we created. Alternatively, you can setup the providers yourself, or through another library - (e.g., Azure Monitor) and just call `enable_instrumentation()` to enable instrumentation. + (e.g., Azure Monitor) and just call `enable_sensitive_telemetry()` to opt-in to sensitive data capture. Note: By default, the Agent Framework emits metrics with the prefixes `agent_framework` @@ -1042,7 +1184,6 @@ def configure_otel_providers( from agent_framework.observability import configure_otel_providers # Using environment variables (recommended) - # Set ENABLE_INSTRUMENTATION=true # Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 configure_otel_providers() @@ -1087,18 +1228,25 @@ def configure_otel_providers( .. code-block:: python # when azure monitor is installed - from agent_framework.observability import enable_instrumentation + from agent_framework.observability import enable_sensitive_telemetry from azure.monitor.opentelemetry import configure_azure_monitor connection_string = "InstrumentationKey=your_instrumentation_key_here;..." configure_azure_monitor(connection_string=connection_string) - enable_instrumentation() + # Optional: opt into capturing sensitive data + enable_sensitive_telemetry() References: - https://opentelemetry.io/docs/languages/sdk-configuration/general/ - https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ """ global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled: # type: ignore[reportPrivateUsage] + logger.info( + "configure_otel_providers(): instrumentation was explicitly disabled via " + "disable_instrumentation(); providers and exporters will still be configured but " + "Agent Framework will emit no telemetry until enable_instrumentation(force=True) is called." + ) if env_file_path: # Build kwargs, excluding None values settings_kwargs: dict[str, Any] = { @@ -1280,7 +1428,7 @@ def get_response( if stream: span = _start_streaming_span(attributes, OtelAttr.REQUEST_MODEL) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1344,6 +1492,7 @@ async def _finalize_stream() -> None: OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and isinstance(response, ChatResponse) and response.messages + and span.is_recording() ): _capture_messages( span=span, @@ -1374,7 +1523,7 @@ async def _finalize_stream() -> None: async def _get_response() -> ChatResponse: with _get_span(attributes=attributes, span_name_attribute=OtelAttr.REQUEST_MODEL) as span: - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1408,7 +1557,7 @@ async def _get_response() -> ChatResponse: duration=duration, ) _mark_inner_response_telemetry_captured(response) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages and span.is_recording(): finish_reason = cast( "FinishReason | None", response.finish_reason if response.finish_reason in FINISH_REASON_MAP else None, @@ -1552,7 +1701,7 @@ def _trace_agent_invocation( if stream: span = _start_streaming_span(attributes, OtelAttr.AGENT_NAME) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1613,6 +1762,7 @@ async def _finalize_stream() -> None: OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and isinstance(response, AgentResponse) and response.messages + and span.is_recording() ): _capture_messages( span=span, @@ -1645,7 +1795,7 @@ async def _finalize_stream() -> None: async def _run() -> AgentResponse[Any]: try: with _get_span(attributes=attributes, span_name_attribute=OtelAttr.AGENT_NAME) as span: - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1669,7 +1819,7 @@ async def _run() -> AgentResponse[Any]: ) _apply_accumulated_usage(response_attributes, inner_response_telemetry_captured_fields) _capture_response(span=span, attributes=response_attributes, duration=duration) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 0fc5867d79..aea479ff86 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -4227,9 +4227,7 @@ async def connect(self): self.session.call_tool = AsyncMock( return_value=types.CallToolResult(content=[types.TextContent(type="text", text="result")]) ) - self.session.list_prompts = AsyncMock( - return_value=types.ListPromptsResult(prompts=[]) - ) + self.session.list_prompts = AsyncMock(return_value=types.ListPromptsResult(prompts=[])) def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: return None diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 71b59a351b..6185bccd44 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1015,11 +1015,25 @@ def test_observability_settings_is_setup_initial(monkeypatch): assert settings.is_setup is False -# region Test enable_instrumentation function +def test_enable_sensitive_telemetry_function(monkeypatch): + """Test enable_sensitive_telemetry function enables instrumentation.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True def test_enable_instrumentation_function(monkeypatch): - """Test enable_instrumentation function enables instrumentation.""" + """Test enable_instrumentation function enables instrumentation when disabled via env.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1032,10 +1046,12 @@ def test_enable_instrumentation_function(monkeypatch): observability.enable_instrumentation() assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + # Sensitive data should remain False when not explicitly enabled + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False def test_enable_instrumentation_with_sensitive_data(monkeypatch): - """Test enable_instrumentation function with sensitive_data parameter.""" + """Test enable_instrumentation function with explicit sensitive_data parameter.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1049,24 +1065,119 @@ def test_enable_instrumentation_with_sensitive_data(monkeypatch): assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True -def test_enable_instrumentation_reads_env_sensitive_data(monkeypatch): - """Test enable_instrumentation re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed.""" +def test_enable_instrumentation_explicit_param_overrides_env(monkeypatch): + """Test that explicit enable_sensitive_data parameter to enable_instrumentation overrides env var.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") observability = importlib.import_module("agent_framework.observability") importlib.reload(observability) + # Explicit False should override the env var True + observability.enable_instrumentation(enable_sensitive_data=False) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + +def test_enable_instrumentation_does_not_touch_console_exporters(monkeypatch): + """Test enable_instrumentation does not modify enable_console_exporters (it is an exporter concern).""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False + # Simulate load_dotenv() setting env var after import - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") observability.enable_instrumentation() - assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + # enable_console_exporters is not managed by enable_instrumentation; + # it is only read by configure_otel_providers. + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False + + +def test_enable_instrumentation_does_not_clobber_console_exporters(monkeypatch): + """Test enable_instrumentation does not reset enable_console_exporters set by prior configure call.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + 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) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Set console exporters via configure_otel_providers + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_console_exporters=True) + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + # Calling enable_instrumentation should not clobber the value + observability.enable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + +def test_enable_instrumentation_with_sensitive_data_does_not_touch_console_exporters(monkeypatch): + """Test enable_console_exporters is untouched even when enable_sensitive_data is explicitly passed.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + 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) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Set console exporters via configure_otel_providers + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_console_exporters=True) + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + # Calling enable_instrumentation with explicit sensitive_data should not clobber console exporters + observability.enable_instrumentation(enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + +def test_enable_instrumentation_preserves_console_exporters_after_env_removed(monkeypatch): + """Test enable_instrumentation preserves enable_console_exporters when env var is removed after reload.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + # Remove the env var after reload + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + + # enable_instrumentation should not reset the value + observability.enable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True def test_configure_otel_providers_reads_env_sensitive_data(monkeypatch): @@ -1154,24 +1265,8 @@ def test_configure_otel_providers_explicit_param_overrides_env(monkeypatch): assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False -def test_enable_instrumentation_explicit_param_overrides_env(monkeypatch): - """Test that explicit enable_sensitive_data parameter to enable_instrumentation overrides env var.""" - import importlib - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - # Explicit False should override the env var True - observability.enable_instrumentation(enable_sensitive_data=False) - assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False - - -def test_enable_instrumentation_does_not_touch_console_exporters(monkeypatch): - """Test enable_instrumentation does not modify enable_console_exporters (it is an exporter concern).""" +def test_enable_sensitive_telemetry_does_not_touch_console_exporters(monkeypatch): + """Test enable_sensitive_telemetry does not modify enable_console_exporters (it is an exporter concern).""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1185,14 +1280,14 @@ def test_enable_instrumentation_does_not_touch_console_exporters(monkeypatch): # Simulate load_dotenv() setting env var after import monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") - observability.enable_instrumentation() - # enable_console_exporters is not managed by enable_instrumentation; + observability.enable_sensitive_telemetry() + # enable_console_exporters is not managed by enable_sensitive_telemetry; # it is only read by configure_otel_providers. assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False -def test_enable_instrumentation_does_not_clobber_console_exporters(monkeypatch): - """Test enable_instrumentation does not reset enable_console_exporters set by prior configure call.""" +def test_enable_sensitive_telemetry_does_not_clobber_console_exporters(monkeypatch): + """Test enable_sensitive_telemetry does not reset enable_console_exporters set by prior configure call.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1215,42 +1310,13 @@ def test_enable_instrumentation_does_not_clobber_console_exporters(monkeypatch): observability.configure_otel_providers(enable_console_exporters=True) assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True - # Calling enable_instrumentation should not clobber the value - observability.enable_instrumentation() + # Calling enable_sensitive_telemetry should not clobber the value + observability.enable_sensitive_telemetry() assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True -def test_enable_instrumentation_with_sensitive_data_does_not_touch_console_exporters(monkeypatch): - """Test enable_console_exporters is untouched even when enable_sensitive_data is explicitly passed.""" - import importlib - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) - monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) - monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) - 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) - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - # Set console exporters via configure_otel_providers - with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): - observability.configure_otel_providers(enable_console_exporters=True) - assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True - - # Calling enable_instrumentation with explicit sensitive_data should not clobber console exporters - observability.enable_instrumentation(enable_sensitive_data=True) - assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True - - -def test_enable_instrumentation_preserves_console_exporters_after_env_removed(monkeypatch): - """Test enable_instrumentation preserves enable_console_exporters when env var is removed after reload.""" +def test_enable_sensitive_telemetry_preserves_console_exporters_after_env_removed(monkeypatch): + """Test enable_sensitive_telemetry preserves enable_console_exporters when env var is removed after reload.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1264,8 +1330,8 @@ def test_enable_instrumentation_preserves_console_exporters_after_env_removed(mo # Remove the env var after reload monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) - # enable_instrumentation should not reset the value - observability.enable_instrumentation() + # enable_sensitive_telemetry should not reset the value + observability.enable_sensitive_telemetry() assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True @@ -1321,6 +1387,189 @@ def test_configure_otel_providers_explicit_console_exporters_overrides_env(monke assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False +# region Test default-on instrumentation + + +def test_observability_settings_defaults_instrumentation_true(monkeypatch): + """ENABLE_INSTRUMENTATION unset → ObservabilitySettings defaults to True.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + settings = ObservabilitySettings() + assert settings.enable_instrumentation is True + + +def test_enable_instrumentation_reads_env_sensitive_data(monkeypatch): + """No-arg enable_instrumentation() re-reads ENABLE_SENSITIVE_DATA from env at call time. + + Covers the fallback branch where the env var is set AFTER import (e.g. via load_dotenv()). + """ + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Simulate load_dotenv() setting the env var after import + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + observability.enable_instrumentation() + + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +# region Test disable_instrumentation sticky behavior + + +def test_disable_instrumentation_flips_settings_off(monkeypatch): + """disable_instrumentation() immediately turns instrumentation and sensitive data off.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + assert observability.OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED is True + + observability.disable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + assert observability.OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED is False + assert observability.OBSERVABILITY_SETTINGS.ENABLED is False + + +def test_disable_instrumentation_is_sticky_against_enable_instrumentation(monkeypatch): + """Sticky disable: enable_instrumentation() without force is a no-op after disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_is_sticky_against_enable_sensitive_telemetry(monkeypatch): + """Sticky disable: enable_sensitive_telemetry() without force is a no-op after disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_is_sticky_against_configure_otel_providers(monkeypatch): + """Sticky disable: configure_otel_providers() does not flip instrumentation back on.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_intercepts_direct_attribute_writes(monkeypatch): + """Sticky disable: direct OBSERVABILITY_SETTINGS.enable_instrumentation = True is intercepted.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.OBSERVABILITY_SETTINGS.enable_instrumentation = True + observability.OBSERVABILITY_SETTINGS.enable_sensitive_data = True + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_enable_instrumentation_force_clears_disable(monkeypatch): + """enable_instrumentation(force=True) clears the sticky disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(force=True, enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +def test_enable_sensitive_telemetry_force_clears_disable(monkeypatch): + """enable_sensitive_telemetry(force=True) clears the sticky disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_sensitive_telemetry(force=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +def test_disable_instrumentation_persists_after_force_until_redisabled(monkeypatch): + """After force-enable then disable again, the sticky disable is re-armed.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(force=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + + observability.disable_instrumentation() + observability.enable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + + +def test_disable_instrumentation_in_all(monkeypatch): + """disable_instrumentation must be re-exported from the module's __all__.""" + import agent_framework.observability as observability + + assert "disable_instrumentation" in observability.__all__ + assert callable(observability.disable_instrumentation) + + # region Test _to_otel_part content types @@ -3797,3 +4046,135 @@ class FailingExecuteAgent(AgentTelemetryLayer, _FailingExecuteAgent): agent_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION] assert len(agent_spans) == 1 assert agent_spans[0].status.status_code == StatusCode.ERROR + + +# region Test heavy operations skipped when span is not recording +# +# When ``ENABLE_INSTRUMENTATION`` is on (the default) but no OpenTelemetry +# tracer provider has been configured, the global provider is the +# ``ProxyTracerProvider`` which returns non-recording spans. The telemetry +# layers gate sensitive-data serialization (``_capture_messages``) on +# ``span.is_recording()`` so that we don't pay the JSON-serialization cost +# when the span is going to be dropped anyway. The tests below verify that +# behavior by patching ``get_tracer`` to return a ``NoOpTracer``. + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_capture_messages_skipped_when_span_not_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Heavy message serialization is skipped when no provider is configured (non-streaming).""" + from opentelemetry.trace import NoOpTracer + + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await client.get_response(messages=messages, options={"model": "Test"}) + + assert response is not None + # Sensitive-data serialization must be skipped because span.is_recording() is False. + assert mock_capture_messages.call_count == 0 + # _capture_response still runs so that metric histograms continue to record. + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_streaming_capture_messages_skipped_when_span_not_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Heavy message serialization is skipped when no provider is configured (streaming).""" + from opentelemetry.trace import NoOpTracer + + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + updates: list[ChatResponseUpdate] = [] + stream = client.get_response(messages=messages, stream=True, options={"model": "Test"}) + async for update in stream: + updates.append(update) + await stream.get_final_response() + + assert len(updates) == 2 + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_agent_capture_messages_skipped_when_span_not_recording( + mock_chat_agent, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Agent heavy serialization is skipped when no provider is configured (non-streaming).""" + from opentelemetry.trace import NoOpTracer + + agent = mock_chat_agent() + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await agent.run("Test message") + + assert response is not None + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_agent_streaming_capture_messages_skipped_when_span_not_recording( + mock_chat_agent, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Agent heavy serialization is skipped when no provider is configured (streaming).""" + from opentelemetry.trace import NoOpTracer + + agent = mock_chat_agent() + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + updates: list[Any] = [] + stream = agent.run("Test message", stream=True) + async for update in stream: + updates.append(update) + await stream.get_final_response() + + assert len(updates) == 2 + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_capture_messages_called_when_span_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Sanity check: with a real recording provider, sensitive-data capture still runs.""" + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await client.get_response(messages=messages, options={"model": "Test"}) + + assert response is not None + # Two _capture_messages calls: one for input, one for output messages. + assert mock_capture_messages.call_count == 2 + assert mock_capture_response.call_count == 1 diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index c386da2ff3..c60f5d407f 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -319,9 +319,7 @@ def test_duplicate_directories_deduplicated(self, tmp_path: Path) -> None: refs = skill_dir / "references" refs.mkdir(parents=True) (refs / "doc.md").write_text("content", encoding="utf-8") - resources = FileSkillsSource._discover_resource_files( - str(skill_dir), directories=("references", "references") - ) + resources = FileSkillsSource._discover_resource_files(str(skill_dir), directories=("references", "references")) assert resources == ["references/doc.md"] def test_results_are_sorted(self, tmp_path: Path) -> None: @@ -1675,9 +1673,7 @@ def test_whitespace_only_raises(self) -> None: FileSkillsSource._validate_and_normalize_directory_names([" "]) def test_multiple_directories(self) -> None: - result = FileSkillsSource._validate_and_normalize_directory_names( - [".", "references", "assets", "scripts"] - ) + result = FileSkillsSource._validate_and_normalize_directory_names([".", "references", "assets", "scripts"]) assert result == [".", "references", "assets", "scripts"] def test_default_resource_directories(self) -> None: @@ -5498,9 +5494,7 @@ def my_runner(skill: Any, script: Any, args: Any = None) -> str: return "ok" assert isinstance(my_runner, SkillScriptRunner) - skill = FileSkill( - frontmatter=SkillFrontmatter(name="s", description="d"), content="c", path=f"{_ABS}/test" - ) + skill = FileSkill(frontmatter=SkillFrontmatter(name="s", description="d"), content="c", path=f"{_ABS}/test") script = FileSkillScript(name="run.py", full_path=f"{_ABS}/test/run.py") result = my_runner(skill, script, args=["--flag", "value"]) assert result == "ok" diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 8b737694e3..056d3977af 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -793,8 +793,22 @@ async def configure_azure_monitor( Raises: ImportError: If azure-monitor-opentelemetry-exporter is not installed. """ + from agent_framework.observability import ( + OBSERVABILITY_SETTINGS, + create_metric_views, + create_resource, + enable_instrumentation, + ) from azure.core.exceptions import ResourceNotFoundError + if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "FoundryAgent.configure_azure_monitor(): Skipping setup because instrumentation was " + "explicitly disabled via disable_instrumentation(). Call enable_instrumentation(force=True) " + "to re-enable, then re-invoke configure_azure_monitor()." + ) + return + client = self.client if not isinstance(client, RawFoundryAgentChatClient): raise TypeError("configure_azure_monitor requires a RawFoundryAgentChatClient-based client.") @@ -817,8 +831,6 @@ async def configure_azure_monitor( "Install it with: pip install azure-monitor-opentelemetry" ) from exc - from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation - if "resource" not in kwargs: kwargs["resource"] = create_resource() diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 614efcad15..7f8e033036 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -271,8 +271,22 @@ async def configure_azure_monitor( Raises: ImportError: If azure-monitor-opentelemetry-exporter is not installed. """ + from agent_framework.observability import ( + OBSERVABILITY_SETTINGS, + create_metric_views, + create_resource, + enable_instrumentation, + ) from azure.core.exceptions import ResourceNotFoundError + if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "FoundryChatClient.configure_azure_monitor(): Skipping setup because instrumentation was " + "explicitly disabled via disable_instrumentation(). Call enable_instrumentation(force=True) " + "to re-enable, then re-invoke configure_azure_monitor()." + ) + return + try: conn_string = await self.project_client.telemetry.get_application_insights_connection_string() except ResourceNotFoundError: @@ -291,8 +305,6 @@ async def configure_azure_monitor( "Install it with: pip install azure-monitor-opentelemetry" ) from exc - from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation - if "resource" not in kwargs: kwargs["resource"] = create_resource() diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 40d9063b12..0cc2bfbe8b 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1429,9 +1429,10 @@ def _prepare_message_for_openai( props = content.additional_properties or {} # Local-shell variant serializes as `local_shell_call` carrying a server-issued id; # plain function_call_output pairs by call_id and is safe under storage. - if ( - props.get(OPENAI_SHELL_OUTPUT_TYPE_KEY) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL - and props.get(OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY) + if props.get( + OPENAI_SHELL_OUTPUT_TYPE_KEY + ) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL and props.get( + OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY ): continue new_args: dict[str, Any] = {} diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 325986a730..dfb5bdc9cd 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -4120,9 +4120,7 @@ async def test_prepare_options_with_conversation_id_strips_server_items_for_mixe types = [item.get("type") for item in options["input"]] assert "reasoning" not in types assert "function_call" not in types - output_call_ids = { - item["call_id"] for item in options["input"] if item.get("type") == "function_call_output" - } + output_call_ids = {item["call_id"] for item in options["input"] if item.get("type") == "function_call_output"} assert output_call_ids == {"call_history", "call_live"} assert options["previous_response_id"] == "resp_prev123" diff --git a/python/samples/02-agents/observability/.env.example b/python/samples/02-agents/observability/.env.example index f3dd329bac..c43f8f239c 100644 --- a/python/samples/02-agents/observability/.env.example +++ b/python/samples/02-agents/observability/.env.example @@ -27,6 +27,9 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" # Agent Framework specific settings # ================================== +# Observability is enabled by default. Set to "false" to opt out. +# ENABLE_INSTRUMENTATION=false + # Enable sensitive data logging (prompts, responses, etc.) # WARNING: Only enable in dev/test environments ENABLE_SENSITIVE_DATA=true @@ -34,9 +37,6 @@ ENABLE_SENSITIVE_DATA=true # Optional: Enable console exporters for debugging # ENABLE_CONSOLE_EXPORTERS=true -# Optional: Enable observability (automatically enabled if env vars are set or configure_otel_providers() is called) -# ENABLE_INSTRUMENTATION=true - # OpenAI specific variables # ========================== OPENAI_API_KEY="..." diff --git a/python/samples/02-agents/observability/README.md b/python/samples/02-agents/observability/README.md index b2fbf7400f..d6a251eef5 100644 --- a/python/samples/02-agents/observability/README.md +++ b/python/samples/02-agents/observability/README.md @@ -1,12 +1,12 @@ # Agent Framework Observability -This sample folder shows how a Python application can be configured to send Agent Framework observability data to the Application Performance Management (APM) vendor(s) of your choice based on the OpenTelemetry standard. +These samples show how to send Agent Framework observability data to the Application Performance Management (APM) backend of your choice, based on the OpenTelemetry standard. -In this sample, we provide options to send telemetry to [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview), [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash) and the console. +The samples target [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview), the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash), and the console, but any OTLP-compatible backend works. -> **Quick Start**: For local development without Azure setup, you can use the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) which runs locally via Docker and provides an excellent telemetry viewing experience for OpenTelemetry data. Or you can use the built-in tracing module of the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio). +> **Quick Start**: For local development without Azure setup, use the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) (runs locally via Docker), or the built-in tracing module of the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio). -> Note that it is also possible to use other Application Performance Management (APM) vendors. An example is [Prometheus](https://prometheus.io/docs/introduction/overview/). Please refer to this [page](https://opentelemetry.io/docs/languages/python/exporters/) to learn more about exporters. +> Other backends such as [Prometheus](https://prometheus.io/docs/introduction/overview/) are also supported. See the [OpenTelemetry Python exporters](https://opentelemetry.io/docs/languages/python/exporters/) page for the full list. For more information, please refer to the following resources: @@ -18,19 +18,15 @@ For more information, please refer to the following resources: ## What to expect -The Agent Framework Python SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of agent/model invocation and tool execution. This allows you to effectively monitor your AI application's performance and accurately track token consumption. It does so based on the Semantic Conventions for GenAI defined by OpenTelemetry, and the workflows emit their own spans to provide end-to-end visibility. +The Agent Framework Python SDK is **natively instrumented** to emit logs, traces, and metrics throughout agent/model invocation and tool execution, so you can monitor your AI application's performance and track token consumption. Instrumentation follows the OpenTelemetry [Semantic Conventions for GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/), and workflows emit their own spans for end-to-end visibility. -Next to what happens in the code when you run, we also make setting up observability as easy as possible. By calling a single function `configure_otel_providers()` from the `agent_framework.observability` module, you can enable telemetry for traces, logs, and metrics. The function automatically reads standard OpenTelemetry environment variables to configure exporters and providers, making it simple to get started. - -### MCP trace propagation - -Whenever there is an active OpenTelemetry span context, Agent Framework automatically propagates trace context to MCP servers via the `params._meta` field of `tools/call` requests. It uses the globally-configured OpenTelemetry propagator(s) (W3C Trace Context by default, producing `traceparent` and `tracestate`), so custom propagators (B3, Jaeger, etc.) are also supported. This enables distributed tracing across agent-to-MCP-server boundaries, compliant with the [MCP `_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta). - -**Scope:** automatic `_meta` injection applies only to MCP sessions that the agent process itself opens — `MCPStreamableHTTPTool`, `MCPStdioTool`, and `MCPWebsocketTool` (or any other client-opened `MCPTool` subclass). It does **not** apply to hosted/provider-managed MCP tool configurations such as `FoundryChatClient.get_mcp_tool(...)`, `OpenAIChatClient.get_mcp_tool(...)`, `AnthropicClient.get_mcp_tool(...)`, `GeminiChatClient.get_mcp_tool(...)`, or toolbox-fetched tools (for example, `toolbox = await client.get_toolbox(...)`, then passing `toolbox.tools` into `Agent(tools=...)`), because in those cases the `tools/call` message is issued by the provider service runtime rather than by the agent process. As a result, the framework has no opportunity to inject trace context into those requests, and propagating `traceparent`/`tracestate` across that hosted-service boundary is the responsibility of the service runtime, not Agent Framework. If end-to-end distributed tracing to the downstream MCP server is required, use a client-opened MCP transport instead of a hosted connector. +Setting up observability is also easy: a single call to `configure_otel_providers()` from the `agent_framework.observability` module wires up the trace, log, and metric providers. It reads the standard OpenTelemetry environment variables to configure exporters automatically. ### Five patterns for configuring observability -We've identified multiple ways to configure observability in your application, depending on your needs: +> Setting up observability has two parts: (1) **instrumentation**, the code that generates telemetry, and (2) **exporter/provider configuration**, which decides where that telemetry is sent. Agent Framework is natively instrumented and **enabled by default**, so you only need to handle the second part. + +There are five common ways to do that, depending on your needs: **1. Standard otel environment variables, configured for you** @@ -42,22 +38,29 @@ from agent_framework.observability import configure_otel_providers # Reads OTEL_EXPORTER_OTLP_* environment variables automatically configure_otel_providers() ``` + Or if you just want console exporters: + ```python from agent_framework.observability import configure_otel_providers -# Enable console exporters via environment variable configure_otel_providers(enable_console_exporters=True) +# It is also possible to set ENABLE_CONSOLE_EXPORTERS=true in environment +# variables instead of calling `configure_otel_providers()` with the parameter. +# The framework will automatically read that and set up console exporters. ``` + This is the **recommended approach** for getting started. **2. Custom Exporters** -One level more control over the exporters that are created is to do that yourself, and then pass them to `configure_otel_providers()`. We will still create the providers for you, but you can customize the exporters as needed: + +For more control, construct exporters yourself and pass them to `configure_otel_providers()`. The framework still creates the providers for you: ```python from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.exporter import Compression from agent_framework.observability import configure_otel_providers # Create custom exporters with specific configuration @@ -67,17 +70,17 @@ exporters = [ OTLPMetricExporter(endpoint="http://localhost:4317"), ] -# These will be added alongside any exporters from environment variables -configure_otel_providers(exporters=exporters, enable_sensitive_data=True) +# These are added alongside any exporters configured from environment variables +configure_otel_providers(exporters=exporters) ``` -**3. Third party setup** +**3. Third-party setup** -A lot of third party specific otel package, have their own easy setup methods, for example Azure Monitor has `configure_azure_monitor()`. You can use those methods to setup the third party first, and then call `enable_instrumentation()` from the `agent_framework.observability` module to activate the Agent Framework telemetry code paths. In all these cases, if you already setup observability via environment variables, you don't need to call `enable_instrumentation()` as it will be enabled automatically. +Many third-party OTel packages ship their own setup helpers (for example, Azure Monitor's `configure_azure_monitor()`). You can use those directly — Agent Framework instrumentation is on by default, so no extra wiring is needed. To also capture sensitive data, call `enable_sensitive_telemetry()` from `agent_framework.observability`. ```python from azure.monitor.opentelemetry import configure_azure_monitor -from agent_framework.observability import create_resource, enable_instrumentation +from agent_framework.observability import create_resource, enable_sensitive_telemetry # Configure Azure Monitor first configure_azure_monitor( @@ -86,10 +89,10 @@ configure_azure_monitor( enable_live_metrics=True, ) -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` + For Microsoft Foundry projects, use `client.configure_azure_monitor()` which retrieves the connection string from the project and configures everything: ```python @@ -110,7 +113,7 @@ Or with [Langfuse](https://langfuse.com/integrations/frameworks/microsoft-agent- ```python # environment should be setup correctly, with langfuse urls and keys -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry from langfuse import get_client langfuse = get_client() @@ -121,9 +124,9 @@ if langfuse.auth_check(): else: print("Authentication failed. Please check your credentials and host.") -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Agent Framework instrumentation is on by default. +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` Or with [Comet Opik](https://www.comet.com/docs/opik/integrations/microsoft-agent-framework): @@ -131,53 +134,152 @@ Or with [Comet Opik](https://www.comet.com/docs/opik/integrations/microsoft-agen ```python import os -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry # Use Opik OTLP settings from your project settings os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "" os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "" -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Agent Framework instrumentation is on by default. +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` **4. Manual setup** -Of course you can also do a complete manual setup of exporters, providers, and instrumentation. Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. This gives you full control over which exporters and providers to use. We do have a helper function `create_resource()` in the `agent_framework.observability` module that you can use to create a resource with the appropriate service name and version based on environment variables or standard defaults for Agent Framework, this is not used in the sample. -**5. Auto-instrumentation (zero-code)** -You can also use the [OpenTelemetry CLI tool](https://opentelemetry.io/docs/instrumentation/python/getting-started/#automatic-instrumentation) to automatically instrument your application without changing any code. Please refer to sample [advanced_zero_code.py](./advanced_zero_code.py) for an example of how to use the CLI tool to enable instrumentation for Agent Framework applications. +For full control, set up providers and exporters yourself. See [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a complete example that sends traces, logs, and metrics to the console. The `create_resource()` helper in `agent_framework.observability` can build a resource with the appropriate service name and version from environment variables (or sensible defaults), although the sample does not use it. + +**5. Zero-code provider/exporter configuration** + +Because Agent Framework is **natively instrumented** with OpenTelemetry, you do not need to auto-instrument the framework itself. You can, however, use the [`opentelemetry-instrument`](https://opentelemetry.io/docs/zero-code/python/) CLI wrapper to configure the global tracer/meter providers and exporters from environment variables (or CLI flags) at process startup. Your application code then does not need to call `configure_otel_providers()` — the native spans and metrics from Agent Framework are picked up by the globally configured pipeline. See [advanced_zero_code.py](./advanced_zero_code.py) for an example. + +### MCP trace propagation + +Whenever there is an active OpenTelemetry span context, Agent Framework automatically propagates trace context to MCP servers via the `params._meta` field of `tools/call` requests. It uses the globally configured OpenTelemetry propagator(s) — W3C Trace Context by default (producing `traceparent` and `tracestate`) — so custom propagators (B3, Jaeger, etc.) are also supported. This enables distributed tracing across agent-to-MCP-server boundaries, compliant with the [MCP `_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta). + +**Scope:** automatic `_meta` injection applies only to MCP sessions that the agent process itself opens — `MCPStreamableHTTPTool`, `MCPStdioTool`, and `MCPWebsocketTool` (or any other client-opened `MCPTool` subclass). It does **not** apply to hosted or provider-managed MCP tool configurations such as `FoundryChatClient.get_mcp_tool(...)`, `OpenAIChatClient.get_mcp_tool(...)`, `AnthropicClient.get_mcp_tool(...)`, `GeminiChatClient.get_mcp_tool(...)`, or toolbox-fetched tools (e.g. `toolbox = await client.get_toolbox(...)` then `Agent(tools=toolbox.tools)`). In those cases the `tools/call` message is issued by the provider service runtime rather than by the agent process, so propagating `traceparent`/`tracestate` across that boundary is the service runtime's responsibility. If you need end-to-end distributed tracing to the downstream MCP server, use a client-opened MCP transport instead of a hosted connector. ## Configuration ### Dependencies -As part of Agent Framework we use the following OpenTelemetry packages: -- `opentelemetry-api` -- `opentelemetry-sdk` -- `opentelemetry-semantic-conventions-ai` +Agent Framework's core depends on **`opentelemetry-api`** only — the API package is enough for the instrumentation hooks (spans, meters, log records) to emit telemetry, and it has no runtime side effects when no provider is configured. -We do not install exporters by default, so you will need to add those yourself, this prevents us from installing unnecessary dependencies. For Application Insights, you will need to install `azure-monitor-opentelemetry`. For Aspire Dashboard or other OTLP compatible backends, you will need to install `opentelemetry-exporter-otlp-proto-grpc`. For HTTP protocol support, you will also need to install `opentelemetry-exporter-otlp-proto-http`. +If you want the framework to set up providers / exporters for you via `configure_otel_providers()` (or to use the `create_resource()` / `create_metric_views()` helpers), you also need the OpenTelemetry SDK: -And for many others, different packages are used, so refer to the documentation of the specific exporter you want to use. +```bash +pip install opentelemetry-sdk +``` + +If `opentelemetry-sdk` is missing, those helper functions raise a clear `ImportError` telling you to install it. Day-to-day instrumentation still works without the SDK as long as some other component (e.g. `azure-monitor-opentelemetry`, your application bootstrap, an APM agent) has configured the global OpenTelemetry providers. + +Exporters are **not** installed by default — install only what you need: +- **Application Insights**: `azure-monitor-opentelemetry` +- **Aspire Dashboard or other OTLP/gRPC backends**: `opentelemetry-exporter-otlp-proto-grpc` +- **OTLP over HTTP**: `opentelemetry-exporter-otlp-proto-http` + +For other backends, refer to the documentation of the specific exporter. ### Environment variables -The following environment variables are used to turn on/off observability of the Agent Framework: +Agent Framework reads the following environment variables: + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ENABLE_INSTRUMENTATION` | `true` | Set to `false` to disable native instrumentation. See [Disabling instrumentation](#disabling-instrumentation) for the programmatic alternative with sticky semantics. | +| `ENABLE_SENSITIVE_DATA` | `false` | Set to `true` to emit sensitive data (prompts, responses, etc.). | +| `ENABLE_CONSOLE_EXPORTERS` | `false` | Set to `true` to add console exporters. Only used by `configure_otel_providers()`. | +| `VS_CODE_EXTENSION_PORT` | unset | Port used by the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) tracing integration. Only used by `configure_otel_providers()`. | -- `ENABLE_INSTRUMENTATION` -- `ENABLE_SENSITIVE_DATA` -- `ENABLE_CONSOLE_EXPORTERS` +You can also call `enable_sensitive_telemetry()` from `agent_framework.observability` to opt in to sensitive-data capture programmatically. -All of these are booleans and default to `false`. +> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production. -Finally we have `VS_CODE_EXTENSION_PORT` which you can set to a port, which can be used to setup the AI Toolkit for VS Code tracing integration. See [here](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) for more details. +### Disabling instrumentation -The framework will emit observability data when the `ENABLE_INSTRUMENTATION` environment variable is set to `true`. If both are `true` then it will also emit sensitive information. When these are not set, or set to false, you can use the `enable_instrumentation()` function from the `agent_framework.observability` module to turn on instrumentation programmatically. This is useful when you want to control this via code instead of environment variables. +There are two ways to turn Agent Framework's native instrumentation off, and they have **different scopes**: -> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data. +| Approach | Scope | Sticky? | When framework code calls `enable_instrumentation()` later, what happens? | +|----------|-------|---------|---------------------------------------------------------------------------| +| `ENABLE_INSTRUMENTATION=false` in the environment | Initial settings only | No | Instrumentation flips back **on**. | +| `disable_instrumentation()` called from code | Process-wide, sticky | Yes | Instrumentation **stays off** — the user-disable intent wins. | -The two other variables, `ENABLE_CONSOLE_EXPORTERS` and `VS_CODE_EXTENSION_PORT`, are used to configure where the observability data is sent. Those are only activated when calling `configure_otel_providers()`. +If you want telemetry off **and want it to stay off**, use `disable_instrumentation()`. + +#### Sticky semantics — why this matters + +Framework integrations and third-party libraries can call `enable_instrumentation()`, `enable_sensitive_telemetry()`, or `configure_otel_providers()` as part of their own setup. For example, `FoundryChatClient.configure_azure_monitor()` calls `enable_instrumentation()` after wiring up Azure Monitor. That's normally what you want — but if **you** have explicitly opted out, you don't want any of those calls to silently re-enable telemetry. + +`disable_instrumentation()` solves this by setting a **sticky** flag on `OBSERVABILITY_SETTINGS` that remains in effect until you explicitly clear it. While the flag is set: + +1. `OBSERVABILITY_SETTINGS.enable_instrumentation` and `enable_sensitive_data` **read as `False`** regardless of the stored value. +2. `enable_instrumentation()` and `enable_sensitive_telemetry()` are **no-ops** and log an info-level message. +3. `configure_otel_providers()` still configures providers / exporters / views (so a later force-enable can use them), but does not flip instrumentation on. +4. Direct attribute writes like `OBSERVABILITY_SETTINGS.enable_instrumentation = True` from any code are **silently dropped** (defense in depth). +5. Integrations that consult `OBSERVABILITY_SETTINGS.is_user_disabled` (e.g. `FoundryChatClient.configure_azure_monitor()`, `FoundryAgent.configure_azure_monitor()`) **skip their setup entirely**, so global Azure Monitor providers aren't installed unnecessarily. + +```python +from agent_framework.observability import disable_instrumentation + +# After this call, Agent Framework expresses your intent to opt out of telemetry. +# Library and framework code is expected to honor that intent and not flip +# instrumentation back on (e.g. by calling `enable_instrumentation()`, +# `enable_sensitive_telemetry()`, or writing to public attributes on +# `OBSERVABILITY_SETTINGS`). The framework actively short-circuits the public +# enable paths so the user's intent stays leading. A determined caller can still +# pass `force=True` or mutate private (`_`-prefixed) attributes to bypass it, +# but those are out-of-contract escape hatches that should not be used by +# integrations on the user's behalf. +disable_instrumentation() +``` + +#### Forcing re-enablement after a disable + +To intentionally re-enable telemetry after `disable_instrumentation()`, pass `force=True` to either of the two public enable helpers. This is the only way to clear the sticky disable, so the user's opt-out can only be reversed by a deliberate user opt-in: + +```python +from agent_framework.observability import ( + disable_instrumentation, + enable_instrumentation, + enable_sensitive_telemetry, +) + +disable_instrumentation() + +# Without force=True, these are no-ops while the disable is sticky: +enable_instrumentation() # logs info, does nothing +enable_sensitive_telemetry() # logs info, does nothing + +# With force=True, the sticky disable is cleared and the call proceeds: +enable_instrumentation(force=True) +# or +enable_sensitive_telemetry(force=True) + +# After a force-enable you can `disable_instrumentation()` again to re-arm +# the sticky disable. +``` + +#### Checking the disable state from integrations + +If you're writing an integration that performs telemetry setup as a side effect (e.g. provisioning a third-party exporter), consult the public read-only `is_user_disabled` property and early-return when it's set: + +```python +from agent_framework.observability import OBSERVABILITY_SETTINGS + +if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "Skipping telemetry setup because the user called disable_instrumentation()." + ) + return +``` + +This is what the built-in `FoundryChatClient.configure_azure_monitor()` and `FoundryAgent.configure_azure_monitor()` do — so calling `disable_instrumentation()` reliably prevents Azure Monitor's global providers from being installed by those helpers. + +#### What `disable_instrumentation()` does **not** do + +- It does not tear down OpenTelemetry providers, exporters, or in-flight spans that were already set up before the disable call. It only gates **future** captures by Agent Framework code paths. +- It does not stop telemetry from third-party instrumentations (e.g. `azure-monitor-opentelemetry`'s system metrics) that are wired up outside Agent Framework. Configure those separately if needed. +- It does not persist across processes. Each Python process starts with the disable flag cleared; if you always want telemetry off in a given environment, set `ENABLE_INSTRUMENTATION=false` as an environment variable in addition to (or instead of) the programmatic call. #### Environment variables for `configure_otel_providers()` @@ -202,7 +304,8 @@ The `configure_otel_providers()` function automatically reads **standard OpenTel > **Note**: These are standard OpenTelemetry environment variables. See the [OpenTelemetry spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for more details. #### Logging -Use standard Python logging configuration to align logs with telemetry output. + +Use standard Python logging configuration to align logs with telemetry output: ```python import logging @@ -212,15 +315,14 @@ logging.basicConfig( datefmt="%Y-%m-%d %H:%M:%S", ) ``` -You can control at what level logging happens and thus what logs get exported, you can do this, by adding this: + +To control which logs are exported, adjust the root logger level — other loggers inherit from it by default: ```python import logging -logger = logging.getLogger() -logger.setLevel(logging.NOTSET) +logging.getLogger().setLevel(logging.NOTSET) ``` -This gets the root logger and sets the level of that, automatically other loggers inherit from that one, and you will get detailed logs in your telemetry. ## Samples @@ -228,36 +330,35 @@ This folder contains different samples demonstrating how to use telemetry in var | Sample | Description | |--------|-------------| -| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | **Recommended starting point**: Shows how to create custom exporters with specific configuration and pass them to `configure_otel_providers()`. Useful for advanced scenarios. | -| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | Shows how to setup telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). | -| [agent_observability.py](./agent_observability.py) | Shows telemetry collection for an agentic application with tool calls using environment variables. | -| [foundry_tracing.py](./foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. | -| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: Shows manual setup of exporters and providers with console output. Useful for understanding how observability works under the hood. | -| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: Shows zero-code telemetry setup using the `opentelemetry-enable_instrumentation` CLI tool. | -| [workflow_observability.py](./workflow_observability.py) | Shows telemetry collection for a workflow with multiple executors and message passing. | +| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | **Recommended starting point**: configure telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). | +| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | Create custom exporters with specific configuration and pass them to `configure_otel_providers()`. | +| [agent_observability.py](./agent_observability.py) | Telemetry collection for an agentic application with tool calls. | +| [foundry_tracing.py](./foundry_tracing.py) | Azure Monitor integration with Microsoft Foundry. | +| [workflow_observability.py](./workflow_observability.py) | Telemetry collection for a workflow with multiple executors and message passing. | +| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: manual setup of exporters and providers with console output — useful for understanding how observability works under the hood. | +| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: zero-code provider/exporter setup using the `opentelemetry-instrument` CLI wrapper. | ### Running the samples -1. Open a terminal and navigate to this folder: `python/samples/02-agents/observability/`. This is necessary for the `.env` file to be read correctly. -2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example). - > **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output. -3. Choose one environment-loading approach: - - **A. Sample-managed loading (current samples):** run from this folder so the sample's `load_dotenv()` call can find `.env`. - - **B. Shell/IDE-managed environment:** set/export environment variables directly, or use an IDE run configuration that injects env vars / `.env`. - - **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")` (or your own settings loader path). - - **D. CLI-managed env file:** run with `uv` and pass the file explicitly, for example: - `uv run --env-file=.env python configure_otel_providers_with_env_var.py` -4. Activate your python virtual environment, then run a sample (for example `python configure_otel_providers_with_env_var.py`). +1. Open a terminal in this folder (`python/samples/02-agents/observability/`) so that `.env` is found. +2. Create a `.env` file if you don't already have one. See [.env.example](./.env.example). + > Instrumentation is on by default. Set `OTEL_EXPORTER_OTLP_ENDPOINT` (or other configuration) as needed. With no exporters configured, set `ENABLE_CONSOLE_EXPORTERS=true` for console output. +3. Pick an environment-loading approach: + - **A. Sample-managed:** run from this folder so the sample's `load_dotenv()` call can find `.env`. + - **B. Shell/IDE-managed:** export environment variables, or use an IDE run configuration that injects them. + - **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")`. + - **D. CLI-managed:** run with `uv` and pass the file explicitly, e.g. `uv run --env-file=.env python configure_otel_providers_with_env_var.py`. +4. Activate your virtual environment, then run a sample (e.g. `python configure_otel_providers_with_env_var.py`). -> If you do manual provider setup (e.g., Azure Monitor), call `enable_instrumentation()` to turn on Agent Framework telemetry code paths; if you want Agent Framework to configure exporters/providers for you, call `configure_otel_providers(...)`. +> If you set up providers manually (e.g. Azure Monitor), Agent Framework instrumentation is still on by default. Call `enable_sensitive_telemetry()` if you also want to capture sensitive data. To have Agent Framework configure exporters and providers for you, call `configure_otel_providers(...)`. -> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard. +> Each sample prints its Operation/Trace ID, which you can use to filter logs and traces in Application Insights or the Aspire Dashboard. # Appendix ## Azure Monitor Queries -When you are in Azure Monitor and want to have a overall view of the span, use this query in the logs section: +For an overall view of a span in Azure Monitor, run this query in the Logs section: ```kusto dependencies @@ -280,7 +381,8 @@ dependencies ``` ### Grafana dashboards with Application Insights data -Besides the Application Insights native UI, you can also use Grafana to visualize the telemetry data in Application Insights. There are two tailored dashboards for you to get started quickly: + +In addition to the native Application Insights UI, you can use Grafana to visualize the same telemetry data. Two tailored dashboards are available to get you started: #### Agent Overview dashboard Open dashboard in Azure portal: @@ -292,117 +394,27 @@ Open dashboard in Azure portal: ## Migration Guide -We've done a major update to the observability API in Agent Framework Python SDK. The new API simplifies configuration by relying more on standard OpenTelemetry environment variables and have split the instrumentation from the configuration. - -If you're updating from a previous version of the Agent Framework, here are the key changes to the observability API: - -### Environment Variables - -| Old Variable | New Variable | Notes | -|-------------|--------------|-------| -| `OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Standard OpenTelemetry env var | -| `APPLICATIONINSIGHTS_CONNECTION_STRING` | N/A | Use `configure_azure_monitor()` | -| N/A | `ENABLE_CONSOLE_EXPORTERS` | New opt-in flag for console output | - -### OTLP Configuration - -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability -# Via parameter -setup_observability(otlp_endpoint="http://localhost:4317") +Instrumentation is now **enabled by default** (you no longer have to opt in by calling `enable_instrumentation()` at startup), and the way you opt in to capturing sensitive payloads has its own dedicated function. -# Via environment variable -# OTLP_ENDPOINT=http://localhost:4317 -setup_observability() -``` +If your code previously did: -**After (Current):** ```python -from agent_framework.observability import configure_otel_providers -# Via standard OTEL environment variable (recommended) -# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 -configure_otel_providers() - -# Or via custom exporters -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter -from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter - -configure_otel_providers(exporters=[ - OTLPSpanExporter(endpoint="http://localhost:4317"), - OTLPLogExporter(endpoint="http://localhost:4317"), - OTLPMetricExporter(endpoint="http://localhost:4317"), -]) -``` - -### Azure Monitor Configuration - -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability - -setup_observability( - applicationinsights_connection_string="InstrumentationKey=...", - applicationinsights_live_metrics=True, -) -``` - -**After (Current):** - -```python -from agent_framework.foundry import FoundryChatClient -from agent_framework.observability import create_resource, enable_instrumentation -from azure.identity import AzureCliCredential -from azure.monitor.opentelemetry import configure_azure_monitor - -async def main(): - # For Microsoft Foundry projects - client = FoundryChatClient( - project_endpoint="https://your-project.services.ai.azure.com", - model="gpt-4o", - credential=AzureCliCredential(), - ) - await client.configure_azure_monitor(enable_live_metrics=True) +from agent_framework.observability import enable_instrumentation - # For non-Azure AI projects - configure_azure_monitor( - connection_string="InstrumentationKey=...", - resource=create_resource(), - enable_live_metrics=True, - ) - enable_instrumentation() +enable_instrumentation(enable_sensitive_data=True) ``` -### Console Output +replace it with: -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability - -# Console was used as automatic fallback -setup_observability() # Would output to console if no exporters configured -``` - -**After (Current):** ```python -from agent_framework.observability import configure_otel_providers +from agent_framework.observability import enable_sensitive_telemetry -# Console exporters are now opt-in -# ENABLE_CONSOLE_EXPORTERS=true -configure_otel_providers() - -# Or programmatically -configure_otel_providers(enable_console_exporters=True) +enable_sensitive_telemetry() ``` -### Benefits of New API +`enable_sensitive_telemetry()` ensures that instrumentation is on and turns sensitive-event capture on in one call. `enable_instrumentation()` still exists for the rare case where you want to programmatically force instrumentation on without enabling sensitive data (e.g. to override `ENABLE_INSTRUMENTATION=false`), and it now also accepts `force=True` to clear a previous `disable_instrumentation()` — see [Disabling instrumentation](#disabling-instrumentation). -1. **Standards Compliant**: Uses standard OpenTelemetry environment variables -2. **Simpler**: Less configuration needed, more relies on environment -3. **Flexible**: Easy to add custom exporters alongside environment-based ones -4. **Cleaner Separation**: Azure Monitor setup is in Azure-specific client -5. **Better Compatibility**: Works with any OTEL-compatible tool (Jaeger, Zipkin, Prometheus, etc.) +> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production. ## Aspire Dashboard @@ -437,7 +449,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 Or set it as an environment variable when running your samples: ```bash -ENABLE_INSTRUMENTATION=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py ``` ### Viewing telemetry data diff --git a/python/samples/02-agents/observability/__init__.py b/python/samples/02-agents/observability/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py index 722cbf445a..8ae3b17dfa 100644 --- a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py +++ b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py @@ -7,7 +7,7 @@ from agent_framework import Message, tool from agent_framework.foundry import FoundryChatClient -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry from azure.identity import AzureCliCredential from dotenv import load_dotenv from opentelemetry._logs import set_logger_provider @@ -135,7 +135,8 @@ async def main(): setup_logging() setup_tracing() setup_metrics() - enable_instrumentation() + # Instrumentation is enabled by default; call this to also capture sensitive data. + enable_sensitive_telemetry() await run_chat_client() diff --git a/python/samples/02-agents/observability/advanced_zero_code.py b/python/samples/02-agents/observability/advanced_zero_code.py index dffd26a0fc..cd361b1ef2 100644 --- a/python/samples/02-agents/observability/advanced_zero_code.py +++ b/python/samples/02-agents/observability/advanced_zero_code.py @@ -19,13 +19,20 @@ """ This sample shows how you can configure observability of an application with zero code changes. -It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup -is done via environment variables. -Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool, -when using `uv` there are some additional steps, so follow the instructions carefully. +Agent Framework is natively instrumented with OpenTelemetry, so no auto-instrumentation of the +framework itself is required. Running the `opentelemetry-instrument` CLI wrapper simply configures +the global tracer/meter providers and exporters from environment variables (or CLI flags) at +process startup, so the application code does not need to set them up explicitly. The native +spans/metrics emitted by Agent Framework are then picked up by that globally configured pipeline. -And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below). +See: https://opentelemetry.io/docs/zero-code/python/ + +Install the OpenTelemetry CLI tool following the guidance above (when using `uv` there are some +additional steps, so follow the instructions carefully). + +Then setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update +the endpoint below). Then you can run: ```bash diff --git a/python/samples/03-workflows/observability/executor_io_observation.py b/python/samples/03-workflows/observability/executor_io_observation.py index 3129fcf158..4637f8c975 100644 --- a/python/samples/03-workflows/observability/executor_io_observation.py +++ b/python/samples/03-workflows/observability/executor_io_observation.py @@ -22,9 +22,6 @@ - executor_completed events (type='executor_completed') contain the messages sent via ctx.send_message() in event.data - How to generically observe all executor I/O through workflow streaming events -This approach allows you to enable_instrumentation any workflow for observability without -changing the executor implementations. - Prerequisites: - No external services required. """ diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example index f53b64c8c5..bf9bff7405 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example @@ -1,4 +1,3 @@ FOUNDRY_PROJECT_ENDPOINT="..." AZURE_AI_MODEL_DEPLOYMENT_NAME="..." -ENABLE_INSTRUMENTATION=true ENABLE_SENSITIVE_DATA=true \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md index 9f08baa168..9b592f2079 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md @@ -16,7 +16,7 @@ The agent is hosted using the [Agent Framework](https://github.com/microsoft/age ### Instrumentation -Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution, but it's turned off by default. This sample demonstrates how to enable instrumentation via environment variables in `agent.manifest.yaml` and `agent.yaml`. The relevant environment variables are `ENABLE_INSTRUMENTATION` and `ENABLE_SENSITIVE_DATA`, which can be set to `true` to enable diagnostics and capture sensitive events respectively. +Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution. Instrumentation is enabled by default. To also capture sensitive event payloads (prompts, tool arguments, etc.) set `ENABLE_SENSITIVE_DATA=true`. This sample demonstrates how to manage these settings via environment variables in `agent.manifest.yaml` and `agent.yaml`. Foundry Hosted Agent has built-in observability thus you don't need to set up exporters manually to capture telemetry from your code. The traces, metrics, and logs generated by the agent are automatically collected and made available through Foundry's observability stack via Azure Monitor/Application Insights. The `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable is injected when the agent is deployed to Foundry, however it is still required to be set in your environment if you want to run the agent host locally and have telemetry sent to Application Insights from your local environment. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml index b96c34ad96..845f462952 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml @@ -17,8 +17,6 @@ template: environment_variables: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - - name: ENABLE_INSTRUMENTATION - value: true - name: ENABLE_SENSITIVE_DATA value: true resources: diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml index 216dd415d6..f0e651136e 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml @@ -5,12 +5,10 @@ protocols: - protocol: responses version: 1.0.0 resources: - cpu: '0.25' - memory: '0.5Gi' + cpu: "0.25" + memory: "0.5Gi" environment_variables: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: ENABLE_INSTRUMENTATION - value: true - name: ENABLE_SENSITIVE_DATA - value: true \ No newline at end of file + value: true diff --git a/python/samples/README.md b/python/samples/README.md index 0ff8563933..30b1345d37 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -90,7 +90,7 @@ Example values below are illustrative. For entries not backed by a single public column names the closest public surface, helper, or package-level initialization point that reads the variable. -| package | class | env var | example value | +| package | class/module | env var | example value | | --- | --- | --- | --- | | `agent-framework-anthropic` | `AnthropicClient` | `ANTHROPIC_API_KEY` | `sk-ant-api03-...` | | `agent-framework-anthropic` | `AnthropicClient` | `ANTHROPIC_CHAT_MODEL` | `claude-sonnet-4-5-20250929` | @@ -117,21 +117,21 @@ variable. | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__SCHEMANAME` | `cr123_agentname` | | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__TENANTID` | `11111111-1111-1111-1111-111111111111` | | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__AGENTAPPID` | `22222222-2222-2222-2222-222222222222` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_INSTRUMENTATION` | `true` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_SENSITIVE_DATA` | `false` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_CONSOLE_EXPORTERS` | `true` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `http://localhost:4318/v1/traces` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `http://localhost:4318/v1/metrics` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `http://localhost:4318/v1/logs` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_HEADERS` | `api-key=demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | `api-key=trace-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | `api-key=metric-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | `api-key=log-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_SERVICE_NAME` | `sample-agent` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_SERVICE_VERSION` | `1.0.0` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=dev,service.namespace=agent-framework` | +| `agent-framework-core` | `observability` | `ENABLE_INSTRUMENTATION` | `true` | +| `agent-framework-core` | `observability` | `ENABLE_SENSITIVE_DATA` | `false` | +| `agent-framework-core` | `observability` | `ENABLE_CONSOLE_EXPORTERS` | `true` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `http://localhost:4318/v1/traces` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `http://localhost:4318/v1/metrics` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `http://localhost:4318/v1/logs` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_HEADERS` | `api-key=demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | `api-key=trace-demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | `api-key=metric-demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | `api-key=log-demo` | +| `agent-framework-core` | `observability` | `OTEL_SERVICE_NAME` | `sample-agent` | +| `agent-framework-core` | `observability` | `OTEL_SERVICE_VERSION` | `1.0.0` | +| `agent-framework-core` | `observability` | `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=dev,service.namespace=agent-framework` | | `agent-framework-devui` | `DevUI server` | `DEVUI_AUTH_TOKEN` | `my-devui-token` | | `agent-framework-foundry` | `FoundryChatClient` | `FOUNDRY_PROJECT_ENDPOINT` | `https://my-project.services.ai.azure.com/api/projects/my-project` | | `agent-framework-foundry` | `FoundryChatClient` | `FOUNDRY_MODEL` | `gpt-4o` | diff --git a/python/uv.lock b/python/uv.lock index cd8ee8a0ca..05da4c1be3 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -602,7 +602,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, ] [[package]]