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/protocol-semconv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
livekit-protocol: patch
---

Add `semconv`, the telemetry names and enum values generated from the protocol's Weaver registry, and move the protocol submodule to `003613b7`.
5 changes: 5 additions & 0 deletions .changeset/telemetry-semconv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
livekit-telemetry: minor
---

Take every telemetry name from the protocol registry (`livekit_protocol::semconv`) and send its spelling: `lk.room_sid`, `lk.participant_id`, `lk.track_sid`, `lk.pii.room_name`, `lk.pii.participant_identity`, `lk.pii.remote_participant_identity`, `lk.track_source` with the RTC FFI `TrackSource` names, and `lk.disconnect_reason` with the protobuf `DisconnectReason` names.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions livekit-protocol/generate_proto.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
# dependencies: cargo install protoc-gen-prost@0.5.0 protoc-gen-prost-serde@0.4.0


# The telemetry names (`src/lib.rs`, `mod semconv`) are not generated here: Weaver writes them in
# the protocol repository (`otel/generate.sh`) and the crate reads `protocol/otel/gen/rust/semconv.rs`
# straight from the submodule.

PROTOCOL=${PROTOCOL:-protocol/protobufs}
OUT_RUST=src

Expand Down
2 changes: 1 addition & 1 deletion livekit-protocol/protocol
Submodule protocol updated 117 files
7 changes: 7 additions & 0 deletions livekit-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ pub mod agent {
pub mod logger {
include!("logger.rs");
}

/// The telemetry names and enum values, generated by Weaver in `livekit/protocol`
/// (`otel/gen/rust/semconv.rs`) and read straight from the submodule, so the
/// submodule pointer is the version.
#[path = "../protocol/otel/gen/rust/semconv.rs"]
#[rustfmt::skip]
pub mod semconv;
2 changes: 2 additions & 0 deletions livekit-telemetry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ flate2 = "1"
# OTLP message types only (`gen-tonic-messages` = prost structs, no tonic). Same prost as
# livekit-protocol so a single prost is linked.
opentelemetry-proto = { version = "0.32", default-features = false, features = ["logs", "trace", "gen-tonic-messages"] }
# The registered telemetry names (`semconv`), generated from the protocol's Weaver registry.
livekit-protocol = { workspace = true }
livekit-net = { workspace = true, optional = true }
uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional = true }

Expand Down
40 changes: 23 additions & 17 deletions livekit-telemetry/SPEC.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# Client telemetry spec

