From f8a9e6db8dc973bdf1caf17814ecb5f1fbbbb21b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:10:07 +0000 Subject: [PATCH 1/7] Initial plan From 69a79c96bd4e496b29b09e10182166ddb25afe5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:17:40 +0000 Subject: [PATCH 2/7] Add scope and domain override support for observability Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> --- .../core/exporters/agent365_exporter.py | 9 ++- .../runtime/environment_utils.py | 6 +- .../core/test_agent365_exporter.py | 81 +++++++++++++++++++ tests/runtime/test_environment_utils.py | 17 ++++ 4 files changed, 110 insertions(+), 3 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py index e2018c75..ca867bfa 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py @@ -6,6 +6,7 @@ import json import logging +import os import threading import time from collections.abc import Callable, Sequence @@ -86,8 +87,12 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) # Resolve endpoint + token - discovery = PowerPlatformApiDiscovery(self._cluster_category) - endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) + domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE") + if domain_override: + endpoint = domain_override + else: + discovery = PowerPlatformApiDiscovery(self._cluster_category) + endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) endpoint_path = ( f"/maven/agent365/service/agents/{agent_id}/traces" if self._use_s2s_endpoint diff --git a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py index 63ec52ab..df68ee8a 100644 --- a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py +++ b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py @@ -21,10 +21,14 @@ def get_observability_authentication_scope() -> list[str]: """ Returns the scope for authenticating to the observability service based on the current environment. + The scope can be overridden via the A365_OBSERVABILITY_SCOPE_OVERRIDE environment variable + to enable testing against pre-production environments. + Returns: list[str]: The authentication scope for the current environment. """ - return [PROD_OBSERVABILITY_SCOPE] + override_scope = os.getenv("A365_OBSERVABILITY_SCOPE_OVERRIDE") + return [override_scope] if override_scope else [PROD_OBSERVABILITY_SCOPE] def is_development_environment() -> bool: diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index ec89b1cb..ff053677 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -384,6 +384,87 @@ def test_exporter_is_internal(self): "Exporter class should be prefixed with underscore to indicate it's private/internal", ) + def test_export_uses_domain_override_when_env_var_set(self): + """Test that domain override is used when A365_OBSERVABILITY_DOMAIN_OVERRIDE is set.""" + # Arrange + override_domain = "override.example.com" + import os + + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = override_domain + + try: + spans = [self._create_mock_span("override_test_span")] + + # Mock the PowerPlatformApiDiscovery class (should not be called when override is set) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + # Mock the _post_with_retries method + with patch.object( + self.exporter, "_post_with_retries", return_value=True + ) as mock_post: + # Act + result = self.exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify the call arguments - should use override domain + args, kwargs = mock_post.call_args + url, body, headers = args + + self.assertIn(override_domain, url) + self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + + # Verify PowerPlatformApiDiscovery was not instantiated + mock_discovery_class.assert_not_called() + + finally: + # Cleanup + del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] + + def test_export_uses_default_domain_when_no_override(self): + """Test that default domain resolution is used when no override is set.""" + # Arrange + import os + + # Ensure override is not set + if "A365_OBSERVABILITY_DOMAIN_OVERRIDE" in os.environ: + del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] + + spans = [self._create_mock_span("default_domain_span")] + + # Mock the PowerPlatformApiDiscovery class + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + mock_discovery = Mock() + mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com" + mock_discovery_class.return_value = mock_discovery + + # Mock the _post_with_retries method + with patch.object(self.exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = self.exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify the call arguments - should use default domain + args, kwargs = mock_post.call_args + url, body, headers = args + + self.assertIn("default-endpoint.com", url) + self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + + # Verify PowerPlatformApiDiscovery was called + mock_discovery_class.assert_called_once_with("test") + mock_discovery.get_tenant_island_cluster_endpoint.assert_called_once_with( + "test-tenant-123" + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index 7660b173..91f03117 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -18,6 +18,23 @@ def test_get_observability_authentication_scope(): assert result == [PROD_OBSERVABILITY_SCOPE] +def test_get_observability_authentication_scope_with_override(monkeypatch): + """Test get_observability_authentication_scope returns override when env var is set.""" + override_scope = "https://override.example.com/.default" + monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", override_scope) + + result = get_observability_authentication_scope() + assert result == [override_scope] + + +def test_get_observability_authentication_scope_without_override(monkeypatch): + """Test get_observability_authentication_scope returns default when env var is not set.""" + monkeypatch.delenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", raising=False) + + result = get_observability_authentication_scope() + assert result == [PROD_OBSERVABILITY_SCOPE] + + @pytest.mark.parametrize( "env_value,expected", [ From c66aa4ab439cc6f71bd26b390dfed2a4c65b72ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:19:39 +0000 Subject: [PATCH 3/7] Fix code review feedback: move imports to top of file Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> --- tests/observability/core/test_agent365_exporter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index ff053677..e7d9a29a 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import json +import os import unittest from unittest.mock import Mock, patch @@ -388,7 +389,6 @@ def test_export_uses_domain_override_when_env_var_set(self): """Test that domain override is used when A365_OBSERVABILITY_DOMAIN_OVERRIDE is set.""" # Arrange override_domain = "override.example.com" - import os os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = override_domain @@ -427,8 +427,6 @@ def test_export_uses_domain_override_when_env_var_set(self): def test_export_uses_default_domain_when_no_override(self): """Test that default domain resolution is used when no override is set.""" # Arrange - import os - # Ensure override is not set if "A365_OBSERVABILITY_DOMAIN_OVERRIDE" in os.environ: del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] From 8a94d44f7d6e360d264a68480832ff561d707d36 Mon Sep 17 00:00:00 2001 From: Sergio Escalera Date: Thu, 8 Jan 2026 15:16:07 -0800 Subject: [PATCH 4/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/runtime/test_environment_utils.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index 91f03117..4b89677c 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -27,14 +27,6 @@ def test_get_observability_authentication_scope_with_override(monkeypatch): assert result == [override_scope] -def test_get_observability_authentication_scope_without_override(monkeypatch): - """Test get_observability_authentication_scope returns default when env var is not set.""" - monkeypatch.delenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", raising=False) - - result = get_observability_authentication_scope() - assert result == [PROD_OBSERVABILITY_SCOPE] - - @pytest.mark.parametrize( "env_value,expected", [ From 6ed8c1a94a4fd7d68062d3cff71d27722a71f719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:27:09 +0000 Subject: [PATCH 5/7] Address code review feedback: add validation, improve tests, optimize env var reading Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> --- .../core/exporters/agent365_exporter.py | 31 +++- .../runtime/environment_utils.py | 6 +- .../core/test_agent365_exporter.py | 172 ++++++++++++++---- tests/runtime/test_environment_utils.py | 25 +++ 4 files changed, 195 insertions(+), 39 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py index ca867bfa..1d3c7d87 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py @@ -61,6 +61,8 @@ def __init__( self._token_resolver = token_resolver self._cluster_category = cluster_category self._use_s2s_endpoint = use_s2s_endpoint + # Read domain override once at initialization + self._domain_override = self._get_validated_domain_override() # ------------- SpanExporter API ----------------- @@ -87,9 +89,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) # Resolve endpoint + token - domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE") - if domain_override: - endpoint = domain_override + if self._domain_override: + endpoint = self._domain_override else: discovery = PowerPlatformApiDiscovery(self._cluster_category) endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) @@ -147,6 +148,30 @@ def shutdown(self) -> None: def force_flush(self, timeout_millis: int = 30000) -> bool: return True + # ------------- Helper methods ------------------- + + @staticmethod + def _get_validated_domain_override() -> str | None: + """ + Get and validate the domain override from environment variable. + + Returns: + The validated domain override, or None if not set or invalid. + """ + domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE", "").strip() + if not domain_override: + return None + + # Basic validation: ensure domain doesn't contain protocol or path separators + if "://" in domain_override or "/" in domain_override: + logger.warning( + f"Invalid domain override '{domain_override}': " + "domain should not contain protocol (://) or path separators (/)" + ) + return None + + return domain_override + # ------------- HTTP helper ---------------------- @staticmethod diff --git a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py index df68ee8a..d68aa863 100644 --- a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py +++ b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py @@ -24,10 +24,14 @@ def get_observability_authentication_scope() -> list[str]: The scope can be overridden via the A365_OBSERVABILITY_SCOPE_OVERRIDE environment variable to enable testing against pre-production environments. + Environment Variable: + A365_OBSERVABILITY_SCOPE_OVERRIDE: Full authentication scope URL including the /.default suffix + (e.g., "https://preprod.api.powerplatform.com/.default") + Returns: list[str]: The authentication scope for the current environment. """ - override_scope = os.getenv("A365_OBSERVABILITY_SCOPE_OVERRIDE") + override_scope = os.getenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", "").strip() return [override_scope] if override_scope else [PROD_OBSERVABILITY_SCOPE] diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index e7d9a29a..41f9363f 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -20,12 +20,26 @@ def setUp(self): """Set up test fixtures.""" self.mock_token_resolver = Mock() self.mock_token_resolver.return_value = "test_token_123" - - # Don't patch the class in setUp, do it per test + + # Store original environment variable values for cleanup + self._original_domain_override = os.environ.get("A365_OBSERVABILITY_DOMAIN_OVERRIDE") + + # Ensure no override is set by default for most tests + os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) + + # Create default exporter for tests that don't need special setup self.exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) + def tearDown(self): + """Clean up test environment.""" + # Restore original environment variable value + if self._original_domain_override is None: + os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) + else: + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = self._original_domain_override + def _create_mock_span( self, name: str = "test_span", @@ -389,48 +403,49 @@ def test_export_uses_domain_override_when_env_var_set(self): """Test that domain override is used when A365_OBSERVABILITY_DOMAIN_OVERRIDE is set.""" # Arrange override_domain = "override.example.com" - os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = override_domain + + # Create exporter after setting environment variable so it reads the override + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("override_test_span")] - try: - spans = [self._create_mock_span("override_test_span")] - - # Mock the PowerPlatformApiDiscovery class (should not be called when override is set) - with patch( - "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" - ) as mock_discovery_class: - # Mock the _post_with_retries method - with patch.object( - self.exporter, "_post_with_retries", return_value=True - ) as mock_post: - # Act - result = self.exporter.export(spans) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # Mock the PowerPlatformApiDiscovery class (should not be called when override is set) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + # Mock the _post_with_retries method + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = exporter.export(spans) - # Verify the call arguments - should use override domain - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - self.assertIn(override_domain, url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + # Verify the call arguments - should use override domain with complete URL + args, kwargs = mock_post.call_args + url, body, headers = args - # Verify PowerPlatformApiDiscovery was not instantiated - mock_discovery_class.assert_not_called() + expected_url = f"https://{override_domain}/maven/agent365/agents/test-agent-456/traces?api-version=1" + self.assertEqual(url, expected_url) - finally: - # Cleanup - del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] + # Verify PowerPlatformApiDiscovery was not instantiated + mock_discovery_class.assert_not_called() def test_export_uses_default_domain_when_no_override(self): """Test that default domain resolution is used when no override is set.""" # Arrange # Ensure override is not set - if "A365_OBSERVABILITY_DOMAIN_OVERRIDE" in os.environ: - del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] - + os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) + + # Create exporter after clearing environment variable + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + spans = [self._create_mock_span("default_domain_span")] # Mock the PowerPlatformApiDiscovery class @@ -442,9 +457,9 @@ def test_export_uses_default_domain_when_no_override(self): mock_discovery_class.return_value = mock_discovery # Mock the _post_with_retries method - with patch.object(self.exporter, "_post_with_retries", return_value=True) as mock_post: + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: # Act - result = self.exporter.export(spans) + result = exporter.export(spans) # Assert self.assertEqual(result, SpanExportResult.SUCCESS) @@ -463,6 +478,93 @@ def test_export_uses_default_domain_when_no_override(self): "test-tenant-123" ) + def test_export_ignores_empty_domain_override(self): + """Test that empty or whitespace-only domain override is ignored.""" + # Arrange + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = " " # whitespace only + + # Create exporter after setting environment variable + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("test_span")] + + # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + mock_discovery = Mock() + mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com" + mock_discovery_class.return_value = mock_discovery + + with patch.object(exporter, "_post_with_retries", return_value=True): + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + # Verify PowerPlatformApiDiscovery was called (override was ignored) + mock_discovery_class.assert_called_once_with("test") + + def test_export_ignores_invalid_domain_with_protocol(self): + """Test that domain override containing protocol is ignored.""" + # Arrange + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "https://invalid.example.com" + + # Create exporter after setting environment variable + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("test_span")] + + # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + mock_discovery = Mock() + mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com" + mock_discovery_class.return_value = mock_discovery + + with patch.object(exporter, "_post_with_retries", return_value=True): + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + # Verify PowerPlatformApiDiscovery was called (override was ignored) + mock_discovery_class.assert_called_once_with("test") + + def test_export_ignores_invalid_domain_with_path(self): + """Test that domain override containing path separator is ignored.""" + # Arrange + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "invalid.example.com/path" + + # Create exporter after setting environment variable + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("test_span")] + + # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + mock_discovery = Mock() + mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com" + mock_discovery_class.return_value = mock_discovery + + with patch.object(exporter, "_post_with_retries", return_value=True): + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + # Verify PowerPlatformApiDiscovery was called (override was ignored) + mock_discovery_class.assert_called_once_with("test") + if __name__ == "__main__": unittest.main() diff --git a/tests/runtime/test_environment_utils.py b/tests/runtime/test_environment_utils.py index 4b89677c..cb0c8077 100644 --- a/tests/runtime/test_environment_utils.py +++ b/tests/runtime/test_environment_utils.py @@ -27,6 +27,31 @@ def test_get_observability_authentication_scope_with_override(monkeypatch): assert result == [override_scope] +def test_get_observability_authentication_scope_ignores_empty_override(monkeypatch): + """Test get_observability_authentication_scope ignores empty string override.""" + monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", "") + + result = get_observability_authentication_scope() + assert result == [PROD_OBSERVABILITY_SCOPE] + + +def test_get_observability_authentication_scope_ignores_whitespace_override(monkeypatch): + """Test get_observability_authentication_scope ignores whitespace-only override.""" + monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", " ") + + result = get_observability_authentication_scope() + assert result == [PROD_OBSERVABILITY_SCOPE] + + +def test_get_observability_authentication_scope_trims_whitespace(monkeypatch): + """Test get_observability_authentication_scope trims whitespace from override.""" + override_scope = " https://override.example.com/.default " + monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", override_scope) + + result = get_observability_authentication_scope() + assert result == [override_scope.strip()] + + @pytest.mark.parametrize( "env_value,expected", [ From 4d8a1710026b10fb156001879be0a93f292fe66b Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 9 Jan 2026 18:01:10 +0530 Subject: [PATCH 6/7] fix comments --- .../core/exporters/agent365_exporter.py | 26 ++----------------- .../observability/core/exporters/utils.py | 22 ++++++++++++++++ .../runtime/environment_utils.py | 4 --- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py index 1d3c7d87..5e497641 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py @@ -6,7 +6,6 @@ import json import logging -import os import threading import time from collections.abc import Callable, Sequence @@ -19,6 +18,7 @@ from opentelemetry.trace import StatusCode from .utils import ( + get_validated_domain_override, hex_span_id, hex_trace_id, kind_name, @@ -62,7 +62,7 @@ def __init__( self._cluster_category = cluster_category self._use_s2s_endpoint = use_s2s_endpoint # Read domain override once at initialization - self._domain_override = self._get_validated_domain_override() + self._domain_override = get_validated_domain_override() # ------------- SpanExporter API ----------------- @@ -150,28 +150,6 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # ------------- Helper methods ------------------- - @staticmethod - def _get_validated_domain_override() -> str | None: - """ - Get and validate the domain override from environment variable. - - Returns: - The validated domain override, or None if not set or invalid. - """ - domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE", "").strip() - if not domain_override: - return None - - # Basic validation: ensure domain doesn't contain protocol or path separators - if "://" in domain_override or "/" in domain_override: - logger.warning( - f"Invalid domain override '{domain_override}': " - "domain should not contain protocol (://) or path separators (/)" - ) - return None - - return domain_override - # ------------- HTTP helper ---------------------- @staticmethod diff --git a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/utils.py b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/utils.py index 3eda274f..65782748 100644 --- a/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/utils.py +++ b/libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/utils.py @@ -142,6 +142,28 @@ def partition_by_identity( return groups +def get_validated_domain_override() -> str | None: + """ + Get and validate the domain override from environment variable. + + Returns: + The validated domain override, or None if not set or invalid. + """ + domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE", "").strip() + if not domain_override: + return None + + # Basic validation: ensure domain doesn't contain protocol or path separators + if "://" in domain_override or "/" in domain_override: + logger.warning( + f"Invalid domain override '{domain_override}': " + "domain should not contain protocol (://) or path separators (/)" + ) + return None + + return domain_override + + def is_agent365_exporter_enabled() -> bool: """Check if Agent 365 exporter is enabled.""" # Check environment variable diff --git a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py index d68aa863..4b9cb194 100644 --- a/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py +++ b/libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py @@ -24,10 +24,6 @@ def get_observability_authentication_scope() -> list[str]: The scope can be overridden via the A365_OBSERVABILITY_SCOPE_OVERRIDE environment variable to enable testing against pre-production environments. - Environment Variable: - A365_OBSERVABILITY_SCOPE_OVERRIDE: Full authentication scope URL including the /.default suffix - (e.g., "https://preprod.api.powerplatform.com/.default") - Returns: list[str]: The authentication scope for the current environment. """ From 3b3e90bbfbd12aee47d54562433e5de94ea2cd79 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 9 Jan 2026 18:04:34 +0530 Subject: [PATCH 7/7] fix formatting --- .../core/test_agent365_exporter.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index 41f9363f..e642bf6c 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -20,13 +20,13 @@ def setUp(self): """Set up test fixtures.""" self.mock_token_resolver = Mock() self.mock_token_resolver.return_value = "test_token_123" - + # Store original environment variable values for cleanup self._original_domain_override = os.environ.get("A365_OBSERVABILITY_DOMAIN_OVERRIDE") - + # Ensure no override is set by default for most tests os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) - + # Create default exporter for tests that don't need special setup self.exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" @@ -404,12 +404,12 @@ def test_export_uses_domain_override_when_env_var_set(self): # Arrange override_domain = "override.example.com" os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = override_domain - + # Create exporter after setting environment variable so it reads the override exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) - + spans = [self._create_mock_span("override_test_span")] # Mock the PowerPlatformApiDiscovery class (should not be called when override is set) @@ -440,12 +440,12 @@ def test_export_uses_default_domain_when_no_override(self): # Arrange # Ensure override is not set os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) - + # Create exporter after clearing environment variable exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) - + spans = [self._create_mock_span("default_domain_span")] # Mock the PowerPlatformApiDiscovery class @@ -482,12 +482,12 @@ def test_export_ignores_empty_domain_override(self): """Test that empty or whitespace-only domain override is ignored.""" # Arrange os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = " " # whitespace only - + # Create exporter after setting environment variable exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) - + spans = [self._create_mock_span("test_span")] # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) @@ -511,12 +511,12 @@ def test_export_ignores_invalid_domain_with_protocol(self): """Test that domain override containing protocol is ignored.""" # Arrange os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "https://invalid.example.com" - + # Create exporter after setting environment variable exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) - + spans = [self._create_mock_span("test_span")] # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) @@ -540,12 +540,12 @@ def test_export_ignores_invalid_domain_with_path(self): """Test that domain override containing path separator is ignored.""" # Arrange os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "invalid.example.com/path" - + # Create exporter after setting environment variable exporter = _Agent365Exporter( token_resolver=self.mock_token_resolver, cluster_category="test" ) - + spans = [self._create_mock_span("test_span")] # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid)