Docs/Observability/Opentelemetry # OpenTelemetry (OTLP) Ingestion
ANTS Platform accepts OpenTelemetry data over **OTLP/HTTP**. Any OpenTelemetry
SDK, collector or tool that can export OTLP can send spans to ANTS Platform.
Spans become [traces and observations](/docs/observability/traces), with
model, token, cost, user and session details taken from standard attributes.
The [Python and JS/TS SDKs](/docs/getting-started/installation) use the same
endpoint under the hood, so everything on this page also applies to them.
<Callout type="info">
Setting up Claude Code, GitHub Copilot CLI or OpenAI Codex CLI? Follow
[Coding Agent Telemetry](/docs/integrations/coding-agent-telemetry). It has
the exact environment variables for each tool.
</Callout>
## Endpoints
All endpoints are under `https://<your-ants-host>/api/public/otel`. Replace
`<your-ants-host>` with the host of your ANTS Platform deployment.
<div className="dv2-table-scroll"><table><thead><tr><th>Signal</th><th>Path</th><th>What happens to the data</th></tr></thead><tbody><tr><td>Traces</td><td><code>POST /api/public/otel/v1/traces</code></td><td>Every span is converted into a trace and observations.</td></tr><tr><td>Logs</td><td><code>POST /api/public/otel/v1/logs</code></td><td>Only log records from supported agents are stored: Claude Code, GitHub Copilot, OpenAI Codex and Claude Cowork. Other log records are rejected (see [Responses](#responses)).</td></tr><tr><td>Metrics</td><td><code>POST /api/public/otel/v1/metrics</code></td><td>Accepted and discarded. The endpoint answers <code>200</code> so exporters that also send metrics do not error. No metric data is stored.</td></tr></tbody></table></div>
Most OTLP exporters take the base URL and append `/v1/traces`, `/v1/logs` and
`/v1/metrics` themselves. Set the base to `https://<your-ants-host>/api/public/otel`.
If your exporter needs the full signal URL, use the paths above.
Only **OTLP over HTTP** is supported. OTLP over gRPC is not.
## Authentication
Use HTTP **Basic** authentication with a project key pair:
1. Create a key pair in **Project Settings > API Keys** (see
[API Keys](/docs/administration/api-keys)). The public key starts with
`pk-ap-` and the secret key with `sk-ap-`.
2. Join them with a colon and Base64-encode the result.
3. Send it in the `Authorization` header.
```bash
AUTH=$(printf '%s' "pk-ap-...:sk-ap-..." | base64)
export OTEL_EXPORTER_OTLP_ENDPOINT="https://<your-ants-host>/api/public/otel"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ${AUTH}"
```
Data lands in the project that owns the key. Discover AI project keys (from
**Project Settings > Deployment & Keys**) and enrolled devices are also
accepted on these endpoints. Organization-level API keys are not: they return
`401` because they do not belong to a project.
<Callout type="warning">
The secret key grants write access to the project. Keep it in a secret store
or managed configuration. Do not put it in source code or shared documents.
</Callout>
## Formats and compression
<div className="dv2-table-scroll"><table><thead><tr><th>Header</th><th>Accepted values</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td><code>application/x-protobuf</code> or <code>application/json</code></td></tr><tr><td><code>Content-Encoding</code></td><td><code>gzip</code> (optional)</td></tr></tbody></table></div>
Any other content type returns `400 Invalid content type`. A body that cannot be
decompressed or parsed returns `400`.
In standard OpenTelemetry exporters, `http/protobuf` sends protobuf and
`http/json` sends JSON. Both work.
## Send your first spans
<Tabs _items="Python (OTel SDK),Node.js (OTel SDK),OpenTelemetry Collector,curl">
<Tabs.Tab>
```bash
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```
```python
import base64, os
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
auth = base64.b64encode(
f"{os.environ['ANTS_PLATFORM_PUBLIC_KEY']}:{os.environ['ANTS_PLATFORM_SECRET_KEY']}".encode()
).decode()
provider = TracerProvider(
resource=Resource.create({
"service.name": "support-bot",
"deployment.environment": "production",
})
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="https://<your-ants-host>/api/public/otel/v1/traces",
headers={"Authorization": f"Basic {auth}"},
)))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("support-bot")
with tracer.start_as_current_span("chat gpt-4o") as span:
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.request.model", "gpt-4o")
span.set_attribute("gen_ai.usage.input_tokens", 42)
span.set_attribute("gen_ai.usage.output_tokens", 128)
span.set_attribute("user.id", "user-123")
span.set_attribute("session.id", "session-456")
provider.shutdown()
```
</Tabs.Tab>
<Tabs.Tab>
```bash
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-proto
```
```typescript
import { trace } from "@opentelemetry/api";
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
const auth = Buffer.from(
`${process.env.ANTS_PLATFORM_PUBLIC_KEY}:${process.env.ANTS_PLATFORM_SECRET_KEY}`,
).toString("base64");
const provider = new NodeTracerProvider({
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: "https://<your-ants-host>/api/public/otel/v1/traces",
headers: { Authorization: `Basic ${auth}` },
}),
),
],
});
provider.register();
const tracer = trace.getTracer("support-bot");
tracer.startActiveSpan("chat gpt-4o", (span) => {
span.setAttribute("gen_ai.operation.name", "chat");
span.setAttribute("gen_ai.request.model", "gpt-4o");
span.setAttribute("user.id", "user-123");
span.end();
});
await provider.shutdown();
```
</Tabs.Tab>
<Tabs.Tab>
Forward spans from an existing collector with the `otlphttp` exporter:
```yaml
exporters:
otlphttp/ants:
endpoint: https://<your-ants-host>/api/public/otel
headers:
Authorization: "Basic ${env:ANTS_OTLP_AUTH}"
compression: gzip
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/ants]
```
Set `ANTS_OTLP_AUTH` to the Base64 value of `pk-ap-...:sk-ap-...`.
</Tabs.Tab>
<Tabs.Tab>
```bash
curl -X POST "https://<your-ants-host>/api/public/otel/v1/traces" \
-H "Authorization: Basic $AUTH" \
-H "Content-Type: application/json" \
-d '{
"resourceSpans": [{
"resource": { "attributes": [
{ "key": "service.name", "value": { "stringValue": "curl-test" } }
]},
"scopeSpans": [{
"scope": { "name": "manual" },
"spans": [{
"traceId": "5b8efff798038103d269b633813fc60c",
"spanId": "eee19b7ec3c1b174",
"name": "hello-ants",
"kind": 1,
"startTimeUnixNano": "1760000000000000000",
"endTimeUnixNano": "1760000001000000000",
"attributes": [
{ "key": "user.id", "value": { "stringValue": "user-123" } }
]
}]
}]
}]
}'
```
</Tabs.Tab>
</Tabs>
Open **Tracing** in the sidebar to see the trace. Processing is asynchronous, so
allow a few seconds.
## How spans are mapped
Each OTel trace ID becomes an ANTS Platform trace. Each span becomes an
observation in that trace, nested by its parent span. The root span also sets
trace-level fields such as the trace name, input and output.
### Observation type
ANTS Platform stores three observation types: **span**, **generation** and
**event**. The type is chosen in this order:
1. `gen_ai.operation.name`:
- `chat`, `text_completion`, `generate_content`, `embeddings` become a **generation**.
- `invoke_agent`, `execute_tool`, `invoke_workflow`, `create_agent`, `retrieval` become a **span**.
2. Otherwise the span is a **generation** when `ants_platform.observation.type`
(or `langfuse.observation.type`) is `generation`, when a model name is
present, or when `openinference.span.kind` is `LLM`.
3. It is an **event** when `ants_platform.observation.type` is `event`.
4. Everything else is a **span**.
<Callout type="info">
The SDKs let you mark observations as agent, tool, chain, retriever,
evaluator, guardrail or embedding. These are stored as spans (or as
generations when they carry a model). Use the observation name to tell them
apart.
</Callout>
### OpenTelemetry GenAI semantic conventions
<div className="dv2-table-scroll"><table><thead><tr><th>Attribute</th><th>Maps to</th></tr></thead><tbody><tr><td><code>gen_ai.operation.name</code></td><td>Observation type (see above)</td></tr><tr><td><code>gen_ai.request.model</code>, <code>gen_ai.response.model</code></td><td>Model</td></tr><tr><td><code>gen_ai.request.temperature</code>, <code>top_p</code>, <code>top_k</code>, <code>max_tokens</code>, <code>frequency_penalty</code>, <code>presence_penalty</code>, <code>seed</code>, <code>stop_sequences</code>, <code>stream</code> (all under <code>gen_ai.request.</code>)</td><td>Model parameters</td></tr><tr><td><code>gen_ai.usage.input_tokens</code>, <code>gen_ai.usage.output_tokens</code></td><td>Token usage</td></tr><tr><td><code>gen_ai.usage.cache_read.input_tokens</code>, <code>gen_ai.usage.cache_creation.input_tokens</code>, <code>gen_ai.usage.reasoning.output_tokens</code></td><td>Detailed token usage</td></tr><tr><td><code>gen_ai.usage.prompt_tokens</code>, <code>gen_ai.usage.completion_tokens</code></td><td>Token usage (older names)</td></tr><tr><td><code>gen_ai.usage.cost</code></td><td>Cost</td></tr><tr><td><code>gen_ai.input.messages</code>, <code>gen_ai.system_instructions</code></td><td>Input</td></tr><tr><td><code>gen_ai.output.messages</code></td><td>Output</td></tr><tr><td><code>gen_ai.prompt.*</code>, <code>gen_ai.completion.*</code></td><td>Input and output (older indexed form)</td></tr><tr><td>Span events <code>gen_ai.user.message</code>, <code>gen_ai.system.message</code>, <code>gen_ai.assistant.message</code>, <code>gen_ai.tool.message</code>, <code>gen_ai.choice</code></td><td>Input and output (event form)</td></tr><tr><td><code>gen_ai.conversation.id</code></td><td>Session ID</td></tr></tbody></table></div>
Cost is calculated from token usage and your [model definitions](/docs/finops/models-pricing)
when the span does not carry a cost.
Attributes that are not mapped to a field are kept in the observation
metadata, together with the resource attributes and the instrumentation scope,
so you can still filter and search on them. Span events other than the GenAI
message events above (for example `exception` events) are not stored.
### Trace and observation attributes
Use these attributes to set fields that the GenAI conventions do not cover.
The `ants-platform.` (hyphen) spelling and the `langfuse.` prefix are also
accepted.
<div className="dv2-table-scroll"><table><thead><tr><th>Attribute</th><th>Sets</th></tr></thead><tbody><tr><td><code>user.id</code></td><td>Trace user (see [How users are resolved](#how-users-are-resolved))</td></tr><tr><td><code>session.id</code></td><td>Trace session</td></tr><tr><td><code>ants_platform.trace.name</code></td><td>Trace name</td></tr><tr><td><code>ants_platform.trace.tags</code></td><td>Trace tags (array, JSON array string or comma-separated string)</td></tr><tr><td><code>ants_platform.trace.metadata.<key></code></td><td>One trace metadata entry per attribute</td></tr><tr><td><code>ants_platform.trace.input</code>, <code>ants_platform.trace.output</code></td><td>Trace input and output</td></tr><tr><td><code>langfuse.trace.public</code></td><td>Make the trace public (<code>true</code>)</td></tr><tr><td><code>ants_platform.observation.type</code></td><td><code>span</code>, <code>generation</code> or <code>event</code></td></tr><tr><td><code>ants_platform.observation.input</code>, <code>ants_platform.observation.output</code></td><td>Observation input and output</td></tr><tr><td><code>ants_platform.observation.metadata.<key></code></td><td>One observation metadata entry per attribute</td></tr><tr><td><code>ants_platform.observation.level</code></td><td><code>DEBUG</code>, <code>DEFAULT</code>, <code>WARNING</code> or <code>ERROR</code></td></tr><tr><td><code>ants_platform.observation.status_message</code></td><td>Status message (otherwise the span status message)</td></tr><tr><td><code>ants_platform.observation.model.name</code></td><td>Model</td></tr><tr><td><code>ants_platform.observation.model.parameters</code></td><td>Model parameters, as a JSON-encoded string</td></tr><tr><td><code>ants_platform.observation.completion_start_time</code></td><td>Time to first token, as a JSON-encoded timestamp string</td></tr><tr><td><code>ants_platform.observation.prompt.name</code>, <code>ants_platform.observation.prompt.version</code></td><td>Linked prompt version</td></tr><tr><td><code>ants_platform.environment</code>, <code>deployment.environment.name</code>, <code>deployment.environment</code></td><td>Environment (default <code>default</code>), read from resource or span attributes</td></tr><tr><td><code>ants_platform.release</code></td><td>Release</td></tr><tr><td><code>ants_platform.version</code></td><td>Version (otherwise the resource <code>service.version</code>)</td></tr><tr><td><code>ants_platform.agent.name</code>, <code>ants_platform.agent.id</code>, <code>ants_platform.agent.display_name</code></td><td>Agent (see [How agents are resolved](#how-agents-are-resolved))</td></tr></tbody></table></div>
Span status `ERROR` sets the observation level to Error when no level
attribute is set.
<Callout type="warning">
**Environment names must be lowercase.** The environment can only contain
lowercase letters, digits, hyphens and underscores, up to 40 characters, and
cannot start with `langfuse`. A value such as `Production` makes every event
in the request fail validation. The endpoint still answers `200` and lists
the failures in the `errors` array of the response.
</Callout>
`ants_platform.observation.usage_details` and `ants_platform.observation.cost_details`
are only read from spans produced by the ANTS Platform SDKs. From any other
exporter, send token counts with `gen_ai.usage.*` and cost with
`gen_ai.usage.cost`.
### Other instrumentation libraries
Spans from these libraries are mapped without extra configuration:
<div className="dv2-table-scroll"><table><thead><tr><th>Library</th><th>Attributes read</th></tr></thead><tbody><tr><td>OpenInference (Arize)</td><td><code>openinference.span.kind</code>, <code>input.value</code>, <code>output.value</code>, <code>llm.model_name</code>, <code>llm.invocation_parameters</code>, <code>llm.token_count.*</code></td></tr><tr><td>OpenLLMetry (Traceloop)</td><td><code>traceloop.entity.input</code>, <code>traceloop.entity.output</code>, plus GenAI attributes</td></tr><tr><td>Vercel AI SDK</td><td><code>ai.prompt.*</code>, <code>ai.response.*</code>, <code>ai.usage.tokens</code>, <code>ai.toolCall.*</code>, <code>ai.telemetry.metadata.*</code> (including <code>userId</code>, <code>sessionId</code>, <code>tags</code>)</td></tr><tr><td>Google Agent Development Kit</td><td><code>gcp.vertex.agent.llm_request</code>, <code>gcp.vertex.agent.llm_response</code></td></tr><tr><td>Pydantic Logfire</td><td><code>logfire.msg</code> (span name)</td></tr><tr><td>MLflow</td><td><code>mlflow.spanInputs</code>, <code>mlflow.spanOutputs</code></td></tr><tr><td>Langfuse SDK</td><td><code>langfuse.*</code> attributes</td></tr></tbody></table></div>
## Built-in agent parsers
Some agents export OTLP in their own formats. ANTS Platform detects them and
builds one trace per prompt or run, grouped into sessions.
<div className="dv2-table-scroll"><table><thead><tr><th>Agent</th><th>Detected by</th><th>Signals</th><th>Guide</th></tr></thead><tbody><tr><td><strong>Claude Code</strong></td><td><code>service.name</code> = <code>claude-code</code>, scope <code>com.anthropic.claude_code</code>, or <code>claude_code.*</code> event names</td><td>Traces (span tree) plus logs for request and response bodies</td><td>[Coding Agent Telemetry](/docs/integrations/coding-agent-telemetry)</td></tr><tr><td><strong>GitHub Copilot</strong> (CLI, VS Code chat, SDK)</td><td><code>service.name</code> containing <code>copilot</code>, scope names <code>github.copilot</code> or <code>copilot_chat</code>, or <code>gen_ai.agent.name</code> containing <code>copilot</code></td><td>Traces and <code>copilot_chat.*</code> logs</td><td>[Coding Agent Telemetry](/docs/integrations/coding-agent-telemetry)</td></tr><tr><td><strong>OpenAI Codex CLI</strong></td><td><code>service.name</code> such as <code>codex_cli_rs</code> or <code>codex</code>, scope names containing <code>codex</code>, or <code>codex.*</code> event names</td><td>Logs</td><td>[Coding Agent Telemetry](/docs/integrations/coding-agent-telemetry)</td></tr><tr><td><strong>Claude Cowork</strong></td><td><code>service.name</code> = <code>cowork</code></td><td>Logs, sent with protocol <code>http/json</code></td><td>[Claude Cowork](#claude-cowork)</td></tr><tr><td><strong>CrewAI</strong></td><td>Scope <code>crewai.enterprise</code>, <code>service.name</code> = <code>crewai</code>, or <code>crewai.*</code> span attributes</td><td>Traces</td><td>[CrewAI Native OTel](/docs/integrations/crewai/otel)</td></tr></tbody></table></div>
When an agent does not export a field, the trace shows a placeholder such as
`[<Field> not exported by <Tool>]` instead of an empty value.
<Callout type="warning">
Detection is name based. If your own service has `copilot` in its
`service.name` or agent name, or `codex` in its instrumentation scope name,
its spans are parsed as GitHub Copilot or Codex data. Choose service and
scope names that do not contain these words.
</Callout>
### Claude Cowork
In the Claude desktop app, an admin opens **Admin settings > Cowork >
Monitoring** and sets:
<div className="dv2-table-scroll"><table><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody><tr><td>OTLP endpoint</td><td><code>https://<your-ants-host>/api/public/otel/v1/logs</code></td></tr><tr><td>OTLP protocol</td><td><code>http/json</code></td></tr><tr><td>OTLP headers</td><td><code>Authorization=Basic <base64 of pk-ap-...:sk-ap-...></code></td></tr></tbody></table></div>
This needs a Claude Team or Enterprise plan. Settings load when a Cowork
session starts, so start a new session after saving. The same values are shown
in the in-app setup under the **Claude Cowork** tab.
## How users are resolved
For ordinary spans, the trace user is the first non-empty **span** attribute
in this list:
1. `langfuse.user.id`, then `user.id`
2. W3C baggage `baggage.user.id` or `baggage.langfuse.user.id`
3. `enduser.id`, then `enduser.pseudo.id`
4. `ants_platform.trace.metadata.user_id` (or the observation metadata
equivalent), or `ai.telemetry.metadata.userId`
For ordinary spans, user and session are read from span attributes only. A
resource-level `user.id` is not used. Set `user.id` on the spans, or put it in
baggage so every child span carries it.
For Claude Code, GitHub Copilot, Codex, Claude Cowork and CrewAI, a
**resource** attribute `user.id`, `enduser.id` or `user.account_id` overrides
the identity the tool sends. A resource-level `user.email` is never used as an
override. See
[User attribution](/docs/integrations/coding-agent-telemetry#user-attribution).
Users appear under **Gauge AI > Users** (see [Users](/docs/observability/users)).
Sessions are resolved from `langfuse.session.id`, `session.id`,
`gen_ai.conversation.id`, or `session_id` in metadata.
## How agents are resolved
ANTS Platform keeps a list of agents per project. Agents power the
[AI Agent Center](/docs/aigovernance/command-center) and agent-level cost,
risk and compliance views.
- **Your own agents:** set `ants_platform.agent.name` (a stable identifier),
`ants_platform.agent.id` and optionally `ants_platform.agent.display_name`.
The agent ID must be 16 lowercase hex characters, and both the ID and the
name must be present for the agent to be registered. The Python and JS/TS
SDKs compute the ID for you from the agent name and project ID.
- **Coding agents:** Claude Code, GitHub Copilot, OpenAI Codex and Claude Cowork
are registered automatically as one agent each per project.
- **CrewAI:** each CrewAI agent is registered from its `crewai.agent.role`.
`service.name` and `gen_ai.agent.name` do not create agents on their own.
Spans without the attributes above are still stored as traces, but they are
not linked to an agent.
## Choose which attributes are stored
Organizations can limit which OTel attributes are kept. Attributes that are not
selected are dropped **before** storage, on every OTel span, span event and log
record for every project in the organization.
<Steps>
### Open the setting
Go to **Organization Settings > Features > OTel Attribute Key Selection**. Only
roles with admin access to organization features can view or change it. Among
built-in roles these are Owner and Security & Compliance (see
[Members & Roles](/docs/administration/members-and-roles)).
### Turn on key-level filtering
Switch on **Key-level filtering**. When it is off, every attribute is stored.
### Pick the keys
Move keys between the available and selected lists, grouped by source (Core,
Generic GenAI, ANTS Platform SDK, Claude Code, Codex, Copilot, Claude Cowork,
CrewAI and others). Keys seen in your own traffic appear under **Discovered from
your traffic**. **Remove all free-text content** deselects every key that
carries prompt, response or instruction text in one step.
### Preview and save
Open **Sample output** to see what would be stored for each source. Save. The
selection applies to new data only.
</Steps>
Locked keys (IDs and routing attributes) are always kept, because the parser
needs them to build traces. Deselecting a content key removes the content but
never changes how a span is classified.
<Callout type="info">
If the selection cannot be loaded at ingestion time, the request is refused
with `503` rather than stored unfiltered. OTLP exporters retry, so the data
arrives once the setting is available again.
</Callout>
To keep prompt text out of coding-agent data at the source, leave content
capture off in each tool. See
[Coding Agent Telemetry](/docs/integrations/coding-agent-telemetry).
## Responses
Responses are JSON, also for protobuf requests. The traces endpoint returns
`{"successes": [...], "errors": [...]}`. Check `errors` for events that failed
validation, because the status code is still `200`.
<div className="dv2-table-scroll"><table><thead><tr><th>Status</th><th>Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Accepted. Data is processed asynchronously. Validation failures, if any, are listed in <code>errors</code>.</td></tr><tr><td><code>200</code> with <code>partialSuccess</code></td><td>Logs only: some log records were not stored. <code>rejectedLogRecords</code> counts them and <code>errorMessage</code> says why (unsupported agent format, or a record that could not be processed).</td></tr><tr><td><code>400</code></td><td>Invalid content type, body that cannot be decompressed or parsed, or a logs request where no record was stored.</td></tr><tr><td><code>401</code></td><td>Missing or invalid credentials, an organization key, or a key type not allowed on this endpoint.</td></tr><tr><td><code>402</code></td><td>Ingestion is blocked for the organization, for example because credits are exhausted. See [Credits](/docs/core-concepts/credits).</td></tr><tr><td><code>403</code></td><td>The span was produced by an ANTS Platform SDK configured with a different project key than the one used to authenticate.</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded. Retry with backoff.</td></tr><tr><td><code>503</code></td><td>The attribute selection could not be loaded. Retry.</td></tr></tbody></table></div>
A single span that cannot be parsed is dropped and the rest of the request is
stored.
## Limits and good practice
- **Rate limits** apply per organization on ANTS-hosted deployments and depend
on your plan. Exceeding them returns `429`.
- **Credits:** OTel data is metered like any other ingestion. Each trace and
each generation costs credits. See [Credits](/docs/core-concepts/credits).
- **Batching:** use a batch span processor and the exporter's default batch
size. Enable gzip for large payloads.
- **Retention:** stored traces follow your organization's
[data retention](/docs/administration/data-retention) period.
- **Compression:** only `gzip` is supported as a content encoding.
- **Sensitive data:** prompt and response text is stored as sent. Use
attribute selection, masking in the SDK, or the tool's content settings to
keep it out.
## Troubleshooting
<div className="dv2-table-scroll"><table><thead><tr><th>Symptom</th><th>Check</th></tr></thead><tbody><tr><td><code>401</code></td><td>The header must be <code>Authorization: Basic <base64(pk:sk)></code> with a project key pair. With <code>OTEL_EXPORTER_OTLP_HEADERS</code>, write it as <code>Authorization=Basic <value></code>.</td></tr><tr><td><code>400 Invalid content type</code></td><td>Use <code>http/protobuf</code> or <code>http/json</code>. gRPC is not supported.</td></tr><tr><td>Logs return <code>400</code> with "did not match a supported agent format"</td><td>Only Claude Code, Copilot, Codex and Claude Cowork logs are stored. Send other data as traces.</td></tr><tr><td>Nothing appears</td><td>Confirm the exporter flushes before your process exits (call <code>shutdown()</code> or <code>force_flush()</code>), and that you are viewing the right project and environment.</td></tr><tr><td><code>200</code> but nothing appears</td><td>Read the <code>errors</code> array in the response. A common cause is an environment name with uppercase letters.</td></tr><tr><td>Traces have no user</td><td>Set <code>user.id</code> on the spans or in baggage. A resource-level <code>user.id</code> only works for the coding-agent parsers.</td></tr></tbody></table></div>
## Related
<Cards>
<Card title="Coding Agent Telemetry" href="/docs/integrations/coding-agent-telemetry" />
<Card title="CrewAI Native OTel" href="/docs/integrations/crewai/otel" />
<Card title="Sending Data In" href="/docs/observability/ingestion" />
<Card title="Traces & Observations" href="/docs/observability/traces" />
</Cards>