Skip to main content
Galtea exposes a public OpenTelemetry (OTel) endpoint. OTel is the open standard for emitting traces. The endpoint is an OTLP collector, where OTLP is the OTel export protocol. Send your traces there to enable two use cases: The endpoint, authentication, and exporter configuration on this page are the same for both.

Endpoint and authentication

Configure your OTel exporter to send traces to the Galtea collector. Every request must carry an Authorization: Bearer <Galtea-API-key> header — without it, the gateway returns 401 Unauthorized before the request reaches the collector. All examples below use the Galtea platform hostname: otel.platform.prod-main.galtea.ai.
If your organization has a private Galtea deployment, replace the hostname with your tenant-specific endpoint:
Your Galtea account team will provide the exact hostname during onboarding.

Configure your exporter

The OpenTelemetry SDK reads endpoint and headers from environment variables. Use shell substitution to expand ${GALTEA_API_KEY} before the value is stored in OTEL_EXPORTER_OTLP_TRACES_HEADERS:
The same ${GALTEA_API_KEY} syntax works in a docker-compose.yml environment: block, but only when GALTEA_API_KEY is set in the shell that runs Compose or in a .env file Compose loads. Docker Compose substitutes ${...} from those sources when it renders the config. It does not read another entry in the same environment: block. If the variable is unset there, the exporter receives the literal string ${GALTEA_API_KEY} and the gateway returns 401 Unauthorized.
Use OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (the per-signal variant), not OTEL_EXPORTER_OTLP_ENDPOINT. The base variable auto-appends /v1/traces to the URL, but the Galtea collector expects the /otel/traces path.
The OTel exporter does not interpolate placeholders inside OTEL_EXPORTER_OTLP_TRACES_HEADERS — it sends the value verbatim. Exporting Authorization=Bearer $GALTEA_API_KEY without shell expansion (for example, single-quoted, or written into a .env file that your runtime loads without expanding it) literally sends the string $GALTEA_API_KEY and the gateway returns 401 Unauthorized. Ensure the expansion happens at export time, not later.

Verify your key and endpoint

A quick way to confirm the header is being applied correctly is to POST an empty payload and inspect the status code:

How span content maps to Span records

When a span becomes a Span record — whether it enriches an existing trace or is assembled into a production conversation — its input and output are filled per field, using the first source that provides a value:
  1. galtea.span.* explicit attributes — full control, always wins.
  2. gen_ai.* GenAI semantic conventions — the OpenTelemetry standard for LLM telemetry.
  3. Recognized managed-platform span shapes — platform-specific formats Galtea knows.
  4. A configured span-source rule — otelInputSourceRule or otelOutputSourceRule on the Version, the Product, or the organization, which reads the field out of a named non-LLM span. See Recover the turn input from a non-LLM span.
A turn’s input and output follow a different order, because a turn has many spans: a configured source rule decides the field, else the turn’s single root span, else the earliest input and the latest output. Only an input that carries a user message counts, so an empty value such as {} or [] never becomes the turn input. A configured rule that fires on no span of the turn leaves that field empty; it never falls back. See Monitor real user traffic. If you set no attributes, spans are still ingested. A span type Galtea does not recognize is also ingested: Galtea normalizes the value to a known span type and keeps your original label in the span’s metadata, under the key galtea.span.original_type. The span type follows its own order, because only galtea.span.type is a Galtea attribute:
  1. galtea.span.type — always wins.
  2. The span kind your platform already declares — MLflow’s mlflow.spanType, or a span shape that names the kind by itself.
  3. gen_ai.operation.name — the OpenTelemetry GenAI operation, such as chat or embeddings.
  4. SPAN — when nothing above names a kind.
So a span imported from an instrumented platform is typed without any Galtea attribute: an MLflow RETRIEVER span becomes a RETRIEVER span, which is what Galtea reads retrieval context from. A vendor word Galtea spells differently still maps, so MLflow’s LLM becomes GENERATION.
These keys had different names before version 5.0.0, and Galtea still reads the old ones. See Dataset, Trace, and Span Renames if your collector already sends them.
These OTel fields are mapped automatically for every span, whichever source above you use:
  • span.name → name
  • startTimeUnixNano / endTimeUnixNano → startTime / endTime + latencyMs (computed)
  • span.status → error (fallback when galtea.span.error is not set; only for STATUS_CODE_ERROR)
  • parentSpanId → parentTraceId (parent-child hierarchy)
All remaining unmapped span attributes are collected into the metadata field, so no data is lost.

Token usage and cost

Galtea reads the token counts your spans report and adds them up per turn. Send them with the OpenTelemetry GenAI keys: A turn’s inputTokens, outputTokens and tokens are the sum over every span of that turn, so a turn that makes three LLM calls reports the three calls added together. A span that reports no usage adds nothing. Send no usage at all and the turn keeps what Galtea already had: nothing when no Model is linked to the Version, or the estimate Galtea derived from the turn text when one is. Galtea also recognizes the usage a managed platform reports in its own format. MLflow Tracing’s mlflow.chat.tokenUsage works with no Galtea attribute. When a span carries both, the gen_ai.usage.* keys win, field by field. Cost is derived from those counts and the per-token price of the Model linked to the Version. Link a Model with a known price and the turn’s cost is filled. With no Model, or with no price for it, cost stays empty rather than zero. A reported count replaces one Galtea estimated earlier: your own figure is the accurate one. A cost you reported yourself is kept, and the new counts do not replace it: Galtea recomputes only a cost it derived from the counts and the Model price, never a different figure you set. Spans of a turn may arrive in several export batches, and each batch raises the turn’s total instead of replacing it. Re-sending the same spans changes nothing.
Retrieval context (RAG): to record what your system retrieved, type a span as RETRIEVER and put the retrieved data in its output (for example via galtea.span.output). This is how Galtea reads retrieval context: it is derived from your RETRIEVER spans, and the output has no required shape.
Some common instrumentation styles do not populate input/output yet: Traceloop / OpenLLMetry (gen_ai.prompt.0.content-style flattened attributes) and OpenInference (input.value). Their data is still stored in the span’s metadata field, so nothing is lost, but evaluations that read the span input and output will not see it.

