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

# OpenTelemetry

> Send traces to ZeroEval via the OpenTelemetry Protocol (OTLP)

ZeroEval accepts standard OTLP trace data at `POST /v1/traces`, so any OpenTelemetry-instrumented application can export directly -- through a collector, or straight from your app using an OTLP exporter.

## When to use OTLP

Use this integration when:

* Your application already uses OpenTelemetry for instrumentation
* You want to fan out traces to multiple backends (ZeroEval + Datadog, Jaeger, etc.)
* You're running infrastructure you can't modify but can route through a collector
* You prefer a vendor-neutral instrumentation layer

<Note>
  If you're starting fresh, the [Python SDK](/tracing/sdks/python/setup) or
  [TypeScript SDK](/tracing/sdks/typescript/setup) provide a simpler setup with
  automatic LLM instrumentation.
</Note>

## Endpoint Reference

```
POST https://api.zeroeval.com/v1/traces
```

| Header          | Value                                          |
| --------------- | ---------------------------------------------- |
| `Authorization` | `Bearer YOUR_ZEROEVAL_API_KEY`                 |
| `Content-Type`  | `application/json` or `application/x-protobuf` |

The endpoint accepts the standard `ExportTraceServiceRequest` payload defined in the [OTLP specification](https://opentelemetry.io/docs/specs/otlp/). Spans are converted to ZeroEval's internal format -- trace IDs, parent-child relationships, attributes, and status are all preserved.

***

## Option 1: OpenTelemetry Collector

Route traces through a collector when you need batching, processing, or multi-destination fan-out.

### Collector Configuration

Create `otel-collector-config.yaml`:

```yaml theme={null}
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024

exporters:
  otlphttp:
    endpoint: https://api.zeroeval.com
    headers:
      Authorization: "Bearer ${env:ZEROEVAL_API_KEY}"
    traces_endpoint: https://api.zeroeval.com/v1/traces

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
```

### Run with Docker Compose

```yaml theme={null}
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"
      - "4318:4318"
    environment:
      - ZEROEVAL_API_KEY=${ZEROEVAL_API_KEY}
    restart: unless-stopped
```

### Run with Kubernetes

<Accordion title="Full Kubernetes manifest">
  ```yaml theme={null}
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: otel-collector-config
  data:
    config.yaml: |
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317
            http:
              endpoint: 0.0.0.0:4318
      processors:
        batch:
          timeout: 1s
        k8sattributes:
          extract:
            metadata:
              - k8s.namespace.name
              - k8s.deployment.name
              - k8s.pod.name
      exporters:
        otlphttp:
          endpoint: https://api.zeroeval.com
          headers:
            Authorization: "Bearer ${env:ZEROEVAL_API_KEY}"
          traces_endpoint: https://api.zeroeval.com/v1/traces
      service:
        pipelines:
          traces:
            receivers: [otlp]
            processors: [batch, k8sattributes]
            exporters: [otlphttp]
  ---
  apiVersion: v1
  kind: Secret
  metadata:
    name: zeroeval-secret
  type: Opaque
  stringData:
    api-key: "YOUR_ZEROEVAL_API_KEY"
  ---
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: otel-collector
  spec:
    replicas: 2
    selector:
      matchLabels:
        app: otel-collector
    template:
      metadata:
        labels:
          app: otel-collector
      spec:
        containers:
          - name: otel-collector
            image: otel/opentelemetry-collector-contrib:latest
            args: ["--config=/etc/config.yaml"]
            env:
              - name: ZEROEVAL_API_KEY
                valueFrom:
                  secretKeyRef:
                    name: zeroeval-secret
                    key: api-key
            ports:
              - containerPort: 4317
                name: otlp-grpc
              - containerPort: 4318
                name: otlp-http
            volumeMounts:
              - name: config
                mountPath: /etc/config.yaml
                subPath: config.yaml
        volumes:
          - name: config
            configMap:
              name: otel-collector-config
  ---
  apiVersion: v1
  kind: Service
  metadata:
    name: otel-collector
  spec:
    selector:
      app: otel-collector
    ports:
      - name: otlp-grpc
        port: 4317
      - name: otlp-http
        port: 4318
  ```
</Accordion>

***

## Option 2: Direct from Python

Export OTLP traces directly from your Python application without a collector. Use the `ZeroEvalOTLPProvider` included in the SDK, or configure a standard `OTLPSpanExporter`.

### Using ZeroEvalOTLPProvider

```python theme={null}
from opentelemetry import trace
from zeroeval.providers import ZeroEvalOTLPProvider

provider = ZeroEvalOTLPProvider(
    api_key="YOUR_ZEROEVAL_API_KEY",
    service_name="my-service"
)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("my-service")

with tracer.start_as_current_span("process_request") as span:
    span.set_attribute("user.id", "12345")
    result = do_work()
    span.set_attribute("result.status", "ok")
```

### Using standard OTLPSpanExporter

```python theme={null}
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
    endpoint="https://api.zeroeval.com/v1/traces",
    headers={"Authorization": "Bearer YOUR_ZEROEVAL_API_KEY"}
)

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
```

***

## Option 3: Direct from Node.js

```typescript theme={null}
import { NodeTracerProvider } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";

const exporter = new OTLPTraceExporter({
  url: "https://api.zeroeval.com/v1/traces",
  headers: {
    Authorization: "Bearer YOUR_ZEROEVAL_API_KEY",
  },
});

const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
```

***

## Attribute Mapping

ZeroEval maps standard OpenTelemetry span attributes to its internal format:

| OTLP Attribute          | ZeroEval Field         | Notes                                      |
| ----------------------- | ---------------------- | ------------------------------------------ |
| `span.name`             | `name`                 | Span name                                  |
| `span.kind`             | `kind`                 | Mapped to `generic`, `llm`, etc.           |
| `span.status`           | `status`               | `ok`, `error`, `unset`                     |
| `span.start_time`       | `started_at`           | Nanosecond timestamp converted to ISO 8601 |
| `span.end_time`         | `ended_at`             | Nanosecond timestamp converted to ISO 8601 |
| `span.trace_id`         | `trace_id`             | Hex-encoded trace ID                       |
| `span.parent_span_id`   | `parent_span_id`       | Hex-encoded span ID                        |
| `span.attributes.*`     | `attributes`           | All attributes preserved                   |
| `resource.service.name` | Service identification | Used for grouping                          |

### LLM Spans

To get LLM-specific features (cost calculation, token tracking), set these attributes on your spans:

| Attribute                                               | Description                                 |
| ------------------------------------------------------- | ------------------------------------------- |
| `llm.provider` or `gen_ai.system`                       | Provider name (`openai`, `anthropic`, etc.) |
| `llm.model` or `gen_ai.request.model`                   | Model identifier                            |
| `llm.input_tokens` or `gen_ai.usage.prompt_tokens`      | Input token count                           |
| `llm.output_tokens` or `gen_ai.usage.completion_tokens` | Output token count                          |

### Sessions

Attach session context to your OTLP spans via attributes:

| Attribute               | Description                    |
| ----------------------- | ------------------------------ |
| `zeroeval.session.id`   | Session ID for grouping traces |
| `zeroeval.session.name` | Human-readable session name    |

The `ZeroEvalOTLPProvider` stamps these automatically when you configure a session via environment variables (`ZEROEVAL_SESSION_ID`, `ZEROEVAL_SESSION_NAME`).
