> ## 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 Trace 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 inference results.** Spans correlated to an inference result become [Trace](/concepts/product/version/session/trace) records on it. Correlation uses the shared trace ID from the `traceparent` header Galtea propagates during Direct Inference, or the explicit `galtea.inference_result.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}"
            encoding: json
            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 Trace records

When a span becomes a [Trace](/concepts/product/version/session/trace) record — whether it **enriches an existing inference result** 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.trace.*` 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.

<Tabs>
  <Tab title="Galtea span attributes">
    | Attribute Key                | Type            | Maps To             | Description                                                                                                                                                                                                                                                                                                                                        |
    | ---------------------------- | --------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `galtea.trace.type`          | `string`        | `type`              | Trace type. One of: `SPAN`, `GENERATION`, `EVENT`, `AGENT`, `TOOL`, `CHAIN`, `RETRIEVER`, `EVALUATOR`, `EMBEDDING`, `GUARDRAIL`.                                                                                                                                                                                                                   |
    | `galtea.trace.description`   | `string`        | `description`       | Human-readable description of the operation. Max 1MB.                                                                                                                                                                                                                                                                                              |
    | `galtea.trace.input`         | `string` (JSON) | `inputData`         | Input data, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                                             |
    | `galtea.trace.output`        | `string` (JSON) | `outputData`        | Output data, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                                            |
    | `galtea.trace.error`         | `string`        | `error`             | Error message. Takes precedence over OTel span status error.                                                                                                                                                                                                                                                                                       |
    | `galtea.trace.metadata`      | `string` (JSON) | `metadata`          | Custom metadata, JSON-serialized. Max 10MB.                                                                                                                                                                                                                                                                                                        |
    | `galtea.inference_result.id` | `string`        | `inferenceResultId` | Explicitly links the span to an inference result. 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 an inference result 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.trace.input` / `galtea.trace.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

    <Tip>
      If your system is already instrumented with plain OpenTelemetry GenAI conventions, input and output 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).
  </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.trace.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.trace.output`). This is the convention Galtea will use to read retrieval context from traces (that reader arrives in a later release), 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 trace's `metadata` field, so nothing is lost, but evaluations that read the trace 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>
