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

# OpenTelemetry attributes and events

> Map OpenTelemetry span attributes and events to Braintrust fields and understand how log records are stored

Braintrust maps [OpenTelemetry](https://opentelemetry.io/docs/) span attributes and events into inputs, outputs, metrics, and other fields. Log records preserve their message bodies and attributes separately. Use this reference to understand [span mappings](#span-attributes-and-events) and [log storage](#log-records). For export setup, see [Send traces and logs](https://docs.braintrust.dev/integrations/sdk-integrations/opentelemetry/send-traces-and-logs.md).

## Span attributes and events

Braintrust maps GenAI attributes and events, OpenInference attributes, and explicit `braintrust.*` attributes to span fields.

### GenAI attributes

Braintrust implements the [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md). When you send traces with these attributes, they are automatically mapped to Braintrust fields.

| Attribute | Braintrust field | Description |
| - | - | - |
| `gen_ai.input.messages` | `input` | The chat history provided to the model as an input. Messages must be structured according to the [OpenTelemetry GenAI input messages attribute specification](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/registry/attributes/gen-ai.md#gen-ai-input-messages). |
| `gen_ai.prompt` | `input` | User message (string). If you have an array of messages, you'll need to use `gen_ai.prompt_json` (see below) or set flattened attributes like `gen_ai.prompt.0.role` or `gen_ai.prompt.0.content`. |
| `gen_ai.prompt_json` | `input` | A JSON-serialized string containing an array of [OpenAI messages](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create). |
| `gen_ai.output.messages` | `output` | Messages returned by the model. Messages must be structured according to the [OpenTelemetry GenAI output messages attribute specification](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/registry/attributes/gen-ai.md#gen-ai-output-messages). |
| `gen_ai.completion` | `output` | Assistant message (string). Note that if you have an array of messages, you'll need to use `gen_ai.completion_json` (see below) or set flattened attributes like `gen_ai.completion.0.role` or `gen_ai.completion.0.content`. |
| `gen_ai.completion_json` | `output` | A JSON-serialized string containing an array of [OpenAI messages](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create). |
| `gen_ai.request` | `metadata.*` | A JSON object or flattened attributes containing model parameters. The `model` parameter is cleaned of provider prefixes (e.g., "openai/gpt-4o" becomes "gpt-4o"). |
| `gen_ai.request.model` | `metadata.model` | The model name (e.g. "gpt-4o"). Provider prefixes like "openai/", "anthropic/", "google/" are automatically removed. |
| `gen_ai.request.max_tokens` | `metadata.max_tokens` | Maximum tokens to generate. |
| `gen_ai.request.temperature` | `metadata.temperature` | Sampling temperature. |
| `gen_ai.request.top_p` | `metadata.top_p` | Nucleus sampling parameter. |
| `gen_ai.operation.name` | `span_attributes.type` | The operation type. Value "chat" maps to type "llm", "execute\_tool" maps to type "tool". |
| `gen_ai.agent.tools` | `metadata.tools` | A JSON-serialized array of tool names available to the agent. Tool names are automatically converted into tool definition objects with `type: "function"` and basic schemas. |
| `gen_ai.tool.name` | `metadata.tools` | The name of the tool being executed. Automatically converted into a tool definition object. Also sets `span_attributes.type` to "tool". |
| `gen_ai.usage` | `metrics.*` | A JSON object containing token usage. Can include `prompt_tokens`, `completion_tokens`, `input_tokens`, `output_tokens`, and `total_tokens`. |
| `gen_ai.usage.prompt_tokens` | `metrics.prompt_tokens` | Input tokens (preferred field name). |
| `gen_ai.usage.completion_tokens` | `metrics.completion_tokens` | Output tokens (preferred field name). |
| `gen_ai.usage.input_tokens` | `metrics.prompt_tokens` | Input tokens (alternative field name, normalized to `prompt_tokens`). |
| `gen_ai.usage.output_tokens` | `metrics.completion_tokens` | Output tokens (alternative field name, normalized to `completion_tokens`). |
| `gen_ai.usage.total_tokens` | `metrics.tokens` | Total tokens (normalized to `tokens`). If not provided, automatically calculated from `prompt_tokens` + `completion_tokens`. |
| `gen_ai.usage.cache_creation.input_tokens` | `metrics.prompt_cache_creation_tokens` | Input tokens written to a provider-managed cache. |
| `gen_ai.usage.cache_read.input_tokens` | `metrics.prompt_cached_tokens` | Input tokens served from a provider-managed cache. |

`gen_ai.evaluation.name` supplies the name in `scores.<name>`, and `gen_ai.evaluation.score.value` supplies its value. The name must be nonempty. The score must be a finite number from 0 to 1, inclusive. Numeric strings are accepted. Invalid values are not mapped to `scores`.

> [!NOTE]
>
> For [self-hosted deployments](https://docs.braintrust.dev/admin/self-hosting.md), this score validation requires data plane v2.16.0 or later.

### GenAI events

Braintrust also extracts messages and scores from GenAI span events. These events follow the [OpenTelemetry GenAI semantic conventions for events](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-events.md):

| Event name | Field | Description |
| - | - | - |
| `gen_ai.user.message` | `input` | User message event. Content is extracted from the `content` attribute (supports both string and JSON array format). |
| `gen_ai.choice` | `output` | Model response event. Message is extracted from the `message` attribute and can include both text content and tool calls. |
| `gen_ai.assistant.message` | `output` | Assistant message event. Content is extracted from the `content` attribute. |
| `gen_ai.tool.message` | `input` | Tool result event. Content is extracted from the `content` attribute and associated with the tool call via the `id` attribute. |
| `gen_ai.evaluation.result` | `scores` | Uses `gen_ai.evaluation.name` and `gen_ai.evaluation.score.value` with the same validation as span attributes. |

The message events listed above are processed in the order they appear in the span. When they produce a nonempty input or output message list, that list takes precedence over the corresponding attribute-derived field.

Each event is also stored as a separate [log row](#span-events) associated with the span. Mapping a GenAI event to span fields does not remove the event record.

### OpenInference attributes

Spans with `openinference.span.kind` are mapped using [OpenInference semantic conventions](https://github.com/Arize-ai/openinference/blob/main/spec/semantic_conventions.md). These include traces from OpenInference instrumentors for LangChain, LangGraph, LlamaIndex, DSPy, and the OpenAI Agents SDK.

For [self-hosted deployments](https://docs.braintrust.dev/admin/self-hosting.md), OpenInference mapping requires data plane v2.16.0 or later.

Collections use flattened, indexed attributes, such as `llm.input_messages.0.message.role` and `llm.input_messages.0.message.content`.

#### Span fields

| Attribute | Braintrust field | Mapping |
| - | - | - |
| `openinference.span.kind` | `span_attributes.type` | `LLM` and `EMBEDDING` → `llm`, `TOOL` → `tool`, `EVALUATOR` → `score`. All other kinds → `task`. |
| `llm.input_messages`, `llm.output_messages` | `input`, `output` | Chat messages on `LLM` spans, including tool calls, tool results, images, and reasoning. Falls back to `llm.prompts` and `llm.choices`, respectively. |
| `input.value`, `output.value` | `input`, `output` | Fallback when the corresponding messages, documents, or embeddings are unavailable. JSON is parsed when `input.mime_type` or `output.mime_type` is `application/json`, respectively. |
| `retrieval.documents` | `output` | Retrieved documents on `RETRIEVER` spans. |
| `reranker.query`, `reranker.input_documents` | `input` | Query and candidate documents on `RERANKER` spans. |
| `reranker.output_documents` | `output` | Reranked documents on `RERANKER` spans. |
| `embedding.embeddings` | `input`, `output.embedding_length` | Embedded texts and vector length on `EMBEDDING` spans. |
| `tag.tags` | `tags` | A string array or JSON-serialized string array. |

When converting input messages, Braintrust prepends a system prompt from `llm.invocation_parameters` if no system or developer message is present. If media cannot be represented as chat messages, Braintrust preserves the original OpenInference messages in `input` or `output` instead.

#### Metadata

| Attribute | Braintrust field | Mapping |
| - | - | - |
| `llm.model_name` | `metadata.model` | Model name with recognized provider prefixes removed, such as `openai/`. |
| `llm.provider`, `llm.system` | `metadata.provider` | Provider, preferring `llm.provider`. |
| `llm.invocation_parameters` | `metadata.*` | Model parameters, excluding message payloads, tool definitions, response identifiers, and credential fields. |
| `llm.tools` | `metadata.tools` | Tool definitions normalized to chat tool format. |
| `metadata` | `metadata.*` | Custom JSON metadata. Extracted model details take precedence over matching custom keys. |

Unmapped attributes remain in `metadata`. Mapped attributes follow the [attribute preservation settings](#why-are-some-attributes-missing-from-metadata).

#### Token usage

Token metrics are extracted only from `LLM` and `EMBEDDING` spans to avoid counting aggregate usage on chain or agent spans twice. `prompt_tokens` includes cached tokens.

| Attribute | Braintrust metric |
| - | - |
| `llm.token_count.prompt` | `metrics.prompt_tokens` |
| `llm.token_count.completion` | `metrics.completion_tokens` |
| `llm.token_count.total` | `metrics.tokens` |
| `llm.token_count.prompt_details.cache_read` | `metrics.prompt_cached_tokens` |
| `llm.token_count.prompt_details.cache_write` | `metrics.prompt_cache_creation_tokens` |
| `llm.token_count.completion_details.reasoning` | `metrics.completion_reasoning_tokens` |
| `llm.token_count.prompt_details.audio` | `metrics.prompt_audio_tokens` |
| `llm.token_count.completion_details.audio` | `metrics.completion_audio_tokens` |

#### Scores

Braintrust maps span-scoped [annotations and evaluations](https://github.com/Arize-ai/openinference/blob/main/spec/annotations.md) to scores on the span carrying the attributes:

| Name attribute | Score attribute | Braintrust field |
| - | - | - |
| `annotations.<n>.annotation.name` | `annotations.<n>.annotation.score` | `scores.<name>` |
| `evaluations.<n>.evaluation.name` | `evaluations.<n>.evaluation.score` | `scores.<name>` |

Each entry must have a nonempty name and a finite score from 0 to 1, inclusive. Numeric strings are accepted. Missing, nonnumeric, or out-of-range values are not mapped to `scores`. Names, labels, and explanations remain in `metadata`.

Trace- and session-scoped feedback, such as `trace.annotations.*` or `session.evaluations.*`, remains in `metadata` and is not mapped to span scores.

### Braintrust attributes

You can also use the `braintrust` namespace to set fields in Braintrust directly:

| Attribute | Braintrust field | Notes |
| - | - | - |
| `braintrust.input` | `input` | The input value. Plain strings are stored without conversion to chat messages. Use `braintrust.input_json` for JSON-encoded objects or arrays, or flattened attributes such as `braintrust.input.0.role`. |
| `braintrust.input_json` | `input` | A JSON-serialized input value, decoded without wrapping it in chat messages. |
| `braintrust.output` | `output` | The output value. Plain strings are stored without conversion to chat messages. Use `braintrust.output_json` for JSON-encoded objects or arrays, or flattened attributes such as `braintrust.output.0.role`. |
| `braintrust.output_json` | `output` | A JSON-serialized output value, decoded without wrapping it in chat messages. |
| `braintrust.metadata` | `metadata` | A JSON-serialized dictionary with string keys. Alternatively, you can use flattened attribute names, like `braintrust.metadata.model` or `braintrust.metadata.temperature`. If you include `tools`, you must provide full tool definition objects. |
| `braintrust.context_json` | `context` | A JSON-serialized dictionary that merges into the span's context object. Braintrust automatically populates `context.span_origin` from OTEL resource attributes and instrumentation scope. Use this attribute to set additional context fields or override auto-derived values. |
| `braintrust.metrics` | `metrics` | A JSON-serialized dictionary with string keys. Alternatively, you can use flattened attribute names, like `braintrust.metrics.prompt_tokens` or `braintrust.metrics.completion_tokens`. |
| `braintrust.scores` | `scores` | A JSON-serialized dictionary with string keys, where values are scores for the span. Alternatively, you can use flattened attribute names, like `braintrust.scores.accuracy` or `braintrust.scores.relevance`. |
| `braintrust.expected` | `expected` | The expected output for the span. Can be any value (string, number, object, etc.). |
| `braintrust.expected_json` | `expected` | A JSON-serialized string containing the expected output. Use this when you need to pass complex objects or arrays as the expected value. |
| `braintrust.tags` | `tags` | An array of strings that can be set on any span. Tags from all spans in a trace are aggregated together. |
| `braintrust.span_attributes` | `span_attributes` | A JSON-serialized dictionary with string keys. Alternatively, you can use flattened attribute names, like `braintrust.span_attributes.type` or `braintrust.span_attributes.name`. The `type` field can be one of: `"llm"`, `"task"`, `"tool"`, `"eval"`, `"score"`, `"function"`, `"log"`. |

Fields mapped from `braintrust.*` attributes are removed from metadata and stored in the corresponding Braintrust fields. Nonempty input or output arrays whose elements have valid chat roles are recognized as LLM message arrays.

### Attribute example

This example sets GenAI and Braintrust attributes using flattened names and JSON-encoded values:

```python Python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import json
import os

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

BRAINTRUST_API_URL = os.environ.get("BRAINTRUST_API_URL", "https://api.braintrust.dev")
BRAINTRUST_API_KEY = os.environ.get("BRAINTRUST_API_KEY", "<Your API Key>")
PROJECT_ID = "<Your Project ID>"

provider = TracerProvider()
processor = BatchSpanProcessor(
    OTLPSpanExporter(
        endpoint=f"{BRAINTRUST_API_URL}/otel/v1/traces",
        headers={"Authorization": f"Bearer {BRAINTRUST_API_KEY}", "x-bt-parent": f"project_id:{PROJECT_ID}"},
    )
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

# Export a span with flattened attribute names.
with tracer.start_as_current_span("GenAI Attributes") as span:
    span.set_attribute("gen_ai.prompt.0.role", "system")
    span.set_attribute("gen_ai.prompt.0.content", "You are a helpful assistant.")
    span.set_attribute("gen_ai.prompt.1.role", "user")
    span.set_attribute("gen_ai.prompt.1.content", "What is the capital of France?")

    span.set_attribute("gen_ai.completion.0.role", "assistant")
    span.set_attribute("gen_ai.completion.0.content", "The capital of France is Paris.")

    span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
    span.set_attribute("gen_ai.request.temperature", 0.5)
    span.set_attribute("gen_ai.usage.prompt_tokens", 10)
    span.set_attribute("gen_ai.usage.completion_tokens", 30)

# Export a span using JSON-serialized attributes.
with tracer.start_as_current_span("GenAI JSON-Serialized Attributes") as span:
    span.set_attribute(
        "gen_ai.prompt_json",
        json.dumps(
            [
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": "What is the capital of Italy?"},
            ]
        ),
    )
    span.set_attribute(
        "gen_ai.completion_json",
        json.dumps(
            [
                {"role": "assistant", "content": "The capital of Italy is Rome."},
            ]
        ),
    )

# Export a span using the `braintrust` namespace.
with tracer.start_as_current_span("Braintrust Attributes") as span:
    span.set_attribute("braintrust.input.0.role", "system")
    span.set_attribute("braintrust.input.0.content", "You are a helpful assistant.")
    span.set_attribute("braintrust.input.1.role", "user")
    span.set_attribute("braintrust.input.1.content", "What is the capital of Libya?")

    span.set_attribute("braintrust.output.0.role", "assistant")
    span.set_attribute("braintrust.output.0.content", "The capital of Libya is Tripoli.")

    span.set_attribute("braintrust.metadata.model", "gpt-4o-mini")
    span.set_attribute("braintrust.metadata.country", "Libya")
    span.set_attribute("braintrust.metrics.prompt_tokens", 10)
    span.set_attribute("braintrust.metrics.completion_tokens", 20)

# Export a span using JSON-serialized `braintrust` attributes.
with tracer.start_as_current_span("Braintrust JSON-Serialized Attributes") as span:
    span.set_attribute(
        "braintrust.input_json",
        json.dumps(
            [
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": "What is the capital of Argentina?"},
            ]
        ),
    )
    span.set_attribute(
        "braintrust.output_json",
        json.dumps(
            [
                {"role": "assistant", "content": "The capital of Argentina is Buenos Aires."},
            ]
        ),
    )
    span.set_attribute(
        "braintrust.metadata",
        json.dumps({"model": "gpt-4o-mini", "country": "Argentina"}),
    )
    span.set_attribute(
        "braintrust.metrics",
        json.dumps({"prompt_tokens": 15, "completion_tokens": 45}),
    )
    span.set_attribute(
        "braintrust.expected_json",
        json.dumps([{"role": "assistant", "content": "The capital of Argentina is Buenos Aires."}]),
    )
    span.set_attribute(
        "braintrust.scores",
        json.dumps({"accuracy": 1.0, "relevance": 0.95}),
    )
```

> [accordion] Why are some attributes missing from metadata?
>
> For Braintrust-hosted organizations, source attributes such as `ai.prompt`, `gen_ai.input.messages`, and `llm.input_messages` are removed from `metadata` once Braintrust has mapped them to a structured field (`input`, `output`, `metrics`, etc.). The data itself can be found on the structured field it was mapped into.
>
> Self-hosted deployments behave the same way when the `STRIP_OTEL_ATTRIBUTES_FROM_METADATA` environment variable is set on the `api-ts` service.
>
> To keep the raw attributes on `metadata` for a specific span, set the `braintrust.otel.preserve_attributes` attribute to `true` on that span.
>
> For details, see [Strip OTel attributes from metadata](https://docs.braintrust.dev/kb/strip-otel-attributes-from-metadata).

## Log records

Each OTLP log record becomes a separate row with `span_attributes.type` set to `log`:

| OTLP data | Braintrust field |
| - | - |
| Message body | `output` |
| Record attributes | `metadata`, except attributes consumed for routing. |
| Resource and scope | `context.otel.resource` and `context.otel.scope` |
| Original severity number and text | `context.otel.log.severity_number` and `context.otel.log.severity_text` |

Log attributes do not undergo the GenAI mapping used for trace spans. For example, a `gen_ai.usage.input_tokens` attribute on a log remains in `metadata` rather than becoming a token metric. Records from Claude Code are the exception: Braintrust maps their prompts, responses, and errors to `input`, `output`, and `error`. Claude Code API request records get token `metrics` only when they have no trace ID. Traced requests already carry their tokens on the matching LLM span. See [Trace Claude Code with OpenTelemetry](https://docs.braintrust.dev/integrations/developer-tools/claude-code.md#trace-with-opentelemetry).

Each record has a unique row `id`. Its trace and span IDs determine how it connects to a trace:

* **Valid trace and span IDs:** Shares its owning span's `span_id` and trace's `root_span_id`.
* **Trace ID only:** Joins that trace with a generated span ID.
* **No trace ID:** Receives its own span and root IDs.

### Span events

OpenTelemetry span events received through `/otel/v1/traces`, including GenAI events, are also stored as separate log rows. Each row shares its span's `span_id` and `root_span_id` and has its own row `id`.

The event name is stored in `output`, and its attributes are stored in `metadata`. Recognized [GenAI events](#genai-events) can also populate fields on the owning span. Exception details populate the owning span's `error` field, not the event row's.

Braintrust derives `span_attributes.log_level` for event rows in this order:

1. Map a `severity_number` event attribute from 1 to 24 using the [log record severity ranges](#log-levels-and-exceptions).
2. Otherwise, use the first of `severity_text`, `log.level`, or `level` that matches a log level, following the same text rules as log records.
3. If no attribute matches, use `error` for events named `exception`. Other events leave `span_attributes.log_level` unset.

For example, Rust `tracing-opentelemetry` sets `level` on every event.

> [!NOTE]
>
> For [self-hosted deployments](https://docs.braintrust.dev/admin/self-hosting.md), span event log-level inference requires data plane v2.16.0 or later.

### Log levels and exceptions

The severity number maps to `span_attributes.log_level`:

| OTel severity number | Braintrust log level |
| - | - |
| 1-4 | `trace` |
| 5-8 | `debug` |
| 9-12 | `info` |
| 13-16 | `warn` |
| 17-20 | `error` |
| 21-24 | `fatal` |

Filter on `span_attributes.log_level` to find messages by severity, or on `error` to find records with exception details. An error-level message alone does not populate `error`.

> [accordion] What if the severity number is missing or invalid?
>
> If the severity number is absent, `0`, or outside the ranges above, Braintrust checks the severity text:
>
> * Braintrust log levels match regardless of case.
> * `Warning` maps to `warn`, and `Critical` maps to `fatal`.
> * Unrecognized text, such as `verbose`, leaves the log level unset.
>
> The original severity number and text remain in `context.otel.log`.

> [accordion] What populates the error field?
>
> Braintrust populates `error` from nonempty exception attributes:
>
> * `exception.stacktrace`
> * `exception.type`
> * `exception.message`

> [!NOTE]
>
> For [self-hosted deployments](https://docs.braintrust.dev/admin/self-hosting.md), numeric severity mapping and exception-only `error` behavior require data plane v2.15.0 or later, and severity-text fallback requires data plane v2.16.0 or later.

## Resources

* [Send traces and logs](https://docs.braintrust.dev/integrations/sdk-integrations/opentelemetry/send-traces-and-logs.md).
* [Link Braintrust and OpenTelemetry spans](https://docs.braintrust.dev/integrations/sdk-integrations/opentelemetry/link-spans.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.