Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Export spans to your collector

Turn on OTLP export, point it at a collector you run, and see exactly what arrives — including the two things that do not.

How-to guideLast updated

On this page

By the end of this page TokenTriage is posting one span per costed request to your own OpenTelemetry collector, and you have looked at a payload rather than trusting a description of one.

Export is off out of the box, and it dials nothing until you configure an endpoint. The destination is a collector you name; there is no vendor endpoint and no default.

1. Configure the exporter

export:
  otlp:
    enabled: true # off by default
    endpoint: http://127.0.0.1:4318/v1/traces # your collector's OTLP/HTTP traces path
    headers_env: TT_OTLP_HEADERS # the NAME of an env var, not its value

The endpoint is an OTLP/HTTP JSON traces URL, including the path. The wire format is OTLP/JSON over plain HTTP POST, so any collector that accepts OTLP/HTTP works and no gRPC port is involved.

headers_env names an environment variable. The variable holds comma-separated key=value headers and is read at send time rather than at start-up, so the credential is not held in the config file or in the process’s own state:

$ export TT_OTLP_HEADERS='authorization=Bearer <token>'

Leave headers_env unset for a collector that needs no auth.

2. The flag that is not enough

export.otlp.enabled: true on its own does nothing. The exporter is constructed alongside the dashboard, so it also needs the dashboard switched on:

dashboard:
  enabled: true # required for span export in this build
export:
  otlp:
    enabled: true
    endpoint: http://127.0.0.1:4318/v1/traces

Nothing warns you about the missing half. tokentriage validate accepts the config, the proxy starts, traffic is served and costed, and no span is ever posted. Verified against the shipped build: with dashboard.enabled: false and export configured, a local collector received zero bytes across 65 requests and tokentriage_otlp_dropped_total was absent from /metrics. With the dashboard on and nothing else changed, the same traffic produced a payload.

3. Send traffic and look at a payload

With a collector listening, drive a request that carries a trace id and read what arrives. One span from a lab run, trimmed to its attributes:

{
  "traceId": "f02e9edafef5685de91daa305f511d92",
  "spanId": "5af0107423594140",
  "parentSpanId": "869f9ecc2e818034",
  "name": "gpt-4o-mini",
  "kind": 3,
  "attributes": [
    { "key": "gen_ai.request.model", "value": { "stringValue": "gpt-4o-mini" } },
    { "key": "gen_ai.response.model", "value": { "stringValue": "gpt-4o-mini" } },
    { "key": "gen_ai.provider.name", "value": { "stringValue": "openai" } },
    { "key": "gen_ai.usage.input_tokens", "value": { "intValue": "768" } },
    { "key": "gen_ai.usage.output_tokens", "value": { "intValue": "512" } },
    { "key": "gen_ai.usage.cache_read.input_tokens", "value": { "intValue": "256" } },
    { "key": "gen_ai.cost.amount", "value": { "doubleValue": 0.0004416 } },
    { "key": "gen_ai.cost.currency", "value": { "stringValue": "USD" } },
    { "key": "gen_ai.cost.pricing_source", "value": { "stringValue": "litellm@2026-07-14#179affb" } },
    { "key": "tokentriage.cost.usd", "value": { "doubleValue": 0.0004416 } },
    { "key": "tokentriage.cost.would_usd.frontier", "value": { "doubleValue": 0.00736 } },
    { "key": "tokentriage.trace.agent", "value": { "stringValue": "researcher" } },
    { "key": "tokentriage.dim.tenant", "value": { "stringValue": "acme-corp" } }
  ]
}

That is one span out of a batch, with about a third of its attributes shown; the full set is in the attribute mapping. The upstream was a mock with fixed token counts, so $0.0004416 is arithmetic over those counts rather than a bill. kind: 3 is SPAN_KIND_CLIENT, and the batch’s resource carries a single attribute, service.name: tokentriage.

Spans are batched: a POST goes out every 128 rows or every two seconds, whichever comes first, with a ten-second timeout and two retries on a transient failure.

4. Know what does not arrive

Two categories of row never reach your collector, and neither is a bug in your setup.

Rows with no trace id are skipped. The OTLP trace id is derived from the row’s own trace.trace_id, so a request that carried no x-tt-trace-id has nothing to derive from. In the lab run behind this page, 66 requests produced 5 spans, because only 5 carried identity. If your collector shows a small fraction of your traffic, that fraction is your instrumented share, and putting identity on the wire is the lever.

Rows dropped on a full export queue are counted rather than shipped. The queue holds 4,096 rows and the exporter never blocks the ledger writer, so a slow or unreachable collector costs you spans and costs your traffic nothing. The count is tokentriage_otlp_dropped_total.

A limit worth knowing before you build on the trace view

The parent edge does not survive the export in this build. A span’s own id is derived from the row’s request id, while its parent id is derived from x-tt-parent-id, which is a step id. The two are hashed from different id spaces, so a child’s parentSpanId names a span that does not exist.

You can check it on the payload above. parentSpanId is 869f9ecc2e818034, which is the derived form of step-01; the span for step-01 carries the id 1560680610ff9cd2, derived from its request id. Across the whole lab batch, every one of the three child spans pointed at 869f9ecc2e818034 and no span had it.

What this costs you: spans arrive in the right trace, with the right attributes and the right dollars, and a backend’s waterfall renders them as siblings rather than as a tree. What it does not cost you: the tree itself, which is intact on the ledger and rendered correctly by tokentriage top --trace. This is filed; until it is fixed, read the shape from the ledger and use the collector for correlation with your other services.

What can go wrong

Nothing arrives at all. Check dashboard.enabled, then check the endpoint path. The endpoint is the full traces URL and must end in the collector’s traces path; a host without one gets a 404 that shows up as export failures rather than as a startup error.

Auth is rejected. headers_env is a variable name. Putting the header value directly in the config field sends a literal environment-variable name as your token.

Timestamps look wrong. startTimeUnixNano and endTimeUnixNano are derived from the row’s timestamp and its total latency in milliseconds, so a sub-millisecond call produces a span whose start and end are the same instant. Against a local mock every span is zero-duration for that reason. Span duration is not the place to read latency; the histograms are.

Whether TokenTriage ingests OTel context as well as exporting it

Not for identity. The exporter is one-way: it derives trace and span ids from the ledger’s own ids and does not adopt an inbound traceparent as the trace, which is the same explicit-identity rule that governs the ledger.

There is a separate OTLP ingestion adapter for outcome signals, which accepts evaluation results back into the system. It is gated by its own configuration and is a different subject; the calibration and evals overview is where that path is documented.