Skip to main content
If your AI system serves real users and emits OpenTelemetry (OTel) traces, export them to Galtea. OTel is the open standard for emitting traces. Galtea assembles the spans into production Sessions with conversation turns, and Monitors evaluate those sessions automatically. Spans that name a Test Case open a development session instead, which no Monitor scores.
This page is about real production traffic. To attach OTel spans to the test runs Galtea triggers via Direct Inference, see W3C Trace Context Propagation.

Prerequisites

  • A Product in Galtea. A Version too, if you route by version ID.
  • A Galtea API key.
  • Your system emits OpenTelemetry traces.
1

Send your traces to Galtea

Point your OTel exporter or Collector at Galtea’s public OTel endpoint. Send OpenTelemetry Traces to Galtea covers the endpoint, the Authorization: Bearer <API key> requirement, the exporter and Collector configuration, and a curl check to verify your key.
2

Set a conversation key

The conversation key decides which spans belong to the same Galtea session. Provide it in whichever way fits your setup.
Set the galtea.session.custom_id span attribute to your own conversation identifier, for example your chat session ID. Spans carrying it are grouped into one Galtea session per distinct ID. Within a session, one OTel trace (all spans sharing a W3C trace ID) becomes one conversation turn.The key on the turn’s root span is enough. Galtea holds the spans of a batch that carries no key and joins them to the turn when the batch with the key arrives, so you do not have to stamp every span. See Batched exporters and the conversation key.
3

Route to a product version

Assembly needs to know which Version the spans belong to. You name it with resource attributes. A resource attribute describes the whole telemetry source, not a single span. So you can set it once on your own OTel Collector, even when a managed platform emits the spans and you cannot edit them. Galtea validates it against the organization of the API key you authenticate with. Without a route, uncorrelated spans are rejected.Pick one of two options.
Set galtea.version.id to the ID of a Version you created.
Every change to your agent then needs a new Version and a new ID in this configuration.
If a resource sets galtea.version.id and the product and fact attributes, galtea.version.id wins and Galtea ignores the rest.
4

Link a conversation to a test case (optional)

Skip this step for real user traffic. Use it when the traffic comes from a test run and you want the session to name the Test Case it exercises.Set galtea.test_case.id to the test case ID. Galtea then creates the session as a non-production session linked to that test case. A Monitor scores only production sessions, so it never scores this session and the session spends no evaluation credits. Without the attribute, Galtea creates a production session, exactly as before.Set the key in either place:
  • On the resource, when the whole telemetry source is one test run. Add it next to the version attributes in the resource/galtea processor from the previous step.
  • On a span, when one source sends several test cases. A span value marks one conversation, and it wins over the resource value.
Galtea rejects the conversation, and only that conversation, in four cases:
  • the test case does not exist;
  • the test case belongs to another product than the version the spans route to;
  • two spans of one conversation name different test case IDs, or two resources do (a span value overrides the resource value, so setting both is fine);
  • the conversation key already holds another session on that version, either a production session or one linked to a different test case.
The rest of the payload still ingests. The rejected spans appear in the export response’s partialSuccess.rejectedSpans count.The response’s partialSuccess.errorMessage names the reason. It reports one clause per reason that applied to the batch, for example 5 spans carry neither galtea.version.id nor galtea.product.id with galtea.version.fact.* resource attributes, and match no existing trace, so your collector log says what to change.Spans that carry no test case attribute still attach to whatever session the conversation key already holds. That is how OTel spans join a session the Galtea SDK opened during a test run.A test-case-linked session closes like any other: the inactivity sweep closes it after the product’s auto-close window, unless the product’s auto-close scope excludes development sessions, in which case it stays open until you finish it.
5

Send traffic and verify

Once traffic flows, the assembled conversations appear as sessions under the version you routed to, in the dashboard. They are production sessions, and Monitors evaluate them automatically. A conversation that names a test case appears as a non-production session instead, and no Monitor scores it.Galtea never guesses where your data belongs. Uncorrelated spans missing a conversation key, a version route, or both are rejected.

