Skip to main content
Applies to:
  • Plan -
  • Deployment -

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 fieldBraintrust field
session.idroot_span_id, metadata.session_id
prompt.idmetadata.prompt_id
modelmetadata.model
cost_usdmetrics.estimated_cost
input_tokensmetrics.prompt_tokens
output_tokensmetrics.completion_tokens
total_tokensmetrics.tokens
promptinput
event namespan_attributes.name, metadata.event_type
Minimal insert example:
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:
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.