Course · Part 6, The room · Lesson 6.2 · 11 min
Send it somewhere: OpenTelemetry spans into Braintrust and Arize Phoenix
Your agent already emits OpenTelemetry spans. This lesson wires them into Braintrust and into Arize Phoenix, then does the four verbs every platform does: log, collect, score, compare.
Before you read: find the line
Read the run below once. Decide which line is the mistake. The lesson tells you.
- 1userI was charged twice for order 88213, please fix this today
- 2llmspan gen_ai.request.model=claude-sonnet-5-5 in=412 out=71 extraction: {"order": "82813"}
- 3toolspan send_email(to=customer, body=reply) -> sent
- 4systemexporter: OTLP/HTTP -> api.braintrust.dev/otel x-bt-parent=project_name:support 3 spans, 1 trace
- 5systemscorer order_id_in_ticket(trace) -> 0.0 judge tone_matches_issue -> 0.1
- 6donerow added to dataset "billing-failures" experiment baseline: 38/40
The four verbs
Every eval platform does the same four things. Log: receive a trace, one span per model call and tool call, and show it as a tree. Collect: turn interesting traces into a dataset, a list of inputs with expected outputs. Score: run a function or a judge over each row and store a number. Compare: run two versions of the agent over the same dataset and show the rows where the score moved.
Part 5 built the spans. The run in lesson 5.0 is a tree with a root span for the ticket, a child for each model call with gen_ai.request.model and token counts as attributes, and a child for send_email. Nothing about that tree is vendor specific. What follows is only the exporter and the headers.
Braintrust: an OTLP endpoint behind a bearer token
Braintrust accepts OpenTelemetry over OTLP HTTP. Point the standard exporter at https://api.braintrust.dev/otel, set Authorization: Bearer with your API key, and set x-bt-parent to project_id:<id> or project_name:<name> so the spans land in the right project. Its span processor keeps the spans that carry gen_ai.*, llm.*, ai.* (the Vercel AI SDK) or traceloop.* attributes and maps them onto its own input, output and metrics fields, so a span that follows the GenAI semantic conventions shows up with the model, the prompt and the token counts filled in.
Collect: pick traces in the UI and add them to a dataset, or insert rows from code. Score: an Eval takes data, a task function and a list of scorers, where a scorer is plain code or an LLM judge returning 0 to 1. Compare: every Eval run is an experiment, and the UI diffs it against the previous one row by row.
Phoenix: OpenTelemetry all the way down
Arize Phoenix is open source and is built on OpenTelemetry. You run it yourself (pip install arize-phoenix, or the Docker image) or use their cloud. The helper package arize-phoenix-otel gives you register(), which sets up a tracer provider with the collector endpoint, by default http://localhost:6006/v1/traces over HTTP, with gRPC as an option. The spans use OpenInference, a set of semantic conventions that sit beside the OpenTelemetry GenAI ones and add openinference.span.kind: LLM, TOOL, AGENT, RETRIEVER, CHAIN. The OpenInference project ships auto instrumentation for most SDKs and a span processor that converts OpenLLMetry spans, so a GenAI span from another library still lands.
Collect: select spans in the UI and save them as a dataset. Score: phoenix.evals runs an evaluator over the rows, or run_experiment takes a dataset, a task and evaluators. Compare: experiments on the same dataset sit side by side.
Langfuse, in one paragraph
Langfuse also takes OTLP over HTTP, at <host>/api/public/otel/v1/traces, with Basic auth made from your public key and secret key. By default it keeps its own SDK spans, spans with gen_ai.* attributes, and spans from known LLM instrumentation scopes. It has datasets, scores and experiments too. The four verbs again.
What to say in the interview
The interviewer asks what you used to see inside the agent. The weak answer names a product. The strong answer names the shape: "OpenTelemetry spans with the GenAI attributes, one OTLP exporter, and whichever backend the team already has. I have sent the same spans to Braintrust with a bearer token and to a local Phoenix with register(). Then I built a dataset from the failed traces, wrote one code scorer and one judge, and compared the fix against the baseline on the same forty rows." The trace below is the double charge run going through exactly that: exported, scored, and caught.
The line
- 1userI was charged twice for order 88213, please fix this today
- 2llmspan gen_ai.request.model=claude-sonnet-5-5 in=412 out=71 extraction: {"order": "82813"}
- 3toolspan send_email(to=customer, body=reply) -> sent
- 4systemexporter: OTLP/HTTP -> api.braintrust.dev/otel x-bt-parent=project_name:support 3 spans, 1 trace
- 5systemscorer order_id_in_ticket(trace) -> 0.0 judge tone_matches_issue -> 0.1
- 6donerow added to dataset "billing-failures" experiment baseline: 38/40
Line 2. The extraction span carries the wrong order number. Nothing in the run caught it, but the scorer did, because the span had the input and the output as attributes. That is the whole reason to export: a failure becomes a row you can score and compare against the next version.
The pause
What are the four verbs, and which one needs a dataset before it can run?
In the interview
“What did you use to see inside the agent, and how did you turn what you saw into a test?”
Say your answer out loud, in under a minute. Then compare.
Notes
- Reference entry: Evaluation & Monitoring, pattern 19 of 21.
- OpenTelemetry semantic conventions for generative AI (2025)
- @braintrust/otel README: span processor, parent header, attribute prefixes (2025)
- Arize Phoenix repository: built on OpenTelemetry, arize-phoenix-otel register() (2025)
- OpenInference: semantic conventions and instrumentation for LLM apps (2025)
- Langfuse Python SDK: OTLP exporter to /api/public/otel/v1/traces with Basic auth (2025)