Skip to content

feat(telemetry): take every telemetry name from the protocol registry - #1566

Open
pblazej wants to merge 3 commits into
blaze/telemetryfrom
blaze/telemetry-stack/9-semconv
Open

pblazej wants to merge 3 commits into
blaze/telemetryfrom
blaze/telemetry-stack/9-semconv

Conversation

@pblazej

@pblazej pblazej commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The Rust telemetry core now takes every name it sends from the semantic conventions registry in livekit/protocol#1866. Semantic conventions are the agreed names, types and units of telemetry data. Every event name, span name, checkpoint name and attribute key comes from a generated constant, and so does every enum value that the registry lists. The core sends no metrics, so no metric name is involved.

The core also sends the spelling that the registry chose for eight keys. Agents already sends this spelling. Two of the keys also change their values: the track source and the disconnect reason now use the enum names that Agents sends.

A pin test runs one of every record, span and checkpoint through the pipeline and compares what goes out with a frozen list. A registry change that renames a name or a value fails that test.

This PR is stacked on #1396 (stage 8 of the telemetry stack).

Protocol change

The livekit-protocol/protocol submodule moves from 3fbe5098 to 003613b7, the head of livekit/protocol#1866. That PR is a draft and must merge first. After it merges, move the submodule to the merged commit and regenerate as described below.

Between the two commits the protobufs change only by field options (logger.sensitivity). generate_proto.sh produces the same livekit.rs and livekit.serde.rs as before.

generate_proto.sh now also copies protocol/otel/gen/rust/semconv.rs to livekit-protocol/src/semconv.rs, and the crate exposes it as livekit_protocol::semconv. livekit-telemetry depends on livekit-protocol for it.

Why a dependency and not a vendored copy
Question Dependency on livekit-protocol Vendored copy in livekit-telemetry
Copies of the generated file one, written by the existing generation path and its CI workflow two, plus a test that compares them
Shipped library size no cost from the dependency: livekit-ffi links livekit-protocol in every build mode already the same
Release order automatic: cargo release publish --workspace publishes in dependency order, and knope bumps the workspace dependency none
Clean build of livekit-telemetry alone 11.0 s to 27.2 s wall time (adds livekit-protocol, pbjson, serde, chrono) no change
Clean build of livekit-ffi no change: it builds livekit-protocol already no change

The extra compile time hits only a project that builds livekit-telemetry on its own. The only shipped consumer, livekit-ffi, already pays it. So the dependency is not clearly worse, and one generated copy is easier to keep correct.

Renames

Each old key and old value is recorded in the registry as renamed_from on the new one.

Keys
Old key New key Where the core sends it
lk.room.sid lk.room_sid every record and span of a room session
lk.room.name lk.pii.room_name every record and span of a room session
lk.participant.sid lk.participant_id every record and span of a room session
lk.participant.identity lk.pii.participant_identity every record and span of a room session
lk.participant.remote_identity lk.pii.remote_participant_identity lk.subscribe
lk.track.sid lk.track_sid lk.publish, lk.subscribe, lk.rtc.stats.sample
lk.track.source lk.track_source lk.publish, lk.subscribe
lk.disconnect.reason lk.disconnect_reason lk.room.disconnected
Values of lk.track_source (the RTC FFI TrackSource names)
Old value New value
camera SOURCE_CAMERA
microphone SOURCE_MICROPHONE
screen_share SOURCE_SCREENSHARE
screen_share_audio SOURCE_SCREENSHARE_AUDIO
unknown SOURCE_UNKNOWN
Values of lk.disconnect_reason (the protobuf DisconnectReason names)
Old value New value
client_initiated … agent_error (16 values) the same name in upper case, for example CLIENT_INITIATED
unknown UNKNOWN_REASON
reconnect_failed RECONNECT_FAILED, the one value that only clients send

The severity rule does not change. If the client hung up, the severity is info. For every other reason it is warn. The display body keeps snake case, for example disconnected: client_initiated.

Names the registry does not define, which stay literals
Literal Why
unknown thermal state never sent: an unknown reading makes no event
vpn, bluetooth, other for network.connection.type not OTel members, but the registry notes that clients send them
timed_out for error.type error.type is open, and the registry gives it as an example only
livekit-client and livekit-client-<sdk> for service.name the registry gives examples only
the custom. event prefix the registry defines the custom namespace, not the names in it
the attempt <n> <mode> checkpoint the registry defines it as a pattern, and the mode is a generated constant
the lk.device.audio_route.outputs values the registry types the key as a string with no member list
the reserved prefixes lk., otel., code. the registry has them as an annotation, not as constants
severity texts (INFO, WARN, …) OTLP fields, not registry names

