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

# OpenTelemetry (OTLP)

> Send traces from an existing OpenTelemetry, OpenLLMetry or OpenInference setup to AgentHub over OTLP/HTTP, with no code changes at your call sites.

AgentHub has a standard OpenTelemetry trace receiver. If your agent is already instrumented, or you're happy to use an off-the-shelf instrumentor, you can point the exporter at AgentHub and skip the [JSON ingest format](/agenthub/developers/trace-format) entirely. There's still no CorePillar SDK: everything below uses public OpenTelemetry packages.

## Endpoint and authentication

| Setting | Value |
| - | - |
| Base endpoint (for `OTEL_EXPORTER_OTLP_ENDPOINT`, the Collector, the Java agent and Traceloop) | `https://pillarforge.corepillar.ai/api` |
| Full traces URL (for exporter constructors that take a URL) | `https://pillarforge.corepillar.ai/api/v1/traces` |
| Protocol | OTLP over HTTP: `http/protobuf` or `http/json`. gRPC is **not** supported. |
| Auth header | `Authorization: Bearer tk_live_...` (or `x-tracing-key: tk_live_...`) |

Create the key under **Settings → Tracing keys**, exactly as in the [Tracing quickstart](/agenthub/developers/tracing-quickstart). Spans land in the key's project.

<Warning>
  The two endpoint shapes aren't interchangeable. `OTEL_EXPORTER_OTLP_ENDPOINT`, the Collector's `otlphttp` exporter, the Java agent and `Traceloop.init(api_endpoint=...)` append `/v1/traces` themselves, so give them the base. Exporter constructors such as Python's `OTLPSpanExporter(endpoint=...)`, Node's `new OTLPTraceExporter({ url })` and Go's `WithEndpointURL` take the full URL. Mixing them up is the most common reason a first trace never arrives.
</Warning>

The word `Bearer` must be capitalized exactly like that. If the `Authorization` header doesn't start with `Bearer `, AgentHub falls back to the `x-tracing-key` header and, if that's missing too, rejects the export with `401`.

## Setup

<Tabs>
  <Tab title="Python · OpenLLMetry">
    OpenLLMetry auto-instruments the OpenAI, Anthropic, Bedrock, LangChain and LlamaIndex clients you already call.

    ```bash theme={null}
    pip install traceloop-sdk
    ```

    ```python main.py theme={null}
    import os
    from traceloop.sdk import Traceloop

    Traceloop.init(
        app_name="my-agent",
        api_endpoint="https://pillarforge.corepillar.ai/api",
        headers={"Authorization": f"Bearer {os.environ['COREPILLAR_TRACING_KEY']}"},
    )

    # Every LLM call, tool call and chain step is now a span.
    ```
  </Tab>

  <Tab title="Python · OTel">
    ```bash theme={null}
    pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http \
        opentelemetry-instrumentation-httpx opentelemetry-instrumentation-requests
    ```

    ```python tracing.py theme={null}
    import os

    from opentelemetry import trace
    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
    from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    from opentelemetry.sdk.resources import Resource
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor

    provider = TracerProvider(resource=Resource.create({"service.name": "my-agent"}))

    # The constructor takes the full signal path.
    provider.add_span_processor(
        BatchSpanProcessor(
            OTLPSpanExporter(
                endpoint="https://pillarforge.corepillar.ai/api/v1/traces",
                headers={"Authorization": f"Bearer {os.environ['COREPILLAR_TRACING_KEY']}"},
            )
        )
    )
    trace.set_tracer_provider(provider)

    # The OpenAI and Anthropic Python SDKs both use httpx.
    HTTPXClientInstrumentor().instrument()
    RequestsInstrumentor().instrument()
    ```
  </Tab>

  <Tab title="TypeScript">
    ```bash theme={null}
    npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http \
      @opentelemetry/auto-instrumentations-node
    ```

    ```typescript instrumentation.ts theme={null}
    import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
    import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
    import { NodeSDK } from "@opentelemetry/sdk-node";

    // Load it before your entrypoint:  node --import ./instrumentation.js app.js
    const sdk = new NodeSDK({
      serviceName: "my-agent",
      traceExporter: new OTLPTraceExporter({
        url: "https://pillarforge.corepillar.ai/api/v1/traces",
        headers: { Authorization: `Bearer ${process.env.COREPILLAR_TRACING_KEY}` },
      }),
      instrumentations: [getNodeAutoInstrumentations()],
    });

    sdk.start();
    ```
  </Tab>

  <Tab title="Environment variables">
    Works for any language whose OpenTelemetry SDK is already set up.

    ```bash .env theme={null}
    OTEL_EXPORTER_OTLP_ENDPOINT=https://pillarforge.corepillar.ai/api
    OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer tk_live_...
    OTEL_SERVICE_NAME=my-agent
    # Several SDKs default to gRPC, which AgentHub doesn't accept.
    OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
    ```

    Some SDKs require the space in the header value to be URL-encoded: `authorization=Bearer%20tk_live_...`.
  </Tab>

  <Tab title="Collector">
    Add AgentHub as an extra exporter next to whatever you already export to.

    ```yaml otel-collector-config.yaml theme={null}
    exporters:
      otlphttp/corepillar:
        endpoint: https://pillarforge.corepillar.ai/api
        headers:
          authorization: "Bearer tk_live_..."

    service:
      pipelines:
        traces:
          exporters: [otlp/existing, otlphttp/corepillar]
    ```
  </Tab>

  <Tab title="Go">
    ```go tracing.go theme={null}
    exp, err := otlptracehttp.New(ctx,
    	otlptracehttp.WithEndpointURL("https://pillarforge.corepillar.ai/api/v1/traces"),
    	otlptracehttp.WithHeaders(map[string]string{
    		"Authorization": "Bearer " + os.Getenv("COREPILLAR_TRACING_KEY"),
    	}),
    )
    if err != nil {
    	return err
    }

    tp := sdktrace.NewTracerProvider(
    	sdktrace.WithBatcher(exp),
    	sdktrace.WithResource(resource.NewWithAttributes(
    		semconv.SchemaURL, semconv.ServiceName("my-agent"),
    	)),
    )
    otel.SetTracerProvider(tp)
    defer tp.Shutdown(ctx)
    ```
  </Tab>

  <Tab title="Java">
    ```bash theme={null}
    java -javaagent:opentelemetry-javaagent.jar \
      -Dotel.service.name=my-agent \
      -Dotel.exporter.otlp.protocol=http/protobuf \
      -Dotel.exporter.otlp.endpoint=https://pillarforge.corepillar.ai/api \
      "-Dotel.exporter.otlp.headers=authorization=Bearer tk_live_..." \
      -jar my-agent.jar
    ```
  </Tab>
