OpenTelemetry (OTLP)
The hosted service speaks OTLP/HTTP in both directions. Export sends the trace of every hosted request to your own tracing backend, so CLI decisions sit next to the rest of your application’s traces. Ingest accepts your own OpenTelemetry spans, for example from an agent running in local mode, and files them next to hosted requests.
Export
Add destinations under Observability > Data settings, in the
OTLP export section (owners and admins). Each destination has a name, an
endpoint, a protocol (http/protobuf, the default, or http/json), and
headers. Header values, which usually hold an API key, are encrypted at
rest with the same scheme as provider connections
and never shown again; Test sends one span to check the destination.
| Preset | Endpoint | Headers to fill in |
|---|---|---|
| Datadog | https://otlp.datadoghq.com/v1/traces | dd-api-key |
| Langfuse | https://cloud.langfuse.com/api/public/otel/v1/traces | authorization, x-langfuse-ingestion-version |
| LangSmith | https://api.smith.langchain.com/otel/v1/traces | x-api-key, langsmith-project |
| Arize Phoenix | your Phoenix collector’s /v1/traces URL | authorization |
| Honeycomb | https://api.honeycomb.io/v1/traces | x-honeycomb-team |
| Generic OTLP/HTTP | any OTLP/HTTP traces endpoint | as your collector needs |
A preset only pre-fills the form: change the endpoint for another region
or a self-hosted instance. Endpoints must be https and resolve to public
addresses.
What is sent
One trace per hosted request, with the same span tree as the dashboard’s Timeline:
| Where | Attributes |
|---|---|
| Resource | service.name = "cci-hosted-api", cci.org.id |
| Root span (named after method and path) | cci.request_id, cci.operation.name, http.response.status_code, url.path |
| Model-call spans | gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.response.finish_reasons, cci.purpose, cci.query.id, the estimated cost |
| Other spans | cci.* attributes such as cci.query.type, cci.profile.name, cci.binding.decision, cci.guarantee.type, cci.evidence.access_level |
| Every span | openinference.span.kind (LLM, CHAIN or EVALUATOR) and langfuse.observation.type (generation, span or evaluator), so Phoenix and Langfuse render model calls and statistics natively |
The trace id is the request’s trace id, so a request you sent with a
traceparent header joins your own trace in the destination. Prompt and
reply previews (gen_ai.input.preview, gen_ai.output.preview) are
exported only when payload capture
kept them; requests on zero-retention profiles carry none.
Delivery
Traces are queued when they are written and sent every 15 seconds. A failed delivery is retried with exponential backoff, from 15 seconds up to an hour between attempts, and dropped after about ten attempts. Export never delays an API response.
Ingest
POST https://cci.gitdate.ink/api/v1/traces
Authorization: Bearer cci_live_... (a key with the events:write scope)
Content-Type: application/x-protobuf (or application/json)
Content-Encoding: gzip (optional)The endpoint is a standard OTLP/HTTP traces receiver, so any
OpenTelemetry exporter can send to it. With the Python SDK
(pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http):
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http import Compression
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
exporter = OTLPSpanExporter(
endpoint="https://cci.gitdate.ink/api/v1/traces",
headers={"Authorization": f"Bearer {os.environ['CLI_API_KEY']}"},
compression=Compression.Gzip,
)
provider = TracerProvider(resource=Resource.create({"service.name": "claims-agent"}))
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("claims-agent")
with tracer.start_as_current_span("guard payment") as span:
span.set_attribute("session.id", "triage-run-42")
span.set_attribute("gen_ai.conversation.id", "th_77")
answer = local_client.evaluate(context, {"payment": payment_gate}).answers["payment"]
span.set_attribute("cci.profile.name", "claim-payments-v1")
span.set_attribute("cci.decision", answer.decision)
span.set_attribute("cci.guarantee.type", answer.guarantee.type)The same exporter can be configured from the environment instead:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://cci.gitdate.ink/api/v1/traces
and OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20cci_live_...
(the header value URL-encoded).
How ingested spans are stored:
- Spans are grouped by trace id. Each trace becomes one request with
source: "otlp"and a request idotl_<16 hex>that is stable for the trace, so spans of one trace sent in several batches land in the same request. Search them withsource:otlp. - The request’s operation is
otlp.<service.name>, its route is the root span’s name, and it is marked as an error when any span has error status. - The session comes from the root span’s
session.id(orcci.session.id) attribute, and the thread fromgen_ai.conversation.id(orcci.thread.id), so ingested traces appear in Sessions next to hosted requests and guard events of the same thread. - Provider, model and token counts are read from
gen_ai.provider.name(orgen_ai.system),gen_ai.request.model,gen_ai.usage.input_tokensandgen_ai.usage.output_tokens. - Content attributes (
gen_ai.input.messages,gen_ai.output.messages,gen_ai.system_instructions,gen_ai.tool.call.arguments,gen_ai.tool.call.result,input.value,output.value) are dropped when payload capture is Off or zero retention is the organization default.
| Limit or error | Value |
|---|---|
| Body | 20 MB, and 16 MB after gzip decompression (413) |
| Spans per request | 5,000; spans beyond that, and spans without a valid trace id, are rejected |
| Response | An OTLP ExportTraceServiceResponse in the request’s format, with partialSuccess.rejectedSpans when spans were rejected |
| Other content types | 415 unsupported_media_type |
| A body that does not decode | 422 otlp_decode_failed |
Ingest records traces; it does not evaluate anything. Guarantees still
come from evaluate (hosted) or LocalCLIClient (local); ingest makes
the local decisions visible in the same place as hosted ones.