livekit-ffi has no telemetry name literal in its code, so nothing there changes. Its doc comments name only lk.outcome, lk.connect, lk.subscribe and lk.room.disconnected, which keep their names.

How to regenerate

To change a name, change it in livekit/protocol first, as otel/README.md there describes. Then do these steps:

  1. Move the livekit-protocol/protocol submodule to the new protocol commit.
  2. In livekit-protocol, run ./generate_proto.sh. It needs protoc, protoc-gen-prost@0.5.0 and protoc-gen-prost-serde@0.4.0. It regenerates the protobuf code and copies semconv.rs.
  3. Run cargo test -p livekit-telemetry. If a name that the core sends changed, wire_tests::every_name_and_value_on_the_wire_is_pinned fails. Update the pin and SPEC.md in the same PR.

Do not edit src/semconv.rs by hand. The next run of generate_proto.sh overwrites it, and the Generate protocol workflow runs that script on pull requests to main that touch livekit-protocol/.

Testing

livekit-telemetry has a new test, wire_tests::every_name_and_value_on_the_wire_is_pinned. It drives one pipeline through every device state value and device event, log records from each source, and a self-telemetry report with every counter. A room then gets every typed span, checkpoint, reconnect reason, track source and outcome, and four RTC windows with every counter and gauge. All 18 disconnect reasons follow. Then it decodes the OTLP that the pipeline exports and compares one line per signal with a frozen list. The lines hold the record and span keys, severities, checkpoints, resource and session keys, and the values of 20 enum-valued keys. To prove that it catches a registry change, I changed one generated value (SOURCE_SCREENSHARE_AUDIO) by hand. The test failed. The other tests now use the new names.

Gates at f562db58

Test counts are collected test cases: unit tests, then doc tests.

Gate Result
cargo fmt --check pass
cargo clippy -p livekit-telemetry --no-deps --all-targets -- -D warnings, also with --all-features and as a cargo hack feature powerset at depth 2 pass
cargo test -p livekit-telemetry 276 + 2 pass
RUSTDOCFLAGS="-D warnings" cargo doc -p livekit-telemetry --no-deps pass
cargo test -p livekit-ffi --no-default-features --features core-modules 34 pass
livekit-ffi core-modules clippy no new warning
cargo clippy -p telemetry_ping --no-deps --all-targets -- -D warnings pass
cargo make --profile release android-package in livekit-ffi Kotlin bindings generated and compiled, assembleRelease succeeds. The size check fails as it does on the base branch (see Follow-ups)
./gradlew compileReleaseKotlin --rerun-tasks in livekit-ffi/support/android pass
cargo make dart-package, then dart test in livekit-ffi/packages/dart 7 pass, 1 skip. The skipped live collector test passes against a local collector

--no-deps matters now. Without it, cargo clippy -p livekit-telemetry also lints its new path dependency livekit-protocol and reports 687 errors. All of them are in the generated protobuf files (livekit.serde.rs, livekit.agent.serde.rs, livekit.rs, logger.serde.rs), which this PR does not change. No CI job runs clippy.

Live-check and size
Check Result
otel/live-check.sh (Weaver 0.28.0) on the pin test's capture: 68 log records, 17 spans, 25 resources exit 0, no violation, no improvement. 4 information notes for values that the registry does not define: network.connection.type = vpn, bluetooth, other, and error.type = timed_out
arm64-v8a liblivekit_ffi.so, release 2,298,256 bytes before, 2,298,424 bytes after: +168 bytes

Weaver is not part of rust-sdks CI.

Follow-ups

  • Merge feat(otel): add the telemetry semantic-conventions registry (Weaver) protocol#1866, then move the submodule to the merged commit and regenerate.
  • Change the platform branches to the new keys and values. The Swift, Android and Flutter SDKs hard-code the old names only in their telemetry tests. The JS port emits its own names, so its sources change too.
  • Allow clippy lints on the generated protobuf code in livekit-protocol, so that cargo clippy -p livekit-telemetry passes without --no-deps.
  • Decide on the Android size limit. The library was over ANDROID_SIZE_LIMIT_BYTES before this PR, and this PR adds 168 bytes.
  • Register the four values that the live-check reports as unknown, in livekit/protocol: the network.connection.type values vpn, bluetooth and other, and the error.type value timed_out. error.type is an open value in OpenTelemetry, so the registry has to decide whether to list timed_out as a value or leave the key open. Until then, the core sends these four as literals.