What You Get

  • Production sessions. Sessions created this way are production sessions, so Monitors evaluate them automatically. The one exception is a conversation that names a test case: it creates a non-production session, which no Monitor scores.
  • Automatic input and output. Each turn’s input and output are read from the turn’s own spans. A managed platform whose LLM text lives only in gen_ai.* attributes or vendor span events still produces turns with non-empty input and output. When one span is the root of the turn (for example, an agent span that wraps child LLM and tool spans, the usual shape from MLflow autolog or an agent framework), the turn takes the root span’s input and output: the user message and the agent’s final answer. This applies per field. The input always comes from the root when the root carries it. The output comes from the root only when the root’s output is a plain answer (a text reply, or a chat-completion or assistant_message envelope that reduces to text); when the root’s output is a wrapper object that is not the answer (for example a status or tool-decision blob), the turn keeps the child span’s real reply instead. A field the root does not carry, or a turn with no single root (for example, several sibling LLM calls), falls back to the first prompt as the turn input and the last completion as the turn output. A configured input or output source rule replaces that fallback and the root span for its field, and leaves the field empty when it matches no span rather than falling back: see Recover the turn input from a non-LLM span. If the chosen output is a raw OpenAI chat-completion object, the turn shows its message text; the span record keeps the full object. See How span content maps to Span records for the per-field precedence. For turns answered without any LLM call, see Recover replies that do not come from the LLM.
  • Idempotent ingestion. Re-sent or late span batches do not create duplicate sessions, turns, or spans. Later turns append to the same session.
  • Works together with the SDK. If a session with the same custom ID already exists (for example, created by the SDK), ingestion attaches to it instead of creating a new one.
Two behaviors to plan for:
  • A closed session can reopen for a new turn. A session is closed once it reaches COMPLETED or FAILED, either by an explicit finish or by the auto-close sweep (see Session lifecycle). Late spans for turns it already has are still accepted. Spans for a new turn follow the product’s Closed Session Inference Creation Policy: by default (REOPEN_AND_REEVALUATE) the turn is stored, the session goes back to PENDING, and a Monitor scores it again once it closes. With IGNORE the turn is dropped silently. With REJECT, its spans appear in the export response’s rejected-span count and nothing is stored; start a new conversation with a fresh galtea.session.custom_id (or a new Temporal workflow) to keep those turns. A non-production session follows the same policy; it has no monitor evaluations, so a reopen only stores the turn.
  • One payload can open at most 100 new conversations. New sessions beyond that in the same OTLP payload are rejected (they appear in the export response’s rejected-span count). Continuations of conversations that already exist do not count toward the limit. If you send a large backfill or a high-volume export, split it across smaller export batches. Self-hosted deployments can raise the limit with the OTEL_MAX_NEW_CONVERSATIONS_PER_PAYLOAD setting.

Batched Exporters and the Conversation Key

Every OpenTelemetry SDK exports through a BatchSpanProcessor, which sends spans as they end and flushes every few seconds. A turn’s tool calls and intermediate LLM calls end first, so they travel in earlier requests, and the turn’s root span travels in the last one. When you set the conversation key on the root span, those earlier requests reach Galtea with no key at all. Galtea holds them. A request carrying no key waits for one, and the request that carries the key replays everything held for the same trace into the same turn. The spans are not reported as rejected while they wait. Order does not matter: a keyless request arriving after the keyed one attaches straight away. Two things to know:
  • The wait is bounded. Spans wait 15 minutes by default. If no request ever carries a key for that trace, they are dropped and the reason is logged. So an instrumentation that never sets a conversation key stores nothing, exactly as before. Self-hosted deployments can change the window with the OTEL_KEYLESS_HOLD_WINDOW_SECONDS setting, and the per-trace cap on held spans with OTEL_KEYLESS_HOLD_MAX_SPANS.
  • You may still set the key on every span. It is not required, and it costs nothing to keep if your instrumentation already does it.
Parent and child links survive the same split. A span whose parent travels in a later request is still nested under that parent in the trace tree.

Recover Replies That Do Not Come from the LLM

