Early access is open. Free plan during early access. Start free
Logthread

Package

Logthread for LiveKit Agents (Python)

logthread-livekitv0.1.1Download logthread_livekit-0.1.1-py3-none-any.whl25.6 KBSHA-256 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.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. 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#

signalwhatwhere
TracesLiveKit's agent spans (lk.* and gen_ai.* attributes), plus livekit.room.sid, livekit.room.name, livekit.job.id, livekit.agent.name, livekit.worker.idOTLP/HTTP to {endpoint}/v1/traces, Authorization: Bearer <key>
Metricsthe instruments below, cumulative, exported every 60 s and at shutdownOTLP/HTTP to {endpoint}/v1/metrics
Logsevery 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):

nametypeattributes
livekit.agent.llm.tokenscounter ({token})provider, model, direction (input/output), cached (true/false)
livekit.agent.tts.audio_secondscounter (s)provider, model
livekit.agent.stt.audio_secondscounter (s)provider, model
livekit.agent.llm.ttft_mshistogram (ms)provider, model
livekit.agent.tts.ttfb_mshistogram (ms)provider, model
livekit.agent.stt.duration_mshistogram (ms)provider, model (non-streaming STT only; only on releases without ChatMessage.metrics, see Compatibility)
livekit.agent.eou.delay_mshistogram (ms)none
livekit.agent.e2e_latency_mshistogram (ms)none (per assistant turn)
livekit.agent.sessionscounterstate (started/ended)
livekit.agent.interruptionscounternone

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.

optionenvdefaultnotes
endpointLOGTHREAD_ENDPOINTnoneingest 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_keyLOGTHREAD_INGEST_KEYnonesent as Authorization: Bearer <key>; never logged
serviceLOGTHREAD_SERVICEagent name, else livekit-agentservice.name
environmentLOGTHREAD_ENVIRONMENTnonedeployment.environment
allow_piiFalselet Logthread receive conversation content in spans
include_transcriptFalseput user/assistant text in the session report record
traces, metrics, logsLOGTHREAD_LIVEKIT_NO_TRACES, ..._NO_METRICS, ..._NO_LOGS set to trueall onswitch a signal off
metrics_interval_s60metric export interval
shutdown_timeout_s5bound for flush/shutdown, capped at 5
attributesnoneextra attributes added to every log record and span
allow_insecureLOGTHREAD_ALLOW_INSECUREFalseallow plain http to a non-local host
debugLOGTHREAD_DEBUGFalseone stderr line per problem (missing API, failed setup)
LOGTHREAD_DISABLED=truekill 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.