---
title: "OpenTelemetry"
description: "Opt-in OTLP export of traces, metrics, and logs to your own collector or backend."
image: "https://shunt-docs.pages.dev/og.png"
---

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

# OpenTelemetry

shunt can export **traces, metrics, and logs** over OTLP/HTTP to your own OpenTelemetry Collector (or any OTLP-compatible backend). It is **opt-in and off by default** — with no `[otel]` section, nothing leaves the machine — and runs independently of Sentry, so you can enable either or both.

## Enable it

One key turns it on — point it at your Collector's OTLP/HTTP receiver:

```toml
[otel]
endpoint = "http://localhost:4318"   # OTLP/HTTP base URL; shunt appends /v1/{traces,metrics,logs}
```

Everything else has a sensible default:

```toml
[otel]
endpoint = "http://localhost:4318"
service_name = "shunt"     # (default) service.name resource attribute
environment = "prod"       # optional deployment.environment.name
sample_ratio = 1.0         # (default) head-based trace sampling, 0.0–1.0
traces = true              # (default) export request spans
metrics = true             # (default) export usage metrics
logs = true                # (default) export log events (stderr logs unaffected)
include_session_id = false # (default) keep the client session id off spans

[otel.headers]             # optional per-request headers, e.g. a hosted-collector token
authorization = "Bearer <token>"
```

Setting `endpoint = ""` (e.g. `SHUNT_OTEL__ENDPOINT=""`) disables export again without deleting the section. An invalid endpoint, a non-`http(s)` URL, or an out-of-range `sample_ratio` is a **startup error**, so a typo can't silently drop every export.

## The three signals

| Signal | What's exported | Notes |
| :-- | :-- | :-- |
| **Traces** | The per-request `proxy_request` span | Head-based sampling via `sample_ratio`. Low-cardinality; no request/response bodies. |
| **Metrics** | The low-cardinality series listed below | The same series shunt sends to Sentry when `[sentry] metrics = true`. |
| **Logs** | shunt's `tracing` log events, bridged to OTLP | The stderr logs are unaffected. |

Each signal toggles independently via `traces` / `metrics` / `logs`.

### Metric series

| Series | Type | Attributes | Meaning |
| :-- | :-- | :-- | :-- |
| `shunt.requests` | Counter | `provider`, `model`, `http.response.status_code` | Proxied inference requests. |
| `shunt.latency` | Histogram (ms) | `provider`, `model`, `http.response.status_code` | Header latency for streams; full latency otherwise. |
| `shunt.ttft` | Histogram (ms) | `provider`, `model` | Request start to the first SSE body chunk. |
| `shunt.stream_outcome` | Counter | `provider`, `model`, `outcome` | One final SSE result: `completed`, `error_event`, `upstream_cut`, or `client_disconnect`. |
| `shunt.tokens` | Counter | `provider`, `model`, `kind` | Last reported streaming token usage (`input`, `output`, `cache_read`, `cache_creation`). Non-streaming usage is not recorded. |
| `shunt.codex_continuation` | Counter | `provider`, `outcome` | Codex WebSocket continuation hit or fallback. |
| `shunt.codex_client_events` | Counter | `event` | Codex CLI analytics events by sanitized event name; payloads and properties are discarded. |
| `shunt.upstream_retries` | Counter | `provider`, `reason` | Bounded transient upstream retries. |
| `shunt.failover` | Counter | `provider`, `state` | Ordered-upstream failover transitions: `attempted`, `advanced`, or `exhausted`. |
| `shunt.pool.quota_utilization` | Gauge | `provider`, `window` | Minimum utilization across enabled, observed, non-expired quota values for `5h`, `7d`, or `7d_oi`. |
| `shunt.pool.rotations` | Counter | `provider`, `reason` | Moves off an account and requests that exhaust the pool. |

## Privacy

shunt never exports request/response bodies, headers, or credentials in **metrics and traces**.

- **Metrics and traces** stay low-cardinality and body-free. With OTLP trace export, the request span's client **session id** is sent to the collector **only** when `include_session_id = true` (default off), and only while trace export is active. The same rule governs Sentry trace export (`[sentry] traces_sample_rate` / `include_session_id`); with no span export active at all, the id stays on the local request span as before.
- **Logs** mirror shunt's own diagnostic events as written, so — like the stderr logs — they can include request-derived fields (an upstream error body, an authenticated client id). For strictly body-free export, set `logs = false` and keep metrics/traces.

The exported resource advertises `service.*`, `telemetry.sdk.*`, and — when `environment` is set — `deployment.environment.name`; no host or process detector runs, so the machine's hostname is not attached, plus whatever you put in the standard `OTEL_RESOURCE_ATTRIBUTES`.

> **Caution**
>
> If `[otel.headers]` carries a secret (e.g. a collector bearer token) and the endpoint is plaintext `http://` to a non-loopback host, shunt logs a warning at startup: the token would travel in the clear. Use `https://` for a remote collector.

## Standard `OTEL_` env vars

- `endpoint` and `service_name` come from this config and **take precedence** over `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_SERVICE_NAME`.
- The standard `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_RESOURCE_ATTRIBUTES` are still **merged in**, on top of `[otel.headers]` and the built-in resource attributes.

> **Note**
>
> The exporters are initialized once at startup. Editing `[otel]` and hot-reloading warns and **requires a restart** to take effect — unlike most config, which reloads live.

See the [`[otel]` configuration reference](/reference/configuration/#otel-optional) for every key.

Source: https://shunt-docs.pages.dev/guides/opentelemetry/index.mdx
