Package
Logthread for LiveKit Agents (Python)
d0d92c20a023… One line in your agent entrypoint sends the agent's OpenTelemetry traces, usage and latency metrics, and correlated logs to Logthread. Everything goes to your Logthread endpoint with your Logthread ingest key. It never raises into your agent.
Install#
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.whlThe 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. 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#
export LOGTHREAD_ENDPOINT=https://ingest.example.com # shown in Logthread onboarding
export LOGTHREAD_INGEST_KEY=<your ingest key>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.
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=Falseis passed to LiveKit'sset_tracer_provider, and enforced again on the Logthread exporter, which drops everylk.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.*) andgen_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 inextraare 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):
_CloudTelemetryin 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 notesRoomproperties may be unavailable beforectx.connect().rtc.Room.sidis an async property:livekit-rtc/livekit/rtc/room.py(python-sdks).AgentSessionevents (EventTypes) includemetrics_collected,session_usage_updated,conversation_item_added,error,close;SessionUsageUpdatedEvent.usageis anAgentSessionUsagewithmodel_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,-1when no token),TTSMetrics.ttfb,STTMetrics.duration/audio_duration,EOUMetrics.end_of_utterance_delay,metadata.model_name/model_provider:metrics/base.py. NoteMetricsCollectedEventis marked deprecated in favour ofsession_usage_updatedfor usage; per-turn latency is onChatMessage.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_sessionwithagent_turn,llm_node,llm_request,llm_request_run,session_close, ...;room_connect;session_end_upload), every span stamped withlivekit.room.sid, nolk.pii.*or GenAI content attributes; - metrics:
livekit.agent.llm.tokens(inputcached=false90,cached=true30, output 15 for 3 x 40/10/5 tokens),livekit.agent.llm.ttft_ms(provider/modelfrom the message metadata),livekit.agent.sessions(started 1, ended 1); - logs: every record of the job with
correlation_id= the room sid (RM_...), includingagent session startedand thesession report(3 turns, usage totals, close reasonuser_initiated,trace_idof 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
logthreadhandler 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
INFOif it was higher, likelogthread.install()). - Interruptions are counted from assistant messages marked
interrupted;agent_false_interruptionevents 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.