</Tabs>

The **Overview** page in AgentHub shows the same snippets with your deployment's endpoint filled in.

## Request requirements

| Aspect | Accepted |
| - | - |
| `Content-Type` | `application/x-protobuf`, `application/protobuf` or `application/json`. Anything else returns `415`. With no `Content-Type`, the body is sniffed (JSON starts with `{`). |
| `Content-Encoding` | none, `identity`, `gzip` or `deflate`. Anything else returns `400`. |
| Body size | Up to 32 MiB after decompression. |
| OTLP/JSON field names | The spec's lowerCamelCase, or the proto snake\_case names some emitters send. |
| IDs | Raw bytes (protobuf), hex (OTLP/JSON), or base64. An all-zero parent id means "no parent". |
| Older SDKs | `instrumentationLibrarySpans` is accepted as an alias for `scopeSpans`. |

## Response

AgentHub answers with a standard OTLP `ExportTraceServiceResponse` and HTTP `200`, encoded the same way as the request, not with the `{ data }` envelope the rest of the API uses (exporters would log a transport error otherwise).

* Full success: an empty message (`{}` for JSON).
* Some spans dropped: `partialSuccess` with `rejectedSpans` and an `errorMessage`, for example:

```json theme={null}
{ "partialSuccess": { "rejectedSpans": "3", "errorMessage": "span was missing a span_id or valid start/end timestamps" } }
```

Authentication and decoding failures return the normal API error envelope (`401 UNAUTHORIZED`, `400`/`415 VALIDATION_ERROR`) instead.

A span is dropped if it has no usable trace id, no span id, or no valid start and end time. If storing a whole trace fails, all of that trace's spans are counted as rejected and the rest of the export is still processed.

## How OpenTelemetry spans are mapped

AgentHub reads OpenLLMetry (Traceloop), the OpenTelemetry GenAI semantic conventions (`gen_ai.*`) and OpenInference (`llm.*`, `openinference.*`) attributes. It maps attributes, not client libraries, so any instrumentor that emits those conventions works.

### Span kind

Checked in this order; the first match wins.

