# Send existing traces

> Point an OpenTelemetry GenAI exporter at Pyrula Cloud with environment variables, without installing pyrula or changing code.

Source: https://docs.pyrula.io/getting-started/otel/

Cloud accepts OpenTelemetry GenAI spans over OTLP/HTTP. Set env vars on the process that
exports the spans; no pyrula install or code change is needed.

This page uses Python 3.11 to 3.15 and a POSIX shell, in the directory your app runs from.

## 1. Copy the origin and the key

In Cloud, open **Setup** for the project and environment. It shows the API origin and the key (a key is for one environment).

## 2. Point the exporter at Cloud

Set these where the app starts, with your key. Hosted Cloud's API origin is `https://api.pyrula.io`; a self-hosted deployment shows its own under **Setup**:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.pyrula.io/otel
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <PYRULA_CLOUD_API_KEY>"
export OTEL_RESOURCE_ATTRIBUTES="${OTEL_RESOURCE_ATTRIBUTES:+$OTEL_RESOURCE_ATTRIBUTES,}deployment.environment=production"
```

- OTLP over HTTP only. Many SDKs default to gRPC, hence the protocol line. The exporter
  appends `/v1/traces` itself.
- The last line appends to `OTEL_RESOURCE_ATTRIBUTES` so it keeps `service.name`, which Cloud
  groups runs by.
- Replace `production` with your key's environment. Without the attribute the intake uses the
  key's environment; an SDK-set `deployment.environment.name` takes precedence.

Name the service if your instrumentation does not:

```bash
export OTEL_SERVICE_NAME=support-bot
```

## 3. Try it without an app

The following script sends one model-call span.

```bash
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

Save this as `send_trace.py`:

```python
# send_trace.py
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# TracerProvider() reads OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES, and
# OTLPSpanExporter() reads the OTEL_EXPORTER_OTLP_* variables: nothing else to set.
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
tracer = provider.get_tracer("quickstart")

with tracer.start_as_current_span("chat gpt-6-luna") as span:
    span.set_attribute("gen_ai.operation.name", "chat")
    span.set_attribute("gen_ai.request.model", "gpt-6-luna")
    span.set_attribute("gen_ai.usage.input_tokens", 120)
    span.set_attribute("gen_ai.usage.output_tokens", 45)

provider.shutdown()
```

Run it in the same shell:

```bash
python send_trace.py
```

It prints nothing. In **Runs** for your environment, a run named `chat gpt-6-luna` appears under the service name with one model step of 120 input and 45 output tokens. Its id is `otel-` plus the trace id.

## Mapping

Each trace becomes one run and each `service.name` one telemetry runtime. Only GenAI spans are
mapped: model calls become model steps (with tokens and cost), `execute_tool` tool steps,
`invoke_agent` agent steps. Other spans are dropped.

An imported run has no worker behind it, so Cloud can't replay, pause or cancel it. For that,
run the app on pyrula ([Connect to Pyrula Cloud](https://docs.pyrula.io/getting-started/cloud.md)).

## Intake accepted, but no run

A trace becomes a run only if it has a span that looks like a model call: a
`gen_ai.operation.name` of `chat`, `text_completion`, `generate_content` or `embeddings`, or a
model or token-usage attribute (`gen_ai.request.model`, `gen_ai.usage.input_tokens`, or the
older `llm.*` spellings). HTTP-only traces produce nothing.

If the exporter cannot connect, check that the endpoint ends in `/otel`, the protocol is
`http/protobuf`, and the key belongs to the project you are viewing.