Some turns are answered without an LLM completion. A guardrail can replace the answer, a template can redirect the user, or a tool can return the final reply on its own. For these turns the user-visible answer lives on a non-LLM span. So by default the assembled turn output is empty, or it shows an internal label such as an intent-classifier tag, instead of the real reply. The otelOutputSourceRule config field fixes this. It names the exact span and field that carry the real reply, so the turn shows the true answer. Set it on a Product or a Version. The Version value overrides the Product value. Leave it null (the default) to keep the rule off. The rule has four parts:
  • spanName — the exact OTLP span name to match. For a Temporal (managed-platform) activity, this is the activity span name.
  • attribute — the span attribute that holds the reply, stored as a JSON string.
  • path — a JSONPath into that parsed JSON that points at the output text. JSONPath is a small query syntax for JSON, for example $[0].message.content. It must start with $, the root of the parsed value; a path without it matches nothing, so the API and the dashboard both reject one.
  • predicate (optional) — an object { path, equals }. The rule applies only when the node at predicate.path equals equals. Use it when the user message and the assistant message share the same span and attribute, so you do not pick the user’s own message as the reply.
Here is a complete rule:
Read it step by step. The assistant’s reply is saved on the ExecuteActivity:save_reply span, inside the app.arguments attribute, which holds a JSON array. $[0].message.content is the reply text inside that array. The predicate keeps user messages out: the same span also carries the user’s message with role "user", so the predicate makes the rule fire only for role "assistant". Name a span your system emits once per turn here too, because a replayed span appears several times and the latest copy may belong to another turn. The input rule section explains why.
The rule decides the turn output, whatever order your spans are emitted in. A rule fires on a span when the span name matches and the predicate passes. Galtea takes the turn output from the value the rule extracted, on the latest span where it fired. A span that matches the name but fails the predicate never wins the turn. The rule wins over the root-span and latest-span fallbacks. It also wins over a galtea.span.output set on a different span of the same turn.So if you already set the Galtea attributes on the right span, you do not need a rule. The rule does not fire on a span that already holds the output through a higher source. galtea.span.output and gen_ai.* are the two common ones, and there the normal sources decide. When the rule fires on no span of the turn, the turn output is empty. The fallbacks do not apply: a rule is configured exactly where they give a wrong value, so falling back would restore the answer you rejected. If you ever need the fallback under a configured rule, ask us and it becomes an explicit setting on the rule.Per span, nothing changes. Each Span record still fills its own fields from galtea.span.*, then gen_ai.*, then a recognized managed-platform span shape, and the rule fills a field only when none of those did. The Span records you see in the dashboard are the same with or without a rule.
Set otelOutputSourceRule on a product or a version. In the Python SDK you can set it when you create a version. The field is otel_output_source_rule, and you pass the same four-part object shown above. The SDK does not set it on products or on version updates. To set it on a product, or to change it on an existing version, use the API or the dashboard. The version value wins over the product value.

Recover the Turn Input from a Non-LLM Span

The turn input has the mirror problem. Many agents save the user message on their own span, and they save it after the LLM spans of the same turn. Galtea then reads the turn input from the earliest span that carries one, which is the LLM prompt: your system prompt plus a copy of the user message. So the turn shows the prompt instead of what the user typed. The otelInputSourceRule config field fixes this. It names the exact span and field that carry the user message, so the turn shows the real question. Set it on a Product or a Version. The Version value overrides the Product value. Leave it null (the default) to keep the rule off. The rule has the same four parts:
  • spanName — the exact OTLP span name to match. For a Temporal (managed-platform) activity, this is the activity span name.
  • attribute — the span attribute that holds the user message, stored as a JSON string.
  • path — a JSONPath into that parsed JSON that points at the input text. JSONPath is a small query syntax for JSON, for example $[0].message.content. It must start with $, the root of the parsed value; a path without it matches nothing, so the API and the dashboard both reject one.
  • predicate (optional) — an object { path, equals }. The rule applies only when the node at predicate.path equals equals. Use it when the user message and the assistant message share the same span and attribute, so you do not pick the assistant’s reply as the input.
