Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b4da17a
Add A365 S2S exporter sample
nikhilc-microsoft Jun 24, 2026
389aa74
Address PR feedback for A365 S2S sample
nikhilc-microsoft Jul 9, 2026
906b7fc
Fix broad-exception pylint pragma placement for pinned pylint
nikhilc-microsoft Jul 9, 2026
a120b05
Cap openai<2.45.0 to fix openai-agents integration test failures
nikhilc-microsoft Jul 9, 2026
d6e627e
Document A365 S2S store validation design
nikhilc-microsoft Sep 25, 2026
ed4e0b3
Plan A365 S2S store validation update
nikhilc-microsoft Sep 25, 2026
d21df75
Ignore local git worktrees
nikhilc-microsoft Sep 25, 2026
8f4a1a6
Merge main into A365 S2S sample
nikhilc-microsoft Sep 25, 2026
bc339c4
Restore A365 S2S sample README entry
nikhilc-microsoft Sep 25, 2026
d9d35f2
Fix A365 S2S sample setup guidance
nikhilc-microsoft Sep 25, 2026
512bf94
Remove dotenv from A365 S2S sample
nikhilc-microsoft Sep 25, 2026
bcd0fa4
Use shell-neutral S2S sample path
nikhilc-microsoft Sep 25, 2026
55914d7
Remove Task 1 S2S README catalog row
nikhilc-microsoft Sep 25, 2026
8812f4a
Add store-ready telemetry to A365 S2S sample
nikhilc-microsoft Sep 25, 2026
9442404
Remove implementation planning artifacts
nikhilc-microsoft Sep 25, 2026
f942536
Fix S2S sample validation
nikhilc-microsoft Sep 25, 2026
3b9fb97
Design S2S sample organization
nikhilc-microsoft Sep 25, 2026
7990d72
Extract validated S2S sample configuration
nikhilc-microsoft Sep 28, 2026
8cfa132
Extract S2S token resolver and telemetry scenario
nikhilc-microsoft Sep 28, 2026
d2ba7dc
Restore S2S token flow documentation
nikhilc-microsoft Sep 28, 2026
1e91709
Finalize S2S sample entry point
nikhilc-microsoft Sep 28, 2026
cae9efc
Address S2S review findings
nikhilc-microsoft Sep 28, 2026
e32669f
Merge remote-tracking branch 'origin/main' into nikhilc/add-a365-s2s-…
nikhilc-microsoft Sep 28, 2026
8006910
Remove sample-specific test suite
nikhilc-microsoft Sep 28, 2026
d86d8fb
Remove sample changelog entry
nikhilc-microsoft Sep 28, 2026
38196ca
Reuse Blueprint client ID in S2S sample
nikhilc-microsoft Sep 28, 2026
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ remain enabled by default.
|---|---|---|
| [samples/a365/exporter.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/a365/exporter.py) | A365 | LangChain with A365 auto-instrumentation |
| [samples/a365/manual_telemetry.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/a365/manual_telemetry.py) | A365 | Manual instrumentation using all scope classes |
| [samples/a365/s2s/s2s_exporter.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/a365/s2s/s2s_exporter.py) | A365 | Store-ready S2S export with all manual observability scopes |
| [samples/distro/tracing.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/distro/tracing.py) | Azure Monitor | Basic tracing |
| [samples/distro/metrics.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/distro/metrics.py) | Azure Monitor | Metrics collection |
| [samples/distro/logging_sample.py](https://github.com/microsoft/opentelemetry-distro-python/blob/main/samples/distro/logging_sample.py) | Azure Monitor | Log export |
Expand Down
109 changes: 109 additions & 0 deletions samples/a365/s2s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# A365 S2S Exporter Sample

This sample exports [Agent 365](https://learn.microsoft.com/en-us/microsoft-agent-365/)
telemetry through the service-to-service (S2S) endpoint and demonstrates every
public manual observability scope.

The service authenticates on its own behalf. The token resolver uses the
Blueprint application credentials to obtain an agent-instance token and then an
application token for the A365 observability scope. It deliberately omits the
agentic-user FIC step and does not emit `microsoft.agent.user.id` or
`microsoft.agent.user.email`.

## Sample organization

| File | Responsibility |
| --- | --- |
| `s2s_exporter.py` | Executable entry point that loads configuration, enables S2S export, and runs the scenario |
| `sample_config.py` | Environment validation and deterministic agent, caller, and request inputs |
| `token_resolver.py` | Blueprint-to-agent token exchange, observability token acquisition, and token caching |
| `sample_scenario.py` | Deterministic manual-scope telemetry used for Store validation |

## Prerequisites

- Python 3.10+
- [uv](https://docs.astral.sh/uv/)
- A Blueprint app registration granted the
`Agent365.Observability.OtelWrite` **application** permission with admin
consent. See [`MIGRATION_A365.md`](../../../MIGRATION_A365.md) under
"Troubleshooting - Permissions and Setup".
- Real tenant, Blueprint app client ID, agent app instance client ID, and human caller
values from the deployment being validated. Angle-bracket placeholders are
rejected at startup.

## Configure and run

PowerShell:

```powershell
$env:ENABLE_OBSERVABILITY = "true"
$env:ENABLE_A365_OBSERVABILITY_EXPORTER = "true"
$env:CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID = "<blueprint-app-client-id>"
$env:CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET = "<blueprint-app-secret>"
$env:CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID = "<tenant-guid>"
$env:A365_AGENT_APP_INSTANCE_ID = "<agent-app-instance-id>"
$env:A365_CALLER_USER_ID = "<caller-user-id>"
$env:A365_CALLER_USER_EMAIL = "<caller-user-email>"
$env:A365_CALLER_CLIENT_IP = "<caller-client-ip>"

uv run --with msal python samples\a365\s2s\s2s_exporter.py
```

Bash:

```bash
export ENABLE_OBSERVABILITY=true
export ENABLE_A365_OBSERVABILITY_EXPORTER=true
export CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID="<blueprint-app-client-id>"
export CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET="<blueprint-app-secret>"
export CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID="<tenant-guid>"
export A365_AGENT_APP_INSTANCE_ID="<agent-app-instance-id>"
export A365_CALLER_USER_ID="<caller-user-id>"
export A365_CALLER_USER_EMAIL="<caller-user-email>"
export A365_CALLER_CLIENT_IP="<caller-client-ip>"

uv run --with msal python samples/a365/s2s/s2s_exporter.py
```

`a365_use_s2s_endpoint=True` routes exports to the S2S ingest endpoint. The
tenant and agent ID in each export request must match the configured tenant and
agent app instance client ID; the resolver rejects mismatches before token
acquisition. `gen_ai.agent.id` and the `{agentId}` export URL segment therefore
use `A365_AGENT_APP_INSTANCE_ID`, not the Blueprint client ID.
The Blueprint telemetry attribute uses the same
`CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID` value used to authenticate
the Blueprint application.

## Scope and Store-validation coverage

| Scope | Sample behavior | Store validation |
| --- | --- | --- |
| `InvokeAgentScope` | Root request, agent/Blueprint/caller identity, endpoint, input, and final output | Required |
| `InferenceScope` | Model/provider, messages, token usage, and finish reason | Required |
| `ExecuteToolScope` | Tool identity, arguments, and result | Required |
| `OutputScope` | Child span representing asynchronous/final output | Validate before publishing |
| `ApplyGuardrailScope` | Input-safety decision and finding event | Additional security telemetry |

The sample populates the publishing attributes documented in
[Validate for Store publishing](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/observability?tabs=python#validate-for-store-publishing),
including the tenant, agent, Blueprint, human caller, client address, channel,
conversation, operation, message, endpoint, model, and tool fields applicable
to each span. Human caller identity uses the standard `user.id` and
`user.email` attributes. S2S agentic-user attributes
`microsoft.agent.user.id` and `microsoft.agent.user.email` are intentionally
absent.

## Verify export

The sample enables DEBUG logging for the A365 exporter. A successful run reports
the HTTP status and correlation ID on stderr:

```text
DEBUG ...agent365_exporter: HTTP 200 success on attempt 1. Correlation ID: <id>. Response: ...
```

HTTP 401 usually indicates an invalid token audience or tenant. HTTP 403
usually indicates missing `Agent365.Observability.OtelWrite` application
permission, missing admin consent, or an agent/Blueprint identity that is not
onboarded for the tenant. Use the logged correlation ID when investigating
either response.
63 changes: 63 additions & 0 deletions samples/a365/s2s/s2s_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""
Sample: A365 Exporter with S2S (Service-to-Service) authentication

Demonstrates how to export A365 telemetry using the S2S endpoint with a
service-principal token resolver. The resolver performs the app-to-instance
exchange and acquires an application token for the A365 observability scope;
the deterministic scenario exercises every public manual observability scope.

Run this file directly from the repository root as documented in
samples/a365/s2s/README.md.
"""

import logging

from microsoft.opentelemetry import use_microsoft_opentelemetry

from sample_config import SampleConfig
from sample_scenario import emit_sample_telemetry
from token_resolver import build_s2s_token_resolver


def _configure_export_logging() -> None:
"""Surface exporter status and correlation IDs without duplicate handlers."""
exporter_logger = logging.getLogger("microsoft.opentelemetry.a365.core.exporters.agent365_exporter")
exporter_logger.setLevel(logging.DEBUG)
exporter_logger.propagate = False
handler_name = "a365-s2s-sample-export-logging"
if any(getattr(handler, "name", None) == handler_name for handler in exporter_logger.handlers):
return
handler = logging.StreamHandler()
handler.name = handler_name
handler.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
exporter_logger.addHandler(handler)


def main() -> None:
config = SampleConfig.load()
_configure_export_logging()
use_microsoft_opentelemetry(
enable_a365=True,
a365_use_s2s_endpoint=True,
a365_token_resolver=build_s2s_token_resolver(config),
)
print("Telemetry configured for S2S export.\n")

emit_sample_telemetry(
config.create_agent_details(),
config.create_user_details(),
config.create_request(),
)

print(
"\nDone. All spans have been recorded. They are flushed to the A365 "
"batch span processor and exported on shutdown when "
"ENABLE_A365_OBSERVABILITY_EXPORTER=true."
)


if __name__ == "__main__":
main()
83 changes: 83 additions & 0 deletions samples/a365/s2s/sample_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

"""Validated configuration and deterministic inputs for the A365 S2S sample."""

import os
from dataclasses import dataclass, field

from microsoft.opentelemetry.a365.core import AgentDetails, Channel, Request, UserDetails

A365_SERVICE_CLIENT_ID_ENV = "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID"
A365_SERVICE_CLIENT_SECRET_ENV = "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET"
A365_SERVICE_TENANT_ID_ENV = "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID"
A365_AGENT_APP_INSTANCE_ID_ENV = "A365_AGENT_APP_INSTANCE_ID"
A365_CALLER_USER_ID_ENV = "A365_CALLER_USER_ID"
A365_CALLER_USER_EMAIL_ENV = "A365_CALLER_USER_EMAIL"
A365_CALLER_CLIENT_IP_ENV = "A365_CALLER_CLIENT_IP"


def _require_env(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value or (value.startswith("<") and value.endswith(">")):
raise SystemExit(
f"Environment variable {name} is not set. Set the required shell variables "
"and run the sample as described in samples/a365/s2s/README.md."
)
return value


@dataclass(frozen=True)
class SampleConfig:
"""Validated environment settings for the A365 S2S sample."""

client_id: str
client_secret: str = field(repr=False)
tenant_id: str
agent_instance_id: str
caller_user_id: str
caller_user_email: str
caller_client_ip: str

@classmethod
def load(cls) -> "SampleConfig":
"""Load all required settings from the environment."""
return cls(
client_id=_require_env(A365_SERVICE_CLIENT_ID_ENV),
client_secret=_require_env(A365_SERVICE_CLIENT_SECRET_ENV),
tenant_id=_require_env(A365_SERVICE_TENANT_ID_ENV),
agent_instance_id=_require_env(A365_AGENT_APP_INSTANCE_ID_ENV),
caller_user_id=_require_env(A365_CALLER_USER_ID_ENV),
caller_user_email=_require_env(A365_CALLER_USER_EMAIL_ENV),
caller_client_ip=_require_env(A365_CALLER_CLIENT_IP_ENV),
)

def create_agent_details(self) -> AgentDetails:
"""Create the deterministic agent identity used by the sample."""
return AgentDetails(
agent_id=self.agent_instance_id,
agent_name="Weather Agent",
agent_description="Answers weather-related questions",
agent_blueprint_id=self.client_id,
tenant_id=self.tenant_id,
provider_name="azure-openai",
agent_version="1.0.0",
)

def create_user_details(self) -> UserDetails:
"""Create the deterministic human caller used by the sample."""
return UserDetails(
user_id=self.caller_user_id,
user_email=self.caller_user_email,
user_name="Sample Caller",
user_client_ip=self.caller_client_ip,
)

def create_request(self) -> Request:
"""Create the deterministic request used by the sample."""
return Request(
content="What's the weather in Seattle?",
session_id="session-s2s-123",
channel=Channel(name="service", link="https://contoso.example/a365-s2s"),
conversation_id="conv-s2s-789",
)
Loading
Loading