Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The Agent 365 exporter requires a token resolver to authenticate when exporting telemetry. This guide covers setup for agents built with the Microsoft 365 Agents SDK, covering both Agent 365-enabled agents and custom engine agents across .NET, Python, and Node.js.
For distro installation, general configuration, and non-Agent SDK scenarios, see Microsoft OpenTelemetry Distro.
Overview
There are four authentication scenarios, depending on your agent type and how it acquires tokens. Token acquisition can use On-Behalf-Of flow (OBO) or Service-to-Service (S2S). Choose the scenario that matches your setup:
| Scenario | Description |
|---|---|
| Agent 365-enabled using OBO | The distro's built-in AgenticTokenCache handles token acquisition automatically on the delegated route. No custom resolver needed. This route needs the delegated Agent365.Observability.OtelWrite permission and admin consent, which a365 setup all doesn't configure for blueprint agents. |
| Agent 365-enabled using S2S | The agent acquires an app-only token by using the agentic identity chain (getAgenticApplicationToken + Microsoft Authentication Libraries (MSAL)). Requires a custom TokenResolver and the S2S endpoint option. A registered agent instance doesn't need an Observability permission or admin consent, so this is the recommended approach for blueprint agents set up with a365 setup all. |
| Custom engine using OBO | The agent gets a user token via Azure Bot OAuth, scoped to the observability API. Requires a custom TokenResolver, an Azure Bot OAuth connection, and the delegated Agent365.Observability.OtelWrite permission with admin consent. |
| Custom engine using S2S | The agent acquires an app-only token using client credentials. Requires a custom TokenResolver. The app registration must be a standard (non-agentic) app. Standard app registrations aren't registered agent instances, so they need the Agent365.Observability.OtelWrite application permission with admin consent. |
Agent 365-enabled using OBO
Agent 365-enabled agents receive requests with agentic identity (agenticAppId, agenticUserId) from the Agent 365 platform. By using OBO, the distro's built-in AgenticTokenCache handles token acquisition automatically on the delegated route, so you don't need a custom token resolver.
Prerequisites
Microsoft Entra app registration: A service principal (app registration) with Client ID, Client Secret, and Tenant ID.
Delegated API permissions: Add
Agent365.Observability.OtelWrite(Delegated) and grant admin consent. Thea365 setup allcommand doesn't add this permission for blueprint agents. A Global Administrator can add it and grant admin consent by running the following command:a365 setup permissions custom --resource-app-id 9b975845-388f-4429-889e-eab1ef63949c --scopes Agent365.Observability.OtelWriteFor more options, including the Microsoft Entra admin center, see Grant the permission.
Tip
To avoid the delegated Observability permission and admin consent, use Agent 365-enabled using S2S for blueprint agents set up with a365 setup all.
Setup
On each turn, your agent calls the RegisterObservability function with the turn context. The built-in cache uses the user's delegated token from the AgenticUserAuthorization handler to perform an OBO exchange, acquiring a token scoped to Agent365.Observability.OtelWrite.
For full setup instructions including packages, configuration, and code examples, see Agentic token cache with Agent Framework apps.
Agent 365-enabled using S2S
Agent 365-enabled agents can also use S2S (service-to-service) authentication instead of OBO. This path is recommended for blueprint agents set up with a365 setup all. The agent acquires a token using its own service principal identity via a two-step agentic identity chain:
- Call
getAgenticApplicationToken(tenantId, agentId)to get a client credentials token with the Federated Managed Identity (FMI) path. - Call MSAL
acquireTokenForClientwith the app token asclientAssertionand scopeapi://9b975845-388f-4429-889e-eab1ef63949c/.default.
Note
Federated Managed Identity (FMI) is an architecture where a managed identity participates in workload identity federation via federated identity credentials, enabling token exchange and secretless authentication based on trust relationships between identities.
The Microsoft OpenTelemetry distro defaults to the delegated route, so you must provide a custom TokenResolver and set the S2S endpoint option for your language, such as UseS2SEndpoint = true, useS2SEndpoint: true, or a365_use_s2s_endpoint=True.
Prerequisites
- Agent 365 registration: The agent instance must be registered with Agent 365. The
a365 setup allcommand registers blueprint agent instances. Creating a Microsoft Entra identity alone doesn't register an agent instance. - Blueprint credential for FMI exchange: Use a blueprint client secret or managed identity to get the Federated Managed Identity (FMI) token for
api://AzureADTokenExchange/.default, then exchange it for the agent-instance Observability token. - Matching identity values: The agent ID and tenant ID in baggage and the export URL must match the agent instance client ID and tenant in the app-only token.
Note
An Agent 365-registered agent instance can export on the S2S route with an app-only token that has no Agent365.Observability.OtelWrite role. It doesn't need an Observability permission or admin consent. Unregistered identities, including standard app registrations, still need the Agent365.Observability.OtelWrite app role and admin consent.
Step 1: Environment configuration
The following code examples show how to set the required connection, tenant, client credential, and observability exporter environment settings before enabling the custom S2S token flow.
No AgenticUserAuthorization handler needed. S2S uses the manual agentic identity chain (get_agentic_application_token + MSAL acquire_token_for_client) to get a token scoped to the observability resource.
CONNECTIONSMAP__0__SERVICEURL=*
CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<your-client-id>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<your-client-secret>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<your-tenant-id>
ENABLE_A365_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=true
Step 2: Configure the distro with custom token resolver
The following examples show how to enable Agent 365 exporting and register a custom TokenResolver so the exporter can retrieve S2S tokens for each agent and tenant.
from microsoft.opentelemetry import use_microsoft_opentelemetry
_token_cache: dict[str, str] = {}
def token_resolver(agent_id: str, tenant_id: str) -> str | None:
return _token_cache.get(f"{agent_id}:{tenant_id}")
use_microsoft_opentelemetry(
enable_a365=True,
a365_token_resolver=token_resolver,
a365_use_s2s_endpoint=True,
a365_enable_observability_exporter=True,
)
Step 3: Acquire and cache the S2S token
On each incoming message, acquire the S2S token through the agentic identity chain and cache it for the resolver.
import asyncio
from msal import ConfidentialClientApplication
from microsoft.opentelemetry.a365.core import BaggageBuilder, InvokeAgentScope, InvokeAgentScopeDetails, Request
OBSERVABILITY_S2S_SCOPE = "api://9b975845-388f-4429-889e-eab1ef63949c/.default"
async def get_agentic_s2s_token(connection, tenant_id: str, agent_id: str) -> str:
# Step 1: Get agentic application token (client_credentials + fmi_path)
app_token = await connection.get_agentic_application_token(tenant_id, agent_id)
if not app_token:
raise ValueError(f"Failed to get agentic app token for agent {agent_id}")
# Step 2: Exchange for observability-scoped token
cca = ConfidentialClientApplication(
client_id=agent_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential={"client_assertion": app_token},
)
result = await asyncio.to_thread(
lambda: cca.acquire_token_for_client(scopes=[OBSERVABILITY_S2S_SCOPE])
)
if not result or "access_token" not in result:
raise ValueError(f"Token acquisition failed: {result}")
return result["access_token"]
# In your message handler : use SDK helpers to get agent/tenant from the activity:
@AGENT_APP.activity("message")
async def on_message(context: TurnContext, _state: TurnState):
# get_agentic_instance_id reads from recipient (SDK convention)
agent_id = context.activity.get_agentic_instance_id()
tenant_id = context.activity.get_agentic_tenant_id()
# Acquire S2S token and cache BEFORE creating spans
connection = CONNECTION_MANAGER.get_connection("SERVICE_CONNECTION")
token = await get_agentic_s2s_token(connection, tenant_id, agent_id)
_token_cache[f"{agent_id}:{tenant_id}"] = token
# Wrap spans in BaggageBuilder so the exporter can resolve the token
request = Request(content=user_message, session_id=None)
with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build():
invoke_scope = InvokeAgentScope.start(request, InvokeAgentScopeDetails(), agent_details)
with invoke_scope:
invoke_scope.record_input_messages([user_message])
invoke_scope.record_output_messages([response])
Important
The manual two-step flow (get_agentic_application_token + MSAL acquire_token_for_client) is required for S2S. AgenticUserAuthorization.get_token() returns a token scoped to 5a807f24-.../.default (Bot Framework), not the observability resource api://9b975845-.../.default : the S2S endpoint rejects it with 401 InvalidAudience.
- Use
context.activity.get_agentic_instance_id()andget_agentic_tenant_id()to read the agent and tenant from the activity (reads fromrecipientper SDK convention). - Acquire and cache the S2S token before creating spans. The exporter's
BatchSpanProcessormight flush before the handler finishes : if the token isn't cached yet, the export fails. - Wrap all A365 scopes in
BaggageBuilderso the exporter knows which agent and tenant to resolve tokens for. Without baggage, spans are silently dropped with "No spans with tenant/agent identity found."
Important
Before your resolver returns and caches the final Observability token:
- Validate that the token is app-only. Accept
idtyp=app. Ifidtypis absent, accept only a token that has a non-emptyrolesclaim or a non-emptyoidclaim equal tosub. Reject tokens that have anscpclaim or anotheridtypvalue. - Check that
audis9b975845-388f-4429-889e-eab1ef63949corapi://9b975845-388f-4429-889e-eab1ef63949c, and that the token isn't expired. - Never send the intermediate blueprint assertion, a blueprint token, or a user or OBO token to Observability.
- Cache the final token and refresh it before it expires. The exporter calls the resolver once for each tenant and agent identity in every export batch.
For complete implementations that use the distro, see the Agent 365 samples for Node.js, Python, and .NET.
Custom engine using OBO
Custom engine agents use standard app registrations with Azure Bot OAuth connections, not the agentic identity chain. By using OBO, the agent gets a user token through Azure Bot OAuth that's already scoped to the A365 observability API by the Bot Framework Token Service. A single getToken or GetTurnTokenAsync call returns the correctly scoped token, so you don't need exchangeToken.
Prerequisites
Microsoft Entra app registration with Delegated API permissions. Add Agent365.Observability.OtelWrite (Delegated) and grant admin consent.
Important
The agentId in the token cache must match the app registration's Client ID - not the activity's agenticAppId, which doesn't exist for custom engine agents. The export URL includes the agentId, and a mismatch causes HTTP 403.
Step 1: Environment and app configuration
The following examples show how to configure your app and runtime environment, including service connection values, tenant and client settings, and required authorization mappings.
# .env
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<your-client-id>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<your-client-secret>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<your-tenant-id>
CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION
CONNECTIONSMAP__0__SERVICEURL=*
# Auth handler config : TYPE is required, name is uppercased by load_configuration_from_env
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__OBOCONNECTIONPROFILE__TYPE=UserAuthorization
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__OBOCONNECTIONPROFILE__SETTINGS__AZUREBOTOAUTHCONNECTIONNAME=oboConnectionProfile
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__OBOCONNECTIONPROFILE__SETTINGS__SCOPES=api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite
ENABLE_A365_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=true
Important
load_configuration_from_env uppercases all environment variable keys. The handler name becomes OBOCONNECTIONPROFILE and you must reference it with that exact casing in auth_handlers and get_token() calls. Missing TYPE causes Auth handler ... not recognized or not configured at runtime.
Step 2: Configure the distro for OBO
The following examples show how to enable Agent 365 exporting, keep the exporter on the OBO endpoint, and register a custom TokenResolver that returns delegated tokens during export.
from microsoft.opentelemetry import use_microsoft_opentelemetry
_token_cache: dict[str, str] = {}
def token_resolver(agent_id: str, tenant_id: str) -> str | None:
return _token_cache.get(f"{agent_id}:{tenant_id}")
environ["ENABLE_A365_OBSERVABILITY_EXPORTER"] = "true"
use_microsoft_opentelemetry(
enable_a365=True,
a365_token_resolver=token_resolver,
a365_use_s2s_endpoint=False, # OBO uses /observability endpoint
a365_enable_observability_exporter=True,
)
Note
OBO mode requires jwt_authorization_middleware on the aiohttp Application (validates the inbound JWT (JSON Web Token) from Bot Framework). The S2S/emulator path shouldn't include this middleware.
from microsoft_agents.hosting.aiohttp import jwt_authorization_middleware
app = Application(middlewares=[jwt_authorization_middleware])
Step 3: Acquire the OBO token
The following examples show how to request a delegated OBO token from the configured Azure Bot OAuth connection, then cache it by app client and tenant for the exporter.
from microsoft_agents.hosting.core import (
AgentApplication, Authorization, MemoryStorage, TurnContext, TurnState,
)
from microsoft_agents.activity import load_configuration_from_env
from microsoft_agents.authentication.msal import MsalConnectionManager
from microsoft_agents.hosting.aiohttp import CloudAdapter
# Auth handlers are loaded from .env via load_configuration_from_env (see Environment config above)
agents_sdk_config = load_configuration_from_env(environ)
STORAGE = MemoryStorage()
CONNECTION_MANAGER = MsalConnectionManager(**agents_sdk_config)
ADAPTER = CloudAdapter(connection_manager=CONNECTION_MANAGER)
AUTHORIZATION = Authorization(STORAGE, CONNECTION_MANAGER, **agents_sdk_config)
AGENT_APP = AgentApplication[TurnState](
storage=STORAGE, adapter=ADAPTER, authorization=AUTHORIZATION, **agents_sdk_config,
)
CLIENT_ID = environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID", "")
TENANT_ID = environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID", "")
# Message handler : get_token returns a token already scoped to the observability API.
# The Azure Bot Token Service performs the OBO exchange internally based on the
# OAuth connection's configured scope. No manual MSAL exchange_token call is needed.
@AGENT_APP.activity("message", auth_handlers=["OBOCONNECTIONPROFILE"])
async def on_message(context: TurnContext, _state: TurnState):
token_response = await AGENT_APP.auth.get_token(context, "OBOCONNECTIONPROFILE")
# token_response.token has aud=<a365-observability-app-id>,
# scp=Agent365.Observability.OtelWrite
_token_cache[f"{CLIENT_ID}:{TENANT_ID}"] = token_response.token
Important
Azure portal prerequisite: Set the Scopes for the Azure Bot OAuth connection named oboConnectionProfile to api://9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite. Without this setting, the token is scoped to the bot's own audience (api://botid-...) and export fails with HTTP 401 InvalidAudience.
Note
AGENT_APP.auth.get_token() returns the correctly scoped token directly - no exchange_token() call is needed. The Bot Framework Token Service handles the OBO exchange when the OAuth connection scope targets the A365 observability resource.
Custom engine using S2S
Custom engine agents can use S2S (client credentials) to get an app-only token by using the service connection credentials. This method uses standard MSAL client credentials - it doesn't need an agentic identity chain.
Prerequisites
- Microsoft Entra app registration: The app registration must be a custom engine (standard) app. Agent 365-enabled app registrations can't use plain
client_credentialsfor the observability resource (AADSTS82001). - Application permissions: Add
Agent365.Observability.OtelWrite(Application, not Delegated), and grant admin consent. Standard app registrations aren't Agent 365-registered agent instances, so they need this app role on the S2S route.
Important
The agentId you use for caching must be the ServiceConnection's ClientId. The export URL is /observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces : a mismatch causes HTTP 403.
Step 1: Environment and app configuration
The following examples show how to configure your app and runtime environment, including service connection values, tenant and client settings, and required authorization mappings.
# .env
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=<your-client-id>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=<your-client-secret>
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=<your-tenant-id>
CONNECTIONSMAP__0__CONNECTION=SERVICE_CONNECTION
CONNECTIONSMAP__0__SERVICEURL=*
ENABLE_A365_OBSERVABILITY=true
ENABLE_A365_OBSERVABILITY_EXPORTER=true
Step 2: Configure the distro for S2S
The following examples show how to enable Agent 365 exporting, set the exporter to the S2S endpoint, and register a custom TokenResolver for token lookup during export.
from microsoft.opentelemetry import use_microsoft_opentelemetry
_token_cache: dict[str, str] = {}
def token_resolver(agent_id: str, tenant_id: str) -> str | None:
return _token_cache.get(f"{agent_id}:{tenant_id}")
use_microsoft_opentelemetry(
enable_a365=True,
a365_token_resolver=token_resolver,
a365_use_s2s_endpoint=True, # S2S uses /observabilityService endpoint
a365_enable_observability_exporter=True,
)
Step 3: Acquire the S2S token
The following examples show how to request an app-only access token for the observability resource by using the service connection credentials, then cache it by agent and tenant for the exporter.
# Force agentId to ServiceConnection ClientId (custom engine agents have no agenticAppId)
agent_id = os.environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID")
tenant_id = os.environ.get("CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID")
connection = CONNECTION_MANAGER.get_connection("SERVICE_CONNECTION")
token = await connection.get_access_token(
resource_url="https://login.microsoftonline.com",
scopes=["api://9b975845-388f-4429-889e-eab1ef63949c/.default"],
)
_token_cache[f"{agent_id}:{tenant_id}"] = token
Step 4: Set baggage for span export
The Agent365 exporter requires baggage (tenant ID and agent ID) on the span context. Without this baggage, the exporter drops spans and logs the message No spans with tenant/agent identity found..
from microsoft.opentelemetry.a365.core import BaggageBuilder, InvokeAgentScope
# Baggage must wrap the span as a context manager
with BaggageBuilder().tenant_id(tenant_id).agent_id(agent_id).build():
invoke_scope = InvokeAgentScope.start(request, InvokeAgentScopeDetails(), agent_details)
with invoke_scope:
invoke_scope.record_input_messages([user_message])
invoke_scope.record_output_messages([response])