Skip to main content
To set up Braintrust as an OpenTelemetry backend, you’ll need to route the traces to Braintrust’s OpenTelemetry endpoint, set your API key, and specify a parent project or experiment. Braintrust supports configuring OTel with our SDK, as well as libraries like OpenLLMetry and the Vercel AI SDK. You can also use OTel’s built-in exporters to send traces to Braintrust if you don’t want to install additional libraries or write code. OpenLLMetry supports a range of languages including Python, TypeScript, Java, and Go, so you can start logging to Braintrust from many different environments.

Python SDK configuration

Install the Braintrust Python SDK with OpenTelemetry support:
Configure these environment variables:
.env
For Python applications, use the BraintrustSpanProcessor for simplified configuration:
opentelemetry-braintrust.py
For more advanced configuration, you can pass in the following arguments to BraintrustSpanProcessor:
  • api_key: The API key to use for Braintrust. Defaults to the BRAINTRUST_API_KEY environment variable.
  • api_url: The URL of the Braintrust API. Defaults to the BRAINTRUST_API_URL environment variable or https://api.braintrust.dev if not set.
  • parent: The parent project or experiment to use for Braintrust. Defaults to the BRAINTRUST_PARENT environment variable.
  • filter_ai_spans: Defaults to False. If True, only AI-related spans will be sent to Braintrust.
  • custom_filter: A function that gives you fine-grained control over which spans are sent to Braintrust. It takes a span and returns a boolean. If True, the span will be sent to Braintrust. If False, the span will be dropped. If None, don’t influence the sampling decision.

TypeScript SDK configuration

Starting with v1.0, OpenTelemetry functionality has been moved to the separate @braintrust/otel npm package. This solves ESM build issues in Next.js (edge), Cloudflare Workers, Bun, and TanStack applications, and adds support for both OpenTelemetry v1 and v2. BraintrustSpanProcessor works with @opentelemetry/sdk-trace-base@1.x and v2 spans.If you’re upgrading from v0.x, see the upgrade guide for migration instructions.
Install the Braintrust TypeScript SDK with the following OpenTelemetry dependencies:
For TypeScript/JavaScript applications, use the BraintrustSpanProcessor with NodeSDK:
opentelemetry-braintrust.ts
Or configure it manually with a custom tracer provider:
opentelemetry-braintrust.ts
For more advanced configuration, you can pass in the following arguments to BraintrustSpanProcessor:
  • apiKey: The API key to use for Braintrust. Defaults to the BRAINTRUST_API_KEY environment variable.
  • apiUrl: The URL of the Braintrust API. Defaults to the BRAINTRUST_API_URL environment variable or https://api.braintrust.dev if not set.
  • parent: The parent project or experiment to use for Braintrust. Defaults to the BRAINTRUST_PARENT environment variable.
  • filterAISpans: Defaults to false. If true, only AI-related spans will be sent to Braintrust.
  • customFilter: A function that gives you fine-grained control over which spans are sent to Braintrust. It takes a span and returns a boolean. If true, the span will be sent to Braintrust. If false, the span will be dropped. If null, don’t influence the sampling decision.

OTel compatibility

Braintrust interoperates with OpenTelemetry at two levels: an OTel-compatible ID and export format, and compatibility mode.

ID and export format

Braintrust can represent span and trace IDs as OTel-compatible hexadecimal values and serialize them in a shared export format, so exported spans interoperate with OpenTelemetry. Support for the OTel-compatible format, including reading spans propagated through the x-bt-parent header or other distributed-tracing channels, differs by SDK:
  • Python SDK: reads either format (auto-detecting) as of v0.3.0, and exports the OTel-compatible format by default as of v0.26.0. To export Braintrust’s legacy UUID-style format instead, set BRAINTRUST_LEGACY_IDS=true.
  • TypeScript SDK: reads and exports only the legacy format by default. Enable compatibility mode with @braintrust/otel’s setupOtelCompat() to read either format (auto-detecting) and export the OTel-compatible one.
For distributed tracing between Python and TypeScript services, make sure both services use the same format. Either set BRAINTRUST_LEGACY_IDS=true on the Python service, or enable compatibility mode on the TypeScript service.

Compatibility mode

In addition to using the compatible format, compatibility mode stores the active span in OpenTelemetry’s context, so Braintrust and OpenTelemetry instrumentation contribute to the same trace within a process. Use it when running evals around OpenTelemetry-instrumented code. How you enable compatibility mode differs by SDK: in Python, set an environment variable; in TypeScript, call a setup function. The examples below enable it and send spans to Braintrust.
Set BRAINTRUST_OTEL_COMPAT=true before importing Braintrust:
Compatibility mode requires the braintrust[otel] package, v0.3.0 or later. As of v0.26.0, the OTel-compatible ID and export format is the default, but to also get compatibility mode’s context sharing, set BRAINTRUST_OTEL_COMPAT=true.
Python

