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
8 changes: 8 additions & 0 deletions Cargo.lock

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

17 changes: 17 additions & 0 deletions livekit-telemetry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,34 @@ edition.workspace = true
repository.workspace = true

[dependencies]
tokio = { workspace = true, default-features = false, features = ["macros", "sync", "time"] }
log = { workspace = true }
thiserror = { workspace = true }
async-trait = "0.1"
prost = { workspace = true }
# `google.protobuf.Any`/`Duration` for the `google.rpc.Status` error body (RetryInfo).
prost-types = { workspace = true }
rand = { workspace = true }
# Unverified JWT claims (observability grant, expiry); both already linked by livekit-api.
base64 = "0.22"
serde_json = { workspace = true }
# Server URLs are parsed, never string-matched, before a token may follow them.
url = "2.3"
# 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"] }
livekit-net = { workspace = true, optional = true }
uniffi = { workspace = true, features = ["scaffolding-ffi-buffer-fns"], optional = true }

[features]
# Default HTTP transport over the pluggable `livekit-net` client (native backend, or one
# the host registered with `livekit_net::set_http_client`).
net = ["dep:livekit-net"]

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.

🔴 Default network transport cannot start

With only net enabled, NetTransport::from_registry() returns None unless the host registers a client. livekit-net has no native backend enabled by default, leaving these builds unable to export telemetry.

Learn more

The net feature activates the dependency but not its native backend. In a build that selects only this crate's net feature, http_client returns None unless an application called set_http_client, because native is disabled by default. NetTransport::from_registry therefore cannot construct the transport promised by this feature.

Example: An application enables livekit-telemetry/net on a native target without registering an HTTP client. from_registry() returns None instead of a built-in client, so no default exporter can send its batches.

Recommended fix: Arrange for the native livekit-net backend and an appropriate TLS backend to be enabled for native default-transport builds, while retaining an explicit registered-client path for host-provided builds. Validate the standalone net feature without another dependency enabling livekit-net/native.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

uniffi = ["dep:uniffi"]

[dev-dependencies]
tokio = { workspace = true, default-features = false, features = ["rt", "rt-multi-thread", "macros", "sync", "time", "test-util", "net", "io-util"] }

# How CI checks this crate's features, read by
# `.github/workflows/feature-combinations-curated.yml` via `cargo metadata`.
[package.metadata.feature-combinations]
Expand Down
49 changes: 49 additions & 0 deletions livekit-telemetry/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,55 @@ Set once per pipeline (`TelemetryConfig.resource`):
| `device.model.identifier` | platform SDK | `iPhone16,1` |
| `telemetry.sdk.name/language/version` | core | `livekit-telemetry`, `rust`, `0.1.0` |

## Pipeline, scopes and destination

### Destination and credentials

The platform passes exactly two things per room, through `Scope::set_server(url, token)`: the
LiveKit server URL the room connects to and the participant token it connects with — at connect,
and again with **every** refreshed token (the SFU sends one right after join, then every few
minutes). The call is cheap and idempotent. There is no client-side endpoint, header or sink.

- **Ingest URL:** derived from the server URL's host, `https://<host>/observability/client/{logs,traces}/otlp/v0`,
for LiveKit Cloud hosts only (`*.livekit.cloud`, including `*.staging.livekit.cloud`). Any other
host (self-hosted, OSS) has no ingest: one local warning, and that room's records are dropped
at the door instead of cached.
- **Token:** `Authorization: Bearer <token>`. The core reads the token's *unverified* claims: the
observability grant (`observability.write` or `observability.clientWrite`) and `exp`. It never
sends a token known to be expired or refused; batches wait for the next token instead
(a hard hold). A project whose first token has no grant never opted in: nothing is collected
for it. A refreshed token without the grant (today's SFU drops it) does not replace a granted
one that is still valid. Tokens live in memory only — never in a batch, never on disk.
- **Server URL validation:** parsed with WHATWG URL rules, never string-matched. A token is only
sent to `https://<project>.livekit.cloud/…` built from the parsed host alone: TLS scheme
(`wss`/`https`), a domain under the Cloud suffix with a label of its own, the default port, no
userinfo.
- **Ownership:** every record captures its owner when it is captured — its session and the
project that session is routed to at that moment — and keeps it: a Room that reconnects to
another project takes nothing queued or cached along. Credentials are keyed by (project,
session): a live Room uploads with its **own** latest token for that project, never another
Room's. A Room's records captured before it had a server go to its own first project, never to
another Room's. Process-level records (device state, pre-room errors, self-telemetry) go to
the project most recently handed a token, with that project's latest token; so do batches from
a previous launch, which wait — up to the 24 h age limit — for a token of the same project.
The answer to a request is attributed to the project it was sent to — its 404, disable or
pause never lands on another project. A session's credentials stay while the session is alive
(its Room, or records, windows or spans still referencing it) — for every project it was routed
to, so records captured for an earlier project can still be sent; that is the residual: a live
Room keeps one credential per project it has used — and while cached batches need them;
project-level copies only while a live session is routed there or the backlog has batches for
it. A token the collector refused is recorded by identity (a hash) and never sent again from
any slot, until it expires (a token without `exp` stays refused for the process; like the
per-host project table, which keeps one small entry per host ever seen, that grows only with
what a process meets — bounded in practice, not by a cap). A past,
negative or non-numeric `exp` counts as expired.
- **Waiting** for a first destination or a usable token is uncapped, bounded only by the cache.

Local end-to-end tests point everything at an OpenTelemetry collector of their own with the
`LK_TELEMETRY_ENDPOINT` environment variable, read by the core at start (a base URL gets
`/v1/logs` and `/v1/traces`; a URL ending in `logs` is used as is). It is not part of any platform
API. With it, every batch goes there without Cloud rules or tokens.

## Events

An event with no `body` is exported with its name as the body as well as in `event_name`: log
Expand Down
Loading
Loading