---
title: "Tracing (OpenTelemetry)"
description: "Opt-in OpenTelemetry tracing for STT, LLM, TTS, and tool calls."
icon: "chart-network"
---

Patter ships opt-in OpenTelemetry tracing that covers the four hot spots on the voice pipeline: STT, LLM, TTS, and tool calls. Every span carries the current `getpatter.call.id` so you can group by call in your backend.

## Enable tracing

Tracing is disabled by default. Install the optional dependency and set the env flag:

```bash
pip install 'getpatter[tracing]'
export PATTER_OTEL_ENABLED=1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318   # any OTLP/HTTP collector
```

Then call `init_tracing` once at process start — typically from the same module that creates your `Patter` client:

```python
from getpatter.observability import init_tracing

init_tracing(
    service_name="my-voice-bot",
    otlp_endpoint="http://localhost:4318",
    resource_attributes={"deployment.environment": "staging"},
)
```

If `PATTER_OTEL_ENABLED` is not set, `init_tracing` returns `False` and every span becomes a no-op — **zero cost** when disabled.

## Emitted spans

| Span name | Fires | Attributes |
|---|---|---|
| `getpatter.call` | One per call (root span) | `getpatter.call.id`, `getpatter.call.direction`, `getpatter.call.provider_mode` |
| `getpatter.stt` | One per final transcript | `getpatter.stt.text_len`, `getpatter.stt.confidence` |
| `getpatter.endpoint` | One per detected end-of-utterance | `getpatter.endpoint.silence_ms` |
| `getpatter.llm` | One per LLM iteration (incl. tool rounds) — wraps the pipeline LLM call so TTFT is measured per turn | `getpatter.llm.iteration`, `getpatter.llm.history_size`, `getpatter.llm.ttft_ms` |
| `getpatter.tts` | One per synthesized sentence | `getpatter.tts.text_len` |
| `getpatter.bargein` | One per barge-in event | `getpatter.bargein.cancelled_text_len` |
| `getpatter.tool` | One per tool invocation | `getpatter.tool.name`, `getpatter.tool.transport` |

All TS spans use the same `getpatter.*` namespace as Python (the legacy `patter.*` names were normalised in 0.5.3 — both SDKs now emit identical span names so a single dashboard query works across runtimes).

## PII hygiene

Patter never exports user utterances, tool payloads, or LLM content as span attributes. Only sizes, counts, and identifiers are emitted — traces are safe to ship to a shared Jaeger / Honeycomb / Grafana Cloud instance.

## Cost and latency attributes (`patter.*`)

Beyond the spans listed above, every billable hot path stamps `patter.cost.*` and `patter.latency.*` attributes on its span starting in 0.6.0. External aggregators (e.g. the [patter-agent-runner acceptance suite](https://github.com/PatterAI/patter-agent-runner)) read these directly to compute per-call USD and latency without touching the SDK's pricing table.

| Attribute | Type | Where it fires |
|---|---|---|
| `patter.call_id`, `patter.side` | str | Every cost/latency span (routing tags) |
| `patter.cost.telephony_minutes`, `patter.telephony`, `patter.direction` | float / str | Twilio + Telnyx adapters on call end |
| `patter.cost.stt_seconds`, `patter.stt.provider` | float / str | Each final transcript (Deepgram, AssemblyAI, Whisper, OpenAI-Transcribe, Soniox, Speechmatics, Cartesia) |
| `patter.cost.tts_chars`, `patter.tts.provider` | int / str | Each synthesis (ElevenLabs, OpenAI, Cartesia, LMNT, Rime) |
| `patter.cost.llm_input_tokens`, `patter.cost.llm_output_tokens`, `patter.llm.provider` | int / int / str | Each completion (OpenAI, Anthropic, Google Gemini, Groq, Cerebras) |
| `patter.cost.realtime_minutes`, `patter.realtime.provider` | float / str | OpenAI Realtime + ElevenLabs ConvAI on session end |
| `patter.latency.ttfb_ms`, `patter.latency.turn_ms` | float | Per completed agent turn (pipeline mode) |

The two routing tags (`patter.call_id`, `patter.side`) propagate via asyncio-safe ContextVars set at the top of the per-call WebSocket bridge, so all spans emitted under a call inherit them automatically — no manual wiring per span.

### Attach a custom exporter (`Patter._attach_span_exporter`)

Embedding tools that observe Patter from the outside can wire their own exporter without touching `PATTER_OTEL_ENABLED` or `init_tracing`:

```python
from opentelemetry.sdk.trace.export import SpanExporter
from getpatter import Patter

exporter: SpanExporter = my_exporter()  # any OTel SpanExporter
phone = Patter(...)
phone._attach_span_exporter(exporter, side="driver")  # or side="uut" (default)
```

`side` is stamped on every cost/latency span this Patter instance emits during its call lifecycle. It exists to disambiguate two-`Patter`-instances-in-one-process layouts (e.g. driver vs unit-under-test in agent-to-agent acceptance tests). Default is `"uut"` if you only have one instance.

The leading underscore signals this is not part of the customer-facing API surface — it is a stable, public-but-underscore hook for tooling.

## Shutdown

Call `shutdown_tracing()` during graceful shutdown to flush any pending spans:

```python
from getpatter.observability import shutdown_tracing

shutdown_tracing()
```

## Troubleshooting

**No spans appear** → confirm `PATTER_OTEL_ENABLED=1` is set *in the process that calls `init_tracing`*. Quick check:

```python
from getpatter.observability import is_enabled
print(is_enabled())
```

**Collector refuses spans** → the endpoint defaults to OTLP/HTTP on port 4318. If you're running the gRPC-only OTel Collector image, switch to `otel/opentelemetry-collector-contrib` or enable the HTTP receiver in your collector config.

<Info>
Tracing has full TypeScript parity since 0.5.3 — span names, attributes, and PII hygiene are identical. See [TS Tracing](/typescript-sdk/tracing).
</Info>
