Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/telemetry-device-rtc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
livekit-telemetry: minor
---

Add device state and device events (`DeviceState`, `DeviceEvent`) and RTC stats samples (`RtcStatsSample`, `RtcStat`) with their on-device windowing.
82 changes: 77 additions & 5 deletions livekit-telemetry/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ owner and timestamp, are taken at capture, its session's (`lk.room.*`, `lk.parti
`session.id`) included, so a record captured before a disconnect keeps its room; only
pipeline-wide attributes are added at export. A record ships each key once: of a key it repeats,
the last value supplied wins, and its session's keys replace its own. Attribute keys the SDK
owns are `lk.*`, `otel.*`, `code.*`, `session.id` and `error.type`: an app's attributes and
custom events cannot set them, and on a span only the core writes `lk.outcome` and `error.type`
(see [Spans](#spans)).
owns are `lk.*`, `otel.*`, `code.*`, `session.id`, `error.type` and `network.connection.type`:
an app's attributes and custom events cannot set them, and on a span only the core writes
`lk.outcome` and `error.type` (see [Spans](#spans)).

## Resource attributes

Expand Down Expand Up @@ -183,8 +183,8 @@ platforms: ios, macos, android — web: Chromium only
event: lk.device.battery.changed
area: device
attributes:
hw.battery.charge: double # 0.0–1.0 (OTel hardware semconv)
hw.battery.state: enum(charging | discharging) # OTel hardware semconv
lk.device.battery.level: int # percent, 0–100
lk.device.battery.charging: bool
cadence: on charging change and when the level crosses 20 % or 10 % unplugged — never per
percent; silent where the level is unknown (desktops, tvOS)
platforms: ios, android — optional elsewhere
Expand Down Expand Up @@ -220,6 +220,78 @@ cadence: on failure
platforms: all — ios: authorization status, capture interruptions; android: permission checks, camera callbacks; web: DOMException names
```

```yaml
event: lk.rtc.stats.sample
area: rtc
severity: info
cadence: one per track and direction per stats window (default 60 s, stretched by the cadence
factor); closed early on background, when the track leaves (`track_ended`), at
disconnect and at shutdown. Produced by the core from raw readings — one getStats()
report per peer connection (`record_peer_stats`), polled when the core says
(`stats_poll_interval_ms`). Keyed by session and track: two rooms receiving the same
track keep two windows.
attributes:
lk.track.sid: string
lk.track.kind: enum(audio | video)
lk.track.direction: enum(inbound | outbound)
lk.rtc.codec: string # mimeType, when known
lk.rtc.window_ms: int # actual window length
lk.rtc.samples: int # readings (polls) in the window; one poll's layers are one reading
# cumulative counters — the last reading's value, monotonic (W3C webrtc-stats model). A
# layered outbound track's (one whose readings name their RTP streams) bytes and packets are its
# running total: each poll adds every stream's growth since the previous poll, so a stream's
# first poll only sets its starting point (a new or returning stream's first poll is not
# counted, never a double count), and the order of a report's streams changes nothing
lk.rtc.bytes: int
lk.rtc.packets: int
lk.rtc.packets_lost: int # inbound
lk.rtc.freeze_count: int # inbound video
lk.rtc.freezes_duration_ms: int # inbound video
lk.rtc.concealed_samples: int # inbound audio
lk.rtc.concealment_events: int # inbound audio
lk.rtc.jitter_buffer_delay_ms: int # inbound
lk.rtc.jitter_buffer_emitted_count: int # inbound
lk.rtc.quality_limitation.bandwidth_ms: int # outbound video
lk.rtc.quality_limitation.cpu_ms: int # outbound video
lk.rtc.quality_limitation.other_ms: int # outbound video
lk.rtc.pause_count: int # inbound video
lk.rtc.pauses_duration_ms: int # inbound video
lk.rtc.silent_concealed_samples: int # inbound audio
lk.rtc.interruption_count: int # inbound audio
lk.rtc.interruptions_duration_ms: int # inbound audio
# gauges — min / max / avg over the window's readings, one reading per poll; a poll's value is
# the largest any of its streams reports, so a value one stream lacks comes from the others
lk.rtc.jitter_ms.{min,max,avg}: double
lk.rtc.rtt_ms.{min,max,avg}: double # remote-inbound RTT for outbound, the selected candidate pair for inbound
lk.rtc.fps.{min,max,avg}: double # video
lk.rtc.audio_level.{min,max,avg}: double # audio, 0.0–1.0 linear
platforms: all
```

## Cadence policy

`flush_interval × factor` and `stats_window × factor`, capped at 4× (60 s → 4 min at the default
cadence: one export and one RTC window per minute, conservative for the collector's shared
per-project quota). Factors multiply; a change applies at the next tick, and a *shorter* period
applies at once (pressure relieved → no waiting out a stretched period).

| Condition | factor | source |
|---|---|---|
| thermal `serious` | 2 | host, `DeviceState.thermal` |
| thermal `critical` | 4 | host |
| memory pressure `warning` | 2 | host, `DeviceState.memory` |
| memory pressure `critical` | 4 | host |
| low-power mode | 2 | host |
| background | 2 | host |
| battery ≤ 20 % and unplugged | 2 | host, `DeviceState.battery_*` |
| constrained network (Low Data Mode / Data Saver) | 2 | host, `DeviceState.network_constrained` |
| encoder CPU-limited: an outbound track's `qualityLimitationDurations.cpu` grew within the last 60 s | 2 | core, from `record_stats` |

CPU is never measured by the pipeline itself (measuring CPU costs CPU): thermal state is the OS's
judgement and `qualityLimitationReason` is WebRTC's. The core also paces the platform's
`getStats()` polling (`Scope::stats_poll_interval_ms`): every second while a subscribe waits for
its first media, else twice per (stretched) window — 30 s by default.

## Log records

A `TelemetryEvent` with an empty `name` is a plain log record (OTLP log without `event_name`):
Expand Down
Loading
Loading