| AgentHub `kind` | When |
| - | - |
| `tool` | `gen_ai.tool.name` is set, `gen_ai.operation.name` is `execute_tool`, `traceloop.span.kind` is `tool`, or `openinference.span.kind` is `TOOL`, `AGENT` or `CHAIN`. |
| `llm` | `gen_ai.system`, `gen_ai.provider.name`, `gen_ai.request.model` or `llm.request.type` is set, or `openinference.span.kind` is `LLM`. |
| `http` | `http.request.method` or `http.method` is set. |
| `db` | `db.system` or `db.statement` is set. |
| `llm` | The span came from a model-provider instrumentation scope (for example OpenLLMetry's Anthropic or OpenAI instrumentor) with no other signal. |
| `function` | Everything else. |

### Fields

| AgentHub field | Read from |
| - | - |
| `trace_id`, `span_id`, `parent_span_id` | The OTel ids, as lowercase hex. |
| `name` | Span name (truncated to 512 characters). |
| `status` | `ERROR` when the span status code is `ERROR`; otherwise `OK`. |
| `model` | `gen_ai.response.model`, `gen_ai.request.model`, `llm.request.model` or `llm.model_name`. |
| `tokens_in` | `gen_ai.usage.input_tokens` / `prompt_tokens`, `llm.usage.prompt_tokens` or `llm.token_count.prompt`, plus cache-read and cache-write tokens. |
| `tokens_out` | `gen_ai.usage.output_tokens` / `completion_tokens`, `llm.usage.completion_tokens` or `llm.token_count.completion`. |
| `cost_usd` | Computed by AgentHub for `llm` spans from the model's catalog price, with cache-read tokens at the cached rate where one is published. Models AgentHub doesn't recognize are priced at 0 rather than guessed. Non-LLM spans are 0. |
| `latency_ms` | End time minus start time. |
| `input` | `system` and `user` from the prompt messages (`gen_ai.input.messages`, `gen_ai.prompt.N.*`, `llm.input_messages.N.*`), the full `messages` list when there are more than two, or `traceloop.entity.input` / `input.value` as `value`. |
| `output` | `content` from the first completion message, `messages`, `value`, `finish_reason`, and `error` (from an `exception` event or the status message). |
| `attributes` | Every other span attribute, plus `otel.scope.name` and `otel.scope.version`. |
| Trace `environment` | Resource `deployment.environment.name` or `deployment.environment`; `production` if neither is set. |
| Trace `root_name` | Name of the span with no parent. Until that span arrives, the earliest span's name is used as a placeholder. |
| Trace `tags` | `otel`, `service:<service.name>`, `sdk:<traceloop / openinference / otel>` and `scope:<scope name>`, up to 12 tags. |
| Trace `source` | Always `live`. |

### Spans that arrive in several exports

OpenTelemetry exports a span when it **ends**, on a batch timer, so a trace usually arrives across several requests with its root span last. That's expected: each export is merged into the same trace, totals are recomputed from all stored spans, and the trace name is corrected once the root span arrives. Exports with more than 500 spans for one trace are split automatically.

### Session stitching

Many instrumentors start a new trace for every top-level model or tool call unless you wrap the run in a span of your own. To keep one agent run together, AgentHub merges such fragments into one trace when they share a **session key** and arrive close together:

* **Session key:** the first of `session.id`, `session_id`, `gen_ai.conversation.id`, `conversation.id`, `thread.id`, `langfuse.session.id`, `traceloop.association.properties.session_id`, `openinference.session.id`, `agent.run.id` or `run.id` found on any span or on the resource. If none is set, the pair `service.name` plus `service.instance.id` is used. With neither, no stitching happens.
* **Window:** a fragment joins the session's first trace if it starts no more than 2 minutes after that trace's last span ended.
* **Cap:** a stitched trace stops growing at 2,000 spans; later fragments start a new trace.
* Stitched traces carry a `session:<hash>` tag, plus `stitched` once a merge has happened.

The most reliable way to get one trace per run is still to open your own root span (for example `with tracer.start_as_current_span("agent_run"):`) around the whole run.

## Metrics and logs

`Traceloop.init()` and several OTel SDKs also export metrics and logs to the same base endpoint. AgentHub accepts them at `POST /api/v1/metrics` and `POST /api/v1/logs` so your console doesn't fill with `404` errors, but **it discards them**: only traces are stored. Both endpoints still require a valid tracing key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.