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.
- Your own session ID
- OpenTelemetry standard keys
- Managed platform (vendor-derived)
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 three options.When a resource carries more than one of these, How Galtea Routes a Trace gives the order.You can also leave out
- By version ID
- By product and facts
- By version attributes
Set Every change to your agent then needs a new Version and a new ID in this configuration.
galtea.version.id to the ID of a Version you created.galtea.product.id and let Galtea find the product from standard attributes such as service.name. See Route Traces to a Product by Resource Attributes.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 rejects the conversation, and only that conversation, in four cases:
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/galteaprocessor 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.
- 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.
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.version.fact.* resource attributes, nor a resource attribute named in the version attributes in force for their product, 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 orassistant_messageenvelope 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
COMPLETEDorFAILED, 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 toPENDING, and a Monitor scores it again once it closes. WithIGNOREthe turn is dropped silently. WithREJECT, its spans appear in the export response’s rejected-span count and nothing is stored; start a new conversation with a freshgaltea.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_PAYLOADsetting.
How Galtea Routes a Trace
Galtea routes each resource once, when it ingests it. At each step, the first attribute that is present decides. Galtea does not try the next one when it fails. 1. How the resource names its Versiongaltea.version.id: Galtea uses this Version and reads nothing else.galtea.version.fact.*: the facts are these attributes, without the prefix.- The version attributes: the facts are the listed attributes the resource carries. A product’s own list replaces the organization list.
- None of these: Galtea rejects the spans as
no_version_id.
galtea.product.id.- The key values: the product whose key values the resource carries. When several products match, the key with the most attributes wins. A tie routes nowhere.
version_facts_without_product, or as product_not_found for a galtea.product.id you cannot reach. On step 1.3 this happens only when the resource carries an attribute of the organization list. Otherwise the spans are no_version_id.
Where to set what steps 1.3 and 2.2 read:
- The version attributes:
- Dashboard: the OpenTelemetry card of the Update Organization page. A product replaces the list in Advanced Options of its edit form.
- API:
PATCH /organizations/{id}andPATCH /products/{id}, fieldotelVersionAttributes. - CLI:
galtea organizations updateandgaltea products update.
- The key and the key values:
- Dashboard: the OpenTelemetry card of the Update Organization page, for the key. Advanced Options of each product’s edit form, for its values or its own key.
- API: the same two endpoints, fields
otelProductKeyandotelProductKeyValues. - CLI: the same two commands.
- The input and output source rules (output, input):
- Dashboard: the OpenTelemetry card of the Update Organization page, and Advanced Options of each product’s edit form.
- API: the same two endpoints, fields
otelInputSourceRuleandotelOutputSourceRule. - CLI: the same two commands.
Route Traces to a Product by Resource Attributes
Your telemetry may carry no Galtea ID. A collector for a whole company often does not know the Galtea product ID of each agent. It does know standard resource attributes such asservice.namespace and service.name. Galtea can use them to find the product.
Two settings make this work:
- The product key belongs to the organization. It lists 1 to 10 resource attribute names that identify a product, for example
service.namespaceandservice.name. A name cannot start withgaltea.and cannot becustomer.id. A new organization usesservice.name. - The key values belong to the product. They give one value for each attribute of the key, for example
service.namespaceischeckoutandservice.nameischeckout-agent.
galtea.version.id or galtea.product.id goes to the product whose key values it carries. It must carry galtea.version.fact.* attributes, or attributes that the version attributes of your organization or product list. Galtea then finds or creates the Version from the facts, as in the “By product and facts” and “By version attributes” tabs of Route to a product version. The route follows the first attribute that is present: galtea.version.id, then galtea.product.id, then the key values. A galtea.product.id that is present but not usable text is rejected. Galtea does not fall back to the key values.
Rules to know:
- Every attribute is required. A resource must carry every attribute of the key. A resource that misses one matches no product. Matching is exact and case-sensitive.
- A product can use its own key. Set
otelProductKeyon the product to use other attributes than the organization. It isnullwhile the product follows the organization key, and then it follows every later change. - The key with the most attributes wins. When a resource matches several products, the product whose key has the most attributes wins, whether that key is its own or the organization key. A tie routes the resource nowhere.
- Key values are unique. Two live products of one organization cannot hold the same key values. The second request answers
409. - A value cannot be
unknown_service. The OpenTelemetry SDK setsservice.nametounknown_serviceorunknown_service:<process>when you set no name. Galtea refuses these as key values, because they would name no real product. - The key is locked while products use it. The organization cannot change the set of attributes of its key while a live product that follows the key holds key values. The request answers
409. You can reorder the same attributes. To change the set, clear those key values first or give the products their own key. - Stored traces keep their product. Galtea routes once, when it ingests a trace. A later change of a key or of key values moves no stored trace.
null clears a field. The organization key cannot be null. The same two routes set the version attributes and the span source rules of an organization and of a product. POST /products accepts the same fields for a new product. For version attributes, an organization sends [] to turn the list off, and a product sends null to follow the organization’s list.
In the dashboard, open the OpenTelemetry card of the Update Organization page, or Advanced Options of the product edit form. Both edit the key, the version attributes, and the span source rules. The product form also edits the key values.
Then send traces with the attributes of the key on the resource and the facts of the agent state. This resource goes to the product above:
partialSuccess.errorMessage of the response then reads, for example, 5 spans carry galtea.version.fact.* resource attributes, or resource attributes named in the version attributes of their organization, but no usable galtea.product.id, and their resource attributes route to no single product.
Batched Exporters and the Conversation Key
Every OpenTelemetry SDK exports through aBatchSpanProcessor, 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_SECONDSsetting, and the per-trace cap on held spans withOTEL_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.
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. TheotelOutputSourceRule 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 an organization, a Product or a Version. The Version value overrides the Product value, which overrides the organization 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 atpredicate.pathequalsequals. 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.
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.otelOutputSourceRule on a version, a product, or an organization. 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, on organizations, or on version updates. To set it on a product or an organization, use the dashboard, the API or the CLI, as Where to set what lists. To change it on an existing version, use the API or the dashboard. The version value wins over the product value, which wins over the organization 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. TheotelInputSourceRule 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 an organization, a Product or a Version. The Version value overrides the Product value, which overrides the organization 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 atpredicate.pathequalsequals. 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.
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.otelInputSourceRule on a version, a product, or an organization, 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, on organizations, or on version updates. To set a rule on a product or an organization, use the dashboard, the API or the CLI, as Where to set what lists. 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, and a product inherits the organization rule. Galtea uses the version rule when the version has one, then the product rule, then the organization rule. So a version whose rule is
nulluses the product rule, and a product whose rule isnulluses the organization rule. A version cannot opt out of an inherited rule today. Pointing the version rule at a span name that version never emits does not restore the inherited rule: it empties the field for every turn of that version. To stop recovering a field, clear the product and organization rules, 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_ruleorgaltea.span.output_from_source_rulein 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.