# OpenTelemetry

> Send traces you already collect to ai-tally: settings, the attributes ai-tally reads, and what it never stores.

OpenTelemetry is a common standard for tracing, which means recording what your app does. A trace is made of spans, and a span is one traced unit of work, like one AI call. If your app already sends traces, point them at ai-tally. It reads the AI calls in them and works out what each one cost.

## Settings

Set these where your app sends its traces:

```bash
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://ingest.ai-tally.com/v1/otlp/traces"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/json"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Bearer ${TALLY_KEY}"
```

Three things trip people up:

- **Use the `_TRACES_` settings.** The shorter `OTEL_EXPORTER_OTLP_ENDPOINT` adds `/v1/traces` to the URL, which is the wrong address.
- **The format must be JSON.** ai-tally does not accept the binary protobuf format or gRPC. The Python OpenTelemetry exporter only sends protobuf, so from Python use the [Python SDK](https://ai-tally.com/docs/connect/python-sdk) instead.
- **Collector users:** use the `otlphttp` exporter with `encoding: json` and `compression: none`.

The API key needs `write` scope.

## What ai-tally reads

These attributes, the named values on a span, tell ai-tally what a call was:

| Attribute | Example | Why |
|---|---|---|
| `gen_ai.system` | `openai` | The AI provider. Needed for a price. |
| `gen_ai.request.model` or `gen_ai.response.model` | `gpt-4o-mini` | The model. Needed for a price. |
| `gen_ai.operation.name` | `chat` | The kind of call (see below). |
| `gen_ai.usage.input_tokens` | `12` | Tokens sent in. Needed for a price. |
| `gen_ai.usage.output_tokens` | `5` | Tokens sent back. Needed for a price, except for embeddings. |
| `gen_ai.usage.cached_input_tokens` | `8` | Input tokens the provider served from its cache, which cost less. |
| `gen_ai.feature_tag` | `support-chat` | Which feature made the call. |
| `gen_ai.account_id_hash` | 64-character code | Which customer, as a hashed customer id that cannot be traced back. Make it with `python -m tally.hash_account your-customer-id`. |
| `gen_ai.session_id` | `sess_42` | Groups calls from one user session. |

- **The provider attribute.** ai-tally reads `gen_ai.system`, not the newer `gen_ai.provider.name`. Set `gen_ai.system`.
- **Missing values.** If a price value is missing, the call is still saved, but its cost shows as a blank, never $0.

## Kinds of call

`gen_ai.operation.name` decides where a call is counted in the dashboard:

| Value | Counted as |
|---|---|
| `chat` | LLM |
| `embeddings` | Embeddings |
| `tool` | Tool calls. Send the cost in `gen_ai.tool.cost_micro_usd`, in whole micro-dollars. |
| `vector` | Vector DB. Send the cost in `gen_ai.tool.cost_micro_usd`. |

## What ai-tally never stores

- **Text.** Attributes that hold text are dropped before anything is saved: any whose name ends in `prompt`, `completion`, `messages`, `content`, `text` or `body`.
- **Personal details.** A span containing an email address, or attributes named like `email`, `user_id` or `phone`, is refused. Hash ids before you send them.

## Limits

- **Span size.** Each span can be up to 64 KiB.
- **Sending too fast.** A `429` reply means slow down. Wait the number of seconds in its `Retry-After` header, then send again.

## Next

- Nothing showing up? See [Check it works](https://ai-tally.com/docs/get-started/check-it-works).
