> ## Documentation Index
> Fetch the complete documentation index at: https://braintrust.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest OTLP log records into Braintrust

export const plans_0 = "Any"

export const deployments_0 = "Any"

export const data_plane_version_0 = undefined

export const use_case_0 = "Use case - Sending telemetry from a source that exports only the OTel logs signal (e.g., Claude Cowork) to Braintrust, which only accepts OTLP traces"

<Note>
  **Applies to:**

  * Plan - {plans_0}
  * Deployment - {deployments_0}
  * {data_plane_version_0}
  * {use_case_0}
</Note>

## Summary

Braintrust's OTLP intake accepts traces, not OTLP logs. If a source only emits the OTel logs signal, such as Claude Cowork, translate those log records into Braintrust spans before sending them to Braintrust.

## What is happening

Braintrust does not provide an OTLP logs endpoint such as `/v1/logs`. The OTLP traces endpoint is:

* US hosted: `https://api.braintrust.dev/otel/v1/traces`
* EU hosted: `https://api-eu.braintrust.dev/otel/v1/traces`
* Self-hosted/custom data plane: use your stack's Universal API URL with `/otel/v1/traces`

For Collector or SDK configs that expect the base OTLP endpoint, use `/otel` instead of `/otel/v1/traces`.

Sources that emit only OTLP logs need a translation step. The usual pattern is to group related log records into one trace, create a synthetic root span for the group, then convert each log event into a child span.

## Fix or suggestion

### Option 1: Use a small adapter service

This is the most direct approach.

1. Read OTLP log records from the source.
2. Group related records by a stable ID, such as `session.id` or `prompt.id`.
3. Create one synthetic root span per group.
4. Convert each log record into a child span.
5. Insert the translated spans with `POST /v1/project_logs/{project_id}/insert`.

Example span mapping:

| Source field    | Braintrust field                              |
| --------------- | --------------------------------------------- |
| `session.id`    | `root_span_id`, `metadata.session_id`         |
| `prompt.id`     | `metadata.prompt_id`                          |
| `model`         | `metadata.model`                              |
| `cost_usd`      | `metrics.estimated_cost`                      |
| `input_tokens`  | `metrics.prompt_tokens`                       |
| `output_tokens` | `metrics.completion_tokens`                   |
| `total_tokens`  | `metrics.tokens`                              |
| `prompt`        | `input`                                       |
| event name      | `span_attributes.name`, `metadata.event_type` |

Minimal insert example:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -X POST https://api.braintrust.dev/v1/project_logs/{project_id}/insert \
  -H "Authorization: Bearer $BRAINTRUST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "id": "<root-row-uuid>",
        "root_span_id": "<session.id>",
        "span_id": "<session.id>",
        "span_parents": [],
        "metadata": { "session_id": "<session.id>" },
        "span_attributes": { "type": "task", "name": "claude_cowork_session" }
      },
      {
        "id": "<child-row-uuid>",
        "root_span_id": "<session.id>",
        "span_id": "<event-uuid>",
        "span_parents": ["<session.id>"],
        "input": "<prompt text>",
        "metadata": {
          "model": "claude-3-5-sonnet",
          "event_type": "api_request",
          "session_id": "<session.id>"
        },
        "metrics": {
          "prompt_tokens": 512,
          "estimated_cost": 0.0014
        },
        "span_attributes": { "type": "llm", "name": "api_request" }
      }
    ]
  }'
```

Use `api-eu.braintrust.dev` for EU-hosted organizations, or the customer's Universal API URL for self-hosted/custom data plane deployments.

### Option 2: Emit real OTLP spans

If your adapter can emit real spans instead of log records, forward those spans to Braintrust's OTLP traces endpoint.

Collector example:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
exporters:
  otlphttp/braintrust:
    endpoint: https://api.braintrust.dev/otel
    headers:
      Authorization: "Bearer ${BRAINTRUST_API_KEY}"
      x-bt-parent: "project_id:${BRAINTRUST_PROJECT_ID}"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/braintrust]
```

Use `https://api-eu.braintrust.dev/otel` for EU-hosted organizations.

## Important limitation

The standard OTel Collector `transform` processor can modify log attributes, but it does not by itself promote log records into trace spans. For production log-to-span ingestion, use a small adapter service or a custom Collector connector/processor that creates real spans.

## How to confirm it worked

* Open Braintrust Logs and confirm one trace appears per grouped `session.id` or `prompt.id`.
* Open a trace and verify the synthetic root span contains session metadata.
* Verify each source log event appears as a child span with the expected `span_attributes.name`.
* Confirm token and cost fields appear under `metrics`.
