diff --git a/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md b/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md index 8de2f04c..dcf0f6a9 100644 --- a/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md +++ b/libraries/microsoft-agents-a365-observability-core/CHANGELOG.md @@ -2,7 +2,45 @@ All notable changes to this package will be documented in this file. -## [Unreleased] +## [0.3.0] + +### Breaking Changes + +- **New permission required: `Agent365.Observability.OtelWrite`** — The observability exporter now requires this scope as both a delegated and application permission on your agent blueprint. See [Upgrade Instructions](#upgrade-instructions-observability-permission-for-existing-agents) below. + +--- + +### Upgrade Instructions: Observability Permission for Existing Agents + +Existing agent blueprints need `Agent365.Observability.OtelWrite` granted as both a **delegated permission** and an **application permission**. Choose either option below. + +#### Option A — Agent 365 CLI (requires both config files) + +Requires `a365.config.json` and `a365.generated.config.json` in your config directory, a Global Administrator account, and [Agent 365 CLI v1.1.139-preview](https://www.nuget.org/packages/Microsoft.Agents.A365.DevTools.Cli/1.1.139-preview) or later. + +``` +a365 setup admin --config-dir "" +``` + +This grants all missing permissions including the new Observability scopes. + +#### Option B — Entra Portal (no config files required) + +Requires Global Administrator access to the blueprint app registration. + +1. Go to **Entra portal** > **App registrations** > select your Blueprint app +2. Go to **API permissions** > **Add a permission** > **APIs my organization uses** > search for `9b975845-388f-4429-889e-eab1ef63949c` +3. Select **Delegated permissions** > check `Agent365.Observability.OtelWrite` > **Add permissions** +4. Repeat step 2–3, this time select **Application permissions** > check `Agent365.Observability.OtelWrite` > **Add permissions** +5. Click **Grant admin consent** and confirm + +Both `Agent365.Observability.OtelWrite` (Delegated) and `Agent365.Observability.OtelWrite` (Application) should show **Granted** status. + +> **Note:** If your agent is autonomous, you only need the **Application permission**. The delegated permission is required for agents that authenticate via a user session. + +--- + +## [0.2.1.dev46] ### Breaking Changes 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 5920dec3..fb170cec 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 @@ -46,7 +46,8 @@ class _Agent365Exporter(SpanExporter): Agent 365 span exporter for Agent 365: * Partitions spans by (tenantId, agentId) * Builds OTLP-like JSON: resourceSpans -> scopeSpans -> spans - * POSTs per group to https://{endpoint}/maven/agent365/agents/{agentId}/traces?api-version=1 + * POSTs per group to https://{endpoint}/observability/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1 + * or, when use_s2s_endpoint is True, https://{endpoint}/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1 * Adds Bearer token via token_resolver(agentId, tenantId) """ 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 d48bd1ab..aabe9953 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 @@ -221,9 +221,9 @@ def build_export_url( The fully constructed export URL with path and query parameters. """ 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) diff --git a/tests/observability/core/test_agent365_exporter.py b/tests/observability/core/test_agent365_exporter.py index 8cc6fc91..0220b52c 100644 --- a/tests/observability/core/test_agent365_exporter.py +++ b/tests/observability/core/test_agent365_exporter.py @@ -124,7 +124,7 @@ def test_export_success(self): self.assertIn(DEFAULT_ENDPOINT_URL, url) self.assertIn( - "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + "/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces", url ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -237,10 +237,11 @@ def test_s2s_endpoint_path_when_enabled(self): self.assertIn(DEFAULT_ENDPOINT_URL, url) self.assertIn( - "/observabilityService/tenants/test-tenant-123/agents/test-agent-456/traces", url + "/observabilityService/tenants/test-tenant-123/otlp/agents/test-agent-456/traces", + url, ) self.assertNotIn( - "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + "/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces", url ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -269,10 +270,11 @@ def test_default_endpoint_path_when_s2s_disabled(self): self.assertIn(DEFAULT_ENDPOINT_URL, url) self.assertIn( - "/observability/tenants/test-tenant-123/agents/test-agent-456/traces", url + "/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces", url ) self.assertNotIn( - "/observabilityService/tenants/test-tenant-123/agents/test-agent-456/traces", url + "/observabilityService/tenants/test-tenant-123/otlp/agents/test-agent-456/traces", + url, ) self.assertEqual(headers["authorization"], "Bearer test_token_123") self.assertEqual(headers["content-type"], "application/json") @@ -318,7 +320,7 @@ def test_export_logging(self, mock_logger): unittest.mock.call.debug("Found 1 identity groups with 2 total spans to export"), # Should log endpoint being used at DEBUG (default endpoint) unittest.mock.call.debug( - f"Exporting 2 spans to endpoint: {DEFAULT_ENDPOINT_URL}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1 " + f"Exporting 2 spans to endpoint: {DEFAULT_ENDPOINT_URL}/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1 " "(tenant: test-tenant-123, agent: test-agent-456)" ), # Should log token resolution success at DEBUG @@ -391,7 +393,7 @@ def test_export_uses_domain_override_when_env_var_set(self): args, kwargs = mock_post.call_args url, body, headers = args - expected_url = f"https://{override_domain}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + expected_url = f"https://{override_domain}/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_uses_default_endpoint_when_no_override(self): @@ -420,7 +422,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}/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + expected_url = f"{DEFAULT_ENDPOINT_URL}/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_ignores_empty_domain_override(self): @@ -474,7 +476,7 @@ def test_export_uses_valid_url_override_with_https(self): args, kwargs = mock_post.call_args url, body, headers = args - expected_url = "https://override.example.com/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + expected_url = "https://override.example.com/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_uses_valid_url_override_with_http(self): @@ -502,7 +504,7 @@ def test_export_uses_valid_url_override_with_http(self): args, kwargs = mock_post.call_args url, body, headers = args - expected_url = "http://localhost:8080/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + expected_url = "http://localhost:8080/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_uses_valid_domain_override_with_port(self): @@ -530,7 +532,7 @@ def test_export_uses_valid_domain_override_with_port(self): args, kwargs = mock_post.call_args url, body, headers = args - expected_url = "https://example.com:8080/observability/tenants/test-tenant-123/agents/test-agent-456/traces?api-version=1" + expected_url = "https://example.com:8080/observability/tenants/test-tenant-123/otlp/agents/test-agent-456/traces?api-version=1" self.assertEqual(url, expected_url) def test_export_ignores_invalid_domain_with_protocol(self): diff --git a/versioning/TARGET-VERSION b/versioning/TARGET-VERSION index 12980388..9325c3cc 100644 --- a/versioning/TARGET-VERSION +++ b/versioning/TARGET-VERSION @@ -1 +1 @@ -0.2.1 +0.3.0 \ No newline at end of file