Distributed tracing

You can do distributed tracing between services instrumented with the Braintrust SDK and OpenTelemetry, either to create OpenTelemetry spans as children of Braintrust spans or to create Braintrust spans as children of OpenTelemetry spans.
The examples below stitch the OpenTelemetry SDK and Braintrust SDK together using the @braintrust/otel package. If you only need to propagate Braintrust trace context between TypeScript services over HTTP, the core braintrust package has native W3C helpers — injectTraceContext and extractTraceContextFromHeaders — that emit and read standard traceparent headers without the extra package.
These examples require the OTel integration: the braintrust[otel] Python package (v0.3.5 or later) or the @braintrust/otel TypeScript package (v0.1.0 or later). The Python version requirement is higher than for compatibility mode (v0.3.0) because these examples use the distributed-tracing helper functions (context_from_span_export, add_span_parent_to_baggage, and parent_from_headers), which shipped in v0.3.5.
These examples use fetch and requests to make HTTP requests. The trace context can also be transmitted via message queue metadata, gRPC metadata, or any other inter-service communication mechanism that supports custom headers.

Create OpenTelemetry spans as children of Braintrust spans

Export the Braintrust span context and use it to create an OpenTelemetry context.

Create Braintrust spans as children of OpenTelemetry spans

Propagate the OpenTelemetry context using W3C Trace Context headers.
If you have the Braintrust parent as a string (for example, "project_name:my-project") rather than a span, use add_parent_to_baggage(parent) (Python) instead of add_span_parent_to_baggage(span).

OTLP configuration

If you are using a different language or want to use pure OTel code, you can set up the OpenTelemetry Protocol Exporter (OTLP) to send traces to Braintrust. Once you set up an OTLP exporter to send traces to Braintrust, we automatically convert LLM calls into Braintrust LLM spans, which can be saved as prompts and evaluated in the playground. For JavaScript/TypeScript applications, you can use the BraintrustExporter directly:
For collectors that use the OpenTelemetry SDK to export traces, set the following environment variables:
The trace endpoint URL is https://api.braintrust.dev/otel/v1/traces. If your exporter uses signal-specific environment variables, you’ll need to set the full path: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.braintrust.dev/otel/v1/traces
If your organization is on the EU data plane, use https://api-eu.braintrust.dev/otel instead. If you’re self-hosting Braintrust, substitute your stack’s Universal API URL. For example: OTEL_EXPORTER_OTLP_ENDPOINT=https://dfwhllz61x709.cloudfront.net/otelSee Data plane region.
The x-bt-parent header sets the trace’s parent project or experiment. You can use a prefix like project_id:, project_name:, or experiment_id: here, or pass in a span slug (span.export()) to nest the trace under a span within the parent object.
To find your project ID, navigate to your project’s configuration page and find the Copy Project ID button at the bottom of the page.

Vercel AI SDK

The Vercel AI SDK natively supports OpenTelemetry and works out of the box with Braintrust, either via Next.js or Node.js.

Next.js

If you are using Next.js, use the Braintrust exporter with @vercel/otel:
Traced LLM calls will appear under the Braintrust project or experiment provided in the parent field. When you call the AI SDK, make sure to set experimental_telemetry:
The integration supports streaming functions like streamText. Each streamed call will produce ai.streamText spans in Braintrust.

Node.js

If you are using Node.js without a framework, you must configure the NodeSDK directly. Here, it’s more straightforward to use the BraintrustSpanProcessor. First, install the necessary dependencies:
Then, set up the OpenTelemetry SDK:

Manual tracing

If you want to log LLM calls directly to the OTel endpoint, you can set up a custom OpenTelemetry tracer and add the appropriate attributes to your spans. This gives you fine-grained control over what data gets logged.
For attaching custom application metadata and tags to spans — like user IDs or request context — see Add metadata and tags.

GenAI attributes

