Metadata-Version: 2.4
Name: logthread-livekit
Version: 0.1.1
Summary: Logthread helper for LiveKit Agents (Python): OpenTelemetry traces, usage and latency metrics, and correlated logs in one line.
Keywords: livekit,livekit-agents,observability,opentelemetry,logthread
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Logging
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: logthread>=0.1.1
Requires-Dist: opentelemetry-sdk<2,>=1.27
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.27
Provides-Extra: livekit
Requires-Dist: livekit-agents<2,>=1.2; extra == "livekit"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# Logthread for LiveKit Agents (Python)

One line in your agent entrypoint sends the agent's OpenTelemetry traces, usage and latency
metrics, and correlated logs to [Logthread](https://logthread.com). Everything goes to your
Logthread endpoint with your Logthread ingest key. It never raises into your agent.

## Install

```bash
pip install https://downloads.logthread.com/sdk/python/0.1.1/logthread-0.1.1-py3-none-any.whl \
  https://downloads.logthread.com/sdk/livekit-agents-python/0.1.1/logthread_livekit-0.1.1-py3-none-any.whl
```

The packages are hosted by Logthread, not on PyPI yet, so the command names both wheels by URL: this
helper and `logthread` (the logs SDK). Exact URLs mean PyPI is never asked for these names. The current versions and checksums are on the
[download page](https://logthread.com/sdk#livekit-agents-python).
`livekit-agents` (`>=1.2,<2`) is a peer: your agent already installs it. This package depends on
`logthread` (the logs SDK), `opentelemetry-sdk` and `opentelemetry-exporter-otlp-proto-http`.
Python 3.10 or newer.

## Quick start

```bash
export LOGTHREAD_ENDPOINT=https://ingest.example.com   # shown in Logthread onboarding
export LOGTHREAD_INGEST_KEY=<your ingest key>
```

```python
from logthread_livekit import instrument

async def entrypoint(ctx: JobContext):
    lt = instrument(ctx)          # reads LOGTHREAD_ENDPOINT / LOGTHREAD_INGEST_KEY
    session = AgentSession(...)
    lt.watch(session)             # usage + latency metrics + session report
    ...
```

A runnable agent is in [`examples/agent.py`](examples/agent.py).

## What it sends

| signal | what | where |
|---|---|---|
| Traces | LiveKit's agent spans (`lk.*` and `gen_ai.*` attributes), plus `livekit.room.sid`, `livekit.room.name`, `livekit.job.id`, `livekit.agent.name`, `livekit.worker.id` | OTLP/HTTP to `{endpoint}/v1/traces`, `Authorization: Bearer <key>` |
| Metrics | the instruments below, cumulative, exported every 60 s and at shutdown | OTLP/HTTP to `{endpoint}/v1/metrics` |
| Logs | every stdlib `logging` record (the Logthread logs SDK), with `correlation_id` and `livekit.*` attributes, plus the `session started`, `agent session error` and `session report` records | `{endpoint}/v1/logs` |

**Metrics** (`provider` and `model` are attributes on all of them except the last four; attribute
values are strings):

| name | type | attributes |
|---|---|---|
| `livekit.agent.llm.tokens` | counter (`{token}`) | `provider`, `model`, `direction` (`input`/`output`), `cached` (`true`/`false`) |
| `livekit.agent.tts.audio_seconds` | counter (`s`) | `provider`, `model` |
| `livekit.agent.stt.audio_seconds` | counter (`s`) | `provider`, `model` |
| `livekit.agent.llm.ttft_ms` | histogram (`ms`) | `provider`, `model` |
| `livekit.agent.tts.ttfb_ms` | histogram (`ms`) | `provider`, `model` |
| `livekit.agent.stt.duration_ms` | histogram (`ms`) | `provider`, `model` (non-streaming STT only; only on releases without `ChatMessage.metrics`, see Compatibility) |
| `livekit.agent.eou.delay_ms` | histogram (`ms`) | none |
| `livekit.agent.e2e_latency_ms` | histogram (`ms`) | none (per assistant turn) |
| `livekit.agent.sessions` | counter | `state` (`started`/`ended`) |
| `livekit.agent.interruptions` | counter | none |

Token counters are cumulative and monotonic. They are fed with the difference between each
`session_usage_updated` value and the previous one for the same model, so repeating or reordering
an update cannot move a counter backwards. `input` tokens with `cached=true` are the cache reads;
`cached=false` is input minus cache reads, so the two sum to the model's input tokens.

**Correlation.** `correlation_id` is the room session id (`RM_...`), and the job id until the room
sid is known. The sid comes from `ctx.job.room.sid` (set by the server before your entrypoint
runs); if it is empty it is picked up from `Room.sid` (an awaitable that resolves after connect)
or the room's `connected` event, and every later log record, span and metric stamp uses it. Agent
spans keep their own trace ids; agent log records carry `trace_id`/`span_id` (logs SDK) and the
room sid, so traces and room events join up.

**Privacy.** PII is off by default.
- Spans: `allow_pii=False` is passed to LiveKit's `set_tracer_provider`, and enforced again on the
  Logthread exporter, which drops every `lk.pii.*` attribute, the GenAI content attributes
  (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`,
  `gen_ai.tool.*`, `gen_ai.prompt.variable.*`) and `gen_ai.*` message events, and redacts
  exception messages and stack traces. This also holds when the application shares its own tracer
  provider. It does not change what LiveKit Cloud receives (that is the project's PII setting).
- Logs: `lk.pii.*` keys in `extra` are removed from the records Logthread receives (records seen
  by other handlers are untouched).
- The session report has no transcript unless `include_transcript=True`.

## Configuration

`instrument(ctx, **options)`; explicit options win over the environment.

| option | env | default | notes |
|---|---|---|---|
| `endpoint` | `LOGTHREAD_ENDPOINT` | none | ingest base URL; `/v1/...` is appended (a value ending in a signal path is accepted). HTTPS required unless the host is `localhost`, `127.0.0.1`, `::1` or `*.localhost`, or `allow_insecure=True` |
| `api_key` | `LOGTHREAD_INGEST_KEY` | none | sent as `Authorization: Bearer <key>`; never logged |
| `service` | `LOGTHREAD_SERVICE` | agent name, else `livekit-agent` | `service.name` |
| `environment` | `LOGTHREAD_ENVIRONMENT` | none | `deployment.environment` |
| `allow_pii` | | `False` | let Logthread receive conversation content in spans |
| `include_transcript` | | `False` | put user/assistant text in the `session report` record |
| `traces`, `metrics`, `logs` | `LOGTHREAD_LIVEKIT_NO_TRACES`, `..._NO_METRICS`, `..._NO_LOGS` set to `true` | all on | switch a signal off |
| `metrics_interval_s` | | `60` | metric export interval |
| `shutdown_timeout_s` | | `5` | bound for flush/shutdown, capped at 5 |
| `attributes` | | none | extra attributes added to every log record and span |
| `allow_insecure` | `LOGTHREAD_ALLOW_INSECURE` | `False` | allow plain `http` to a non-local host |
| `debug` | `LOGTHREAD_DEBUG` | `False` | one stderr line per problem (missing API, failed setup) |
| | `LOGTHREAD_DISABLED=true` | | kill switch with no code change |

With no key or endpoint the helper disables itself, prints one stderr line and the agent runs on.

The handle: `lt.watch(session)`, `lt.flush(timeout=None)`, `lt.shutdown(timeout=None)`,
`await lt.ashutdown()`, `lt.correlation_id`, `lt.enabled`. You do not need to call `shutdown`:
`instrument` registers a `ctx.add_shutdown_callback` that logs the session report and flushes
traces, metrics and logs within the job's shutdown window, bounded at 5 s (each exporter runs on
its own daemon thread, so a hung endpoint cannot hold the agent).

If the application already set a tracer provider (yours, or the one LiveKit Cloud's pipeline
uses), Logthread is added to it as one more span processor; the provider is not replaced.

## Compatibility

Supported: `livekit-agents >=1.2,<2`, Python 3.10+, OpenTelemetry Python 1.27 to 1.x.
When an API is missing the affected feature is switched off and one debug line is printed
(`debug=True`): no `set_tracer_provider` means no traces; no `session_usage_updated` event (the
installed `voice.events.EventTypes` lacks it) means usage counters are derived from
`metrics_collected` instead; an unusable `session.on` skips that subscription.

Verified against the LiveKit docs MCP on 2026-10-18 (docs and `livekit/agents`, `livekit/python-sdks`
main; code-search commit `cf7cc3f`):

- `livekit.agents.telemetry.set_tracer_provider(tracer_provider, *, metadata=None, allow_pii=None)`
  and its behaviour (adds a metadata processor, installs PII redaction once per provider, sets the
  tracer): `livekit-agents/livekit/agents/telemetry/traces.py`; docs page
  `/testing/observability/tracing` (Other backends, "Strip PII from exported traces", `lk.pii.`
  attribute table, Agents 1.7/1.8 renames).
- LiveKit Cloud adopts the application's provider and adds its exporter to it (so a shared
  provider keeps Agent insights working): `_CloudTelemetry` in the same file.
- `JobContext`: `job.id`, `job.agent_name`, `job.room.sid`, `job.room.name` (available in the
  entrypoint), `worker_id`, `room`, `add_shutdown_callback(callback)` (optionally taking the
  shutdown reason), `make_session_report(session=None)`: `livekit-agents/livekit/agents/job.py`.
  The docstring there notes `Room` properties may be unavailable before `ctx.connect()`.
- `rtc.Room.sid` is an async property: `livekit-rtc/livekit/rtc/room.py` (python-sdks).
- `AgentSession` events (`EventTypes`) include `metrics_collected`, `session_usage_updated`,
  `conversation_item_added`, `error`, `close`; `SessionUsageUpdatedEvent.usage` is an
  `AgentSessionUsage` with `model_usage`; `AgentSession.usage`: `voice/events.py`,
  `voice/agent_session.py`.
- Usage entries: `LLMModelUsage` (`provider`, `model`, `input_tokens`, `input_cached_tokens`,
  `output_tokens`, ...), `TTSModelUsage` (`audio_duration`, `characters_count`, ...),
  `STTModelUsage` (`audio_duration`, ...), all cumulative per provider/model:
  `metrics/usage.py`.
- Metrics: `LLMMetrics.ttft` (seconds, `-1` when no token), `TTSMetrics.ttfb`,
  `STTMetrics.duration` / `audio_duration`, `EOUMetrics.end_of_utterance_delay`, `metadata.model_name`
  / `model_provider`: `metrics/base.py`. Note `MetricsCollectedEvent` is marked deprecated in
  favour of `session_usage_updated` for usage; per-turn latency is on `ChatMessage.metrics`
  (`e2e_latency`, seconds): `llm/chat_context.py`. This package reads latency from both.

**Verified live on 2026-10-18** against real, self-hosted LiveKit software (no LiveKit Cloud):
`livekit-server` 1.13.10 (`livekit/livekit-server` image), `livekit-agents` 1.8.8 (`livekit`
rtc 1.1.20, `livekit-api` 1.2.2), Python 3.12 (`python:3.12-slim`, linux/arm64), OpenTelemetry
Python 1.45.1, `lk` CLI 2.18.4, sending to the Logthread dev gateway. A real worker
(`AgentServer` + `@server.rtc_session(agent_name=...)`, `cli.run_app(server)`, process executor)
took an explicit dispatch (`lk dispatch create`); the entrypoint called `instrument(ctx)`,
`ctx.connect()`, started a real `AgentSession` with an offline scripted `llm.LLM` (no STT/TTS/VAD,
`RoomOptions(audio_input=False, audio_output=False)`), ran three `generate_reply` turns and ended
when the room was deleted. Seen through the Logthread APIs:
- traces: 27 LiveKit spans in 3 traces (`agent_session` with `agent_turn`, `llm_node`,
  `llm_request`, `llm_request_run`, `session_close`, ...; `room_connect`; `session_end_upload`),
  every span stamped with `livekit.room.sid`, no `lk.pii.*` or GenAI content attributes;
- metrics: `livekit.agent.llm.tokens` (input `cached=false` 90, `cached=true` 30, output 15 for
  3 x 40/10/5 tokens), `livekit.agent.llm.ttft_ms` (`provider`/`model` from the message metadata),
  `livekit.agent.sessions` (started 1, ended 1);
- logs: every record of the job with `correlation_id` = the room sid (`RM_...`), including
  `agent session started` and the `session report` (3 turns, usage totals, close reason
  `user_initiated`, `trace_id` of the session).

Not exercised live: STT/TTS/EOU latency and audio-seconds counters (no audio providers),
interruptions, `agent session error`, a shared application or LiveKit Cloud tracer provider, the
thread executor.

Found live and fixed: `AgentSession.on("metrics_collected", ...)` logs a deprecation warning in
current releases ("metrics_collected is deprecated. Use session_usage_updated ... and
ChatMessage.metrics ..."), so the helper put one WARNING into every job's logs. When the installed
release has both `session_usage_updated` and `ChatMessage.metrics` latencies
(`llm.chat_context.MetricsReport.llm_node_ttft`), the helper no longer subscribes to
`metrics_collected` and reads latency from the conversation items: `llm_node_ttft` -> `llm.ttft_ms`,
`tts_node_ttfb` -> `tts.ttfb_ms` (provider/model from `llm_metadata`/`tts_metadata`),
`end_of_turn_delay` -> `eou.delay_ms`, `e2e_latency` -> `e2e_latency_ms`. Older releases keep the
`metrics_collected` path.

The unit test suite uses fakes with these documented shapes; it does not run a real LiveKit server
or install `livekit-agents`.

## Limits

- Python agents run each job in its own process by default. With the thread executor, several jobs
  share the root logger and the tracer provider: logs are routed per job through a context
  variable, but the tracer provider is process-wide.
- Spans are exported by a batch processor every 2 s; spans that end after the shutdown callback
  has flushed (the last moments of a closing session) can be missed.
- If a `logthread` handler was already installed on the root logger it is replaced by the
  LiveKit-aware one (two would duplicate every record).
- `lk.pii.*` stripping on logs is by key. Values are not scanned; value-level masking is the job of
  server-side Pipelines.
- Logs only include what your logging configuration lets through (the helper lowers the root logger
  to `INFO` if it was higher, like `logthread.install()`).
- Interruptions are counted from assistant messages marked `interrupted`; `agent_false_interruption`
  events are not counted.
- Reading the application's tracer provider uses a private LiveKit attribute
  (`telemetry.traces.tracer._tracer_provider`), with the OpenTelemetry global provider as fallback.
