From 9963902f5728f0e08afbf54d186af0316443320c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:49:01 +0000 Subject: [PATCH 1/5] Initial plan From 5e773aec35f55954ac8325fc7682edfe279727db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:57:55 +0000 Subject: [PATCH 2/5] Default a365_exporter endpoint to agent365.svc.cloud.microsoft with IL tenant fallback Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --- .../core/exporters/agent365_exporter.py | 54 ++- .../core/test_agent365_exporter.py | 405 ++++++++++-------- 2 files changed, 261 insertions(+), 198 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 da51c41f..462ff847 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 @@ -39,6 +39,7 @@ # Hardcoded constants - not configurable DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0 DEFAULT_MAX_RETRIES = 3 +DEFAULT_ENDPOINT_URL = "https://agent365.svc.cloud.microsoft" # Create logger for this module - inherits from 'microsoft_agents_a365.observability.core' logger = logging.getLogger(__name__) @@ -97,30 +98,13 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: payload = self._build_export_request(activities) body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) - # Resolve endpoint + token + # Resolve endpoint: domain override > default URL > IL tenant fallback if self._domain_override: endpoint = self._domain_override else: - discovery = PowerPlatformApiDiscovery(self._cluster_category) - endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) + endpoint = DEFAULT_ENDPOINT_URL - endpoint_path = ( - f"/maven/agent365/service/agents/{agent_id}/traces" - if self._use_s2s_endpoint - else f"/maven/agent365/agents/{agent_id}/traces" - ) - - # Construct URL - if endpoint has a scheme (http:// or https://), use it as-is - # Otherwise, prepend https:// - # Note: Check for "://" to distinguish between real protocols and domain:port format - # (urlparse treats "example.com:8080" as having scheme="example.com") - parsed = urlparse(endpoint) - if parsed.scheme and "://" in endpoint: - # Endpoint is a full URL, append path - url = f"{endpoint}{endpoint_path}?api-version=1" - else: - # Endpoint is just a domain (possibly with port), prepend https:// - url = f"https://{endpoint}{endpoint_path}?api-version=1" + url = self._build_url(endpoint, agent_id) # Debug: Log endpoint being used logger.info( @@ -146,6 +130,19 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: # Basic retry loop ok = self._post_with_retries(url, body, headers) + + # Fallback to IL tenant endpoint if default endpoint failed + # and no domain override was set + if not ok and not self._domain_override: + discovery = PowerPlatformApiDiscovery(self._cluster_category) + fallback_endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) + fallback_url = self._build_url(fallback_endpoint, agent_id) + logger.info( + f"Falling back to IL tenant endpoint: {fallback_url} " + f"(tenant: {tenant_id}, agent: {agent_id})" + ) + ok = self._post_with_retries(fallback_url, body, headers) + if not ok: any_failure = True @@ -171,6 +168,23 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # ------------- Helper methods ------------------- + def _build_url(self, endpoint: str, agent_id: str) -> str: + """Construct the full export URL from endpoint and agent ID. + + If the endpoint has a scheme (http:// or https://), use it as-is. + Otherwise, prepend https://. + """ + endpoint_path = ( + f"/maven/agent365/service/agents/{agent_id}/traces" + if self._use_s2s_endpoint + else f"/maven/agent365/agents/{agent_id}/traces" + ) + + parsed = urlparse(endpoint) + if parsed.scheme and "://" in endpoint: + return f"{endpoint}{endpoint_path}?api-version=1" + return f"https://{endpoint}{endpoint_path}?api-version=1" + # ------------- HTTP helper ---------------------- @staticmethod diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index c3b215b9..400b0aeb 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -8,6 +8,7 @@ from microsoft_agents_a365.observability.core.constants import GEN_AI_AGENT_ID_KEY, TENANT_ID_KEY from microsoft_agents_a365.observability.core.exporters.agent365_exporter import ( + DEFAULT_ENDPOINT_URL, _Agent365Exporter, ) from opentelemetry.sdk.trace import ReadableSpan @@ -108,47 +109,39 @@ def test_export_success(self): self._create_mock_span("span2", trace_id=111, span_id=333, parent_id=222), ] - # Mock the PowerPlatformApiDiscovery class that gets created inside export() - 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 = "test-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 - args, kwargs = mock_post.call_args - url, body, headers = args - - self.assertIn("test-endpoint.com", url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) - self.assertEqual(headers["authorization"], "Bearer test_token_123") - self.assertEqual(headers["content-type"], "application/json") + # 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) - # Verify JSON structure - request_data = json.loads(body) - self.assertIn("resourceSpans", request_data) - self.assertEqual(len(request_data["resourceSpans"]), 1) # One resource group - self.assertEqual(len(request_data["resourceSpans"][0]["scopeSpans"]), 1) # One scope - self.assertEqual( - len(request_data["resourceSpans"][0]["scopeSpans"][0]["spans"]), 2 - ) # Two spans + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify the call arguments - should use default endpoint + args, kwargs = mock_post.call_args + url, body, headers = args + + self.assertIn(DEFAULT_ENDPOINT_URL, url) + self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + self.assertEqual(headers["authorization"], "Bearer test_token_123") + self.assertEqual(headers["content-type"], "application/json") + + # Verify JSON structure + request_data = json.loads(body) + self.assertIn("resourceSpans", request_data) + self.assertEqual(len(request_data["resourceSpans"]), 1) # One resource group + self.assertEqual(len(request_data["resourceSpans"][0]["scopeSpans"]), 1) # One scope + self.assertEqual( + len(request_data["resourceSpans"][0]["scopeSpans"][0]["spans"]), 2 + ) # Two spans def test_export_failure_with_retries(self): - """Test 2: Test export failure and retry mechanism.""" + """Test 2: Test export failure with fallback to IL tenant endpoint.""" # Arrange spans = [self._create_mock_span("failed_span")] - # Mock the PowerPlatformApiDiscovery class + # Mock the PowerPlatformApiDiscovery class for fallback with patch( "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" ) as mock_discovery_class: @@ -156,14 +149,23 @@ def test_export_failure_with_retries(self): mock_discovery.get_tenant_island_cluster_endpoint.return_value = "test-endpoint.com" mock_discovery_class.return_value = mock_discovery - # Mock the _post_with_retries method to return False (failure) + # Mock the _post_with_retries method to always return False (failure) with patch.object(self.exporter, "_post_with_retries", return_value=False) as mock_post: # Act result = self.exporter.export(spans) - # Assert + # Assert - should fail after both default and fallback attempts self.assertEqual(result, SpanExportResult.FAILURE) - mock_post.assert_called_once() + # Should be called twice: once for default URL, once for IL tenant fallback + self.assertEqual(mock_post.call_count, 2) + + # Verify first call uses default endpoint + first_url = mock_post.call_args_list[0][0][0] + self.assertIn(DEFAULT_ENDPOINT_URL, first_url) + + # Verify second call uses fallback endpoint + second_url = mock_post.call_args_list[1][0][0] + self.assertIn("test-endpoint.com", second_url) def test_partitioning_by_scope(self): """Test 3: Test that spans are properly partitioned by instrumentation scope.""" @@ -183,27 +185,19 @@ def test_partitioning_by_scope(self): ), ] - # 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 = "test-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) + # 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() + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - # Get the request body and parse it - args, kwargs = mock_post.call_args - url, body, headers = args - request_data = json.loads(body) + # Get the request body and parse it + args, kwargs = mock_post.call_args + url, body, headers = args + request_data = json.loads(body) # Should have 1 resource span (all spans share same resource and identity) self.assertEqual(len(request_data["resourceSpans"]), 1) @@ -238,32 +232,24 @@ def test_s2s_endpoint_path_when_enabled(self): spans = [self._create_mock_span("s2s_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 = "test-endpoint.com" - mock_discovery_class.return_value = mock_discovery + # Mock the _post_with_retries method + with patch.object(s2s_exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = s2s_exporter.export(spans) - # Mock the _post_with_retries method - with patch.object(s2s_exporter, "_post_with_retries", return_value=True) as mock_post: - # Act - result = s2s_exporter.export(spans) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - # Verify the call arguments - should use S2S path - args, kwargs = mock_post.call_args - url, body, headers = args + # Verify the call arguments - should use S2S path with default endpoint + args, kwargs = mock_post.call_args + url, body, headers = args - self.assertIn("test-endpoint.com", url) - self.assertIn("/maven/agent365/service/agents/test-agent-456/traces", url) - self.assertNotIn("/maven/agent365/agents/test-agent-456/traces", url) - self.assertEqual(headers["authorization"], "Bearer test_token_123") - self.assertEqual(headers["content-type"], "application/json") + self.assertIn(DEFAULT_ENDPOINT_URL, url) + self.assertIn("/maven/agent365/service/agents/test-agent-456/traces", url) + self.assertNotIn("/maven/agent365/agents/test-agent-456/traces", url) + self.assertEqual(headers["authorization"], "Bearer test_token_123") + self.assertEqual(headers["content-type"], "application/json") def test_default_endpoint_path_when_s2s_disabled(self): """Test 5: Test that default endpoint path is used when use_s2s_endpoint is False.""" @@ -274,48 +260,28 @@ def test_default_endpoint_path_when_s2s_disabled(self): spans = [self._create_mock_span("default_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 = "test-endpoint.com" - mock_discovery_class.return_value = mock_discovery - - # Mock the _post_with_retries method - with patch.object( - default_exporter, "_post_with_retries", return_value=True - ) as mock_post: - # Act - result = default_exporter.export(spans) + # Mock the _post_with_retries method + with patch.object(default_exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = default_exporter.export(spans) - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - # Verify the call arguments - should use default path - args, kwargs = mock_post.call_args - url, body, headers = args + # Verify the call arguments - should use default path with default endpoint + args, kwargs = mock_post.call_args + url, body, headers = args - self.assertIn("test-endpoint.com", url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) - self.assertNotIn("/maven/agent365/service/agents/test-agent-456/traces", url) - self.assertEqual(headers["authorization"], "Bearer test_token_123") - self.assertEqual(headers["content-type"], "application/json") + self.assertIn(DEFAULT_ENDPOINT_URL, url) + self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + self.assertNotIn("/maven/agent365/service/agents/test-agent-456/traces", url) + self.assertEqual(headers["authorization"], "Bearer test_token_123") + self.assertEqual(headers["content-type"], "application/json") @patch("microsoft_agents_a365.observability.core.exporters.agent365_exporter.logger") - @patch( - "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" - ) - def test_export_logging(self, mock_discovery, mock_logger): + def test_export_logging(self, mock_logger): """Test that the exporter logs appropriate messages during export.""" - # Mock the discovery service - mock_discovery_instance = Mock() - mock_discovery_instance.get_tenant_island_cluster_endpoint.return_value = ( - "test-endpoint.com" - ) - mock_discovery.return_value = mock_discovery_instance - # Mock successful HTTP response with patch("requests.Session.post") as mock_post: mock_response = Mock() @@ -348,13 +314,13 @@ def test_export_logging(self, mock_discovery, mock_logger): # Verify export succeeded self.assertEqual(result, SpanExportResult.SUCCESS) - # Verify logging calls + # Verify logging calls - should use default endpoint URL expected_log_calls = [ # Should log groups found unittest.mock.call.info("Found 1 identity groups with 2 total spans to export"), - # Should log endpoint being used + # Should log endpoint being used (default endpoint) unittest.mock.call.info( - "Exporting 2 spans to endpoint: https://test-endpoint.com/maven/agent365/agents/test-agent-456/traces?api-version=1 " + f"Exporting 2 spans to endpoint: {DEFAULT_ENDPOINT_URL}/maven/agent365/agents/test-agent-456/traces?api-version=1 " "(tenant: test-tenant-123, agent: test-agent-456)" ), # Should log token resolution success @@ -436,8 +402,8 @@ def test_export_uses_domain_override_when_env_var_set(self): # 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.""" + def test_export_uses_default_endpoint_when_no_override(self): + """Test that default endpoint URL is used when no override is set.""" # Arrange # Ensure override is not set os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) @@ -449,35 +415,23 @@ def test_export_uses_default_domain_when_no_override(self): 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(exporter, "_post_with_retries", return_value=True) as mock_post: - # Act - result = exporter.export(spans) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # 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 default domain - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - self.assertIn("default-endpoint.com", url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + # Verify the call arguments - should use default endpoint URL + args, kwargs = mock_post.call_args + url, body, headers = args - # 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" - ) + expected_url = ( + f"{DEFAULT_ENDPOINT_URL}/maven/agent365/agents/test-agent-456/traces?api-version=1" + ) + self.assertEqual(url, expected_url) def test_export_ignores_empty_domain_override(self): """Test that empty or whitespace-only domain override is ignored.""" @@ -491,22 +445,19 @@ def test_export_ignores_empty_domain_override(self): 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 + # Mock the _post_with_retries method + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = exporter.export(spans) - with patch.object(exporter, "_post_with_retries", return_value=True): - # Act - result = exporter.export(spans) + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - # Verify PowerPlatformApiDiscovery was called (override was ignored) - mock_discovery_class.assert_called_once_with("test") + # Verify the default endpoint URL is used (override was ignored) + args, kwargs = mock_post.call_args + url = args[0] + self.assertIn(DEFAULT_ENDPOINT_URL, url) def test_export_uses_valid_url_override_with_https(self): """Test that domain override with https:// protocol is accepted and used correctly.""" @@ -625,51 +576,149 @@ def test_export_ignores_invalid_domain_with_protocol(self): spans = [self._create_mock_span("test_span")] - # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) + # Mock the _post_with_retries method + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify the default endpoint URL is used (invalid override was ignored) + args, kwargs = mock_post.call_args + url = args[0] + self.assertIn(DEFAULT_ENDPOINT_URL, url) + + 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 _post_with_retries method + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() + + # Verify the default endpoint URL is used (invalid override was ignored) + args, kwargs = mock_post.call_args + url = args[0] + self.assertIn(DEFAULT_ENDPOINT_URL, url) + + def test_export_falls_back_to_il_tenant_on_default_failure(self): + """Test that IL tenant endpoint is tried when default endpoint fails.""" + # Arrange + os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) + + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("fallback_span")] + + # Mock the PowerPlatformApiDiscovery class for fallback 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.get_tenant_island_cluster_endpoint.return_value = "fallback-endpoint.com" mock_discovery_class.return_value = mock_discovery - with patch.object(exporter, "_post_with_retries", return_value=True): + # Mock _post_with_retries: first call fails (default), second call succeeds (fallback) + with patch.object( + exporter, "_post_with_retries", side_effect=[False, True] + ) as mock_post: # Act result = exporter.export(spans) - # Assert + # Assert - should succeed because fallback succeeded self.assertEqual(result, SpanExportResult.SUCCESS) - # Verify PowerPlatformApiDiscovery was called (override was ignored) + self.assertEqual(mock_post.call_count, 2) + + # Verify first call uses default endpoint + first_url = mock_post.call_args_list[0][0][0] + self.assertIn(DEFAULT_ENDPOINT_URL, first_url) + + # Verify second call uses IL tenant fallback endpoint + second_url = mock_post.call_args_list[1][0][0] + self.assertIn("fallback-endpoint.com", second_url) + + # Verify PowerPlatformApiDiscovery was called for fallback mock_discovery_class.assert_called_once_with("test") + mock_discovery.get_tenant_island_cluster_endpoint.assert_called_once_with( + "test-tenant-123" + ) - def test_export_ignores_invalid_domain_with_path(self): - """Test that domain override containing path separator is ignored.""" + def test_export_no_fallback_when_domain_override_fails(self): + """Test that no fallback happens when domain override is set and fails.""" # Arrange - os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "invalid.example.com/path" + os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "https://custom-override.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")] + spans = [self._create_mock_span("no_fallback_span")] - # Mock the PowerPlatformApiDiscovery class (should be called since override is invalid) + # Mock the PowerPlatformApiDiscovery class (should NOT be called) 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 _post_with_retries to return False (failure) + with patch.object(exporter, "_post_with_retries", return_value=False) as mock_post: + # Act + result = exporter.export(spans) + + # Assert - should fail without fallback + self.assertEqual(result, SpanExportResult.FAILURE) + # Should only be called once (no fallback for override) + mock_post.assert_called_once() + + # Verify the override URL was used + args, kwargs = mock_post.call_args + url = args[0] + self.assertIn("custom-override.com", url) + + # Verify PowerPlatformApiDiscovery was NOT called + mock_discovery_class.assert_not_called() + + def test_export_no_fallback_when_default_succeeds(self): + """Test that IL tenant fallback is not attempted when default endpoint succeeds.""" + # Arrange + os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) + + exporter = _Agent365Exporter( + token_resolver=self.mock_token_resolver, cluster_category="test" + ) + + spans = [self._create_mock_span("success_span")] - with patch.object(exporter, "_post_with_retries", return_value=True): + # Mock the PowerPlatformApiDiscovery class (should NOT be called) + with patch( + "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" + ) as mock_discovery_class: + # Mock _post_with_retries to return True (success) + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: # 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") + mock_post.assert_called_once() + + # Verify PowerPlatformApiDiscovery was NOT called (no fallback needed) + mock_discovery_class.assert_not_called() if __name__ == "__main__": From 261d336d06980304e6d2f002d295b41afffff641 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:59:33 +0000 Subject: [PATCH 3/5] Address code review feedback: rename test and improve docstring Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --- .../observability/core/exporters/agent365_exporter.py | 7 +++++++ tests/observability/core/test_agent365_exporter.py | 4 ++-- 2 files changed, 9 insertions(+), 2 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 462ff847..d46496df 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 @@ -173,6 +173,13 @@ def _build_url(self, endpoint: str, agent_id: str) -> str: If the endpoint has a scheme (http:// or https://), use it as-is. Otherwise, prepend https://. + + Args: + endpoint: Base endpoint URL or domain. + agent_id: The agent identifier to include in the URL path. + + Returns: + The fully constructed export URL with path and query parameters. """ endpoint_path = ( f"/maven/agent365/service/agents/{agent_id}/traces" diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index 400b0aeb..df498c2f 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -136,8 +136,8 @@ def test_export_success(self): len(request_data["resourceSpans"][0]["scopeSpans"][0]["spans"]), 2 ) # Two spans - def test_export_failure_with_retries(self): - """Test 2: Test export failure with fallback to IL tenant endpoint.""" + def test_export_fails_after_default_and_fallback_attempts(self): + """Test export failure when both default and IL tenant fallback endpoints fail.""" # Arrange spans = [self._create_mock_span("failed_span")] From d93e8d074ba264b80c001faa773375ad38cdf021 Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 27 Feb 2026 01:24:06 +0530 Subject: [PATCH 4/5] remove island tenant reference --- .../core/exporters/agent365_exporter.py | 43 +-- .../observability/core/exporters/utils.py | 26 ++ .../core/test_agent365_exporter.py | 288 +++++++----------- 3 files changed, 139 insertions(+), 218 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 d46496df..091fc54e 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 @@ -11,10 +11,8 @@ import time from collections.abc import Callable, Sequence from typing import Any, final -from urllib.parse import urlparse import requests -from microsoft_agents_a365.runtime.power_platform_api_discovery import PowerPlatformApiDiscovery from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult from opentelemetry.trace import StatusCode @@ -25,6 +23,7 @@ INVOKE_AGENT_OPERATION_NAME, ) from .utils import ( + build_export_url, get_validated_domain_override, hex_span_id, hex_trace_id, @@ -104,7 +103,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: else: endpoint = DEFAULT_ENDPOINT_URL - url = self._build_url(endpoint, agent_id) + url = build_export_url(endpoint, agent_id, tenant_id, self._use_s2s_endpoint) # Debug: Log endpoint being used logger.info( @@ -131,18 +130,6 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: # Basic retry loop ok = self._post_with_retries(url, body, headers) - # Fallback to IL tenant endpoint if default endpoint failed - # and no domain override was set - if not ok and not self._domain_override: - discovery = PowerPlatformApiDiscovery(self._cluster_category) - fallback_endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id) - fallback_url = self._build_url(fallback_endpoint, agent_id) - logger.info( - f"Falling back to IL tenant endpoint: {fallback_url} " - f"(tenant: {tenant_id}, agent: {agent_id})" - ) - ok = self._post_with_retries(fallback_url, body, headers) - if not ok: any_failure = True @@ -166,32 +153,6 @@ def shutdown(self) -> None: def force_flush(self, timeout_millis: int = 30000) -> bool: return True - # ------------- Helper methods ------------------- - - def _build_url(self, endpoint: str, agent_id: str) -> str: - """Construct the full export URL from endpoint and agent ID. - - If the endpoint has a scheme (http:// or https://), use it as-is. - Otherwise, prepend https://. - - Args: - endpoint: Base endpoint URL or domain. - agent_id: The agent identifier to include in the URL path. - - Returns: - The fully constructed export URL with path and query parameters. - """ - endpoint_path = ( - f"/maven/agent365/service/agents/{agent_id}/traces" - if self._use_s2s_endpoint - else f"/maven/agent365/agents/{agent_id}/traces" - ) - - parsed = urlparse(endpoint) - if parsed.scheme and "://" in endpoint: - return f"{endpoint}{endpoint_path}?api-version=1" - return f"https://{endpoint}{endpoint_path}?api-version=1" - # ------------- 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 b6d687c0..21261c9e 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 @@ -197,6 +197,32 @@ def get_validated_domain_override() -> str | None: return domain_override +def build_export_url( + endpoint: str, agent_id: str, tenant_id: str, use_s2s_endpoint: bool = False +) -> str: + """Construct the full export URL from endpoint and agent ID. + + Args: + endpoint: Base endpoint URL or domain. + agent_id: The agent identifier to include in the URL path. + tenant_id: The tenant identifier to include in the URL path. + use_s2s_endpoint: Whether to use the S2S endpoint path format. + + Returns: + The fully constructed export URL with path and query parameters. + """ + endpoint_path = ( + f"/observabilityService/tenants/{tenant_id}/agents/{agent_id}/traces" + if use_s2s_endpoint + else f"/observability/tenants/{tenant_id}/agents/{agent_id}/traces" + ) + + parsed = urlparse(endpoint) + if parsed.scheme and "://" in endpoint: + return f"{endpoint}{endpoint_path}?api-version=1" + return f"https://{endpoint}{endpoint_path}?api-version=1" + + def is_agent365_exporter_enabled() -> bool: """Check if Agent 365 exporter is enabled.""" # Check environment variable diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index df498c2f..c44cb31c 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -123,7 +123,9 @@ def test_export_success(self): url, body, headers = args self.assertIn(DEFAULT_ENDPOINT_URL, url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) + self.assertIn( + "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -137,35 +139,23 @@ def test_export_success(self): ) # Two spans def test_export_fails_after_default_and_fallback_attempts(self): - """Test export failure when both default and IL tenant fallback endpoints fail.""" + """Test export failure when default endpoint fails (no fallback in current implementation).""" # Arrange spans = [self._create_mock_span("failed_span")] - # Mock the PowerPlatformApiDiscovery class for fallback - 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 = "test-endpoint.com" - mock_discovery_class.return_value = mock_discovery - - # Mock the _post_with_retries method to always return False (failure) - with patch.object(self.exporter, "_post_with_retries", return_value=False) as mock_post: - # Act - result = self.exporter.export(spans) - - # Assert - should fail after both default and fallback attempts - self.assertEqual(result, SpanExportResult.FAILURE) - # Should be called twice: once for default URL, once for IL tenant fallback - self.assertEqual(mock_post.call_count, 2) + # Mock the _post_with_retries method to always return False (failure) + with patch.object(self.exporter, "_post_with_retries", return_value=False) as mock_post: + # Act + result = self.exporter.export(spans) - # Verify first call uses default endpoint - first_url = mock_post.call_args_list[0][0][0] - self.assertIn(DEFAULT_ENDPOINT_URL, first_url) + # Assert - should fail after default attempt (no fallback in current implementation) + self.assertEqual(result, SpanExportResult.FAILURE) + # Should be called once: only for default URL (no fallback logic) + self.assertEqual(mock_post.call_count, 1) - # Verify second call uses fallback endpoint - second_url = mock_post.call_args_list[1][0][0] - self.assertIn("test-endpoint.com", second_url) + # Verify call uses default endpoint + first_url = mock_post.call_args_list[0][0][0] + self.assertIn(DEFAULT_ENDPOINT_URL, first_url) def test_partitioning_by_scope(self): """Test 3: Test that spans are properly partitioned by instrumentation scope.""" @@ -246,8 +236,12 @@ def test_s2s_endpoint_path_when_enabled(self): url, body, headers = args self.assertIn(DEFAULT_ENDPOINT_URL, url) - self.assertIn("/maven/agent365/service/agents/test-agent-456/traces", url) - self.assertNotIn("/maven/agent365/agents/test-agent-456/traces", url) + self.assertIn( + "/observabilityService/tenants/test-tenant-123/agents/test-agent-456/traces", url + ) + self.assertNotIn( + "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -274,8 +268,12 @@ def test_default_endpoint_path_when_s2s_disabled(self): url, body, headers = args self.assertIn(DEFAULT_ENDPOINT_URL, url) - self.assertIn("/maven/agent365/agents/test-agent-456/traces", url) - self.assertNotIn("/maven/agent365/service/agents/test-agent-456/traces", url) + self.assertIn( + "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + ) + self.assertNotIn( + "/observabilityService/tenants/test-tenant-123/agents/test-agent-456/traces", url + ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -320,7 +318,7 @@ def test_export_logging(self, mock_logger): unittest.mock.call.info("Found 1 identity groups with 2 total spans to export"), # Should log endpoint being used (default endpoint) unittest.mock.call.info( - f"Exporting 2 spans to endpoint: {DEFAULT_ENDPOINT_URL}/maven/agent365/agents/test-agent-456/traces?api-version=1 " + f"Exporting 2 spans to endpoint: {DEFAULT_ENDPOINT_URL}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1 " "(tenant: test-tenant-123, agent: test-agent-456)" ), # Should log token resolution success @@ -379,28 +377,21 @@ def test_export_uses_domain_override_when_env_var_set(self): 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(exporter, "_post_with_retries", return_value=True) as mock_post: - # Act - result = exporter.export(spans) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # 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 with complete URL - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - expected_url = f"https://{override_domain}/maven/agent365/agents/test-agent-456/traces?api-version=1" - self.assertEqual(url, expected_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}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + self.assertEqual(url, expected_url) def test_export_uses_default_endpoint_when_no_override(self): """Test that default endpoint URL is used when no override is set.""" @@ -428,9 +419,7 @@ def test_export_uses_default_endpoint_when_no_override(self): args, kwargs = mock_post.call_args url, body, headers = args - expected_url = ( - f"{DEFAULT_ENDPOINT_URL}/maven/agent365/agents/test-agent-456/traces?api-version=1" - ) + expected_url = f"{DEFAULT_ENDPOINT_URL}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_ignores_empty_domain_override(self): @@ -471,28 +460,21 @@ def test_export_uses_valid_url_override_with_https(self): spans = [self._create_mock_span("test_span")] - # Mock the PowerPlatformApiDiscovery class (should NOT be called since override is valid) - 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) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # 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 URL without duplicating protocol - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - expected_url = "https://override.example.com/maven/agent365/agents/test-agent-456/traces?api-version=1" - self.assertEqual(url, expected_url) + # Verify the call arguments - should use override URL without duplicating protocol + args, kwargs = mock_post.call_args + url, body, headers = args - # Verify PowerPlatformApiDiscovery was not called - mock_discovery_class.assert_not_called() + expected_url = "https://override.example.com/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + self.assertEqual(url, expected_url) def test_export_uses_valid_url_override_with_http(self): """Test that domain override with http:// protocol is accepted and used correctly.""" @@ -506,28 +488,21 @@ def test_export_uses_valid_url_override_with_http(self): spans = [self._create_mock_span("test_span")] - # Mock the PowerPlatformApiDiscovery class (should NOT be called since override is valid) - 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) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # 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 URL with http protocol - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - expected_url = "http://localhost:8080/maven/agent365/agents/test-agent-456/traces?api-version=1" - self.assertEqual(url, expected_url) + # Verify the call arguments - should use override URL with http protocol + args, kwargs = mock_post.call_args + url, body, headers = args - # Verify PowerPlatformApiDiscovery was not called - mock_discovery_class.assert_not_called() + expected_url = "http://localhost:8080/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + self.assertEqual(url, expected_url) def test_export_uses_valid_domain_override_with_port(self): """Test that domain override with port (no protocol) is accepted and https:// is prepended.""" @@ -541,28 +516,21 @@ def test_export_uses_valid_domain_override_with_port(self): spans = [self._create_mock_span("test_span")] - # Mock the PowerPlatformApiDiscovery class (should NOT be called since override is valid) - 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) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() + # 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 prepend https:// to domain with port - args, kwargs = mock_post.call_args - url, body, headers = args + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() - expected_url = "https://example.com:8080/maven/agent365/agents/test-agent-456/traces?api-version=1" - self.assertEqual(url, expected_url) + # Verify the call arguments - should prepend https:// to domain with port + args, kwargs = mock_post.call_args + url, body, headers = args - # Verify PowerPlatformApiDiscovery was not called - mock_discovery_class.assert_not_called() + expected_url = "https://example.com:8080/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + self.assertEqual(url, expected_url) def test_export_ignores_invalid_domain_with_protocol(self): """Test that domain override with invalid protocol is ignored.""" @@ -617,7 +585,7 @@ def test_export_ignores_invalid_domain_with_path(self): self.assertIn(DEFAULT_ENDPOINT_URL, url) def test_export_falls_back_to_il_tenant_on_default_failure(self): - """Test that IL tenant endpoint is tried when default endpoint fails.""" + """Test default endpoint failure (no fallback in current implementation).""" # Arrange os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) @@ -627,38 +595,18 @@ def test_export_falls_back_to_il_tenant_on_default_failure(self): spans = [self._create_mock_span("fallback_span")] - # Mock the PowerPlatformApiDiscovery class for fallback - 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 = "fallback-endpoint.com" - mock_discovery_class.return_value = mock_discovery - - # Mock _post_with_retries: first call fails (default), second call succeeds (fallback) - with patch.object( - exporter, "_post_with_retries", side_effect=[False, True] - ) as mock_post: - # Act - result = exporter.export(spans) - - # Assert - should succeed because fallback succeeded - self.assertEqual(result, SpanExportResult.SUCCESS) - self.assertEqual(mock_post.call_count, 2) - - # Verify first call uses default endpoint - first_url = mock_post.call_args_list[0][0][0] - self.assertIn(DEFAULT_ENDPOINT_URL, first_url) - - # Verify second call uses IL tenant fallback endpoint - second_url = mock_post.call_args_list[1][0][0] - self.assertIn("fallback-endpoint.com", second_url) - - # Verify PowerPlatformApiDiscovery was called for fallback - mock_discovery_class.assert_called_once_with("test") - mock_discovery.get_tenant_island_cluster_endpoint.assert_called_once_with( - "test-tenant-123" - ) + # Mock _post_with_retries: call fails (no fallback logic in current implementation) + with patch.object(exporter, "_post_with_retries", return_value=False) as mock_post: + # Act + result = exporter.export(spans) + + # Assert - should fail because no fallback exists in current implementation + self.assertEqual(result, SpanExportResult.FAILURE) + self.assertEqual(mock_post.call_count, 1) + + # Verify call uses default endpoint + first_url = mock_post.call_args_list[0][0][0] + self.assertIn(DEFAULT_ENDPOINT_URL, first_url) def test_export_no_fallback_when_domain_override_fails(self): """Test that no fallback happens when domain override is set and fails.""" @@ -671,30 +619,23 @@ def test_export_no_fallback_when_domain_override_fails(self): spans = [self._create_mock_span("no_fallback_span")] - # Mock the PowerPlatformApiDiscovery class (should NOT be called) - with patch( - "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" - ) as mock_discovery_class: - # Mock _post_with_retries to return False (failure) - with patch.object(exporter, "_post_with_retries", return_value=False) as mock_post: - # Act - result = exporter.export(spans) - - # Assert - should fail without fallback - self.assertEqual(result, SpanExportResult.FAILURE) - # Should only be called once (no fallback for override) - mock_post.assert_called_once() + # Mock _post_with_retries to return False (failure) + with patch.object(exporter, "_post_with_retries", return_value=False) as mock_post: + # Act + result = exporter.export(spans) - # Verify the override URL was used - args, kwargs = mock_post.call_args - url = args[0] - self.assertIn("custom-override.com", url) + # Assert - should fail without fallback + self.assertEqual(result, SpanExportResult.FAILURE) + # Should only be called once (no fallback for override) + mock_post.assert_called_once() - # Verify PowerPlatformApiDiscovery was NOT called - mock_discovery_class.assert_not_called() + # Verify the override URL was used + args, kwargs = mock_post.call_args + url = args[0] + self.assertIn("custom-override.com", url) def test_export_no_fallback_when_default_succeeds(self): - """Test that IL tenant fallback is not attempted when default endpoint succeeds.""" + """Test that no fallback is attempted when default endpoint succeeds.""" # Arrange os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None) @@ -704,21 +645,14 @@ def test_export_no_fallback_when_default_succeeds(self): spans = [self._create_mock_span("success_span")] - # Mock the PowerPlatformApiDiscovery class (should NOT be called) - with patch( - "microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery" - ) as mock_discovery_class: - # Mock _post_with_retries to return True (success) - with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: - # Act - result = exporter.export(spans) - - # Assert - self.assertEqual(result, SpanExportResult.SUCCESS) - mock_post.assert_called_once() - - # Verify PowerPlatformApiDiscovery was NOT called (no fallback needed) - mock_discovery_class.assert_not_called() + # Mock _post_with_retries to return True (success) + with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post: + # Act + result = exporter.export(spans) + + # Assert + self.assertEqual(result, SpanExportResult.SUCCESS) + mock_post.assert_called_once() if __name__ == "__main__": From 57bab169a4131c7cafe11c0774614114a24959ac Mon Sep 17 00:00:00 2001 From: "Nikhil Chitlur Navakiran (from Dev Box)" Date: Fri, 27 Feb 2026 01:28:20 +0530 Subject: [PATCH 5/5] update comments --- .../observability/core/exporters/agent365_exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 091fc54e..27b54e23 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 @@ -97,7 +97,7 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: payload = self._build_export_request(activities) body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) - # Resolve endpoint: domain override > default URL > IL tenant fallback + # Resolve endpoint: domain override > default URL if self._domain_override: endpoint = self._domain_override else: