Integrate agent observability using direct OTel

This guide walks you end-to-end through sending agent telemetry to Agent 365 directly over OpenTelemetry (OTLP/HTTP+JSON). Before starting, read Agent 365 observability concepts to understand the model, the auth flows, and the surfaces your data lands in.

Important

The direct OTel path is the exception, not the default. Use it only if you already have an OpenTelemetry pipeline, your framework can't use the Agent 365 SDK, or your agent is in a language the SDK doesn't yet support (such as Java). For everyone else, the recommended path is the Microsoft OpenTelemetry Distro, which provides a unified observability SDK across Agent 365, Microsoft Foundry, Azure Monitor, and more. The earlier Observability SDK continues to work without breaking changes but is no longer recommended for new integrations; migration guidance for existing SDK users is coming.

Prerequisites

Ensure the following configurations are in place before any telemetry flows.

Who What
Tenant admin Sign up for Agent 365 and grant consent for your agent app. See Onboard to Agent 365. Without a licensed tenant, ingestion is silently dropped - the request returns 200 OK with partialSuccess: null but data never appears downstream.
Tenant admin Assign a Microsoft 365 E7 or Microsoft Agent 365 license to at least one user in the tenant. The SKU being present isn't enough. Assignment to a user starts the Defender backend workflow that enables ingestion. Without an assigned license, requests return 200 OK with partialSuccess: null and data is silently dropped.
Tenant admin Grant tenant consent. See Grant agents access to Microsoft 365 resources. Without it, tokens are issued without the role/scope and requests return 403.
Your dev team Register your app (standard Microsoft Entra app or blueprint). See Get started with Agent 365 development.
Your dev team Add Agent365.Observability.OtelWrite under API permissions (app role for S2S, scope for delegated). For blueprints, see Configure inheritable permissions. Coordinate with the Agent 365 onboarding team to enable the permission.

Authentication recipes

All four recipes use the standard Microsoft Entra token endpoint:

Field Value
Token endpoint https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
Resource (aud in returned token) 9b975845-388f-4429-889e-eab1ef63949c (also accepts api://9b975845-388f-4429-889e-eab1ef63949c)
S2S scope 9b975845-388f-4429-889e-eab1ef63949c/.default
OBO scope 9b975845-388f-4429-889e-eab1ef63949c/Agent365.Observability.OtelWrite

The recipes below show raw HTTP for clarity. In production, prefer Microsoft.Identity.Web or another MSAL library, which handles token refresh and caching.

Which recipe do I need?

My app model My OAuth flow Go to
Standard Microsoft Entra app registration S2S (client credentials) S2S, Standard Microsoft Entra app
Standard Microsoft Entra app registration OBO (delegated) OBO, Standard Microsoft Entra app
Blueprint-derived agent identity S2S (client credentials) S2S, Blueprint-derived agent identity
Blueprint-derived agent identity OBO / AI teammate OBO, Blueprint-derived agent identity

S2S, Standard Microsoft Entra app

One POST to the tenant's token endpoint with grant_type=client_credentials. Authenticate the app by using a client secret, a certificate (signed JWT assertion), or a managed identity or federated credential.

POST https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={your-app-id}
&scope=9b975845-388f-4429-889e-eab1ef63949c%2F.default
&client_secret={secret}
&grant_type=client_credentials

The returned token has appid/azp = {your-app-id}, roles containing Agent365.Observability.OtelWrite, and aud = 9b975845-.... Use it on the /observabilityService/.../traces route.

For certificate-based authentication, replace client_secret={secret} with client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={signed-jwt}.

S2S, Blueprint-derived agent identity

Agent identities don't have credentials of their own. The agent identity blueprint holds the credentials (managed identity FIC, certificate, or client secret) and mints tokens on behalf of its child agent identities through a two-step exchange. For more information, see autonomous app OAuth flow.

  1. The blueprint authenticates and gets a federated identity exchange token T1:

    • {blueprint-credential} is the blueprint's MSI token, certificate-signed JWT, or secret exchange-token assertion - per blueprint configuration.
    POST https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded
    
    client_id={blueprint-app-id}
    &scope=api%3A%2F%2FAzureADTokenExchange%2F.default
    &fmi_path={agent-identity-app-id}
    &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
    &client_assertion={blueprint-credential}
    &grant_type=client_credentials
    
  2. The agent identity exchanges T1 for the Agent 365 Observability resource token:

    POST https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded
    
    client_id={agent-identity-app-id}
    &scope=9b975845-388f-4429-889e-eab1ef63949c%2F.default
    &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
    &client_assertion={T1}
    &grant_type=client_credentials
    
    • The returned token has appid/azp = {agent-identity-app-id}, roles containing Agent365.Observability.OtelWrite, and aud = 9b975845-....
    • Use this token on the /observabilityService/.../traces route.
    • The URL {agentId} is the agent identity appId, not the blueprint appId.

OBO, Standard Microsoft Entra app

Receive the user's incoming token Tc from your upstream caller (Bearer or PFAT), then exchange it:

POST https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={your-app-id}
&scope=9b975845-388f-4429-889e-eab1ef63949c%2FAgent365.Observability.OtelWrite
&client_secret={secret}
&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion={Tc}
&requested_token_use=on_behalf_of

For certificate auth, replace client_secret={secret} with the same client_assertion_type + client_assertion pair as in S2S.

The returned token has appid/azp = {your-app-id}, scp containing Agent365.Observability.OtelWrite, and aud = 9b975845-.... Use it on the /observability/.../traces route. A refresh token is returned alongside; cache and reuse it instead of rerunning the exchange on every call.

OBO, Blueprint-derived agent identity (including AI teammate)

There are three main steps to the on-behalf-of flow. For more information, see Agent OAuth flows: On behalf of flow.

  1. Receive the user token Tc. For an AI teammate, this token represents the agent's own user account; otherwise, it represents the human caller.

  2. The blueprint authenticates and gets T1, the same as the S2S blueprint-derived agent identity flow.

  3. The agent identity exchanges T1 and Tc for a delegated resource token:

    POST https://login.microsoftonline.com/{your-tenant-id}/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded
    
    client_id={agent-identity-app-id}
    &scope=9b975845-388f-4429-889e-eab1ef63949c%2FAgent365.Observability.OtelWrite
    &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
    &client_assertion={T1}
    &grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
    &assertion={Tc}
    &requested_token_use=on_behalf_of
    

The returned token has appid/azp = {agent-identity-app-id}, scp containing Agent365.Observability.OtelWrite, and represents the agent's user. Use it on the /observability/.../traces route. The URL {agentId} is the agent identity appId, not the blueprint appId. A refresh token is returned alongside; cache and reuse it.

Required claims on the returned token

S2S route (/observabilityService/...) - app-only token:

Claim Required value
aud 9b975845-388f-4429-889e-eab1ef63949c (or api://9b975845-...)
roles Must contain Agent365.Observability.OtelWrite
appid (v1) or azp (v2) Must equal URL {agentId}
scp Must be absent

Delegated route (/observability/...) - user-delegated token (Bearer or PFAT):

Claim Required value
aud 9b975845-388f-4429-889e-eab1ef63949c (or api://9b975845-...)
scp Must contain Agent365.Observability.OtelWrite
appid / azp Must equal URL {agentId}

The delegated route accepts both Bearer and MSAuth1.0 PFAT tokens. Direct callers should use Bearer. If you don't know which one you have, use Bearer.

Endpoints

Two routes; pick by how your service authenticates, not by what the user is doing:

POST https://agent365.svc.cloud.microsoft/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1   # S2S
POST https://agent365.svc.cloud.microsoft/observability/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1          # OBO

Headers:

Authorization: Bearer <token>      # or MSAuth1.0 ... for delegated PFAT
Content-Type: application/json

URL parameters

  • {tenantId} - the customer tenant GUID. The server treats this as authoritative; if your spans set microsoft.tenant.id and it disagrees, the request is rejected.
  • {agentId} - the calling application's appId (also the OAuth client_id). For blueprint-derived identities, this is the agent identity appId, not the blueprint appId. Must equal the appid / azp claim of your token.
  • api-version=1 - required.

Request body encoding

The body is the standard OTLP/HTTP+JSON shape: an ExportTraceServiceRequest with resourceSpansscopeSpansspans. Keep in mind the following details:

  • traceId (16 bytes) and spanId (8 bytes) are sent as lowercase hex strings.
  • startTimeUnixNano / endTimeUnixNano are strings holding Unix epoch nanoseconds.
  • kind is the integer OTLP enum value (for example 1 for INTERNAL); status.code is the integer enum (for example 1 for OK, 2 for ERROR).
  • All attribute values are sent as stringValue.

Response shape

A successful call returns 200 OK:

{ "partialSuccess": null }

If some spans were rejected by the per-span filter:

{
  "partialSuccess": {
    "rejectedSpans": 2,
    "errorMessage": "Dropped 2 non-A365 span(s) ..."
  }
}

Field names are camelCase on the wire. Always check partialSuccess: a 200 with all your spans rejected is a real outcome you must surface. Limits and drop conditions lists the silent drop cases where a 200 returns with partialSuccess: null despite no data appearing downstream.

Smallest possible request

The simplest end-to-end test sends a single invoke_agent span. This span is the smallest body that lands in Microsoft Defender.

Step 1. Get a Bearer token. For S2S, use client credentials with scope 9b975845-388f-4429-889e-eab1ef63949c/.default (see Authentication recipes for the full recipe).

Step 2. POST a single span:

TOKEN="$(./get-token.sh)"
TENANT_ID="<customer-tenant-guid>"
AGENT_ID="<your-agent-app-id>"

curl -i -X POST \
  "https://agent365.svc.cloud.microsoft/observabilityService/tenants/${TENANT_ID}/otlp/agents/${AGENT_ID}/traces?api-version=1" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "resourceSpans": [{
    "scopeSpans": [{
      "scope": { "name": "my-instrumentation", "version": "1.0.0" },
      "spans": [{
        "traceId": "0102030405060708090a0b0c0d0e0f10",
        "spanId":  "1111111111111111",
        "parentSpanId": "",
        "name": "invoke_agent",
        "kind": 1,
        "startTimeUnixNano": "1736175600000000000",
        "endTimeUnixNano":   "1736175601500000000",
        "status": { "code": 1 },
        "attributes": [
          { "key": "gen_ai.operation.name", "value": { "stringValue": "invoke_agent" } },
          { "key": "gen_ai.agent.id",       "value": { "stringValue": "${AGENT_ID}" } },
          { "key": "gen_ai.agent.name",     "value": { "stringValue": "MyAgent" } },
          { "key": "microsoft.a365.agent.blueprint.id", "value": { "stringValue": "${AGENT_ID}" } },
          { "key": "gen_ai.conversation.id","value": { "stringValue": "conv-001" } },
          { "key": "microsoft.channel.name","value": { "stringValue": "web" } },
          { "key": "user.id",               "value": { "stringValue": "<entra-user-objectid>" } },
          { "key": "client.address",        "value": { "stringValue": "10.1.2.80" } },
          { "key": "server.address",        "value": { "stringValue": "myagent.example.com" } },
          { "key": "server.port",           "value": { "stringValue": "443" } },
          { "key": "gen_ai.input.messages", "value": { "stringValue": "[{\"role\":\"user\",\"content\":\"hi\"}]" } },
          { "key": "gen_ai.output.messages","value": { "stringValue": "[{\"role\":\"assistant\",\"content\":\"hello\"}]" } }
        ]
      }]
    }]
  }]
}
EOF

Step 3. Expect 200 OK with this body:

{ "partialSuccess": null }

Step 4. Confirm the data actually landed. A 200 OK is not proof of ingestion; Verifying ingestion walks through the verification flow. To POST a saved body file instead, replace --data @- <<EOF ... EOF with --data @./otlp-request.json.

Agent run example

A user on Microsoft Teams asks "What's the weather in Seattle?". Your agent calls a GetWeather function, asks an LLM to format the answer, and replies. That single run is four spans:

graph TD
    A["<b>invoke_agent</b> · spanId=A · parentSpanId=∅<br/><i>root - the run itself</i>"]
    B["<b>chat</b> · spanId=B · parentSpanId=A<br/><i>LLM picks the tool / formats reply</i>"]
    C["<b>execute_tool</b> · spanId=C · parentSpanId=A<br/><i>the GetWeather call</i>"]
    D["<b>output_messages</b> · spanId=D · parentSpanId=A<br/><i>final reply emitted to the user</i>"]
    A --> B
    A --> C
    A --> D

Run-wide attributes set on every span:

Attribute Example value
traceId 0102030405060708090a0b0c0d0e0f10
gen_ai.conversation.id 19:abc@thread.tacv2
microsoft.session.id session-1234
microsoft.channel.name msteams
gen_ai.agent.id <AGENT_APP_ID>
gen_ai.agent.name WeatherBot
microsoft.a365.agent.blueprint.id <BLUEPRINT_APP_ID>
user.id <entra-user-objectid>
client.address 10.1.2.80
server.address weatherbot.example.com
server.port 443

Important

These run-wide attributes are not propagated automatically. You must set gen_ai.conversation.id, microsoft.channel.name, and microsoft.session.id on every span yourself.

Span A: invoke_agent (root)

{
  "traceId": "0102030405060708090a0b0c0d0e0f10",
  "spanId": "1111111111111111",
  "parentSpanId": "",
  "name": "invoke_agent",
  "kind": 1,
  "startTimeUnixNano": "1736175600000000000",
  "endTimeUnixNano":   "1736175601500000000",
  "status": { "code": 1 },
  "attributes": [
    { "key": "gen_ai.operation.name",   "value": { "stringValue": "invoke_agent" } },
    { "key": "gen_ai.execution.type",   "value": { "stringValue": "HumanToAgent" } },
    { "key": "gen_ai.input.messages",   "value": { "stringValue": "[{\"role\":\"user\",\"content\":\"What's the weather in Seattle?\"}]" } },
    { "key": "gen_ai.output.messages",  "value": { "stringValue": "[{\"role\":\"assistant\",\"content\":\"It's 65F and partly cloudy in Seattle.\"}]" } },
    { "key": "user.email",              "value": { "stringValue": "alice@contoso.com" } }
    /* plus all the run-wide attributes listed above */
  ]
}

Span B: chat (LLM call)

{
  "traceId": "0102030405060708090a0b0c0d0e0f10",
  "spanId": "2222222222222222",
  "parentSpanId": "1111111111111111",
  "name": "chat",
  "kind": 1,
  "startTimeUnixNano": "1736175600200000000",
  "endTimeUnixNano":   "1736175600900000000",
  "status": { "code": 1 },
  "attributes": [
    { "key": "gen_ai.operation.name",      "value": { "stringValue": "chat" } },
    { "key": "gen_ai.request.model",       "value": { "stringValue": "gpt-4o" } },
    { "key": "gen_ai.provider.name",       "value": { "stringValue": "openai" } },
    { "key": "gen_ai.usage.input_tokens",  "value": { "stringValue": "42" } },
    { "key": "gen_ai.usage.output_tokens", "value": { "stringValue": "23" } }
    /* plus all the run-wide attributes */
  ]
}

Span C: execute_tool

{
  "traceId": "0102030405060708090a0b0c0d0e0f10",
  "spanId": "3333333333333333",
  "parentSpanId": "1111111111111111",
  "name": "execute_tool",
  "kind": 1,
  "startTimeUnixNano": "1736175600950000000",
  "endTimeUnixNano":   "1736175601200000000",
  "status": { "code": 1 },
  "attributes": [
    { "key": "gen_ai.operation.name",      "value": { "stringValue": "execute_tool" } },
    { "key": "gen_ai.tool.name",           "value": { "stringValue": "GetWeather" } },
    { "key": "gen_ai.tool.type",           "value": { "stringValue": "function" } },
    { "key": "gen_ai.tool.call.id",        "value": { "stringValue": "call-001" } },
    { "key": "gen_ai.tool.call.arguments", "value": { "stringValue": "{\"location\":\"Seattle\"}" } },
    { "key": "gen_ai.tool.call.result",    "value": { "stringValue": "{\"tempF\":65,\"condition\":\"partly cloudy\"}" } }
    /* plus all the run-wide attributes */
  ]
}

Span D: output_messages

{
  "traceId": "0102030405060708090a0b0c0d0e0f10",
  "spanId": "4444444444444444",
  "parentSpanId": "1111111111111111",
  "name": "output_messages",
  "kind": 1,
  "startTimeUnixNano": "1736175601400000000",
  "endTimeUnixNano":   "1736175601500000000",
  "status": { "code": 1 },
  "attributes": [
    { "key": "gen_ai.operation.name",  "value": { "stringValue": "output_messages" } },
    { "key": "gen_ai.output.messages", "value": { "stringValue": "[{\"role\":\"assistant\",\"content\":\"It's 65F and partly cloudy in Seattle.\"}]" } }
    /* plus all the run-wide attributes */
  ]
}

Sending telemetry

Using an OTel SDK

Most partners send traces through an OTel SDK rather than hand-rolled HTTP. The SDK handles batching, retry, and the OTLP/HTTP+JSON encoding for you. Set the exporter endpoint and inject the Authorization header.

The exporter endpoint is the route URL itself, including the query string:

https://agent365.svc.cloud.microsoft/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1

(Use /observability/... instead of /observabilityService/... for the delegated route.)

Python

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
    endpoint="https://agent365.svc.cloud.microsoft/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1",
    headers={"Authorization": f"Bearer {token}"},
)

Package: opentelemetry-exporter-otlp-proto-http.

Node.js / TypeScript

import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const exporter = new OTLPTraceExporter({
  url: "https://agent365.svc.cloud.microsoft/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1",
  headers: { Authorization: `Bearer ${token}` },
});

Package: @opentelemetry/exporter-trace-otlp-http.

.NET

using OpenTelemetry.Exporter;

services.AddOpenTelemetry().WithTracing(b => b
    .AddOtlpExporter(o =>
    {
        o.Endpoint = new Uri("https://agent365.svc.cloud.microsoft/observabilityService/tenants/{tenantId}/otlp/agents/{agentId}/traces?api-version=1");
        o.Headers = $"Authorization=Bearer {token}";
        o.Protocol = OtlpExportProtocol.HttpJson;
    }));

Package: OpenTelemetry.Exporter.OpenTelemetryProtocol.

Manual HTTP

If you can't or don't want to use an OTel SDK, build the OTLP/HTTP+JSON request yourself and POST it. The body shape is defined by the OpenTelemetry OTLP/HTTP+JSON spec:

{
  "resourceSpans": [{
    "resource":  { "attributes": [ ... ] },          // optional
    "scopeSpans": [{
      "scope":  { "name": "<your-instrumentation>", "version": "1.0.0" },
      "spans":  [ <span>, <span>, ... ]
    }]
  }]
}

Each <span> is an object whose required fields are traceId, spanId, name, kind, startTimeUnixNano, endTimeUnixNano, attributes, and (for non-root spans) parentSpanId. See Endpoints and Request body encoding for the encoding rules (string-encoded times, hex traceId / spanId, integer kind / status.code, all attribute values as stringValue).

The set of attributes to set on each span is defined in Message contracts. See Attribute reference for the full attribute list. Refer to the Agent run example for an end-to-end working sample with the Bearer token in the header and the body inline.

You can send all the spans of a run in a single POST body (preferred - one request, one trace) or across multiple POSTs. The server reconstructs the run from traceId + parentSpanId + gen_ai.conversation.id, so each span carries enough to be correlated either way.

Message contracts

This section defines what spans you can emit and which attributes go on each. For the full attribute-by-attribute spec, see the Attribute reference.

Operation types

Every span you send must carry gen_ai.operation.name set to one of these four values (case-insensitive). Any span with a missing or unrecognized value is silently dropped and counted in partialSuccess.rejectedSpans.

gen_ai.operation.name Meaning Most-googled gotcha
invoke_agent An invocation of an agent. The "root" of an agent run. Required for the run to appear in Microsoft Defender agent-activity views or the Microsoft 365 admin center. Without it, the telemetry only lands in Microsoft Defender advanced hunting (CloudAppEvents).
execute_tool A tool / function call performed by an agent. --
chat An LLM inference call. Use the literal chat, NOT inference.
output_messages A final emitted output message. --

Span hierarchy and run grouping

Agent 365 reconstructs a run from the standard OTLP span graph (traceId, spanId, parentSpanId) plus the run-wide attributes from the Attribute reference.

Six rules:

  1. Always set parentSpanId on every non-root span. Without it, the run's tree structure can't be reconstructed.
  2. Reuse the same traceId across every span in a run.
  3. Set gen_ai.conversation.id on every span with the same value. This is the primary join key for "all spans in this run". It is not propagated automatically.
  4. Set microsoft.channel.name on every span with the same value. Tool spans missing the channel / conversation can inherit them from their parent invoke_agent only if the parent is in the same OTLP request, so set them on every span yourself.
  5. Set microsoft.session.id on every span when you have a logical session.
  6. For agent-to-agent calls where the child agent is in a separate request, reuse the same gen_ai.conversation.id and use the microsoft.a365.caller.agent.* attributes (see Attribute reference) to capture the caller-agent context.

The four-span tree in the Agent run example is the canonical shape.

Common run shapes

Shape Spans to emit Notes
Single-agent chatbot (no tools, no LLM span) One invoke_agent only Set run-wide attributes plus gen_ai.input.messages and gen_ai.output.messages. Identical to Smallest possible request.
Agent with tools (most common) invoke_agent root + chat, execute_tool, output_messages children All children share the root's traceId and set parentSpanId = root.spanId. All carry the same run-wide attributes. See Agent run example for a full example.
Agent-to-agent Each agent emits its own invoke_agent Reuse the same gen_ai.conversation.id across both agents. On the target's invoke_agent, set gen_ai.execution.type = "Agent2Agent" and the microsoft.a365.caller.agent.* attributes (calling agent's appId, name, blueprint appId, user id, and email). If the calling agent has no Entra registration, use microsoft.a365.caller.agent.platform.id and gen_ai.caller.agent.type instead.

Onboarding checklist

Run through this checklist before going to production.

Category Check
Auth Your Entra app (or blueprint) is registered and you can mint tokens for it.
Auth Your app has been granted Agent365.Observability.OtelWrite (app role for S2S, scope for delegated).
Auth Each agent has its own Entra appId as {agentId} in the URL. For blueprint-derived identities, that appId is the agent identity appId, not the blueprint appId. If the agent has no Entra registration, see Picking values.
Auth A tenant admin has granted consent for Agent365.Observability.OtelWrite. Without consent, tokens are issued without the role/scope and requests are rejected with 403.
Licensing At least one user in the customer tenant has a Microsoft 365 E7 or Microsoft Agent 365 license assigned (assignment, not just SKU presence in the tenant). Without an assigned license, ingestion is silently dropped. See Prerequisites.
Spans Every span sets the run-wide essentials (Span hierarchy and run grouping).
Spans invoke_agent spans set gen_ai.input.messages and gen_ai.output.messages.
Spans execute_tool spans set gen_ai.tool.name, gen_ai.tool.type, gen_ai.tool.call.id, gen_ai.tool.call.arguments, gen_ai.tool.call.result.
Spans chat spans set gen_ai.request.model and gen_ai.provider.name (and ideally gen_ai.usage.input_tokens / gen_ai.usage.output_tokens - string-encoded).
Spans All non-root spans set parentSpanId; all spans in a run share the same traceId.
Payload Request body is ≤ 1 MB.
Verification You parse partialSuccess on every response and log rejections.
Verification You ran the verification flow in Verifying ingestion against your first runs.

Next steps