Conversation
Integrates livekit-telemetry (livekit-uniffi, local debug package for now) as a connection-scoped subsystem, with the bare minimum end to end: error reporting and RTC stats reporting. - RoomOptions.telemetry: TelemetryOptions (endpoint, headers, storage directory defaulting to Caches/livekit-telemetry, flush interval, stats window). Prototype surface: the design routes enablement through the token's observability grant. - RoomTelemetry on ConnectionDependencies: created when connect() starts, torn down with the connection. Registers on every published/subscribed track (MetricsManager's pattern), enables reportStatistics, maps TrackStatistics to RtcStatsSample per RTP stream; the core windows them into lk.rtc.stats.sample. Sets lk.room.sid/name and lk.participant.sid/identity as session attributes once connected. - Logger: an internal LogRelay next to Loggable.log fans out .warning and .error calls as structured LogRecords to in-process sinks; the app-facing Logger protocol and sharedLogger are untouched, debug/info never take this path. RoomTelemetry is a sink and emits them as OTLP log records with code.function / code.file.path / code.line.number / lk.log.type. - Device state: ProcessInfo thermal + power-state notifications and AppStateListener feed Telemetry.setDeviceState (events + cadence). - URLSessionTelemetryTransport: the host's dumb bytes mover, mapping HTTP outcomes onto ExportError. - E2E test (needs livekit-server --dev and grafana/otel-lgtm): a room publishes synthetic video, logs an error, and the test asserts the outbound video window, the session attributes and the error record in Loki. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The core constructor now takes an optional transport (falling back to the HTTP client registered with livekit-net) and can fail when neither exists. RoomTelemetry.init is failable and ConnectionDependencies skips telemetry rather than failing the connection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The locally built LiveKitUniFFI now carries the data-streams-v2 types, which introduce a second EncryptionType visible to the test support target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Span is no longer a stopwatch: kind, outcome (ok | error | cancelled), typed attributes, an error type, and a SpanContext (trace id + span id) assigned by the telemetry sink. record() keeps its shape - the nine connect checkpoints are span events now. Fan-out instead of replacing LoggingTracer: RoomTracer asks the app-facing Tracing (LiveKitSDK.setTracing, slot untouched) for the Span, then the Room's sinks observe it - RoomTelemetry (beginSpan/addSpanEvent/endSpan on the Rust core, under the session trace) and, in debug builds, an os_signpost sink (Instruments' Points of Interest, as opentelemetry-swift does). Room gets a `tracer`; ConnectionDependencies owns it. Spans wired: lk.connect (kind client, lk.connect.attempt, ends ok / error / cancelled from the connect catch) and lk.reconnect (lk.reconnect.reason, mode and attempts as attributes, one "attempt <n> <mode>" checkpoint per attempt, cancelled when disconnect() or a newer cycle wins). Warn/error records emitted during connect point at the connect span. E2E: the telemetry test now also asserts a trace rooted at lk.connect in Tempo (Grafana datasource proxy). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Span.current (@TaskLocal): bound around the connect attempt and the reconnect cycle. RoomTracer.beginSpan defaults its parent to it, so spans nest without handles being passed around; warn/error records emitted in a task with an ambient span point at it (the connectSpan heuristic is gone). - lk.publish: LocalParticipant._publish wraps the publish sequence in a span (lk.track.kind/source, sid on success; ok / error / cancelled), nested under the connect span for the pre-connect microphone. - lk.subscribe: from the intent to subscribe (remote publish under autoSubscribe, or the manual subscribe) to the first media - the first stats sample with bytes received, the natural end of "time to media"; "subscribed" checkpoint on the way; cancelled on unsubscribe/unpublish, error on subscription failure or after 30 s without media. - E2E: the subscriber room carries telemetry too; the session trace of each room must contain lk.connect and, respectively, lk.publish or lk.subscribe (one trace per session holds several root spans, so span names are checked, not Tempo's single root name). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RoomTelemetry now observes memory pressure (DispatchSource), the network path (NWPathMonitor: type, expensive, constrained), battery level and charging (iOS/visionOS) and the audio session (route changes and interruptions as events), alongside the existing thermal, low-power and app-state hooks. Everything is notification-driven; nothing polls or samples CPU. The core turns these into cadence stretching and upload holds. The transport moves to a dedicated ephemeral URLSession with networkServiceType = .background: the local stack queues telemetry behind best-effort media and signaling (fq_codel BK class, Wi-Fi AC_BK) and its TCP flows switch to LEDBAT while foreground traffic is active. One connection per host, 10 s request timeout. The e2e test additionally asserts the initial thermal, memory-pressure and network-path values reach the collector. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Apps can record their own events in the session trace next to the SDK's:
`room.emitTelemetryEvent("checkout.started", attributes: ["acme.step": .int(3)])`.
The core namespaces them under `custom.` and applies the same flood guard;
a no-op when telemetry is off. The e2e test emits one and asserts it reaches
the collector with its attribute.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ruments Telemetry now starts with the SDK, not with connect(): `LiveKitSDK.setTelemetry(options)` configures a process-wide `TelemetryHub` (core pipeline, transport, destination, log relay sink), so pre-connect work - track creation, permission failures, connect attempts that never reach a server - is captured. `RoomOptions.telemetry` is gone. Each Room gets a session (own trace id) from the hub in `init`, exposed as `room.telemetryTraceId`; `emitTelemetryEvent` works before connect. The destination is derived from the first connect (`https://<host>/observability/logs/otlp/v0`, `Authorization: Bearer <token>`) unless the options name an endpoint; until then everything is buffered on device. The instruments are three independent components, one per area of the design doc: `DeviceTelemetry` (process-level: thermal, low power, memory, network, battery, app state, audio session), `RoomTelemetry` (session identity, spans, subscribe spans, app-defined events) and `RTCTelemetry` (track statistics -> session; one signal out, `onFirstMedia`, which the Room wires to the subscribe spans). `ConnectionDependencies` only settles open spans and flushes when a connection ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shape every client SDK converges on (Embrace CaptureService, Sentry integrations, OpenTelemetry Instrumentation / AndroidInstrumentation): an instrument is a platform-side object with an explicit lifecycle, owned and listed by a core. DeviceTelemetry, RoomTelemetry and RTCTelemetry conform; the hub starts and stops the process-level instruments, the Room its own (started in init, stopped in deinit, which also settles the session's open spans). Kept on the Swift side rather than as a UniFFI foreign trait: the design doc's FFI is push-only, and uniffi-dart cannot call foreign traits from Rust threads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r spans Review round on the Swift surface: - `TelemetryHub` becomes `Telemetry`, a global actor. Options are set statically before use (`LiveKitSDK.setTelemetry` / `disableTelemetry`, like the logger); the core pipeline is built once on first use and latched. Swift-side instrument state is actor-isolated; the thread-safe core handles stay nonisolated so Room.init and span hooks need no hop. - `LoggingTelemetry` is its own instrument. The log relay runs before the app's `Logger`, so `setLogger` cannot displace telemetry, and carries every level; the instrument decides what leaves the device. - Spans: `SpanSink`, the per-span sink arrays and `SignpostSpanSink` are gone. A `Span` has `onEnd` (public, as before) and an internal `onRecord`; `RoomTracer` hands new spans to `RoomTelemetry.attach`, which gives them identity and wires both hooks. - `lk.subscribe` moves to `RTCTelemetry`: its end is an RTC fact (first inbound bytes), so no first-media signal between instruments. - `Telemetry.setAttribute` sets pipeline-wide attributes (every record of every session). - The e2e test reads OTLP/JSON written by `otelcol-contrib` (file exporter, `Tests/LiveKitCoreTests/Telemetry/otelcol.yaml`) instead of querying Loki and Tempo; CI downloads and runs the collector next to the dev server. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…racer One tracer. As the default `Tracing` it creates spans and logs them at debug when they end, as `LoggingTracer` did (kept as a deprecated alias). Bound to a Room's telemetry session it also gives each span an identity in the session's trace at creation; the `Span` then reports checkpoints and its end to the session itself, so the post-hoc `attach` and its closures are gone. An app-injected `Tracing` still creates the span objects. `kind` is a required argument on the Room-scoped `beginSpan`, so a one-argument call cannot resolve to the protocol overload and skip the binding - which is how `lk.publish` briefly went missing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RoomTelemetry had become an actor-isolated class with no isolated state: every member forwarded to the thread-safe session handle and had to be `nonisolated`. The Room now holds `telemetrySession` directly (identity attributes on connect, `emitTelemetryEvent`, `telemetryTraceId`), the tracer takes the session, and the connection's teardown flushes. Room-scoped instruments are `RTCTelemetry` and `TelemetryTracer`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`Telemetry.shared.configure(options)` may be called at any time: before the pipeline exists it shapes it, afterwards the destination and headers apply at once, `nil` stops it after a bounded flush - no latching, no replacing. The pipeline bootstraps on the first `register(room)` from `Room.init`, which also starts the process-level instruments and creates the Room's session and `RTCTelemetry` in a registry keyed by the Room. `Room` holds no telemetry state any more. Its whole internal surface is `register` / `unregister` / `roomDidConnect` / `connecting` / `traceId(for:)` / `emit(_:from:)` / `tracer(for:)`; `TelemetrySession` never leaves the telemetry files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Options, core, attributes, rooms and instruments are plain actor state; the StateSync is gone. Entry points are async: fire-and-forget where nothing comes back (register in init, unregister in deinit, emit, configure), awaited where a value does (tracer(for:) in connect(), telemetryTraceId as a get-async property). Sessions are created lazily on first use, so call ordering cannot matter, and state is updated before every await so re-entrancy cannot duplicate a session or the pipeline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`Loggable.log` hands warn/error records to `Telemetry.log` directly: timestamp and ambient span are captured where the log happened, delivery hops to the actor. `LogRelay`, `LogSink`, `LogRecord` and `LoggingTelemetry` are gone. Rust-core logs have a single consumer, so `FFILogForwarder` drains the stream once and fans out - to the console handler `OSLogger` registers and, at warn/error, to telemetry - started by whichever needs it first at the lowest level asked for. WebRTC gets its own callback-logger sink at warning level. Records carry `lk.log.source = sdk | ffi | webrtc`. `TelemetryOptions.instruments` (`.room`, `.rtc`, `.logs`, `.device`; default `.all`) decides what the pipeline starts; app-defined events and session identity are always on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Logs from outside the SDK's own logger go through a single generic `LogForwarder`: `LogForwarders.ffi` drains the core's single-consumer log stream, `LogForwarders.rtc` owns the one WebRTC callback logger. Each is started by whoever needs it first (`OSLogger`, or telemetry when the app uses its own logger) at the lowest level asked for, and fans out to the console handlers that registered and, at warn/error, to `Telemetry.log`. `Telemetry` no longer owns a WebRTC sink of its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every log line the SDK produces or captures - its own `Loggable.log` calls, the Rust core's stream, WebRTC's callback logger - enters `LogHub` once as a `LogRecord` value captured where it happened (level, origin, code location, timestamp, ambient span) and fans out two ways: to the app's `Logger`, which filters by its own level and receives the span as `lk.trace_id` / `lk.span_id` metadata, and, at warn/error, to telemetry. Neither side learns the other's level (swift-log's MultiplexLogHandler + MetadataProvider, swift-otel's OTelLogRecord, Datadog's console vs remote threshold). External sources (`LogSources.ffi`, `.rtc`) are subscribed once per process at the lowest level any consumer asked for; `OSLogger`'s `rtc:`/`ffi:` flags request them for the console, telemetry requests warn/error. A custom `Logger` now receives WebRTC and Rust-core records too, typed `WebRTCLog` / `FFILog`. The core's own warnings are kept out of telemetry to avoid a rejected-batch feedback loop. No public logger API changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The iOS-only battery reader carried both attributes, which the compiler rejects; macOS builds never compiled the block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ixes From the first run on an iPhone: - `TelemetryOptions.logLevel` (default `.warning`) is the threshold a log record needs to leave the device; WebRTC's own logs go from `.error` regardless - 60 of 77 records in one call were WebRTC's internal warnings (duplicate codecs, disabled field trials, RTCP timeouts). - Device state no longer seeds the network path from `currentPath`, which reads `unavailable` on iOS before the monitor starts; the monitor's first update carries the first device state. - Audio route and interruption events carry a display body. - The e2e test enforces the rule: every record has a body, log records are warn/error only, events carry `otel.event.name`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An error logged inside a span bound to room 0 must carry that span's id and land in that Room's trace; a record logged outside any span lands in the process session. Caught a core bug: records arriving after their span ended were misfiled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simulcast reports one outbound-rtp stream per layer under the same track sid, while the core keeps one window per track. Interleaved layer samples froze the byte counter on a suspended top layer (0 kbps for 7 minutes on a real device) and produced first-layer-to-last-layer deltas as bitrate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e prefix Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…core Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… come from the core Span is a handle over the core's span (task-local current and onEnd stay here); SDK checkpoints are typed steps; the Tracing protocol keeps its string API for app-defined spans. The simulcast fold moves back to the core via RtcStatsSample.layer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TelemetryTracer is gone. Span.begin(name, in: session) creates an SDK span in the core, or a no-op handle when telemetry is off; the Tracing protocol stays as an optional app hook with no default instance. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
App spans made through the default tracer join the process trace; the core handle sits in a nonisolated box so a span is stamped synchronously where it starts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Swift keeps a typealias and an extension for the task-local current span, the Error-to-error.type bridge and the track helper; SpanOutcome and SpanContext are the core's. Tracing/setTracing stay as a deprecated no-op for source compatibility; Room.connectSpan is internal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Span.begin returns nil when the Room has no scope; call sites chain optionally, so tracing costs nothing with telemetry off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The core owns the process pipeline; Telemetry keeps only the platform instruments. A Room takes its scope at init and owns its RTC instrument; logs, server, attributes and device signals go straight to the core. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re's No global actor and no singleton: Telemetry.configure hands the platform instruments (device signals, log capture) to the core, which starts and stops them with the pipeline. DeviceTelemetry and RTCTelemetry are their own actors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e the public types LiveKit imports LiveKitUniFFI publicly for telemetry: no Swift options class, option set, attribute enum or lowering. Swift adds only the resource and the cache directory to the config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s internal TelemetryOptions mirrors the core's TelemetryConfig (URLs, seconds, an option set) and lowers to it; SpanAttribute lowers to AttributeValue. No public import of LiveKitUniFFI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`URLSessionTelemetryTransport` returns the collector's status, headers and body untouched; the Rust core now decides retry / drop / go-silent (Retry-After, RetryInfo body, "disabled"), so the policy is identical on every platform. The session is injectable for tests. `TelemetryHarness` runs a whole session on macOS with synthetic media — connect, publish audio and video, subscriber first media, quick and full reconnect, disconnect — and checks the spans, steps, stats windows and the pipeline's own health in the collector output. `otelcol-lgtm.yaml` fans the same collector out to a local Grafana LGTM stack; `make telemetry-harness` runs it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Swift maps its own types onto the enums the Rust core now shares with every SDK: `StartReconnectReason → ReconnectReason`, `Track.Source → TrackSource`, `AVAudioSession.Port → AudioOutput`, and `LiveKitError.type → DisconnectReason` for the new `lk.room.disconnected` record, emitted from `cleanUp` for a session that existed (not on the clean-up `connect()` does first, not on a full reconnect). Stats windows carry the extra inbound pause / silent-concealment counters and the outbound `other` quality-limitation duration. The harness lives inside the serialized `TelemetryTests` suite (the pipeline is process-wide) and scopes its assertions to its own two traces; it also checks that both Rooms reported `client_initiated` before the disconnect flush. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`RTCTelemetry` keeps no RTC state any more: it reports the remote tracks' lifecycle (`subscribeStarted` / `subscribed` / `subscribeCancelled` / `subscribeFailed`) and hands each observed track the Room's scope; the track's own stats timer forwards every raw `getStats()` report (entries with their standard members, nested maps flattened) and the core resolves codec, RTT, layers and units. The Swift field mapping and the 30 s subscribe timeout are gone; every SDK now gets them from the same place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…span `Loggable.log` passes the emitter's telemetry scope along (`TelemetryScoped`: the Room, and participants through their Room), and `Telemetry.log` files a record through `scope.log` when no ambient span names the session. Before, a Room's warning outside an operation went to the process scope; now it joins the Room's trace, the same rule the Dart and Kotlin SDKs apply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 15, 2026
LIVEKIT_TELEMETRY_ENDPOINT and LIVEKIT_TELEMETRY_TOKEN send the harness session to LiveKit Cloud instead of the local collector, which is how the upload policy was exercised against the real ingest — a 429 from the observations quota holds uploads for a minute while the cache keeps collecting, then the backlog ships. A cloud run has no collector file to assert against, so it reports the pipeline's own diagnostics for as long as a hold can last. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Telemetry
Draft: integrates the shared Rust telemetry core (
livekit-telemetry, livekit/rust-sdks#1396) into the Swift SDK, per the "Client Telemetry" design doc. The core owns policy and vocabulary; the SDK contributes a transport, instruments and the wiring. The Android (livekit/client-sdk-android) and Flutter (livekit/client-sdk-flutter) drafts follow the same shape.Design
Telemetryis the one entry point:LiveKitSDK.setTelemetry(options)/Telemetry.configure(options)configure the process-wide pipeline (it lives in the Rust core) at any time; everyRoomtakes a scope with its own trace id at init (room.telemetryTraceId), so pre-connect failures are captured and nothing leaks a session handle.TelemetryOptionsmirrors the core's config; nothing from UniFFI is public.TelemetryOptions.instruments(.room,.rtc,.logs,.device; all by default):DeviceTelemetry(thermal, low power, memory, network, battery, app state, audio route and interruptions), warn/error records from the SDK, the Rust core and WebRTC throughLogHub, and per Room thelk.connect/lk.reconnect/lk.publishspans with their checkpoints;RTCTelemetryreports the remote tracks' lifecycle and the core runs thelk.subscribespan itself; each track's stats timer hands the rawgetStats()report to the core, which maps and windows it.Spanis the core's span with Swift helpers and a task-localSpan.current;Tracing/setTracingremain as a deprecated no-op.Telemetry.setAttributeattaches pipeline-wide attributes; the transport is a dedicatedURLSessionwithnetworkServiceType = .backgroundthat returns the collector's raw answer (the core decides retry / drop / go-silent); the destination is derived from the connect URL unlessTelemetryOptions.endpointis set.Testing
TelemetryTestsruns end to end againstlivekit-server --devand anotelcol-contribwriting OTLP/JSON to disk (Tests/LiveKitCoreTests/Telemetry/otelcol.yaml); CI downloads and runs the collector next to the dev server. The headless macOS harness in the same suite drives connect, publish, first media, quick and full reconnect and disconnect on synthetic media;otelcol-lgtm.yamlfans the same stream into a local Grafana LGTM (make telemetry-harness).Package.swiftpoints at a locally built UniFFI package for now.