diff --git a/CHANGELOG.md b/CHANGELOG.md index fe171aa6..b73204a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ - Remove tenant_id, agent_id from config options ([#59](https://github.com/microsoft/opentelemetry-distro-python/pull/59)) +### Features Added + +- Match Upstream Changes: Update scope value to support new exporter path + ([#62](https://github.com/microsoft/opentelemetry-distro-python/pull/62)) + ## 0.1.0a3 (2026-04-22) ### Features Added diff --git a/src/microsoft/opentelemetry/a365/core/exporters/utils.py b/src/microsoft/opentelemetry/a365/core/exporters/utils.py index 6120cce9..9fe28624 100644 --- a/src/microsoft/opentelemetry/a365/core/exporters/utils.py +++ b/src/microsoft/opentelemetry/a365/core/exporters/utils.py @@ -208,9 +208,9 @@ def get_validated_domain_override() -> str | None: 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.""" endpoint_path = ( - f"/observabilityService/tenants/{tenant_id}/agents/{agent_id}/traces" + f"/observabilityService/tenants/{tenant_id}/otlp/agents/{agent_id}/traces" if use_s2s_endpoint - else f"/observability/tenants/{tenant_id}/agents/{agent_id}/traces" + else f"/observability/tenants/{tenant_id}/otlp/agents/{agent_id}/traces" ) parsed = urlparse(endpoint) @@ -239,7 +239,7 @@ def is_agent365_exporter_enabled() -> bool: return enable_exporter in ("true", "1", "yes", "on") -_A365_DEFAULT_SCOPE = "api://9b975845-388f-4429-889e-eab1ef63949c/.default" +_A365_DEFAULT_SCOPE = "api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite" def _create_fic_token_resolver() -> Callable[[str, str], Optional[str]]: diff --git a/src/microsoft/opentelemetry/a365/runtime/environment_utils.py b/src/microsoft/opentelemetry/a365/runtime/environment_utils.py index c9604460..eb5e5d6c 100644 --- a/src/microsoft/opentelemetry/a365/runtime/environment_utils.py +++ b/src/microsoft/opentelemetry/a365/runtime/environment_utils.py @@ -8,7 +8,7 @@ import os # Authentication scopes for different environments -PROD_OBSERVABILITY_SCOPE = "https://api.powerplatform.com/.default" +PROD_OBSERVABILITY_SCOPE = "api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite" # Cluster categories for different environments PROD_OBSERVABILITY_CLUSTER_CATEGORY = "prod" diff --git a/tests/a365/runtime/test_export_config_consistency.py b/tests/a365/runtime/test_export_config_consistency.py new file mode 100644 index 00000000..36ef0117 --- /dev/null +++ b/tests/a365/runtime/test_export_config_consistency.py @@ -0,0 +1,107 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Snapshot tests for observability export configuration. + +These tests pin the production export URL path and authentication scope together. +If the export URL path changes (e.g. from a backend migration), the scope must +be reviewed and updated in lockstep — and vice versa. A failure here is a +reminder to verify both values are consistent with the deployed backend. +""" + +import unittest + +from microsoft.opentelemetry.a365.core.exporters.agent365_exporter import ( + DEFAULT_ENDPOINT_URL, +) +from microsoft.opentelemetry.a365.core.exporters.utils import build_export_url +from microsoft.opentelemetry.a365.runtime.environment_utils import ( + PROD_OBSERVABILITY_SCOPE, + get_observability_authentication_scope, +) + + +class TestExportConfigConsistency(unittest.TestCase): + """Ensure export URL, endpoint, and auth scope stay in sync. + + These are intentionally pinned snapshot values. If any of the three + production constants change (endpoint, scope, URL path), **all three + tests below will likely need updating together**. That forced review is + the whole point — it prevents one value from drifting without the others. + """ + + # ---- pinned production values ---- + + EXPECTED_ENDPOINT = "https://agent365.svc.cloud.microsoft" + EXPECTED_SCOPE = "api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite" + EXPECTED_STANDARD_PATH = "/observability/tenants/{tid}/otlp/agents/{aid}/traces" + EXPECTED_S2S_PATH = "/observabilityService/tenants/{tid}/otlp/agents/{aid}/traces" + + # ---- snapshot assertions ---- + + def test_default_endpoint_url(self): + """DEFAULT_ENDPOINT_URL must match the expected production endpoint.""" + self.assertEqual( + DEFAULT_ENDPOINT_URL, + self.EXPECTED_ENDPOINT, + "DEFAULT_ENDPOINT_URL changed — also review PROD_OBSERVABILITY_SCOPE " + "and build_export_url() path. All three must stay in sync.", + ) + + def test_prod_observability_scope_value(self): + """PROD_OBSERVABILITY_SCOPE must match the expected production scope.""" + self.assertEqual( + PROD_OBSERVABILITY_SCOPE, + self.EXPECTED_SCOPE, + "PROD_OBSERVABILITY_SCOPE changed — also review DEFAULT_ENDPOINT_URL " + "and build_export_url() path. All three must stay in sync.", + ) + + def test_export_url_standard_path_structure(self): + """Standard export URL must use the pinned path pattern.""" + url = build_export_url(self.EXPECTED_ENDPOINT, "a1", "t1") + expected = ( + f"{self.EXPECTED_ENDPOINT}" + f"{self.EXPECTED_STANDARD_PATH.format(tid='t1', aid='a1')}?api-version=1" + ) + self.assertEqual( + url, + expected, + "Standard export URL path changed — also review PROD_OBSERVABILITY_SCOPE " + "and DEFAULT_ENDPOINT_URL. All three must stay in sync.", + ) + + def test_export_url_s2s_path_structure(self): + """S2S export URL must use the pinned path pattern.""" + url = build_export_url(self.EXPECTED_ENDPOINT, "a1", "t1", use_s2s_endpoint=True) + expected = ( + f"{self.EXPECTED_ENDPOINT}" + f"{self.EXPECTED_S2S_PATH.format(tid='t1', aid='a1')}?api-version=1" + ) + self.assertEqual( + url, + expected, + "S2S export URL path changed — also review PROD_OBSERVABILITY_SCOPE " + "and DEFAULT_ENDPOINT_URL. All three must stay in sync.", + ) + + def test_scope_and_endpoint_are_coherent(self): + """Auth scope and endpoint must both target the agent365 service. + + This is a coarse sanity check: if the endpoint domain changes away + from 'agent365' but the scope still references the old AAD app, or + vice versa, something is likely wrong. + """ + scopes = get_observability_authentication_scope() + self.assertEqual(len(scopes), 1) + scope = scopes[0] + + # Scope should reference the Agent365 Observability permission + self.assertIn("Agent365.Observability", scope) + # Endpoint should be the agent365 service + self.assertIn("agent365", DEFAULT_ENDPOINT_URL) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/a365/test_utils.py b/tests/a365/test_utils.py index 3aa82c0d..fd7ee6a1 100644 --- a/tests/a365/test_utils.py +++ b/tests/a365/test_utils.py @@ -154,7 +154,7 @@ def test_standard_endpoint(self): url = build_export_url("https://example.com", "agent1", "tenant1") self.assertEqual( url, - "https://example.com/observability/tenants/tenant1/agents/agent1/traces?api-version=1", + "https://example.com/observability/tenants/tenant1/otlp/agents/agent1/traces?api-version=1", ) def test_s2s_endpoint(self):