OpenTelemetry ingestion
What the OpenTelemetry ingestion connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/otel.py; endpoint in app/api/routers/otel.py. Kind: internal. Display name: "Internal agents (OpenTelemetry)". Auth: otel (keyed push endpoint). Requires the otel_ingestion feature (Scale and Enterprise).
The span attribute convention is specified in docs/otel-convention.md; this page covers the connector's behaviour.
Purpose
Lets a customer's own agents assert outcomes (as claims with vendor = internal) and record actions (as facts) by exporting OTLP traces, so internal automation is held to the same verification as vendor agents and reported with an internal transfer price for definition-delta purposes.
Auth and scopes
| Item | Value |
|---|---|
| Method | Per-source endpoint key otk_… embedded in the URL: POST {API_URL}/v1/otel/{endpoint_key}/v1/traces. No session. |
| Key storage | sha256(key) in otel_sources.endpoint_key_hash; last 4 characters for display. The key is shown once at creation (POST /v1/otel/sources) and cannot be retrieved later; create a new source to rotate. |
| Scopes | None on the customer side. CommitLayer never connects to the customer's tracing backend; the customer pushes. |
| Management routes | GET /v1/otel/sources (viewer), POST /v1/otel/sources {name, mapping?} (admin), PUT /v1/otel/sources/{id}/mapping (admin), DELETE /v1/otel/sources/{id} (admin; revokes the underlying connection) |
| Accepted content type | application/json (OTLP/HTTP JSON). Protobuf is rejected with 415. |
Fields pulled and why
Only these parts of a span are read; everything else is dropped at ingestion:
| Span field / attribute (default mapping) | Used for |
|---|---|
name |
Stored as claim attribute span_name |
startTimeUnixNano, endTimeUnixNano |
Fallback timestamp (end preferred) for claims and facts |
traceId, spanId |
raw_ref (otel:span:{trace}:{span} / otel:action:{trace}:{span}) |
agent.outcome.type (outcome_type_attr) |
Marks the span as a claim; outcome type (optionally remapped through mapping.outcome_types) |
agent.outcome.subject_id (subject_attr) |
Claim subject id, verbatim |
agent.outcome.claimed_at (claimed_at_attr) |
Claim timestamp; defaults to span end |
agent.outcome.unit_price (unit_price_attr) |
Internal transfer price; default from mapping.outcome_types[type].unit_price or 0 |
agent.name |
Claim attribute agent |
agent.channel |
Claim channel |
agent.action.type (action_type_attr) |
Marks the span as a fact; mapped to a fact type |
agent.action.subject_id (action_subject_attr) |
Fact subject id, verbatim |
agent.action.from, agent.action.to |
Status transition attributes |
Resource attributes (e.g. service.name), events, links, status and all other span attributes are ignored. Message bodies are never accepted.
What is hashed / never stored
- Nothing is hashed: the convention carries system-of-record ids only, no personal identifiers. Any other attribute is discarded before persistence.
- The endpoint key is stored only as a SHA-256 hash.
Incremental behaviour and watermark
- Ingestion is synchronous: each POST parses
resourceSpans[].scopeSpans[].spans[](legacyinstrumentationLibrarySpansaccepted) and upserts immediately. The response reports{accepted, claims, facts}. - No watermark. Re-sending the same span (same trace/span ids) updates the same claim/fact because ids derive from
sha256(tenant_id | raw_ref). otel_sourcestrackslast_seen_at,spans_received,claims_created; the connection'shealthshows the last batch.- A
sync_connectionrequest for an OTel connection returnsnot_syncable.
Rate-limit handling
None specific; the endpoint is a normal API route. Batch spans on the client side (standard OTLP exporters do). The reserved ingest_otel_batch job exists for future asynchronous replay.
Fact types produced
agent.action.type |
fact_type |
|---|---|
status_change |
status_change (with from/to) |
escalate, handoff |
escalation |
reply |
agent_message |
| anything else | agent_action |
Facts: source = otel, subject_kind = ticket, attributes = {action, from, to, actor: "agent"}.
Claims produced
vendor = internal, contract_id from mapping.outcome_types[type].contract_id or mapping.contract_id (default internal_agent_otel), subject_kind from the mapping (default ticket), outcome_type (remapped or verbatim), claimed_at, unit_price, channel, attributes = {span_name, agent}.
Subject id format
Verbatim from the attribute. Use the fact source's format so the claim can be matched: zendesk_ticket_{id}, intercom_conv_{id}, sf_case_{Id}, hubspot_ticket_{id}, or the ids used in your CSV facts.
Known limitations
- JSON only; OTLP protobuf and gRPC are not accepted. Configure the exporter for
http/json. - Spans with
agent.action.typebut no recognizable timestamp produce a fact with an emptyoccurred_at; ensure spans carryendTimeUnixNano. - The default
internal_agent_otelcontract's vendor lens is a customer-defined rule (record_status_inor customer confirmation), not a vendor's published definition; there is no external citation. - A deleted source revokes its connection but does not delete already-ingested claims and facts.
- Ingestion is gated by the tenant's entitlement at request time; a tenant that drops below Scale receives
402on the endpoint.