Name a span your system emits once per turn. A framework that replays work, such as a durable-workflow engine, re-emits the same span many times in one trace. The earliest copy can then belong to an earlier turn, and the rule picks the wrong message. In one week of production traces, an update-handler span appeared 685 times for 225 distinct updates. The activity that saved the message appeared exactly once per message. So name the span that saves or records the message, not a handler span that a replay re-enters. Here is a complete rule:
Read it step by step. The user message is saved on the ExecuteActivity:save_message span, inside the app.arguments attribute, which holds a JSON array. $[0].message.content is the message text inside that array. The predicate keeps assistant messages out: the same span also carries the assistant’s reply with role "assistant", so the predicate makes the rule fire only for role "user".
The rule decides the turn input, whatever order your spans are emitted in. A rule fires on a span when the span name matches and the predicate passes. Galtea takes the turn input from the value the rule extracted, on the earliest span where it fired. A span that matches the name but fails the predicate never wins the turn. The rule wins over the root-span and earliest-span fallbacks, and over a galtea.span.input set on any span of the turn, including the span the rule itself names.Per span, nothing changes. A Span record still fills its own fields from galtea.span.*, then gen_ai.*, then a recognized managed-platform span shape, and the rule last, so a span that already holds an input through a higher source keeps showing that value. The turn input is separate: it is computed from your rule and the stored spans, so it takes what the rule extracts.When the rule extracts nothing from any span of the turn, the turn input is empty. The fallbacks do not apply, for the same reason as the output rule: a rule is configured exactly where they give a wrong value. A greeting turn with no user message to save therefore has an empty input, and its output still comes from the output rule. If you ever need the fallback under a configured rule, ask us and it becomes an explicit setting on the rule.
Set otelInputSourceRule on a product or a version, the same way as the output rule. In the Python SDK you can set both when you create a version: the fields are otel_input_source_rule and otel_output_source_rule. The SDK does not set either rule on products or on version updates. To set a rule on a product, or to change one on an existing version, use the API or the dashboard.

How a Rule Is Resolved and Changed

Each source rule behaves the same way, and each direction is resolved on its own:
  • A version inherits the product rule. Galtea uses the version rule when the version has one, and the product rule when it does not. So a version whose rule is null uses the product rule. A version cannot opt out of a product rule today. Pointing the version rule at a span name that version never emits does not restore the product rule: it empties the field for every turn of that version. To stop recovering a field, clear the product rule, or point the version rule at a span that version does emit.
  • A rule change reaches a stored turn when it extracts a value. Galtea keeps every span it stored, so a turn that still receives new spans is recomputed with the rule configured at that moment, over all of its spans. Correcting a rule therefore repairs those turns, and a turn recorded before you set the rule is repaired the same way. A turn that receives no further spans keeps the value it has.
  • A change never empties a field that holds a value. If your new rule extracts nothing from a turn, Galtea leaves that turn’s stored value alone rather than clearing it. Clearing stored values is a separate operation, so an edit to a rule cannot blank recorded traffic as a side effect. A rule change is never what clears a stored field. One platform action can: when a turn’s boundary span arrives after its content, Galtea moves that content to the turn it belongs to, and the turn it came from is left empty rather than keeping the next turn’s words. That clears only a field those same spans filled.
  • A rule that matches nothing empties its field. A configured rule decides its direction outright. When it matches no span of the turn, that field is empty, and the root-span and earliest/latest fallbacks do not apply. Each direction is independent, so an input rule that matches nothing does not stop the output rule. If a rule of yours needs the fallback instead, ask us: it becomes an explicit setting on the rule, never the default.
  • A recovered field is marked on the span. A span whose field a rule filled carries galtea.span.input_from_source_rule or galtea.span.output_from_source_rule in its metadata. Galtea writes and reads these keys itself, and strips them from any value you send. Use them to confirm which span a rule fired on.

Learn More

Send OpenTelemetry Traces to Galtea

Endpoint, authentication, exporter and Collector setup, and the span mapping reference.

Monitor

The always-on rule that evaluates your production sessions automatically.

Span

What a Span record is and which properties it holds.

Tracing Agent Operations

Collect spans with the Galtea SDK’s @traced decorator.