Source of truth for event names, attributes and cadences emitted by LiveKit client SDKs.
Additive-only by convention; LiveKit-defined names carry the `lk.` prefix, everything else
follows [OpenTelemetry semantic conventions](https://github.com/open-telemetry/semantic-conventions).
Event names, attributes and cadences emitted by LiveKit client SDKs. Every fixed name and every
enum value that the protocol's telemetry registry (`otel/` in livekit/protocol) defines comes from
the generated `livekit_protocol::semconv`, and `src/wire_tests.rs` pins what the core sends. Four
values stay literals until the registry defines them: the `network.connection.type` values `vpn`,
`bluetooth` and `other`, and the `error.type` value `timed_out`. Names that the app chooses, such
as custom event names, and the checkpoint name patterns are not registry constants. The registry
records each rename as `renamed_from` on the new name. LiveKit-defined names
carry the `lk.` prefix, everything else follows
[OpenTelemetry semantic conventions](https://github.com/open-telemetry/semantic-conventions).

Every record carries a wall-clock timestamp: one without `timestamp_ns` is stamped when it is
captured (queued), never at export; an explicit `timestamp_ns` is kept. Its attributes, like its
owner and timestamp, are taken at capture, its session's (`lk.room.*`, `lk.participant.*`,
owner and timestamp, are taken at capture, its session's (`lk.room_sid`, `lk.participant_id`, …,
`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
Expand All @@ -30,8 +36,8 @@ Set once per pipeline (`TelemetryConfig.resource`):

One pipeline per process — started at SDK init, so audio pre-initialization, permission failures
and connect attempts that never reach a server are captured — and one **scope** per room (one
call). A scope is a trace id plus the attributes attached to its records (`lk.room.sid`,
`lk.participant.identity`, …); spans, RTC windows and events are filed under the scope that
call). A scope is a trace id plus the attributes attached to its records (`lk.room_sid`,
`lk.pii.participant_identity`, …); spans, RTC windows and events are filed under the scope that
produced them, and `session.id` (OTel semconv) is written on every record as an attribute. A log
record emitted inside a room's span is filed under that room's scope; anything emitted outside
a scope — device state, pre-room errors, self-telemetry — belongs to the pipeline's own process
Expand Down Expand Up @@ -240,7 +246,7 @@ cadence: one per track and direction per stats window (default 60 s, stretched b
(`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_sid: string
lk.track.kind: enum(audio | video)
lk.track.direction: enum(inbound | outbound)
lk.rtc.codec: string # mimeType, when known
Expand Down Expand Up @@ -483,10 +489,10 @@ exempt.
```yaml
event: lk.room.disconnected
area: session
severity: info (client_initiated) | warn (anything else)
severity: info (CLIENT_INITIATED) | warn (anything else)
cadence: once, when the Room leaves connected for good — never on a reconnect
attributes:
lk.disconnect.reason: enum(client_initiated | duplicate_identity | server_shutdown | participant_removed | room_deleted | state_mismatch | join_failure | migration | signal_close | room_closed | user_unavailable | user_rejected | sip_trunk_failure | connection_timeout | media_failure | agent_error | reconnect_failed | unknown) # the protocol's DisconnectReason, plus the client giving up
lk.disconnect_reason: enum(CLIENT_INITIATED | DUPLICATE_IDENTITY | SERVER_SHUTDOWN | PARTICIPANT_REMOVED | ROOM_DELETED | STATE_MISMATCH | JOIN_FAILURE | MIGRATION | SIGNAL_CLOSE | ROOM_CLOSED | USER_UNAVAILABLE | USER_REJECTED | SIP_TRUNK_FAILURE | CONNECTION_TIMEOUT | MEDIA_FAILURE | AGENT_ERROR | RECONNECT_FAILED | UNKNOWN_REASON) # the protobuf DisconnectReason names, as Agents sends them, plus RECONNECT_FAILED (the client giving up); the body keeps snake case (`disconnected: client_initiated`)
platforms: all
```

Expand Down Expand Up @@ -540,8 +546,8 @@ parent: the ambient span, when any; a pre-connect publish (a microphone publishe
connect completes) is its own span in the session's trace — `lk.connect` has ended by then
attributes:
lk.track.kind: enum(audio | video)
lk.track.source: enum(camera | microphone | screen_share | screen_share_audio | unknown)
lk.track.sid: string # on success
lk.track_source: enum(SOURCE_CAMERA | SOURCE_MICROPHONE | SOURCE_SCREENSHARE | SOURCE_SCREENSHARE_AUDIO | SOURCE_UNKNOWN) # the RTC FFI TrackSource names, as Agents sends them
lk.track_sid: string # on success
outcome: ok | error (error.type) | cancelled
```

Expand All @@ -560,10 +566,10 @@ ends: at first media (the first inbound stats reading with bytes; the core see
owner: the core (`Scope::subscribe_started / subscribed / track_ended / subscribe_failed`);
an SDK only reports the remote track's lifecycle
attributes:
lk.track.sid: string
lk.track_sid: string
lk.track.kind: enum(audio | video)
lk.track.source: enum(camera | microphone | screen_share | screen_share_audio | unknown)
lk.participant.remote_identity: string
lk.track_source: enum(SOURCE_CAMERA | SOURCE_MICROPHONE | SOURCE_SCREENSHARE | SOURCE_SCREENSHARE_AUDIO | SOURCE_UNKNOWN)
lk.pii.remote_participant_identity: string
checkpoints: subscribed, first_media
```

Expand All @@ -580,10 +586,10 @@ the policy. `Attribute { key, value }` survives only as the open bag: `emit_cust
| `telemetry_disable()` | the opt-out, in effect when it returns: capture stops; everything unsent is then deleted |
| `TelemetryConfig.sdk: TelemetryResource { sdk: Sdk, sdk_version, os_name, os_version, device_model }` | `service.name = livekit-client-<sdk>`, `service.version`, `os.*`, `device.model.identifier`, plus `telemetry.sdk.*` |
| `log(LogRecord { severity, source: LogSource, body, logger, function, file, line, timestamp_ns, span_id })` | a record with `code.function.name`, `code.file.path`, `code.line.number`, `lk.log.source`, `lk.log.logger`; the per-source floor (WebRTC at `error`, own module never) |
| `Scope::set_room(RoomIdentity { sid, name, participant_sid, participant_identity })` | `lk.room.*`, `lk.participant.*` on every record the scope captures from then on |
| `Scope::set_room(RoomIdentity { sid, name, participant_sid, participant_identity })` | `lk.room_sid`, `lk.pii.room_name`, `lk.participant_id`, `lk.pii.participant_identity` on every record the scope captures from then on |
| `Scope::start(SpanName, parent) -> Span`; `Span::detached(name)` | an OTLP span (`lk.connect` / `lk.reconnect` are `client`, the rest `internal`); `Reconnect { reason }` sets `lk.reconnect.reason` |
| `Span::step(SpanStep)` | a span event named `ws_open` … `room_connected`, `subscribed`, `first_media`, `attempt N quick|full` (which also sets `lk.reconnect.attempts` / `.mode`) |
| `Span::set_track(SpanTrack { sid, kind, source, remote_identity })` | `lk.track.sid`, `lk.track.kind`, `lk.track.source`, `lk.participant.remote_identity` |
| `Span::set_track(SpanTrack { sid, kind, source, remote_identity })` | `lk.track_sid`, `lk.track.kind`, `lk.track_source`, `lk.pii.remote_participant_identity` |
| `Span::end(outcome, error)` / `fail(error)` / `cancel()` | status, `lk.outcome`, and `error.type` when it failed; ending twice is a no-op |
| `Span::describe()` | `lk.connect: ws_open +1.49s, signal +0.03s, total 1.83s, ok` — the console line, identical on every platform |
| `Span::context()` | `TraceContext { trace_id, span_id }` for log correlation; `None` when detached |
Expand All @@ -595,7 +601,7 @@ the policy. `Attribute { key, value }` survives only as the open bag: `emit_cust
| `Scope::record_stats_report(sid, kind, direction, Vec<RtcStat>, ts)` | the same mapping for one track's report (platforms that poll per track) |
| `DisconnectReason::from(i32)`, `ReconnectReason::from(i32)` | the protocol numbers → the shared enums (an unknown number is `Unknown`) |
| `Scope::log(LogRecord)` | a record filed under the session without an ambient span (Dart has no task-local outside a zone); same floor and filters as `Telemetry::log` |
| `Scope::disconnected(DisconnectReason)` | `lk.room.disconnected` with `lk.disconnect.reason`, once — info when the client hung up, warn otherwise; pending subscribes end, the session's RTC windows ship and its state is retired; a repeated call is a no-op until a new `lk.connect` |
| `Scope::disconnected(DisconnectReason)` | `lk.room.disconnected` with `lk.disconnect_reason`, once — info when the client hung up, warn otherwise; pending subscribes end, the session's RTC windows ship and its state is retired; a repeated call is a no-op until a new `lk.connect` |
| `RtcStatsSample.layer` (rid, ssrc or stats id) | simulcast layers folded into one monotonic series per track before windowing |

Timing rule: span calls are synchronous and stamp the clock inside the core, so the only skew is
Expand Down
Loading