> ## Documentation Index
> Fetch the complete documentation index at: https://docs.galtea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Send OpenTelemetry Traces to Galtea

> Configure an OpenTelemetry exporter or Collector to reach Galtea's public OTel endpoint: endpoint, authentication, and how spans map to Span records.

Galtea exposes a public [OpenTelemetry](https://opentelemetry.io/) (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:

* **Create production sessions from real traffic.** Uncorrelated spans become production [Sessions](/concepts/product/version/session) that [Monitors](/concepts/product/monitor) evaluate automatically. See [Monitor Real User Traffic via OpenTelemetry](/sdk/tutorials/monitor-real-user-traffic-with-opentelemetry).
* **Enrich existing Galtea Traces.** Spans correlated to a [Trace](/concepts/product/version/session/trace) become [Span](/concepts/product/version/session/span) records on it. Correlation uses the shared trace ID from the `traceparent` header Galtea propagates during Direct Inference, or the explicit `galtea.trace.id` span attribute. See [W3C Trace Context Propagation](/sdk/tutorials/direct-inferences-and-evaluations-from-platform#option-3-w3c-trace-context-propagation).

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`.

<Accordion title="Using a dedicated tenant deployment?">
  If your organization has a private Galtea deployment, replace the hostname with your tenant-specific endpoint:

  ```
  otel.platform.<TENANT>.galtea.ai
  ```

  Your Galtea account team will provide the exact hostname during onboarding.
</Accordion>

## Configure your exporter

<Tabs>
  <Tab title="Environment variables">
    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`:

    <Tabs>
      <Tab title="HTTP (port 4318)">
        ```bash theme={"system"}
        export GALTEA_API_KEY="gsk_..."
        export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://otel.platform.prod-main.galtea.ai:4318/otel/traces"
        export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer ${GALTEA_API_KEY}"
        ```

        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`.

        <Note>
          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.
        </Note>
      </Tab>

      <Tab title="gRPC (port 4317)">
        The header variable is reused — only the endpoint, protocol, and port change:

        ```bash theme={"system"}
        export GALTEA_API_KEY="gsk_..."
        export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://otel.platform.prod-main.galtea.ai:4317"
        export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="grpc"
        export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer ${GALTEA_API_KEY}"
        ```

        <Note>
          Older deployments may reject the gRPC TLS handshake with `missing selected ALPN property`. This affects modern gRPC clients that strictly enforce ALPN (originally surfaced with grpc-go ≥ 1.67, and reproducible from `grpcio` too). It is a platform-side NLB configuration that is being rolled out — if you encounter it, use the HTTP/4318 endpoint until your environment is updated.
        </Note>
      </Tab>
    </Tabs>

    <Warning>
      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.
    </Warning>
  </Tab>

  <Tab title="Python code">
    If you would rather configure the exporter in code, build the `OTLPSpanExporter` directly and read the API key from the environment at runtime. The exporter sends the header value exactly as provided — the `f"Bearer {galtea_api_key}"` formatting happens once at startup, so there is no ambiguity about when the key is substituted.

    <Tabs>
      <Tab title="HTTP (port 4318)">
        ```python theme={"system"}
        import os

        from opentelemetry import trace
        from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
        from opentelemetry.sdk.trace import TracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor

        # Read the Galtea API key from the environment at runtime.
        # The exporter will send it as `Authorization: Bearer <key>` on every request.
        galtea_api_key = os.environ.get("GALTEA_API_KEY")
        if not galtea_api_key:
            raise ValueError("GALTEA_API_KEY environment variable is not set")

        exporter = OTLPSpanExporter(
            endpoint="https://otel.platform.prod-main.galtea.ai:4318/otel/traces",
            headers={"Authorization": f"Bearer {galtea_api_key}"},
        )

        # Register the exporter without clobbering a provider another library may have
        # already installed — e.g. `opentelemetry-bootstrap` auto-instrumentation (set up
        # in step 2 above) or the Galtea SDK. `set_tracer_provider()` is a silent no-op if
        # a real provider is already active, so only create one when the active provider is
        # still the default proxy; otherwise attach the exporter to the existing provider.
        provider = trace.get_tracer_provider()
        if isinstance(provider, trace.ProxyTracerProvider):
            provider = TracerProvider()
            trace.set_tracer_provider(provider)

        provider.add_span_processor(BatchSpanProcessor(exporter))
        ```
      </Tab>

      <Tab title="gRPC (port 4317)">
        Use the gRPC exporter from `opentelemetry.exporter.otlp.proto.grpc` and point it at port 4317 (no URL path):

        ```python theme={"system"}
        import os

        from opentelemetry import trace
        from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
        from opentelemetry.sdk.trace import TracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor

        # Read the Galtea API key from the environment at runtime.
        # The exporter will send it as `Authorization: Bearer <key>` on every request.
        galtea_api_key = os.environ.get("GALTEA_API_KEY")
        if not galtea_api_key:
            raise ValueError("GALTEA_API_KEY environment variable is not set")

        # gRPC exporter: the endpoint is `host:port` (no URL path), with TLS enabled.
        # gRPC metadata keys must be lowercase, so the header is `authorization`.
        exporter = OTLPSpanExporter(
            endpoint="otel.platform.prod-main.galtea.ai:4317",
            insecure=False,
            headers=(("authorization", f"Bearer {galtea_api_key}"),),
        )

        # Register the exporter without clobbering a provider another library may have
        # already installed — e.g. `opentelemetry-bootstrap` auto-instrumentation (set up
        # in step 2 above) or the Galtea SDK. `set_tracer_provider()` is a silent no-op if
        # a real provider is already active, so only create one when the active provider is
        # still the default proxy; otherwise attach the exporter to the existing provider.
        provider = trace.get_tracer_provider()
        if isinstance(provider, trace.ProxyTracerProvider):
            provider = TracerProvider()
            trace.set_tracer_provider(provider)

        provider.add_span_processor(BatchSpanProcessor(exporter))
        ```

        <Note>
          Older deployments may reject the gRPC TLS handshake with `missing selected ALPN property`. This affects modern gRPC clients that strictly enforce ALPN (originally surfaced with grpc-go ≥ 1.67, and reproducible from `grpcio` too). It is a platform-side NLB configuration that is being rolled out — if you encounter it, use the HTTP/4318 exporter until your environment is updated.
        </Note>
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="OTel Collector">
    If you already run your own OpenTelemetry Collector, configure it to forward the `traces` pipeline to Galtea. Choose the outbound protocol that suits your deployment:

    <Tabs>
      <Tab title="HTTP (port 4318)">
        ```yaml theme={"system"}
        receivers:
          otlp:
            protocols:
              grpc:
                endpoint: 0.0.0.0:4317
              http:
                endpoint: 0.0.0.0:4318

        processors:
          batch:
            timeout: 1s
            send_batch_size: 512

        exporters:
          otlphttp/galtea:
            # Galtea expects /otel/traces, NOT the OTLP default /v1/traces.
            traces_endpoint: https://otel.platform.prod-main.galtea.ai:4318/otel/traces
            headers:
              Authorization: "Bearer ${env:GALTEA_API_KEY}"
            compression: gzip

        service:
          pipelines:
            traces:
              receivers: [otlp]
              processors: [batch]
              exporters: [otlphttp/galtea]
        ```
      </Tab>

      <Tab title="gRPC (port 4317)">
        ```yaml theme={"system"}
        receivers:
          otlp:
            protocols:
              grpc:
                endpoint: 0.0.0.0:4317
              http:
                endpoint: 0.0.0.0:4318

        processors:
          batch:
            timeout: 1s
            send_batch_size: 512

        exporters:
          otlp/galtea:
            endpoint: otel.platform.prod-main.galtea.ai:4317
            tls:
              insecure: false
            headers:
              Authorization: "Bearer ${env:GALTEA_API_KEY}"
            compression: gzip

        service:
          pipelines:
            traces:
              receivers: [otlp]
              processors: [batch]
              exporters: [otlp/galtea]
        ```
      </Tab>
    </Tabs>

    <Warning>
      `${env:GALTEA_API_KEY}` is the OpenTelemetry Collector's **own** environment-variable interpolation syntax — it is resolved by the Collector itself when it loads the config. This is different from the shell `${GALTEA_API_KEY}` syntax used in the SDK env-var tabs (which is resolved by the shell at export time). Using shell syntax in a Collector config (or Collector syntax in a shell export) will send a literal, unexpanded string and the gateway will return `401 Unauthorized`.
    </Warning>
  </Tab>
</Tabs>

## 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:

```bash theme={"system"}
curl -sS -w 'HTTP %{http_code}\n' -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${GALTEA_API_KEY}" \
  -d '{"resourceSpans":[]}' \
  https://otel.platform.prod-main.galtea.ai:4318/otel/traces
# Expect: HTTP 200 with an empty-success body — Galtea's gateway returns {"partialSuccess":{}}
# Without the Authorization header: HTTP 401 — "Provide a valid galtea API key token"
```

## How span content maps to Span records

When a span becomes a [Span](/concepts/product/version/session/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.

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`.

<Note>
  These keys had different names before version 5.0.0, and Galtea still reads the old ones. See
  [Dataset, Trace, and Span Renames](/sdk/migration-guides/entity-renames) if your collector already
  sends them.
</Note>

<Tabs>
  <Tab title="Galtea span attributes">
    | Attribute Key             | Type            | Maps To             | Description                                                                                                                                                                                                                                                                                                                |
    | ------------------------- | --------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `galtea.span.type`        | `string`        | `type`              | Span type. One of: `SPAN`, `GENERATION`, `EVENT`, `AGENT`, `TOOL`, `CHAIN`, `RETRIEVER`, `EVALUATOR`, `EMBEDDING`, `GUARDRAIL`. Any other value is normalized to one of these types instead of being rejected, and your original label is kept in `metadata` under `galtea.span.original_type`.                            |
    | `galtea.span.description` | `string`        | `description`       | Human-readable description of the operation. Max 1MB.                                                                                                                                                                                                                                                                      |
    | `galtea.span.input`       | `string` (JSON) | `inputData`         | Input data, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                     |
    | `galtea.span.output`      | `string` (JSON) | `outputData`        | Output data, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                    |
    | `galtea.span.error`       | `string`        | `error`             | Error message. Takes precedence over OTel span status error.                                                                                                                                                                                                                                                               |
    | `galtea.span.metadata`    | `string` (JSON) | `metadata`          | Custom metadata, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                |
    | `galtea.trace.id`         | `string`        | `inferenceResultId` | Explicitly links the span to a trace. Only needed if the automatic `traceId` correlation does not apply (e.g., spans not originating from a Galtea direct inference call). A span that names a trace which does not exist, or is not visible to your API key, is rejected; it does not fall back to conversation assembly. |
  </Tab>

  <Tab title="OTel GenAI conventions">
    No Galtea attributes needed. When `galtea.span.input` / `galtea.span.output` are not set, input and output are filled from the OpenTelemetry [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) in any of these forms:

    * `gen_ai.input.messages` / `gen_ai.output.messages` (the current conventions)
    * the legacy `gen_ai.prompt` / `gen_ai.completion` attributes
    * the GenAI content span events

    `gen_ai.operation.name` sets the span type when you send no `galtea.span.type`: `chat`, `text_completion` and `generate_content` become `GENERATION`, `embeddings` becomes `EMBEDDING`, `execute_tool` becomes `TOOL`, and `create_agent` / `invoke_agent` become `AGENT`.

    On an `execute_tool` span, send `gen_ai.tool.name` as well. The conventions name that span `execute_tool get_weather`, and Galtea stores the span name as the tool name. So without the attribute the tool reads as `execute_tool get_weather`, and a `tool_correctness` metric never matches it against `get_weather`.

    <Tip>
      If your system is already instrumented with plain OpenTelemetry GenAI conventions, input, output and span type map with **zero** Galtea-specific attributes.
    </Tip>
  </Tab>

  <Tab title="Managed platforms">
    For managed platforms, Galtea recognizes some span shapes automatically, with no configuration needed: platforms whose telemetry carries the abraxas span-event format (such as Mistral's agent platform), and platforms instrumented with [MLflow Tracing](https://mlflow.org/docs/latest/tracing/) (recognized from MLflow's own `mlflow.*` span attributes).

    These shapes also set the **span type**. MLflow's `mlflow.spanType` maps onto the Galtea span type (`RETRIEVER`, `TOOL`, `CHAIN`, `AGENT` and `EMBEDDING` map directly; `LLM` and `CHAT_MODEL` become `GENERATION`), and an abraxas LLM-call span becomes a `GENERATION`. MLflow's `UNKNOWN` names no kind, so a span carrying it falls through to the next source in the order above.
  </Tab>
</Tabs>

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.

<Note>
  **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.
</Note>

<Warning>
  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.
</Warning>

## Learn More

<CardGroup cols={2}>
  <Card title="Monitor Real User Traffic via OpenTelemetry" icon="satellite-dish" href="/sdk/tutorials/monitor-real-user-traffic-with-opentelemetry">
    Turn your live traffic's traces into production sessions that Monitors evaluate.
  </Card>

  <Card title="W3C Trace Context Propagation" icon="tower-broadcast" href="/sdk/tutorials/direct-inferences-and-evaluations-from-platform#option-3-w3c-trace-context-propagation">
    Attach OTel spans to the test runs Galtea triggers via Direct Inference.
  </Card>
</CardGroup>