Braintrust implements the OpenTelemetry GenAI semantic conventions. When you send traces with these attributes, they are automatically mapped to Braintrust fields.
AttributeBraintrust FieldDescription
gen_ai.input.messagesinputThe chat history provided to the model as an input. Messages must be structured according to the OpenTelemetry GenAI input messages attribute specification.
gen_ai.promptinputUser 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_jsoninputA JSON-serialized string containing an array of OpenAI messages.
gen_ai.output.messagesoutputMessages returned by the model. Messages must be structured according to the OpenTelemetry GenAI output messages attribute specification.
gen_ai.completionoutputAssistant 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_jsonoutputA JSON-serialized string containing an array of OpenAI messages.
gen_ai.requestmetadata.*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.modelmetadata.modelThe model name (e.g. “gpt-4o”). Provider prefixes like “openai/”, “anthropic/”, “google/” are automatically removed.
gen_ai.request.max_tokensmetadata.max_tokensMaximum tokens to generate.
gen_ai.request.temperaturemetadata.temperatureSampling temperature.
gen_ai.request.top_pmetadata.top_pNucleus sampling parameter.
gen_ai.operation.namespan_attributes.typeThe operation type. Value “chat” maps to type “llm”, “execute_tool” maps to type “tool”.
gen_ai.agent.toolsmetadata.toolsA 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.namemetadata.toolsThe name of the tool being executed. Automatically converted into a tool definition object. Also sets span_attributes.type to “tool”.
gen_ai.usagemetrics.*A JSON object containing token usage. Can include prompt_tokens, completion_tokens, input_tokens, output_tokens, and total_tokens.
gen_ai.usage.prompt_tokensmetrics.prompt_tokensInput tokens (preferred field name).
gen_ai.usage.completion_tokensmetrics.completion_tokensOutput tokens (preferred field name).
gen_ai.usage.input_tokensmetrics.prompt_tokensInput tokens (alternative field name, normalized to prompt_tokens).
gen_ai.usage.output_tokensmetrics.completion_tokensOutput tokens (alternative field name, normalized to completion_tokens).
gen_ai.usage.total_tokensmetrics.tokensTotal tokens (normalized to tokens). If not provided, automatically calculated from prompt_tokens + completion_tokens.
gen_ai.usage.cache_creation.input_tokensmetrics.prompt_cache_creation_tokensInput tokens written to a provider-managed cache.
gen_ai.usage.cache_read.input_tokensmetrics.prompt_cached_tokensInput tokens served from a provider-managed cache.

Braintrust attributes

You can also use the braintrust namespace to set fields in Braintrust directly:
AttributeBraintrust FieldNotes
braintrust.inputinputTypically a single user message (string). If you have an array of messages, use braintrust.input_json instead (see below) or set flattened attributes like braintrust.input.0.role or braintrust.input.0.content.
braintrust.input_jsoninputA JSON-serialized string containing an array of OpenAI messages.
braintrust.outputoutputTypically a single assistant message (string). If you have an array of messages, use braintrust.output_json instead (see below) or set flattened attributes like braintrust.output.0.role or braintrust.output.0.content.
braintrust.output_jsonoutputA JSON-serialized string containing an array of OpenAI messages.
braintrust.metadatametadataA 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.metricsmetricsA JSON-serialized dictionary with string keys. Alternatively, you can use flattened attribute names, like braintrust.metrics.prompt_tokens or braintrust.metrics.completion_tokens.
braintrust.scoresscoresA 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.expectedexpectedThe expected output for the span. Can be any value (string, number, object, etc.).
braintrust.expected_jsonexpectedA JSON-serialized string containing the expected output. Use this when you need to pass complex objects or arrays as the expected value.
braintrust.tagstagsAn array of strings that can be set on any span. Tags from all spans in a trace are aggregated together.
braintrust.span_attributesspan_attributesA 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".
Fields mapped from braintrust.* attributes are deleted and translated into Braintrust’s native format.

GenAI events

In addition to attributes, Braintrust also processes GenAI events on spans to extract input/output messages. These events follow the OpenTelemetry GenAI semantic conventions for events:
Event NameFieldDescription
gen_ai.user.messageinputUser message event. Content is extracted from the content attribute (supports both string and JSON array format).
gen_ai.system.messageinputSystem message event. Content is extracted from the content attribute.
gen_ai.choiceoutputModel response event. Message is extracted from the message attribute and can include both text content and tool calls.
gen_ai.assistant.messageoutputAssistant message event. Content is extracted from the content attribute.
gen_ai.tool.messageinputTool result event. Content is extracted from the content attribute and associated with the tool call via the id attribute.
Events are processed in chronological order and combined with attribute-based messages to provide a complete view of the conversation flow. Here’s an example of how to set up manual tracing:

Troubleshooting

Why are my traces not showing up?

There are a few common reasons why your traces may not show up in Braintrust:
  • Braintrust’s logs table only shows traces that have a root span (i.e. span_parents is empty). If you only send children spans, they will not appear in the logs table. A common reason for this is only sending spans to Braintrust which have a traceparent header. To fix this, make sure to send a root span for every trace you want to appear in the UI.
  • Make sure the OTEL_EXPORTER_OTLP_ENDPOINT matches your organization’s data plane region. Organizations on the EU data plane should use https://api-eu.braintrust.dev/otel. Self-hosted deployments should use their custom API URL, for example https://dfwhllz61x709.cloudfront.net/otel.
  • You must explicitly set up OpenTelemetry in your application. If you’re using Next.js, then follow the Next.js OpenTelemetry guide. If you are using Node.js without a framework, then follow this example to set up a basic exporter.

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.