Attributes that route a conversation

The keys above, except galtea.trace.id, fill a Span record. The keys below decide where Galtea stores the spans. Galtea stores each conversation under a Product and one of its Versions. Your telemetry answers three questions:
  1. Which product? A resource attribute names it.
  2. Which version of that product? A resource attribute names it, or describes it so that Galtea finds or creates it.
  3. Which conversation? A span attribute groups spans into one Session.
A resource attribute describes the whole telemetry source, not one span. So you can set it once, for example on your OTel Collector, even when a managed platform emits the spans.
A span that names an existing Trace skips the three questions. That is the usual case when Galtea calls your endpoint during Direct Inference. See Spans of a Trace that already exists. Every other span needs all three answers, even when its conversation key joins a session that already exists.
These are the three common setups. Pick one:
The conversation key is a span attribute, so your code sets it on each turn. See Which conversation.

1. Which product

Send one of these on the resource. Skip this step when you send galtea.version.id, because a Version ID already names its product. When the resource carries both, galtea.product.id wins. A galtea.product.id that is empty or not text is rejected, and Galtea does not try the key values.

2. Which version

Send one of these on the resource. Galtea uses the first one present, in the order of the table. When that one fails, Galtea does not try the next one. With facts, Galtea finds the Version of the product that holds exactly these facts, or creates it, with the rules of Get or Create Version. The same facts reuse one Version, whatever the order of the attributes. One changed value creates a new Version, so a new release needs no change in Galtea. A fact value is text or an integer. A fact key such as model or model_name also links a model to a Version that Galtea creates, with the rules of Model facts. So galtea.version.fact.model can link the model, and a version attribute such as app.agent.model never does.
Never use a value that changes per request or per pod as a fact, such as a request ID, a trace ID, a timestamp, host.name, k8s.pod.name or service.instance.id. Every distinct value creates a new Version, and Galtea does not limit how many.

3. Which conversation

Set a conversation key names the managed platforms whose key Galtea derives. A span that carries no key waits for a later batch of its trace that does, up to a size limit. Past that limit, Galtea rejects it at once. See Batched Exporters and the Conversation Key.

Spans of a Trace that already exists

A Trace already exists when Galtea calls your endpoint during Direct Inference, or when your code created it with the SDK, the CLI or the API. Its Session already has its Version, and a Direct Inference session also has its Test Case. Your spans only need to name the Trace. Galtea then ignores the product, version, conversation and test case attributes. Galtea tries the trace ID first, then galtea.trace.id. A galtea.trace.id that names no Trace your API key can see is rejected, and Galtea does not route the span by product, version or conversation. A span with neither goes through the three questions, unless its trace ID matches a Direct Inference turn that was removed.

What Galtea rejects

Galtea never guesses where your data belongs. It rejects the spans of one resource, and the rest of the payload still ingests, in these cases:
  • No version. The resource carries no galtea.version.id, no galtea.version.fact.* attribute, and no attribute of the version attribute list in force for its product: the product’s own list, or else the organization list.
  • No product. The resource carries galtea.version.fact.* attributes or listed version attributes, and no galtea.product.id, and its product key attributes match no single product of your organization. That means no product holds those key values, or two products with keys of the same size do. A resource that routes by version attributes gets this reason only when it carries an attribute of the organization list. Otherwise the reason is no version.
  • Invalid facts. A value is not text or an integer, a key after the galtea.version.fact. prefix is empty, all galtea.version.fact.* values are blank, or the facts have more than 32 keys or more than 2000 bytes in all. For version attributes, a blank or null value counts as absent instead.
  • Unknown version or product. The Version or the product does not exist, or your API key cannot see it. With galtea.product.id, this also covers an API key whose role cannot create a Version, when no Version holds the facts yet. When the key values found the product, Galtea reports that case as no product.
It rejects one conversation, and the rest of the payload still ingests, in these cases:
  • The test case does not exist, or it belongs to another product than the Version.
  • Two spans of one conversation name different test case IDs, or two resources do.
  • The conversation names a test case, and its key already holds another session on that Version: a production session, or one linked to another test case.
  • The conversation is new, and the payload already opened 100 new conversations, the default limit.
  • The session is closed, and the product’s Closed Session Inference Creation Policy is REJECT. Only the spans of the new turn are rejected.
Galtea also rejects a span whose trace ID matches a Direct Inference turn that was removed, unless its galtea.trace.id names a live Trace. It does not assemble that span into a new session. The export response counts the rejected spans in partialSuccess.rejectedSpans. Its partialSuccess.errorMessage names each reason that applied, so your collector log says what to change. Monitor Real User Traffic via OpenTelemetry walks through the full setup, step by step.

Learn More

Monitor Real User Traffic via OpenTelemetry

Turn your live traffic’s traces into production sessions that Monitors evaluate.

W3C Trace Context Propagation

Attach OTel spans to the test runs Galtea triggers via Direct Inference.