Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,45 @@

All notable changes to this package will be documented in this file.

Comment thread
nikhilNava marked this conversation as resolved.
## [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.
Comment thread
nikhilNava marked this conversation as resolved.

---

### 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 "<path-to-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]
Comment thread
nikhilNava marked this conversation as resolved.

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
nikhilNava marked this conversation as resolved.
* Adds Bearer token via token_resolver(agentId, tenantId)
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 13 additions & 11 deletions tests/observability/core/test_agent365_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Comment thread
nikhilNava marked this conversation as resolved.
def test_export_uses_valid_domain_override_with_port(self):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion versioning/TARGET-VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.1
0.3.0
Loading