Platform sites that name a renamed key or value
Platform Branch Sites
Swift blaze/telemetry at 96be2956 Tests/LiveKitCoreTests/Telemetry/TelemetryTests.swift:218, 270
Android blaze/telemetry at 3da1b731 livekit-android-test/.../telemetry/TelemetryMockE2ETest.kt:191-193, 196, 205, 215, 216, 223
Flutter blaze/telemetry at 8755fbe test/telemetry/telemetry_e2e_test.dart:245, 249, 279, 280, 284, 293
JS sources blaze/telemetry at c00ade11 src/telemetry/scope.ts:104-105, 276-279, 501-504, 651, 670-671, src/room/participant/LocalParticipant.ts:657, 662, 663, src/room/CoreRoom.ts:1879
JS tests the same src/telemetry/telemetry.test.ts:704, 720, 779, 972, 1349, 1566, src/telemetry/telemetry.browser.test.ts:81, 184-185, 189, 194, 216-217, 223, 231

In JS, CoreRoom.ts:1879 passes one value as the disconnect reason and as the error.type of the connect and reconnect spans. Only the disconnect reason changes to RECONNECT_FAILED.

@pblazej pblazej added the internal to tag changes that don't require changelog documentation label Oct 23, 2026
@pblazej
pblazej added this pull request to stack #1486 October 23, 2026 06:15
@pblazej
pblazej marked this pull request as ready for review October 23, 2026 06:15
@pblazej
pblazej requested a review from ladvoc as a code owner October 23, 2026 06:15

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Devin Review: 1 flag

Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

@pblazej
pblazej marked this pull request as draft October 23, 2026 06:27
Move the protocol submodule to 003613b7, which adds otel/: the Weaver
registry of telemetry names and the Rust constants generated from it.
generate_proto.sh now copies otel/gen/rust/semconv.rs into src/, and the
crate exposes it as `livekit_protocol::semconv`.

The protobuf changes between the two submodule commits add field options
only; generate_proto.sh produces the same livekit.rs and livekit.serde.rs
as before. The generated file is left out of rustfmt and of the license
header check, like the other generated sources.
Every event, span, checkpoint and attribute name the core sends, and every
enum value with a registered member set, now comes from
`livekit_protocol::semconv` instead of a string literal. Enum values are
explicit matches on the generated constants, so a renamed variant no
longer changes what ships.

The core now sends the registry's spelling for the keys the registry
renamed, the spelling Agents already sends:

- lk.room.sid -> lk.room_sid
- lk.participant.sid -> lk.participant_id
- lk.track.sid -> lk.track_sid
- lk.room.name -> lk.pii.room_name
- lk.participant.identity -> lk.pii.participant_identity
- lk.participant.remote_identity -> lk.pii.remote_participant_identity
- lk.track.source -> lk.track_source, with the RTC FFI TrackSource
  names (SOURCE_CAMERA, SOURCE_MICROPHONE, SOURCE_SCREENSHARE,
  SOURCE_SCREENSHARE_AUDIO, SOURCE_UNKNOWN)
- lk.disconnect.reason -> lk.disconnect_reason, with the protobuf
  DisconnectReason names (CLIENT_INITIATED, ..., UNKNOWN_REASON) and
  RECONNECT_FAILED

The lk.room.disconnected body keeps the snake_case display line.
SPEC.md and the tests follow the new names.

Names the registry does not define stay literals: the unsent `unknown`
thermal state, the `vpn`, `bluetooth` and `other` connection types, the
`timed_out` error type, the `livekit-client` service name, the `custom.`
event prefix, the `attempt <n> <mode>` checkpoint pattern and the audio
route outputs.
One test runs a pipeline through one of every record, span and checkpoint
the core produces, and through every variant of each enum it sends,
decodes the OTLP it exports, and compares one line per signal with a
frozen list: the keys of each record and span, its severities and
checkpoints, the resource keys, the session keys, and the values of every
enum-valued key. A registry change that renames a name or a value now
fails this test.
@pblazej
pblazej force-pushed the blaze/telemetry-stack/9-semconv branch from 692941c to f562db5 Compare October 23, 2026 09:56
@pblazej
pblazej marked this pull request as ready for review October 23, 2026 11:47

@lukasIO lukasIO left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!
I haven't done any tests myself but only reviewed the diff by eye.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the current setup generated output lives both in the protocol repo and here. I know it's only a copy, but it doesn't feel great.
Do you think creating an own crate for it in protocol would be over kill?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really good point, any other benefits that would justify the infra?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal to tag changes that don't require changelog documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants