diff --git a/.changeset/telemetry-conventions-registry.md b/.changeset/telemetry-conventions-registry.md new file mode 100644 index 000000000..62c48111d --- /dev/null +++ b/.changeset/telemetry-conventions-registry.md @@ -0,0 +1,6 @@ +--- +"github.com/livekit/protocol": patch +"@livekit/protocol": patch +--- + +Add the telemetry semantic-conventions registry under `otel/` diff --git a/.gitattributes b/.gitattributes index 2b56c2f24..d93412448 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ packages/js/src/gen/* linguist-generated gen_*.* linguist-generated +otel/gen/** linguist-generated diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index 976508793..43acf9f58 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -65,9 +65,23 @@ jobs: run: | pnpm --filter @livekit/protocol run generate:proto + # Only the telemetry conventions in otel/ need weaver; the protobuf and JS generation + # above do not. Pinned like otel.yaml, which checks the registry. A failure in this step + # or the next leaves otel/gen as it was and never blocks the protobuf and JS commit + # below; otel.yaml's freshness check fails while otel/gen is stale. + - name: Set up weaver + continue-on-error: true + uses: open-telemetry/weaver/.github/actions/setup-weaver@51a5bf95bdbb2e20fb6e16638e6bbe7dde911e93 # v0.28.0 + with: + version: "0.28.0" + + - name: Generate the telemetry conventions + continue-on-error: true + run: go tool mage otel + - name: Add changes uses: EndBug/add-and-commit@645ecc0dd0a57f4d86d26c0aa5fc42c0a856fbca # v11.0.0 with: - add: '["livekit", "replay", "rpc", "infra", "packages/javascript/src/gen"]' + add: '["livekit", "replay", "rpc", "infra", "packages/javascript/src/gen", "otel/gen"]' default_author: github_actions message: generated protobuf diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml new file mode 100644 index 000000000..d10a1bc8b --- /dev/null +++ b/.github/workflows/otel.yaml @@ -0,0 +1,69 @@ +# Copyright 2026 LiveKit, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Telemetry conventions + +on: + workflow_dispatch: + push: + branches: [main] + paths: ["otel/**", ".github/workflows/otel.yaml"] + pull_request: + branches: [main] + paths: ["otel/**", ".github/workflows/otel.yaml"] + +permissions: + contents: read # Checks only read the checkout + +jobs: + weaver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up weaver + uses: open-telemetry/weaver/.github/actions/setup-weaver@51a5bf95bdbb2e20fb6e16638e6bbe7dde911e93 # v0.28.0 + with: + version: "0.28.0" + + - name: Check the registry + run: otel/check.sh + + - name: Policy negatives fail with their named finding + run: otel/tests/negatives.sh + + # The Generate workflow regenerates otel/gen on every push to a branch and commits any + # change, as it does for the protobuf code. That commit is pushed with the workflow + # token and does not run these checks by itself (GitHub can ask a maintainer to approve + # that run); push again to run them. This check fails a commit whose otel/gen is stale, + # also when a fresh run deletes an output: `git add --all` stages the deletion, and the + # diff compares the index with the commit. + - name: Generated outputs are current + run: | + otel/tests/generate.sh + otel/generate.sh + git add --all otel/gen + git diff --cached --exit-code -- otel/gen + + - name: Generated outputs compile + run: | + rustc --edition 2021 --crate-type lib -D warnings -D missing_docs otel/gen/rust/semconv.rs --out-dir "$RUNNER_TEMP" + npx --yes -p typescript@5.9.3 tsc --strict --noEmit --target es2020 otel/gen/ts/semconv.ts + python3 -I -W error otel/gen/python/semconv.py + + - name: Codegen escapes literals (round-trip through rustc, tsc and python3) + run: otel/tests/codegen.sh + + - name: Live-check the smoke sample + run: otel/tests/live-check.sh diff --git a/magefile.go b/magefile.go index b55d1d004..3e6fd7c72 100644 --- a/magefile.go +++ b/magefile.go @@ -259,6 +259,13 @@ func Proto() error { return nil } +// regenerate the telemetry conventions in otel/gen (needs weaver, see otel/README.md) +func Otel() error { + cmd := exec.Command("otel/generate.sh") + connectStd(cmd) + return cmd.Run() +} + // run tests func Test() error { cmd := exec.Command("go", "test", "-race", "./...") diff --git a/otel/README.md b/otel/README.md new file mode 100644 index 000000000..a7cb15598 --- /dev/null +++ b/otel/README.md @@ -0,0 +1,275 @@ +# LiveKit telemetry semantic conventions + +The machine-readable registry of the attribute keys, events and spans LiveKit producers send over +OTLP, in [OpenTelemetry Weaver](https://github.com/open-telemetry/weaver) format. It depends on +[OTel semantic conventions v1.44.0](https://github.com/open-telemetry/semantic-conventions/tree/v1.44.0/model): +upstream keys (`service.*`, `os.*`, `device.*`, `network.connection.type`, `error.type`, +`session.id`, `code.*`) are referenced with `ref:`, never redefined. + +Status: prototype, `development` stability. It registers the shared identity, PII and SDK keys, +what the client cores (rust-sdks `livekit-telemetry` and the JS port) emit today, and what +LiveKit Agents 2.0 (Python and the Rust core) emits today, all under their current names. The +OTel GenAI conventions are a second dependency +([semantic-conventions-genai](https://github.com/open-telemetry/semantic-conventions-genai), +pinned by commit: it has no tags), so Agents' `gen_ai.*` keys are referenced, not redefined. + +The registry is the spec of these conventions: names, types, units, requirement levels, meaning, +event severity and body, span kind, checkpoints and outcome rules. Transport, cadence, upload +policy, storage and the app API are not conventions and live with each SDK. + +| Path | What | +|---|---| +| `model/manifest.yaml` | The pinned OTel dependencies, and a `schema_url` that points at this registry (see [Versioning](#versioning)) | +| `model/shared/registry.yaml` | Shared keys: identity (`lk.room_sid`, `lk.participant_id`, `lk.track_sid`, `lk.job_id`), PII (`lk.pii.*`), SDK identity (`lk.sdk.*`), and the concepts clients and Agents both send (`lk.track_source`, `lk.disconnect_reason`, `lk.agent_name`) | +| `model/client/registry.yaml` | Client-owned attributes (`lk.device.*`, `lk.rtc.*`, `lk.track.*`, `lk.log.*`, `lk.telemetry.*`, …) | +| `model/client/signals.yaml` | Client log events and spans | +| `model/client/groups.yaml` | Attribute sets on every client resource / record (used by live-check) | +| `model/agents/registry.yaml` | Agents-owned attributes (`lk.speech_id`, `lk.eou.*`, `lk.rpc.*`, `lk.blocking.*`, …), the `lk.agents.*` metric dimensions, the `gen_ai.*`/LangFuse keys no upstream registry defines, and the session-report keys | +| `model/agents/signals.yaml` | Agents spans (50, unprefixed as shipped), the 20 `lk.agents.*` metrics, and the GenAI span events and metrics that are deprecated upstream or cannot be imported | +| `model/agents/groups.yaml` | GenAI metrics imported from the GenAI registry, and the values Agents sends for upstream keys that the upstream definitions do not list | +| `policies/` | LiveKit rules (`lib.rego`), applied per file (`livekit.rego`, default mode) and to the resolved registry (`resolved.rego`, default and `--v2`); any finding fails `check.sh` and `generate.sh` | +| `advice/` | Non-failing advice, printed by `check.sh` | +| `templates/registry/{rust,typescript,python,markdown}/` | Codegen templates | +| `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `python/semconv.py`, `docs/semconv.md` | +| `live-check/` | Live-check config, advice rules and the OTLP/JSON converter | +| `tests/` | Policy negatives, live-check samples, codegen escaping | + +The generated docs, [`gen/docs/semconv.md`](gen/docs/semconv.md), are the human-readable spec: +naming rules, the Resource, the attributes on every record, each event and span with its +requirement table, enum values and bindings. + +## Ownership + +Every LiveKit key carries `annotations.livekit.owner`: + +- **`shared`** (`model/shared/`): concepts several producers send. A change needs Client, Agents and + Observability review. +- **`client`** (`model/client/`): the Client team approves alone, unless the change touches or + duplicates a shared concept. +- **`agents`** (`model/agents/`): the Agents team approves alone, unless the change touches or + duplicates a shared concept. Registered as emitted. + +Where a client and Agents named the same concept differently, the Agents spelling and value set +is the shared key, and the client is the side that changes: `lk.track_source` and +`lk.disconnect_reason` take the enum names Agents sends as values (`SOURCE_MICROPHONE`, +`CLIENT_INITIATED`), and `lk.agent_name` is the key a client uses for the agent. The track +source names are the Agents RTC FFI `TrackSource` names, not the signal protocol's (`CAMERA`, +`SCREEN_SHARE`), so clients map them explicitly. + +All eight client renames (the six identity/PII keys, `lk.track.source` and +`lk.disconnect.reason`) are recorded the same way: `renamed_from` on the shared key names the old +spelling and, for an enum, maps each old value to a member. The old spellings are never +registered, deprecated or not. A policy rejects defining one, and live-check reads the map to +name the key and value to send. +- `cloud` is a valid owner for when that team registers its keys, in its own directory. + +The Notion conventions put client-owned attributes under `lk.client.*`. The client core does not do +that yet, so that rule is advice: `check.sh` lists every client key it would move, `browser.*` +included, and never fails on it. + +How a concept is named in other producers or stores (Agents' `room_id`, the collector's `room_id` +column, roomobs) is recorded in `annotations.livekit.bindings`. Weaver carries it into the docs; +nothing validates it. + +Agents' session report sends plain log records whose keys predate this registry. They are +registered as emitted, owner `agents`: `session.options`, `session.report_timestamp` and +`session.tags` sit in the upstream `session.*` namespace and carry +`annotations.livekit.upstream_namespace: session`, and `agent_name`, `sdk_version`, `usage`, +`chat.item`, `evaluation`, `tag` and `outcome` have no namespace. A map-valued key carries an OTLP +map, and its fields (such as `reason` in `outcome`) are payload, not attribute keys. + +## Adding a key + +1. Pick the file by owner. Define keys in the v1 `groups:` files. `client/groups.yaml` is + `definition/2` only because live-check needs public attribute groups; it holds references. +2. Reuse an upstream OTel key when one fits, with `ref:`. +3. Give the key a `brief`, `stability: development`, `examples` and + `annotations.livekit`: + + | Annotation | Values | Rule | + |---|---|---| + | `owner` | `shared`, `client`, `agents`, `cloud` | required | + | `sensitivity` | `none`, `pii`, `secret` (protocol `logger.Sensitivity`) | required; `pii` iff the key has a `pii` segment, or is `chat.item`, which the collector drops by name; `secret` is never emitted | + | `unit` | UCUM, e.g. `s`, `By`, `{packet}` | durations are seconds | + | `unit_exception` | a reason | allows a `*_ms` key with `unit: ms` (the RTC stats keys: `lk.rtc.window_ms` as measured, the others converted from the W3C stats' seconds) | + | `bindings` | map of producer/store → name | informational | + | `cardinality_exception` | `true`, on a metric | allows a high-cardinality ID as a metric dimension | + | `renamed_from` | `{key, values}`, on a shared key | the spelling it replaces, and for an enum a map of old value → member; the old key stays unregistered | + | `pii_heuristic_exception` | a reason | documents a shipped key the PII heuristic would flag; it only takes effect for a key listed in `pii_heuristic_allowlist` in `policies/lib.rego` (today `lk.rpc.caller_identity`, `lk.rpc.destination_identity`), so a new key cannot opt out by annotating itself. The collector still does not redact them | + + Events and spans carry their own `annotations.livekit`, rendered in the docs; live-check + enforces `severity`, `checkpoints` and `checkpoint_pattern`: + + | Annotation | On | Meaning | + |---|---|---| + | `severity` | event | OTLP severity texts the event is sent with, e.g. `[INFO]` | + | `body` | event | What the log body carries | + | `reserved`, `limits` | `custom` event | Keys and prefixes apps may not use; size caps on app events and attributes | + | `checkpoints` | span | Span events in order, each `{name, requirement: required\|best_effort, brief}` | + | `checkpoint_pattern` | span | A dynamic checkpoint name: `{name, pattern, brief}` | + | `limits` | span | Checkpoints and attributes per span; the rest is counted in OTLP `dropped_*_count` | + + Give every key, event and span a `brief` and a `note`, and every non-enum key `examples`; a + `ref` to an OTel key gets a short `note` saying how LiveKit uses it. + + Naming: shared identity keys are flat (`lk.room_sid`, never `lk.room.sid`). Customer-supplied + values go under `lk.pii.*`: the collector deletes keys with a `pii` segment when redaction is + on, and keeps everything else. +4. Reference it from the events or spans that carry it, with a `requirement_level`. +5. Run `otel/check.sh`, then `go tool mage otel` (it runs `otel/generate.sh`), and commit `gen/` + with the change. + +## Versioning + +The registry is unversioned until a producer needs to emit `schema_url`. Weaver requires one, so +`model/manifest.yaml` sets `https://github.com/livekit/protocol/tree/main/otel`, which points at +this registry. Weaver reads its last path segment, `otel`, as the version. No producer emits it, +and the link resolves once this directory is on main. When a producer needs a version, the plan +is to stamp the protocol release version into the manifest in the release step, as OTel +semantic-conventions does. + +## Running the checks locally + +CI (`.github/workflows/otel.yaml`) runs the same scripts on pull requests and pushes to `main` +that touch `otel/**` or the workflow file itself, and on manual dispatch. + +`go tool mage otel` regenerates `otel/gen`, next to `go tool mage proto` for the protobufs. Only a +change under `otel/` needs Weaver: `mage proto` and the JS generation do not. As for the +protobufs, the Generate workflow (`.github/workflows/generate.yaml`) regenerates `otel/gen` on +every push to a branch other than `main` and commits any change. That commit is pushed with the +workflow token, so it does not run the checks by itself (GitHub can ask a maintainer to approve +that run), the same as for the regenerated protobuf code. After a regeneration commit, push +again to run the checks. + +Install weaver 0.28.0, the version CI pins, from its +[release](https://github.com/open-telemetry/weaver/releases/tag/v0.28.0) (`weaver-installer.sh`), +or put the binary anywhere and point `WEAVER` at it. Every run clones OTel semconv v1.44.0 and +the GenAI registry. It prints about 90 warnings: almost all about upstream's own files, and one +for each LiveKit file in the not-yet-stable `definition/2` format (`client/groups.yaml`, +`agents/groups.yaml`). + +```sh +otel/check.sh # registry + LiveKit policies, default and --v2, then advice +otel/tests/negatives.sh # each negative fixture fails with its named finding, both modes +go tool mage otel && git status otel/gen # regenerate (runs otel/generate.sh); no diff expected +otel/tests/live-check.sh # smoke and sparse samples clean, legacy keys flagged, wrapper exit status +otel/tests/generate.sh # generate.sh replaces gen/ whole, and keeps it when a target fails +otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc, tsc and python3 + +rustc --edition 2021 --crate-type lib -D warnings -D missing_docs otel/gen/rust/semconv.rs --out-dir /tmp +npx -p typescript@5.9.3 tsc --strict --noEmit --target es2020 otel/gen/ts/semconv.ts +python3 -I -W error otel/gen/python/semconv.py +``` + +The generated Rust and Python also carry an `upstream` section: every OTel key a LiveKit signal +references (`error.type`, `gen_ai.request.model`, `rpc.method`, …), defined by a dependency and +never here, so a producer names them from one place. TypeScript does not yet. Three mechanisms +add to it: + +- Imported metrics. `model/agents/groups.yaml` imports `gen_ai.invoke_agent.duration` and + `gen_ai.execute_tool.duration` from the GenAI registry, and the upstream section lists them with + their units. Weaver 0.28.0 refuses to import `gen_ai.client.operation.duration`, because semconv + v1.44.0 keeps a deprecated copy excluded from dependency resolution. `model/agents/signals.yaml` + declares that metric as emitted instead. +- Values beyond upstream. A ref to an upstream key can list `annotations.livekit.shipped_values`: + values a producer sends that the upstream definition does not list, for example + `gen_ai.provider.name = mistral_ai`. Each value gets a constant next to the upstream values + (`GEN_AI_PROVIDER_NAME_MISTRAL_AI`). Two rules keep those constants unique. + A shipped `id` must be lowercase snake_case (`^[a-z][a-z0-9_]*$`), so its uppercase form is + exactly the constant name, and it must differ from every upstream member id read the same way + (`aws.bedrock` as `aws_bedrock`). + At most one shipped_values list exists per upstream key in the whole registry, because codegen + merges every ref's list into one set of constants. + `livekit_shipped_value` enforces both in both modes. The CI compile of the generated Rust is + the backstop for any collision the policy misses: a repeated constant does not compile (E0428). +- Rust `log` keys. In a `log` key-value list, wrap a generated constant in parentheses: + `log::warn!(target: "livekit.agents", (semconv::LK_PII_ERROR) = error; "...")`. A bare + identifier also compiles, but `log` sends the identifier's name (`LK_PII_ERROR`) as the key. + +| Finding | Fails | Rule | +|---|---|---| +| `livekit_owner`, `livekit_sensitivity` | yes | annotations present and valid | +| `livekit_secret` | yes | `secret` is never emitted | +| `livekit_pii_marker` | yes | `sensitivity: pii` iff a `pii` key segment, except `chat.item`, which the collector drops by name (`pii_by_name` in `lib.rego`) | +| `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*`, unless the key is on the policy's `pii_heuristic_allowlist` and carries a `pii_heuristic_exception` reason | +| `livekit_flat_identity` | yes | no `lk.room.sid`, `lk.participant.sid`, `lk.track.sid`, `lk.job.id` | +| `livekit_shared_rename` | yes | no old spelling named in a `renamed_from` is defined, deprecated or not (resolved, both modes) | +| `livekit_rename_value` | yes | every value a `renamed_from` maps to is a member of the shared key's enum | +| `livekit_seconds`, `livekit_unit` | yes | seconds, unless `unit_exception` | +| `livekit_metric_unit` | yes | metric units are not `ms` (resolved, so `definition/2` metrics and refinements count) | +| `livekit_cardinality` | yes | high-cardinality IDs are not metric dimensions (resolved, so `extends` and refinements count) | +| `livekit_ref_override` | yes | a `ref` to a key defined in `otel/model` may not override its `annotations` (owner, sensitivity, unit) | +| `livekit_shipped_value` | yes | a `shipped_values` list is on a non-`lk.*` key of type string, string[] or an enum whose values are all strings; every entry has a lowercase snake_case id, a value and a brief; no value repeats an upstream value; no id repeats another entry or an upstream member id (lowercased, non-alphanumerics as `_`); and each key has at most one distinct list in the whole registry. Refs and definitions, resolved, both modes | +| `livekit_client_namespace` | no (advice) | client-owned keys under `lk.client.*` | + +The attribute rules cover every key defined in `otel/model`, `lk.*` or not. +Weaver itself rejects duplicate keys, unknown fields and unresolved references. Every rule runs in +weaver's default mode and with `--v2`: weaver skips per-file (`before_resolution`) policies with +`--v2`, so `resolved.rego` applies the same rules (from `lib.rego`) to the resolved registry there. + +## Live-check + +`live-check.sh` checks captured telemetry against the registry: + +```sh +otel/live-check.sh session.jsonl [out-dir] [--fail-on none] +``` + +It takes OTLP/JSON lines from an otelcol `file` exporter (the SDK e2e setup), or a weaver samples +JSON file. `live-check/weaver.toml` matches spans by name and checks the resource and every +record against `model/client/groups.yaml`. It drops two kinds of finding: `not_stable` (every key +is `development`) and absent `conditionally_required` attributes (their conditions are prose). +`live-check/livekit.rego` adds `livekit_shared_rename` for an old spelling. `live-check/renames.jq` +builds its map from the model's `renamed_from`, so each finding names the key to send and, for an +enum, the value. It also adds `livekit_pii_unmarked` for any other PII-looking key, and +`livekit_unknown_span` for an `lk.*` span no matcher places (weaver +alone reports nothing for an unmatched span that has no attributes). From the event and span +annotations it adds `livekit_severity` (an event at a severity it is not declared with), +`livekit_checkpoint` (a span event the span does not list) and `livekit_span_outcome` (span status +disagreeing with `lk.outcome`, or `error.type` without `lk.outcome = error`). + +On the reference session (Swift 2.17.0 with the core at rust-sdks `a9ec8d52`: 6 spans, 22 logs, +349 attributes), every finding is one of the eight expected renames to shared keys (136 +violations, 62 improvements): + +| Emitted today | Registered key | Findings | +|---|---|---| +| `lk.room.sid` | `lk.room_sid` | `missing_attribute`, `unexpected_attribute`, `livekit_shared_rename` ×10 each | +| `lk.participant.sid` | `lk.participant_id` | same, ×10 each | +| `lk.track.sid` | `lk.track_sid` | same, ×12 each, plus `required_attribute_not_present: lk.track_sid` ×10 | +| `lk.room.name` | `lk.pii.room_name` | `missing_attribute`, `unexpected_attribute`, `livekit_shared_rename` ×10 each | +| `lk.participant.identity` | `lk.pii.participant_identity` | same, ×10 each | +| `lk.participant.remote_identity` | `lk.pii.remote_participant_identity` | same, ×2 each, plus `recommended_attribute_not_present` ×2 | +| `lk.track.source` = `camera`, `microphone` | `lk.track_source` = `SOURCE_CAMERA`, `SOURCE_MICROPHONE` | `missing_attribute`, `unexpected_attribute`, `livekit_shared_rename`, `required_attribute_not_present: lk.track_source` ×4 each | +| `lk.disconnect.reason` = `client_initiated` | `lk.disconnect_reason` = `CLIENT_INITIATED` | same, ×2 each | + +With those eight keys renamed (and the two values mapped) in the same capture, live-check reports +nothing and exits 0. `tests/live-sample.json` is part of that capture, anonymized, and +`tests/live-agents.json` holds the shared keys as Agents sends them; both must pass. + +Known limits: + +- Span names are checked only in the `lk.*` namespace. A span outside it (an app's own + `Span::detached` name) passes without a check; with no attributes it gets no finding at all. + A registered span also needs its matcher in `live-check/weaver.toml`, or it is reported as + `livekit_unknown_span`. +- Checkpoint order is not checked, only names. +- App-defined attributes (custom events, `Scope::set_attribute`) are reported as unknown keys. +- `lk.sdk.*` is not reported when missing: resource attributes are only compared, not required. +- Units are annotations; live-check checks names, types and enum values, never magnitudes. + +### In SDK integration tests + +Every SDK's telemetry e2e test already writes its session to a local collector with a file +exporter. That capture is live-check input as is, so each SDK can validate what it actually +sends against this registry, in CI, with no new harness: + +``` +otel/live-check.sh .jsonl # names, types, enums, severities, checkpoints, outcomes +otel/live-check.sh .jsonl . --fail-on none # report only +``` + +Rollout: run report-only until the eight renames to shared keys land in a core, then fail the +test on violations. One registry version checks all producers the same way, so a platform +cannot drift from the shared keys without its own CI noticing. It checks the vocabulary, not +transport, cadence or storage. diff --git a/otel/advice/livekit.rego b/otel/advice/livekit.rego new file mode 100644 index 000000000..8e8912809 --- /dev/null +++ b/otel/advice/livekit.rego @@ -0,0 +1,27 @@ +package before_resolution + +# Advice, never a failure. `weaver registry check` exits 1 on any finding whatever its level, so +# otel/check.sh runs this directory in a second pass after otel/policies and only prints it. + +import rego.v1 + +# Attribute definitions, lk.* or not, from v1 `groups:` files and definition/2 files alike. +lk_attrs contains attr if { + some group in input.groups + some attr in group.attributes + is_string(attr.id) # a definition, not a ref + not attr.deprecated +} + +lk_attrs contains object.union(attr, {"id": attr.key}) if { + some attr in input.attributes + not attr.deprecated +} + +# The namespace convention puts client-owned attributes under lk.client.*. The registry keeps the +# client's names, so this rule is advice: it lists the client-owned keys outside lk.client.*. +deny contains {"id": "livekit_client_namespace", "level": "improvement", "message": sprintf("%s: client-owned key outside lk.client.*", [attr.id])} if { + some attr in lk_attrs + object.get(attr, ["annotations", "livekit", "owner"], null) == "client" + not startswith(attr.id, "lk.client.") +} diff --git a/otel/check.sh b/otel/check.sh new file mode 100755 index 000000000..301fae0c1 --- /dev/null +++ b/otel/check.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Checks otel/model against OTel semconv v1.44.0 and the LiveKit policies, in weaver's default +# mode and with --v2 (fails on any violation), then prints the non-failing advice from otel/advice. +# usage: otel/check.sh [extra `weaver registry check` args, e.g. --diagnostic-format gh_workflow_command] +# Both modes always run, so a --v2 among the args is accepted and dropped. +set -euo pipefail +cd "$(dirname "$0")" +W=${WEAVER:-weaver} + +args=() +for arg in "$@"; do + [[ $arg == --v2 || $arg == --v2=* ]] || args+=("$arg") +done + +"$W" registry check -r model -p policies ${args[@]+"${args[@]}"} +# With --v2 (the mode generate.sh uses) weaver skips before_resolution policies; the resolved +# policies cover that mode, so it is checked on its own. +"$W" registry check --v2 -r model -p policies ${args[@]+"${args[@]}"} + +# weaver exits 1 whenever a policy has findings, whatever their level, so the advice pass reads +# its JSON diagnostics instead and fails only on something that is not advice. +{ "$W" registry check -r model -p advice --diagnostic-format json --diagnostic-stdout 2>/dev/null || true; } | + python3 -I -c ' +import json, sys +advice, other = [], [] +for d in json.load(sys.stdin): + e = d.get("error", {}) + if e.get("type") == "policy_violation" and e["violation"]["level"] != "violation": + advice.append(e["violation"]) + elif d.get("diagnostic", {}).get("severity") != "Warning": # the strict pass printed warnings + other.append(d.get("diagnostic", {}).get("message", d)) +print("Advice (non-failing): %d finding(s)" % len(advice)) +for a in sorted(advice, key=lambda a: a["message"]): + print(" %s: %s" % (a["id"], a["message"])) +sys.exit("unexpected findings in the advice pass: %s" % other if other else 0) +' diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md new file mode 100644 index 000000000..3c24c52bd --- /dev/null +++ b/otel/gen/docs/semconv.md @@ -0,0 +1,1796 @@ + +# LiveKit telemetry semantic conventions + +The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. +Stability `development`. Built on +[OpenTelemetry semantic conventions v1.44.0](https://github.com/open-telemetry/semantic-conventions/tree/v1.44.0): +OTel keys are used as OTel defines them and are not redefined here. + +Generated from `otel/model` in livekit/protocol; change the model, not this file. It registers +the keys shared across producers and what the LiveKit client SDKs send today (the Rust core +behind the native SDKs, and the JS port). How and when telemetry is sent (transport, cadence, +batching, retries, storage) is not part of the conventions. + +Contents: [Conventions](#conventions) · [Resource](#resource) · [Every record](#every-record) · +[Plain log records](#plain-log-records) · [Events](#events) · [Spans](#spans) · +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) · +[Old spellings](#old-spellings) + +## Conventions + +**OTel first.** OTel defines the record model (timestamp, severity, trace and span ids, span +status, `service.name`) and the keys LiveKit reuses (`service.*`, `os.*`, `device.*`, +`network.connection.type`, `error.type`, `session.id`, `code.*`, `telemetry.sdk.*`). LiveKit keys +carry the `lk.` prefix. Requirement levels have their OTel meaning; a conditional requirement's +condition is prose. + +**Ownership.** `shared` keys are concepts several producers send; changing one needs Client, +Agents and Observability review. `client` keys belong to the Client team. Every LiveKit key's +owner is in the [attribute reference](#attribute-reference). + +**Flat shared identity.** Shared identity keys are flat, `lk._`, never +`lk..sid`: +`lk.job_id`, `lk.participant_id`, `lk.room_sid`, `lk.track_sid`. +They are record and span attributes, never Resource attributes, omitted until the server has +assigned them, and never default metric dimensions (high cardinality). + +**One name per concept.** Where a client and Agents named the same concept differently, the +Agents spelling and value set is the shared key: a flat key, and the enum names Agents sends as +values (`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The +other spelling is never registered, deprecated or not. The shared key records it in +`renamed_from`, listed under [Old spellings](#old-spellings). + +**Upstream metrics.** Agents creates three GenAI metrics that the GenAI registry defines. The +registry imports `gen_ai.invoke_agent.duration` and `gen_ai.execute_tool.duration`, and the +generated code lists them in its upstream section. Weaver cannot import +`gen_ai.client.operation.duration`, because semconv 1.44.0 keeps an excluded, deprecated copy, so +the registry declares it as emitted. + +**Values beyond upstream.** A ref to an upstream key can list `shipped_values`: values a LiveKit +producer sends that the upstream definition does not list. The generated code gives each one a +constant next to the upstream values, and a policy rejects a value that upstream already lists. + +| Key | Value | Meaning | +| --- | --- | --- | +| `gen_ai.provider.name` | `mistral_ai` | Mistral, as Agents sends it. The upstream member is `mistral`. | +| `gen_ai.response.finish_reasons` | `stop` | The generation finished normally. | +| `gen_ai.response.finish_reasons` | `length` | The output reached the token limit. | +| `gen_ai.response.finish_reasons` | `content_filter` | A content filter stopped the output. | +| `gen_ai.response.finish_reasons` | `tool_call` | The generation ended with a tool call. | +| `gen_ai.response.finish_reasons` | `compaction` | Declared in Agents' `GenAIFinishReason`. No Agents code path sends it today. | +| `gen_ai.response.finish_reasons` | `error` | The generation ended abnormally. The upstream note names this value. | + +**Session report.** Agents' session report sends plain log records whose keys predate this +registry, registered as emitted: three in the upstream `session.*` namespace, and seven without a +namespace. A map-valued key carries an OTLP map, and its fields are payload, not attribute keys. +`chat.item` carries conversation content: with redaction on, the collector drops the whole record. + +**PII.** Customer-supplied values go under `lk.pii.*`. The `pii` key segment is the collector's +redaction contract: with redaction on, it deletes every attribute whose key has a `pii` segment and +keeps everything else. Log bodies and metrics are never redacted, so PII must not go there. +PII keys: `chat.item`, `lk.pii.amd.transcript`, `lk.pii.arguments`, `lk.pii.chat_ctx`, `lk.pii.dtmf_inputs`, `lk.pii.error`, `lk.pii.event`, `lk.pii.function_tool.arguments`, `lk.pii.function_tool.output`, `lk.pii.input_text`, `lk.pii.instructions`, `lk.pii.interim_transcript`, `lk.pii.keyterms`, `lk.pii.output`, `lk.pii.participant_identity`, `lk.pii.progress_message`, `lk.pii.raw_arguments`, `lk.pii.remote_participant_identity`, `lk.pii.repaired`, `lk.pii.response.function_calls`, `lk.pii.response.text`, `lk.pii.room_name`, `lk.pii.rpc.payload`, `lk.pii.rpc.response`, `lk.pii.sip.phoneNumber`, `lk.pii.text`, `lk.pii.transcript`, `lk.pii.user_input`, `lk.pii.user_transcript`. + +**Units.** Durations are seconds (OTel), on a monotonic clock; timestamps are the record's own +OTLP time. One documented exception: the RTC stats keys ending in `_ms` carry milliseconds. The +window length (`lk.rtc.window_ms`) is measured by the producer; the others are converted from the +seconds the W3C stats report: +`lk.rtc.freezes_duration_ms`, `lk.rtc.interruptions_duration_ms`, `lk.rtc.jitter_buffer_delay_ms`, `lk.rtc.jitter_ms.avg`, `lk.rtc.jitter_ms.max`, `lk.rtc.jitter_ms.min`, `lk.rtc.pauses_duration_ms`, `lk.rtc.quality_limitation.bandwidth_ms`, `lk.rtc.quality_limitation.cpu_ms`, `lk.rtc.quality_limitation.other_ms`, `lk.rtc.rtt_ms.avg`, `lk.rtc.rtt_ms.max`, `lk.rtc.rtt_ms.min`, `lk.rtc.window_ms`. + +**Keys the SDK owns.** Apps must not set these, on their own events or as session attributes, +and a producer must reject such an attribute: +prefixes `lk.`, `otel.`, `code.` and keys +`session.id`, `error.type`. + +**App events.** An app's events are named `custom.` (see [`custom`](#custom)), so they never +collide with or spoof LiveKit events, and must stay within these limits: +attributes per event 64, attributes per session 64, key bytes 128, name bytes 128, string value bytes 1024. +A producer must reject anything over a limit, never truncate it, and count it in +`lk.telemetry.dropped.invalid`. + +**Records.** An event is an OTLP log record with `event_name`; its severity and body are listed +with it. The JS port sends every event's name as its body. A record's timestamp and attributes are +taken when it is captured. Every record and span of a room session carries the +[session attributes](#every-record). + +**Spans.** A span is one attempt at an operation; the session is the trace. LiveKit span names are +verbs, never ids. Span status is `Unset` on success and on cancellation, `Error` on failure. +`lk.outcome` (`ok`, `error`, `cancelled`) is on every span exactly once, and rollups read it, never +the status. `error.type` is a type name, never a message, at most 128 bytes, present only when the +span failed with a type. Only the SDK sets either; a producer must drop a span attribute of either +name set any other way. Checkpoints are span events without attributes, named from a fixed list +per span. A producer must hold each span to its limits: later checkpoints are dropped and counted +in OTLP `dropped_events_count`; past the attribute limit, app correlation attributes go first, then +the span's own tail, counted in `dropped_attributes_count`; the session's attributes, `lk.outcome` +and `error.type` always ship. + +## Resource + +Resource attributes of the client pipeline, one Resource per process. Set once per pipeline. Session-specific keys (`lk.room_sid`, `lk.participant_id`, `lk.track_sid`) never go here; they stay on records and spans. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `service.name` | string | Required | Logical name of the service. `livekit-client-`: swift, android, flutter, react-native, unity, rust, js; `livekit-client` when the platform names none. Set by the platform SDK. | +| `service.version` | string | Required | The version string of the service component. The format is not defined by these conventions. The LiveKit client SDK's version. Set by the platform SDK. | +| `telemetry.sdk.language` | enum | Required | The language of the telemetry SDK. `rust` from the Rust core (every native SDK), `webjs` from the JS port. | +| `telemetry.sdk.name` | string | Required | The name of the telemetry SDK as defined above. The telemetry library, `livekit-telemetry`, in every client. | +| `telemetry.sdk.version` | string | Required | The version string of the telemetry SDK. The telemetry library's version (the JS port sends `0.1.0`). | +| `browser.name` | string | Conditionally required: The JS SDK running in a browser. | Browser name. Resource attribute, JS port only. | +| `browser.version` | string | Conditionally required: The JS SDK running in a browser. | Browser version. Resource attribute, JS port only. | +| `device.model.identifier` | string | Recommended | The model identifier for the device. Set by the platform SDK where it knows the model; the JS port only when a host SDK supplies it. | +| `lk.sdk.name` | string | Recommended | LiveKit SDK name. Resource attribute. A low-cardinality metric dimension. Matches the `` part of `service.name = livekit-client-`. | +| `lk.sdk.version` | string | Recommended | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. | +| `os.name` | string | Recommended | Human readable operating system name. Set by the platform SDK; the JS port only when a host SDK supplies it. | +| `os.version` | string | Recommended | The version string of the operating system as defined in [Version Attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.44.0/docs/resource/README.md#version-attributes). Set by the platform SDK; the JS port only when a host SDK supplies it. | + +## Every record + +Attributes on every log record and span of a session. One session is one Room connection lifetime, across reconnects, and one trace. A record's attributes and timestamp are taken when it is captured, the session's included, so a record captured before a disconnect keeps its room. Records outside a room (device state, pre-room errors, self-telemetry) belong to the process's own session and carry only `session.id`. Keys the SDK owns (`lk.*`, `otel.*`, `code.*`, `session.id`, `error.type`) must never be set by an app; an app's correlation attributes (e.g. `app.call_id`, `enduser.id`) ride along, within the `custom` event's limits. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `session.id` | string | Required | A unique ID to identify a session. The session's trace id (32 hex characters), also on the process's own records. | +| `lk.participant_id` | string | Conditionally required: Once the client has joined the room. | Server-assigned SID of the local participant (`PA_...`). On every record and span of a session once the client has joined. High cardinality, never a default metric dimension. | +| `lk.pii.participant_identity` | string | Conditionally required: Once the client has joined the room. | Customer-supplied identity of the local participant. On every record and span of a session once the client has joined. | +| `lk.pii.room_name` | string | Conditionally required: Once the client has joined the room. | Customer-supplied room name. On every record and span of a session once the client has joined. | +| `lk.room_sid` | string | Conditionally required: Once the client has joined the room. | Server-assigned room session SID (`RM_...`). On every record and span of a session once the client has joined; absent before join. High cardinality, never a default metric dimension. | +| `otel.event.name` | string | Conditionally required: On event records; duplicates the record's `event_name`. | Identifies the class / type of event. For backends that do not surface the OTLP `event_name` field yet. | + +## Plain log records + +Attributes of a plain log record (no event name). The body is the log message. Only WARN and ERROR records are sent; WebRTC lines only at ERROR, and the telemetry module's own lines never. In records copied from the Rust core (`lk.log.source = ffi`), JWT-shaped substrings must be masked as ``. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.log.logger` | string | Required | Logger, type, module or file that wrote the line. Free text from the platform's logger. | +| `lk.log.source` | enum | Required | Which layer wrote the log line. Only WARN and ERROR records are sent; WebRTC lines only at ERROR. | +| `code.file.path` | string | Recommended | The source code filename that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. The source file that logged the line. | +| `code.function.name` | string | Recommended | The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. The function that logged the line, as the platform names it. | +| `code.line.number` | int | Recommended | The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. The line in `code.file.path`. | + +## Events + +### `custom` + +Namespace of app-defined events, named `custom.` and carrying the app's own attributes. The `custom.` prefix keeps app events from colliding with or spoofing `lk.*` events. An app's attributes, on its events and on the session (correlation attributes such as `app.call_id`), must not use the reserved keys and must stay within the limits; a producer must reject an attribute that breaks either rule, never truncate it, and count it in `lk.telemetry.dropped.invalid`. An event's own attributes override the session's. + +- Severity: `INFO` +- Body: The event name (`custom.`). + +### `gen_ai.assistant.message` + +OTel GenAI assistant message (also a function call), a span event on `llm_node`. Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + +- Severity: +- Body: Not a log record: a span event whose `content` attribute carries the message. + +### `gen_ai.choice` + +OTel GenAI choice, a span event on `llm_node`. Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + +- Severity: +- Body: Not a log record: a span event whose `content` attribute carries the message. + +### `gen_ai.system.message` + +OTel GenAI system (or developer) message, a span event on `llm_node`. Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + +- Severity: +- Body: Not a log record: a span event whose `content` attribute carries the message. + +### `gen_ai.tool.message` + +OTel GenAI tool message (a function call output), a span event on `llm_node`. Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + +- Severity: +- Body: Not a log record: a span event whose `content` attribute carries the message. + +### `gen_ai.user.message` + +OTel GenAI user message, a span event on `llm_node`. Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + +- Severity: +- Body: Not a log record: a span event whose `content` attribute carries the message. + +### `lk.device.app_state.changed` + +The app moved to the foreground or background. Sent once with the initial state, then on each change. All platforms. + +- Severity: `INFO` +- Body: Display line, e.g. `app: background` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.app_state` | enum | Required | Whether the app is in front of the user. All platforms; the web maps page visibility. | + +### `lk.device.audio.interruption` + +An audio session interruption began or ended. iOS; Android from audio focus loss and gain. Not sent by the JS port. + +- Severity: `INFO` +- Body: Display line, e.g. `audio interruption began`. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.audio.interruption` | enum | Required | Phase of an audio session interruption. iOS audio session interruptions; Android audio focus loss and gain. | + +### `lk.device.audio_route.changed` + +The audio route changed. iOS; Android from its audio device callbacks; optional elsewhere. Not sent by the JS port. + +- Severity: `INFO` +- Body: Display line, e.g. `audio route: speaker (old_device_unavailable)`. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.audio_route.outputs` | string | Required | The active outputs, comma-separated. Each item is one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. A string, not an array: the vocabulary is closed but the list is joined. | +| `lk.device.audio_route.reason` | enum | Required | Why the audio route changed. AVAudioSession route change reasons on iOS; Android maps its audio device callbacks; `unknown` where the platform gives none. | + +### `lk.device.battery.changed` + +Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent; silent where the level is unknown. iOS and Android; not sent by the JS port. + +- Severity: `INFO` +- Body: Display line, e.g. `battery: 18%` or `battery: 64%, charging`. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.battery.charging` | boolean | Required | The device is charging. Sent together with `lk.device.battery.level`. | +| `lk.device.battery.level` | int, `%` | Required | Battery charge in percent, 0-100. Only where the level is known (iOS, Android; not desktops or tvOS); never from the JS port. An integer percentage, not a 0-1 ratio. | + +### `lk.device.capture.failed` + +A capture device failed. All platforms. + +- Severity: `WARN` +- Body: Display line, e.g. `capture failed: camera (permission_denied)` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.capture.device` | enum | Required | Capture device that failed. All platforms. | +| `lk.device.capture.reason` | enum | Required | Why capture failed, in the getUserMedia failure taxonomy. iOS: authorization status and capture interruptions; Android: permission checks and camera callbacks; web: DOMException names. | + +### `lk.device.low_power.changed` + +Low Power Mode changed. Sent once with the initial reading, then on each change; never where the platform has no source. + +- Severity: `INFO` +- Body: Display line, e.g. `low power mode: on` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.low_power.enabled` | boolean | Required | Low Power Mode / Battery Saver is on. Absent where the platform has no source. iOS, macOS and Android; optional elsewhere. | + +### `lk.device.memory.changed` + +Memory pressure changed. Sent once with the initial reading, then on each change. + +- Severity: `INFO` +- Body: Display line, e.g. `memory pressure: warning` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.memory.pressure` | enum | Required | Memory pressure as the OS reports it. Apple `DispatchSource` memory-pressure levels; Android `onTrimMemory`: RUNNING_LOW and BACKGROUND map to `warning`, RUNNING_CRITICAL and COMPLETE to `critical`. iOS, macOS and Android; optional elsewhere. | + +### `lk.device.network.changed` + +The network path changed. Sent once with the initial state, then when any of its attributes changes. All platforms. A platform that cannot read the connection's details (a browser without the Network Information API) sends only `network.connection.type`, as `unknown` or `unavailable`. + +- Severity: `INFO` +- Body: Display line, e.g. `network: cell, expensive` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `network.connection.type` | enum | Required | The internet connection type. Clients also send `vpn`, `bluetooth` and `other`, which are not OTel members; `unknown` when the platform cannot tell, `unavailable` when offline. | +| `lk.device.network.constrained` | boolean | Conditionally required: When the platform reports it. | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver; on the web `navigator.connection.saveData`, only where the Network Information API exists. | +| `lk.device.network.expensive` | boolean | Conditionally required: When the platform reports it. | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; on the web only where the Network Information API exists. | + +### `lk.device.thermal.changed` + +Thermal state changed. Sent once with the initial reading, then on each change. A platform without a thermal source sends none. A device event: a record of the process, not of a room session. + +- Severity: `INFO` +- Body: Display line, e.g. `thermal: serious` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.thermal.state` | enum | Required | Thermal pressure as the OS reports it. Apple `ProcessInfo.thermalState`, Android `PowerManager` thermal status, web Compute Pressure API, all mapped to this vocabulary. The platform's `unknown` (no thermal source) is no reading and is never emitted. iOS, macOS and Android; optional elsewhere. | + +### `lk.ping` + +Pipeline smoke test. Never emitted in production paths. Not sent by the JS port. + +- Severity: `INFO` +- Body: The event name. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.ping.seq` | int | Recommended | Monotonically increasing sequence number per pipeline. Optional; on `lk.ping` only. | + +### `lk.room.disconnected` + +The Room left connected for good. Once per session, never on a reconnect. INFO when the client hung up (`CLIENT_INITIATED`), WARN otherwise. + +- Severity: `INFO` or `WARN` +- Body: Display line, e.g. `disconnected: client_initiated` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.disconnect_reason` | enum | Required | Why a participant left the room, as the protobuf `DisconnectReason` enum name. On the client `lk.room.disconnected` event (the local participant) and on the Agents `participant_disconnected` checkpoint (the linked participant). `RECONNECT_FAILED` is the one value outside the protobuf enum: a client that ran out of reconnect attempts sends it. | + +### `lk.rtc.stats.sample` + +One RTC stats window for one track and direction. One per track and direction per window, timestamped at the window's end, also closed early when the track ends, at disconnect and at shutdown. Keyed by session and track: two rooms receiving the same track send two windows. Only the track, kind, direction, window and sample count are always present; counters and gauges a platform or direction lacks are omitted. + +- Severity: `INFO` +- Body: Display summary, e.g. `video inbound: 33 kbps, loss 0.0%, rtt 0 ms, 10 fps` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.rtc.samples` | int, `{reading}` | Required | Stats readings folded into the window. A gauge's `.avg` divides by the readings that reported that gauge, which can be fewer. | +| `lk.rtc.window_ms` | int, `ms` | Required | Actual length of the stats window. From the window's start to its close, on a monotonic clock. | +| `lk.track.direction` | enum | Required | Whether the stats describe sent or received media. On RTC stats windows only. | +| `lk.track.kind` | enum | Required | Media kind of the track. On publish and subscribe spans and RTC stats windows. | +| `lk.track_sid` | string | Required | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | +| `lk.rtc.audio_level.avg` | double, `1` | Conditionally required: Inbound audio. | Average audio level in the window, 0-1, inbound audio. Inbound `audioLevel`, averaged over the window's readings that report it. | +| `lk.rtc.audio_level.max` | double, `1` | Conditionally required: Inbound audio. | Maximum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | +| `lk.rtc.audio_level.min` | double, `1` | Conditionally required: Inbound audio. | Minimum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | +| `lk.rtc.bytes` | int, `By` | Conditionally required: When the stats report it. | Bytes sent or received (cumulative). Outbound `bytesSent`, inbound `bytesReceived`. | +| `lk.rtc.codec` | string | Conditionally required: When known. | Codec mimeType, when known. The `codec` stats entry the RTP stream's `codecId` names. | +| `lk.rtc.concealed_samples` | int, `{sample}` | Conditionally required: Inbound audio. | Concealed audio samples, inbound audio (cumulative). Inbound `concealedSamples`. | +| `lk.rtc.concealment_events` | int, `{event}` | Conditionally required: Inbound audio. | Concealment events, inbound audio (cumulative). Inbound `concealmentEvents`. | +| `lk.rtc.fps.avg` | double, `{frame}/s` | Conditionally required: Video. | Average frame rate in the window, video. `framesPerSecond`, either direction, averaged over the window's readings that report it. | +| `lk.rtc.fps.max` | double, `{frame}/s` | Conditionally required: Video. | Maximum frame rate in the window, video. `framesPerSecond`, either direction. | +| `lk.rtc.fps.min` | double, `{frame}/s` | Conditionally required: Video. | Minimum frame rate in the window, video. `framesPerSecond`, either direction. | +| `lk.rtc.freeze_count` | int, `{freeze}` | Conditionally required: Inbound video. | Video freezes, inbound video (cumulative). Inbound `freezeCount`. | +| `lk.rtc.freezes_duration_ms` | int, `ms` | Conditionally required: Inbound video. | Total freeze time, inbound video (cumulative). Inbound `totalFreezesDuration`. | +| `lk.rtc.interruption_count` | int, `{interruption}` | Conditionally required: Inbound audio. | Audio interruptions, inbound audio (cumulative). Inbound `interruptionCount`. | +| `lk.rtc.interruptions_duration_ms` | int, `ms` | Conditionally required: Inbound audio. | Total interruption time, inbound audio (cumulative). Inbound `totalInterruptionDuration`. | +| `lk.rtc.jitter_buffer_delay_ms` | int, `ms` | Conditionally required: Inbound. | Jitter buffer delay, inbound (cumulative). Inbound `jitterBufferDelay`. Average delay = jitter_buffer_delay_ms / jitter_buffer_emitted_count. | +| `lk.rtc.jitter_buffer_emitted_count` | int, `{sample}` | Conditionally required: Inbound. | Samples or frames emitted by the jitter buffer, inbound (cumulative). Inbound `jitterBufferEmittedCount`. | +| `lk.rtc.jitter_ms.avg` | double, `ms` | Conditionally required: Inbound. | Average jitter in the window, inbound. Inbound `jitter` (seconds × 1000), averaged over the window's readings that report it. | +| `lk.rtc.jitter_ms.max` | double, `ms` | Conditionally required: Inbound. | Maximum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | +| `lk.rtc.jitter_ms.min` | double, `ms` | Conditionally required: Inbound. | Minimum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | +| `lk.rtc.packets` | int, `{packet}` | Conditionally required: When the stats report it. | Packets sent or received (cumulative). Outbound `packetsSent`, inbound `packetsReceived`. | +| `lk.rtc.packets_lost` | int, `{packet}` | Conditionally required: Inbound. | Packets lost, inbound (cumulative). Inbound `packetsLost`. Loss ratio = packets_lost / (packets + packets_lost). | +| `lk.rtc.pause_count` | int, `{pause}` | Conditionally required: Inbound video. | Video pauses, inbound video (cumulative). Inbound `pauseCount`. | +| `lk.rtc.pauses_duration_ms` | int, `ms` | Conditionally required: Inbound video. | Total pause time, inbound video (cumulative). Inbound `totalPausesDuration`. | +| `lk.rtc.quality_limitation.bandwidth_ms` | int, `ms` | Conditionally required: Outbound video. | Time the encoder was limited by bandwidth, outbound video (cumulative). Outbound `qualityLimitationDurations.bandwidth`. | +| `lk.rtc.quality_limitation.cpu_ms` | int, `ms` | Conditionally required: Outbound video. | Time the encoder was limited by CPU, outbound video (cumulative). Outbound `qualityLimitationDurations.cpu`. | +| `lk.rtc.quality_limitation.other_ms` | int, `ms` | Conditionally required: Outbound video. | Time the encoder was limited for other reasons, outbound video (cumulative). Outbound `qualityLimitationDurations.other`. | +| `lk.rtc.rtt_ms.avg` | double, `ms` | Conditionally required: When the window has readings. | Average round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`, averaged over the window's readings that report it. | +| `lk.rtc.rtt_ms.max` | double, `ms` | Conditionally required: When the window has readings. | Maximum round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`. | +| `lk.rtc.rtt_ms.min` | double, `ms` | Conditionally required: When the window has readings. | Minimum round-trip time in the window. Outbound: the `remote-inbound-rtp` entry's `roundTripTime`; inbound: the nominated candidate pair's `currentRoundTripTime` (seconds × 1000). | +| `lk.rtc.silent_concealed_samples` | int, `{sample}` | Conditionally required: Inbound audio. | Concealed samples that were silent, inbound audio (cumulative). Inbound `silentConcealedSamples`. | + +### `lk.telemetry.report` + +Self-telemetry; counts since the previous report. Sent after a loss, failure, refusal, capped hold or disk error, and once at shutdown as the session summary, so fleet-wide success rates have denominators. A record of the process, not of a room session. Losses by policy (a project that receives nothing, the opt-out) are never reported. + +- Severity: `INFO` +- Body: Rust core: a one-line summary, e.g. `telemetry: 4 batches sent (20480 B), 0 failed, 0 dropped, 0 cached`. JS port: the event name. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.telemetry.cache.batches` | int, `{batch}` | Required | Batches waiting in the cache now. A gauge, not a count since the previous report. Always present on a report. | +| `lk.telemetry.uploads.bytes` | int, `By` | Required | Compressed bytes the collector accepted. What telemetry cost the uplink. Always present on a report, even when 0. | +| `lk.telemetry.uploads.failed` | int, `{attempt}` | Required | Upload attempts that failed transiently (no answer, 429, 5xx). Always present on a report, even when 0. | +| `lk.telemetry.uploads.sent` | int, `{batch}` | Required | Batches the collector accepted. Always present on a report, even when 0. | +| `lk.telemetry.cache.delete_errors` | int, `{batch}` | Conditionally required: When non-zero (JS port only). | Sent batches the cache could not delete; never sent twice from the same launch. Only when non-zero. JS port only. | +| `lk.telemetry.cache.list_errors` | int, `{attempt}` | Conditionally required: When non-zero (JS port only). | Times the cache could not be listed, so nothing was reconciled or sent. Only when non-zero. JS port only. | +| `lk.telemetry.cache.read_errors` | int, `{batch}` | Conditionally required: When non-zero (JS port only). | Cached batches that could not be read right now, kept for the next pass. Only when non-zero. JS port only. | +| `lk.telemetry.cache.write_errors` | int, `{batch}` | Conditionally required: When non-zero. | Batches the disk refused, kept in memory instead. Only when non-zero. | +| `lk.telemetry.dropped.cache_error` | int, `{record}` | Conditionally required: When non-zero. | Records no cache, not even memory, could take. Only when non-zero. | +| `lk.telemetry.dropped.cache_full` | int, `{record}` | Conditionally required: When non-zero. | Records evicted by the cache's size or file-count bound. Only when non-zero. | +| `lk.telemetry.dropped.corrupt` | int, `{record}` | Conditionally required: When non-zero. | Records in cached batches that failed their CRC. Only when non-zero. | +| `lk.telemetry.dropped.expired` | int, `{record}` | Conditionally required: When non-zero. | Records in batches past the cache's age limit. Only when non-zero. | +| `lk.telemetry.dropped.invalid` | int, `{record}` | Conditionally required: When non-zero. | Custom events, attributes or SDK strings over the limits, rejected. Only when non-zero. Over-limit values are rejected, never truncated (a truncated id collides). | +| `lk.telemetry.dropped.oversized` | int, `{record}` | Conditionally required: When non-zero. | Single records larger than the collector accepts (413). Only when non-zero. | +| `lk.telemetry.dropped.queue_full` | int, `{record}` | Conditionally required: When non-zero. | Records evicted from the in-memory queue. Only when non-zero. | +| `lk.telemetry.dropped.rate_limited` | int, `{record}` | Conditionally required: When non-zero. | Discrete events dropped by the flood guard. Only when non-zero. RTC stats windows and reports are exempt from the guard. | +| `lk.telemetry.dropped.rejected` | int, `{record}` | Conditionally required: When non-zero. | Records the collector rejected (final 4xx/5xx, OTLP partial success). Only when non-zero. | +| `lk.telemetry.dropped.throttled` | int, `{record}` | Conditionally required: When non-zero (Rust core only). | Records evicted from the cache during a server-directed pause. Only when non-zero. Rust core only; the JS port never sends it. | +| `lk.telemetry.dropped.unattributed` | int, `{record}` | Conditionally required: When non-zero (JS port only). | Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. Only when non-zero. JS port only. | +| `lk.telemetry.holds.capped` | int, `{hold}` | Conditionally required: When non-zero. | Soft upload holds that reached their cap. Only when non-zero. | +| `lk.telemetry.uploads.timeouts` | int, `{attempt}` | Conditionally required: When non-zero. | Upload attempts that hit the export timeout. Only when non-zero. | +| `lk.telemetry.uploads.unauthorized` | int, `{attempt}` | Conditionally required: When non-zero. | 401/403 answers (tokens the collector refused). Only when non-zero. | + +## Spans + +### `agent_session` + +Kind `internal`. The agent session, parent of every turn. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_name` | string | Recommended | Name of the agent a job was dispatched to. On the Agents Resource and the `job_entrypoint` and `agent_session` spans, set when the job has an agent name (explicit dispatch). A client that reports the agent it talks to uses this key. The participant attribute `lk.agent.name` carries the same name on the room; it is not a telemetry key. | +| `lk.participant_id` | string | Recommended | Server-assigned SID of the local participant (`PA_...`). Agents sends the SID of the participant it is linked to, not of the agent itself. | +| `lk.participant_kind` | string | Recommended | Kind of the linked participant (protocol `ParticipantInfo.Kind`). | +| `lk.pii.participant_identity` | string | Recommended | Customer-supplied identity of the local participant. On every record and span of a session once the client has joined. | +| `exception.message` | string | Opt-in | The exception message. Replaced by a fixed marker when redaction is on; `error.type` keeps the class. | +| `exception.stacktrace` | string | Opt-in | A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. Dropped when redaction is on. | +| `exception.type` | string | Opt-in | The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. On the span's exception events, as the OTel SDK records them. | +| `lk.blocking.count` | int | Opt-in | Blocks in the session. | +| `lk.blocking.max_duration` | double, `s` | Opt-in | Seconds of the longest block in the session. | +| `lk.blocking.total_duration` | double, `s` | Opt-in | Seconds blocked in total over the session. | +| `lk.disconnect_reason` | enum | Opt-in | Why a participant left the room, as the protobuf `DisconnectReason` enum name. Sent on the `participant_disconnected` checkpoint, not on the span. | +| `lk.fail` | boolean | Opt-in | The session report is tagged failed. See `lk.success`. | +| `lk.pii.sip.phoneNumber` | string | Opt-in | Phone number of a SIP participant. Camel case as the SIP participant attribute it is copied from; registered as emitted. | +| `lk.success` | boolean | Opt-in | The session report is tagged successful. A tag name in the session report (`observability.py` / `report.rs`); the outcome is derived from which of `lk.success` and `lk.fail` is present. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `participant_linked` | Best effort | The session linked to its participant. | +| `agent_state_changed` | Best effort | The agent state changed (`lk.old_state` -> `lk.new_state`). | +| `user_state_changed` | Best effort | The user state changed (`lk.old_state` -> `lk.new_state`). | +| `participant_disconnected` | Best effort | The linked participant left the room, with `lk.disconnect_reason`. | +| `event_loop_blocked` | Best effort | A synchronous block on the event loop, with `lk.blocking.duration`; mirrors the span of the same name. | +| `exception` | Best effort | The OTel exception event (`exception.*`), recorded on whichever span the exception was caught on; declared here once. | + +No limits declared. + +### `agent_speaking` + +Kind `internal`. The agent's audio playing out. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.interruption.source` | string | Conditionally required: When interrupted. | What triggered the interruption. | +| `lk.playout.position` | double, `s` | Conditionally required: When interrupted. | Seconds of the agent's speech played out when it was interrupted. | +| `lk.interrupted` | boolean | Recommended | The speech was interrupted. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | + +No checkpoints. + +No limits declared. + +### `agent_turn` + +Kind `internal`. One agent turn (generation), parent of the LLM and TTS nodes. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.e2e_latency` | double, `s` | Recommended | Seconds from the end of the user's speech to the agent's first audio. | +| `lk.generation_count` | int | Recommended | Generations in the turn. | +| `lk.generation_id` | string | Recommended | Id of the agent turn (generation). | +| `lk.interrupted` | boolean | Recommended | The speech was interrupted. | +| `lk.speech.queue_wait` | double, `s` | Recommended | Seconds a speech handle waited in the queue before generation was authorized. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | +| `lk.end_of_turn_delay` | double, `s` | Opt-in | Seconds from the end of speech to the turn decision. | +| `lk.llm_metrics` | string | Opt-in | LLM metrics of the turn, serialized. | +| `lk.on_user_turn_completed_delay` | double, `s` | Opt-in | Seconds the `on_user_turn_completed` hook took. | +| `lk.parent_generation_id` | string | Opt-in | Generation this one continues (a tool-call follow-up). | +| `lk.pii.instructions` | string | Opt-in | Agent instructions in force. | +| `lk.pii.user_input` | string | Opt-in | The user's transcript for the turn. | +| `lk.provider_request_ids` | string[] | Opt-in | Provider request ids a span covers, for joining against provider logs. | +| `lk.realtime_model_metrics` | string | Opt-in | Realtime model metrics of the turn, serialized. | +| `lk.transcription_delay` | double, `s` | Opt-in | Seconds from the end of speech to the final transcript. | +| `lk.tts_metrics` | string | Opt-in | TTS metrics of the turn, serialized. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `generation` | Best effort | One generation started (the generation attributes). | +| `preemptive_generation_discarded` | Best effort | A preemptive generation was discarded in favour of this one (`lk.speech_id` of the discarded handle). | + +No limits declared. + +### `amd` + +Kind `internal`. Answering-machine detection on an outbound call. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.amd.category` | string | Required | AMD classification. | +| `lk.amd.delay` | double, `s` | Recommended | Seconds from the call start to the AMD decision. | +| `lk.amd.reason` | string | Recommended | Why the AMD classifier decided. | +| `lk.amd.speech_duration` | double, `s` | Recommended | Seconds of speech the AMD classifier heard. | +| `lk.pii.amd.transcript` | string | Opt-in | Transcript the AMD classifier used. | + +No checkpoints. + +No limits declared. + +### `drain_agent_activity` + +Kind `internal`. Draining an agent activity before it ends. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `eou_detection` + +Kind `internal`. One end-of-utterance inference. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.eou.detection_delay` | double, `s` | Recommended | Seconds the detector inference took. | +| `lk.eou.from_cache` | boolean | Recommended | The detector answer came from the cache. | +| `lk.eou.language` | string | Recommended | Language the detector ran for. | +| `lk.eou.probability` | double | Recommended | End-of-utterance probability. | +| `lk.eou.unlikely_threshold` | double | Recommended | Threshold under which the turn is unlikely to be over. | +| `lk.pii.user_transcript` | string | Opt-in | Transcript the detector scored. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `superseded` | Best effort | A newer detection superseded this one. | + +No limits declared. + +### `eou_wait` + +Kind `internal`. The endpointing wait, from the user's last speech to the turn decision. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.eou.outcome` | enum | Required | How the endpointing wait ended. | +| `lk.eou.endpointing_delay` | double, `s` | Recommended | Seconds of endpointing delay applied. | +| `lk.eou.not_committed_count` | int | Recommended | Turn decisions the wait rejected before it ended. | +| `lk.eou.rearm_count` | int | Recommended | Times the endpointing wait restarted on a later trigger. | +| `lk.eou.wait_duration` | double, `s` | Recommended | Seconds from the end of the user's speech to the turn decision. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `rearmed` | Best effort | A later trigger re-armed the wait. | +| `user_resumed` | Best effort | The user spoke again during the wait. | + +No limits declared. + +### `event_loop_blocked` + +Kind `internal`. A synchronous block on the agent event loop. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.blocking.duration` | double, `s` | Required | Seconds the loop was blocked. | +| `lk.blocking.cpu_time` | double, `s` | Recommended | CPU seconds the loop thread consumed during the block. | +| `lk.blocking.gc_time` | double, `s` | Recommended | Seconds of garbage collection on the loop thread during the block. | +| `lk.blocking.severity` | string | Recommended | Severity bucket of the block. | +| `lk.blocking.stack` | string | Recommended | Loop-thread stack sampled while blocked (source locations only). | +| `lk.blocking.suppressed` | int | Recommended | Reports dropped by rate limiting since the previous emitted span. | +| `lk.blocking.task` | string | Recommended | Name of the task that was running. | +| `lk.blocking.threshold` | double, `s` | Recommended | Seconds above which a block is reported. | +| `lk.blocking.import` | string | Opt-in | Module a lazy import was loading when sampled. | + +No checkpoints. + +No limits declared. + +### `function_tool` + +Kind `internal`. One function tool execution, the OTel GenAI execute-tool span. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.function_tool.name` | string | Required | Tool name. | +| `error.type` | enum | Conditionally required: On failure. | Describes a class of error the operation ended with. The `error.type` SHOULD be predictable, and SHOULD have low cardinality. When `error.type` is set to a type (e.g., an exception type), its canonical class name identifying the type within the artifact SHOULD be used. If the recorded error type is a wrapper that is not meaningful for failure classification, instrumentation MAY use the type of the inner error instead. For example, in Go, errors created with `fmt.Errorf` using `%w` MAY be unwrapped when the wrapper type does not help classify the failure. Instrumentations SHOULD document the list of errors they report. The cardinality of `error.type` within one instrumentation library SHOULD be low. Telemetry consumers that aggregate data from multiple instrumentation libraries and applications should be prepared for `error.type` to have high cardinality at query time when no additional filters are applied. If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), it's RECOMMENDED to: - Use a domain-specific attribute - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. | +| `gen_ai.operation.name` | enum | Recommended | The name of the operation being performed. If one of the predefined values applies, but specific system uses a different name it's RECOMMENDED to document it in the semantic conventions for specific GenAI system and use system-specific name in the instrumentation. If a different name is not documented, instrumentation libraries SHOULD use applicable predefined value. | +| `gen_ai.tool.call.id` | string | Recommended | The tool call identifier supplied by the model or provider. This attribute SHOULD be set to the identifier received from the model or provider if and only if one was supplied. | +| `gen_ai.tool.name` | string | Recommended | Name of the tool utilized by the agent. | +| `gen_ai.tool.type` | string | Recommended | Type of the tool utilized by the agent. Extension: A tool executed on the agent-side to directly call external APIs, bridging the gap between the agent and real-world systems. Agent-side operations involve actions that are performed by the agent on the server or within the agent's controlled environment. Function: A tool executed on the client-side, where the agent generates parameters for a predefined function, and the client executes the logic. Client-side operations are actions taken on the user's end or within the client application. Datastore: A tool used by the agent to access and query structured or unstructured external data for retrieval-augmented tasks or knowledge updates. | +| `lk.function_tool.id` | string | Recommended | Tool call id. | +| `lk.function_tool.is_error` | boolean | Recommended | The tool returned an error. | +| `gen_ai.tool.call.arguments` | any | Opt-in | Parameters passed to the tool call. > [!WARNING] > This attribute may contain sensitive information. It's expected to be an object - in case a serialized string is available to the instrumentation, the instrumentation SHOULD do the best effort to deserialize it to an object. | +| `gen_ai.tool.call.result` | any | Opt-in | The result returned by the tool call (if any and if execution was successful). > [!WARNING] > This attribute may contain sensitive information. It's expected to be an object - in case a serialized string is available to the instrumentation, the instrumentation SHOULD do the best effort to deserialize it to an object. | +| `gen_ai.tool.description` | string | Opt-in | The tool description. > [!WARNING] > This attribute may contain sensitive information. | +| `lk.pii.function_tool.arguments` | string | Opt-in | Tool call arguments. | +| `lk.pii.function_tool.output` | string | Opt-in | Tool output. | + +No checkpoints. + +No limits declared. + +### `job_entrypoint` + +Kind `internal`. The job's entrypoint, root of a job's trace. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.job_id` | string | Required | Agent job ID (`AJ_...`). Sent by Agents, not by clients. High cardinality; Agents' job-scoped metrics are the one sanctioned metric-dimension exception. | +| `lk.agent_name` | string | Recommended | Name of the agent a job was dispatched to. On the Agents Resource and the `job_entrypoint` and `agent_session` spans, set when the job has an agent name (explicit dispatch). A client that reports the agent it talks to uses this key. The participant attribute `lk.agent.name` carries the same name on the room; it is not a telemetry key. | +| `lk.job.accept_latency` | double, `s` | Recommended | Seconds from the job request to its acceptance. | +| `lk.job.agent_id` | string | Recommended | Agent id of the job. | +| `lk.job.assignment_latency` | double, `s` | Recommended | Seconds from acceptance to the job assignment. | +| `lk.job.dispatch_latency` | double, `s` | Recommended | Seconds from the dispatch request to the entrypoint running. | +| `lk.job.entrypoint_latency` | double, `s` | Recommended | Seconds from launch to the entrypoint running. | +| `lk.job.launch_latency` | double, `s` | Recommended | Seconds from assignment to the job process launch. | +| `lk.job.worker_id` | string | Recommended | Worker id the job runs on. | +| `lk.pii.room_name` | string | Recommended | Customer-supplied room name. On every record and span of a session once the client has joined. | +| `lk.room_sid` | string | Recommended | Server-assigned room session SID (`RM_...`). On every record and span of a session once the client has joined; absent before join. High cardinality, never a default metric dimension. | +| `lk.cloud_agent_id` | string | Opt-in | LiveKit Cloud agent id. | +| `lk.deployment_id` | string | Opt-in | LiveKit Cloud deployment id. | +| `lk.dispatch_id` | string | Opt-in | Agent dispatch id. | +| `lk.redaction.enabled` | boolean | Opt-in | PII redaction was requested for the session. Telemetry metadata key; the producer's own `pii` filter reads it from the span attributes. | +| `lk.simulation.enabled` | boolean | Opt-in | The session is a simulation. Telemetry metadata key set by the job context; a span attribute on the session trace. | +| `lk.simulation.job_id` | string | Opt-in | Simulation run job (one scenario) behind the session. | +| `lk.simulation.run_id` | string | Opt-in | Simulation run the session belongs to. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `job_received` | Best effort | The worker received the job request. | +| `job_accepted` | Best effort | The worker accepted the job. | +| `job_assigned` | Best effort | The server assigned the job. | +| `process_assigned` | Best effort | A job process was launched for it. | +| `entrypoint_started` | Best effort | The entrypoint started running. | +| `job_started_on_server` | Best effort | The server-side job start time, as the worker learned it. | +| `entrypoint_cancelled` | Best effort | The entrypoint was cancelled. | +| `entrypoint_returned` | Best effort | The entrypoint returned. | + +No limits declared. + +### `job_shutdown` + +Kind `internal`. The job shutting down. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.shutdown.reason` | string | Recommended | Why the job shut down. | +| `lk.shutdown.user_initiated` | boolean | Recommended | The shutdown was requested by user code. | + +No checkpoints. + +No limits declared. + +### `judge_evaluation` + +Kind `internal`. One LLM-judge evaluation of the session. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `gen_ai.evaluation.name` | string | Recommended | The name of the evaluation metric used for the GenAI response. | +| `gen_ai.evaluation.score.label` | string | Recommended | Human readable label for evaluation. This attribute provides a human-readable interpretation of the evaluation score produced by an evaluator. For example, a score value of 1 could mean "relevant" in one evaluation system and "not relevant" in another, depending on the scoring range and evaluator. The label SHOULD have low cardinality. Possible values depend on the evaluation metric and evaluator used; implementations SHOULD document the possible values. | +| `gen_ai.evaluation.explanation` | string | Opt-in | A free-form explanation for the assigned score provided by the evaluator. | + +No checkpoints. + +No limits declared. + +### `keyterm_detection` + +Kind `internal`. A keyterm update to the STT. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.keyterms.added` | int | Recommended | Keyterms added by the update. | +| `lk.keyterms.count` | int | Recommended | Number of keyterms configured. | +| `lk.keyterms.removed` | int | Recommended | Keyterms removed by the update. | +| `lk.pii.keyterms` | string | Opt-in | The keyterms themselves (customer vocabulary). | + +No checkpoints. + +No limits declared. + +### `lk.connect` + +Kind `client`. One connect attempt. Dashboards compute connect phases only from the required checkpoints, which every platform sends in this order; best-effort ones refine a platform's own view and may be missing or ordered differently. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.connect.attempt` | int | Required | 1-based connect attempt. 1 for the user-initiated connect; a platform that retries sets the next number on the next `lk.connect` span. | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer must drop an attribute of this name set any other way. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `error.type` | enum | Conditionally required: Only when `lk.outcome` is `error` and the failure has a type. | Describes a class of error the operation ended with. A type name, never a message, at most 128 bytes, platform-defined: Swift sends `LiveKitError.`, `CancellationError` or the Swift error type. Dashboards group by it per `service.name`. Set only by the SDK, at most once. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `ws_open` | Required | The signaling WebSocket opened. | +| `signal` | Required | Signaling is up (stamped with ws_open on the web and with join_recv on Swift). | +| `join_recv` | Required | The join response arrived. | +| `pc_created` | Required | The peer connections exist (stamped once the join is adopted). | +| `early_pc_created` | Best effort | A peer connection was created before the join response (JS port only). | +| `offer_sent` | Best effort | The publisher's SDP offer went out. | +| `answer_sent` | Best effort | The subscriber's SDP answer went out. | +| `engine` | Best effort | The RTC engine is ready (Swift stamps it with pc_connected). | +| `pc_connected` | Best effort | The primary peer connection connected. | +| `room_connected` | Best effort | The Room reached the connected state. | + +Limits: 128 attributes, 128 span events per span. + +### `lk.publish` + +Kind `internal`. One track publish. A child of the ambient span when there is one; a publish before the connect completes (a microphone published early) is its own span in the session's trace. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer must drop an attribute of this name set any other way. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `lk.track.kind` | enum | Required | Media kind of the track. On publish and subscribe spans and RTC stats windows. | +| `lk.track_source` | enum | Required | Source of the track, as the Agents RTC FFI `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the RTC FFI enum name exactly, e.g. `SOURCE_SCREENSHARE`. The signal protocol's `TrackSource` names differ (`CAMERA`, `SCREEN_SHARE`), so clients map their track source to these names explicitly. | +| `error.type` | enum | Conditionally required: Only when `lk.outcome` is `error` and the failure has a type. | Describes a class of error the operation ended with. A type name, never a message, at most 128 bytes. Set only by the SDK, at most once. | +| `lk.track_sid` | string | Conditionally required: On success; the server assigns it. | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | + +No checkpoints. + +Limits: 128 attributes, 128 span events per span. + +### `lk.reconnect` + +Kind `client`. One reconnect cycle. `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint named `attempt `, mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer must drop an attribute of this name set any other way. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `lk.reconnect.reason` | enum | Required | What triggered the reconnect cycle. The protocol's `ReconnectReason` (`RR_*`), plus reasons only a client knows. | +| `error.type` | enum | Conditionally required: Only when `lk.outcome` is `error` and the failure has a type. | Describes a class of error the operation ended with. A type name, never a message, at most 128 bytes. Set only by the SDK, at most once. | +| `lk.reconnect.attempts` | int | Conditionally required: After the first attempt. | Reconnect attempts made in the cycle. Equals `` of the last `attempt ` checkpoint. | +| `lk.reconnect.mode` | enum | Conditionally required: After the first attempt. | Mode of the last reconnect attempt. Updated with each `attempt ` checkpoint; matches the last one. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `attempt ` | One per attempt | One reconnect attempt; n counts from 1. Matches `^attempt [1-9][0-9]* (quick\|full)$`. | + +Limits: 128 attributes, 128 span events per span. + +### `lk.subscribe` + +Kind `internal`. One subscription, from the intent to subscribe to the first media. Starts when the intent exists: a remote publish under autoSubscribe, a manual subscribe, or, for tracks already in the room, the join. Ends `ok` at the first inbound stats reading with bytes; `cancelled` on unsubscribe or unpublish before media; `error` on a subscription failure, or with `error.type = timed_out` when no media came within 30 s, also through a later disconnect. The core owns the span; an SDK only reports the remote track's lifecycle. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer must drop an attribute of this name set any other way. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `lk.track.kind` | enum | Required | Media kind of the track. On publish and subscribe spans and RTC stats windows. | +| `lk.track_sid` | string | Required | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | +| `lk.track_source` | enum | Required | Source of the track, as the Agents RTC FFI `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the RTC FFI enum name exactly, e.g. `SOURCE_SCREENSHARE`. The signal protocol's `TrackSource` names differ (`CAMERA`, `SCREEN_SHARE`), so clients map their track source to these names explicitly. | +| `error.type` | enum | Conditionally required: Only when `lk.outcome` is `error` and the failure has a type. | Describes a class of error the operation ended with. A type name, never a message, at most 128 bytes. `timed_out` when no media came within 30 s. | +| `lk.pii.remote_participant_identity` | string | Recommended | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). On subscribe spans: the identity of the participant publishing the track. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `subscribed` | Required | The platform reported the remote track subscribed. | +| `first_media` | Required | The first inbound stats reading with bytes; the span ends ok here. | + +Limits: 128 attributes, 128 span events per span. + +### `llm_fallback_adapter` + +Kind `internal`. The LLM fallback adapter choosing an instance. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.fallback.index` | int | Recommended | Index of the fallback instance that served the request. | +| `lk.fallback.label` | string | Recommended | Label of the fallback instance that served the request. | +| `lk.fallback.recovery` | boolean | Recommended | The request was a recovery probe of a failed instance. | + +No checkpoints. + +No limits declared. + +### `llm_node` + +Kind `internal`. The agent's LLM node for one generation. Carries the OTel GenAI request identity when a custom node does its own inference and no `llm_request` span exists. Its span events are the OTel GenAI message events (see the `event.gen_ai.*` groups below). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | +| `lk.function_tools` | string[] | Recommended | Function tool names offered. | +| `lk.provider_tools` | string[] | Recommended | Provider-side tool names offered. | +| `lk.response.ttft` | double, `s` | Recommended | Seconds to the first LLM token. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | +| `lk.tool_sets` | string[] | Recommended | Tool set names offered. | +| `gen_ai.memory.query.text` | string | Opt-in | The search query used to retrieve memories. Instrumentations SHOULD NOT capture this attribute by default. Capture SHOULD be gated by an explicit user opt-in, for example `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`. > [!Warning] > This attribute may contain sensitive information. | +| `gen_ai.memory.records` | any | Opt-in | The memory records stored or retrieved in a memory operation. Instrumentations SHOULD NOT capture this attribute by default. Capture SHOULD be gated by an explicit user opt-in, for example `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`. > [!Warning] > This attribute may contain sensitive information including user/PII data. | +| `gen_ai.prompt.variable` | template[string] | Opt-in | The variables supplied to the prompt template, the `` being the variable name, the value being the variable value. Prompt templates are parameterized with variables that are filled in at runtime. This attribute records the variable values passed to the template. The attribute name defines the variable name, and the attribute value is the variable value serialized as a string. Examples: - A variable `user_name` with value `Alice` SHOULD be recorded as the `gen_ai.prompt.variable.user_name` attribute with value `"Alice"`. - A variable `language` with value `French` SHOULD be recorded as the `gen_ai.prompt.variable.language` attribute with value `"French"`. > [!Warning] > This attribute may contain sensitive information. | +| `gen_ai.provider.name` | enum | Opt-in | The Generative AI provider as identified by the client or server instrumentation. Semantic conventions for individual GenAI operations SHOULD clarify which kinds of providers (e.g. inference, embeddings, retrieval, memory, hosted agent providers) apply when it is not clear from context. The attribute SHOULD be set based on the instrumentation's best knowledge and may differ from the actual upstream provider. For example, a client SDK may be configured against a proxy or hosting platform that transparently relays requests to a different provider. The `gen_ai.provider.name` attribute acts as a discriminator that identifies the GenAI telemetry format flavor specific to that provider within GenAI semantic conventions. It SHOULD be set consistently with provider-specific attributes and signals. For example, GenAI spans, metrics, and events related to AWS Bedrock should have the `gen_ai.provider.name` set to `aws.bedrock` and include applicable `aws.bedrock.*` attributes and are not expected to include `openai.*` attributes. | +| `gen_ai.request.model` | string | Opt-in | The name of the GenAI model a request is being made to. | +| `gen_ai.retrieval.documents` | any | Opt-in | The documents retrieved. Each document object SHOULD contain the following properties when available: `id` (string): A unique identifier for the document, `score` (double): The relevance score of the document | +| `gen_ai.retrieval.query.text` | string | Opt-in | The query text used for retrieval. > [!Warning] > This attribute may contain sensitive information. | +| `gen_ai.workflow.name` | string | Opt-in | Human-readable name of the GenAI workflow provided by the application. The workflow name is usually a static, application-unique identifier defined in a framework-specific way. For example, it can be the name of the first chain in LangChain, the name of the crew in CrewAI, or the entry point agent in ADK or OpenAI Agents when no explicit workflow name is provided. This attribute MUST have low cardinality. It is NOT RECOMMENDED to use instrumentation-time constants or names of types representing the workflow, such as "StateGraph". When no meaningful, low-cardinality workflow name is available for a given framework, this attribute MUST NOT be captured by default. Semantic conventions for individual Generative AI frameworks SHOULD document what `gen_ai.workflow.name` means in the context of that framework. | +| `lk.input.base_span_id` | string | Opt-in | Span whose input record this one continues. | +| `lk.input.delta` | boolean | Opt-in | The span's input record is a delta over its base span. | +| `lk.input.dropped_from_base` | int | Opt-in | Items of the base record not carried into this one. | +| `lk.pii.chat_ctx` | string | Opt-in | Serialized chat context sent to the LLM. | +| `lk.pii.response.function_calls` | string | Opt-in | Function calls the LLM produced, serialized. | +| `lk.pii.response.text` | string | Opt-in | Text the LLM produced. | + +No checkpoints. + +No limits declared. + +### `llm_request` + +Kind `client`. One LLM inference request, the OTel GenAI inference span. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `gen_ai.operation.name` | enum | Required | The name of the operation being performed. If one of the predefined values applies, but specific system uses a different name it's RECOMMENDED to document it in the semantic conventions for specific GenAI system and use system-specific name in the instrumentation. If a different name is not documented, instrumentation libraries SHOULD use applicable predefined value. | +| `error.type` | enum | Conditionally required: On failure. | Describes a class of error the operation ended with. The `error.type` SHOULD be predictable, and SHOULD have low cardinality. When `error.type` is set to a type (e.g., an exception type), its canonical class name identifying the type within the artifact SHOULD be used. If the recorded error type is a wrapper that is not meaningful for failure classification, instrumentation MAY use the type of the inner error instead. For example, in Go, errors created with `fmt.Errorf` using `%w` MAY be unwrapped when the wrapper type does not help classify the failure. Instrumentations SHOULD document the list of errors they report. The cardinality of `error.type` within one instrumentation library SHOULD be low. Telemetry consumers that aggregate data from multiple instrumentation libraries and applications should be prepared for `error.type` to have high cardinality at query time when no additional filters are applied. If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), it's RECOMMENDED to: - Use a domain-specific attribute - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. | +| `gen_ai.agent.name` | string | Recommended | Human-readable name of the GenAI agent provided by the application. | +| `gen_ai.conversation.id` | string | Recommended | The unique identifier for a conversation (session, thread), used to store and correlate messages within this conversation. Instrumentations SHOULD populate conversation id when they have an identifier for the conversation readily available for a given operation, for example: - when the client framework being instrumented manages conversation history (see [LlamaIndex chat store](https://docs.llamaindex.ai/en/stable/module_guides/storing/chat_stores/), [LangChain `session_id`](https://reference.langchain.com/python/langchain-core/runnables/history/RunnableWithMessageHistory), and [Google ADK sessions](https://adk.dev/sessions/session)) - when instrumenting GenAI client libraries that maintain a conversation on the backend (see [AWS Bedrock agent sessions](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-session-state.html), [OpenAI Assistant threads](https://platform.openai.com/docs/api-reference/threads)) When no identifier for the conversation is available, instrumentations SHOULD NOT populate conversation id. For example, a new UUID, a trace identifier, or a hash of request content SHOULD NOT be used as a fallback value. Application developers that manage conversation history MAY add conversation id to GenAI and other spans or logs using custom span or log record processors or hooks provided by instrumentation libraries. | +| `gen_ai.output.type` | enum | Recommended | Represents the content type requested by the client. This attribute SHOULD be used when the client requests output of a specific type. The model may return zero or more outputs of this type. This attribute specifies the output modality and not the actual output format. For example, if an image is requested, the actual output could be a URL pointing to an image file. Additional output format details may be recorded in the future in the `gen_ai.output.{type}.*` attributes. | +| `gen_ai.provider.name` | enum | Recommended | The Generative AI provider as identified by the client or server instrumentation. Semantic conventions for individual GenAI operations SHOULD clarify which kinds of providers (e.g. inference, embeddings, retrieval, memory, hosted agent providers) apply when it is not clear from context. The attribute SHOULD be set based on the instrumentation's best knowledge and may differ from the actual upstream provider. For example, a client SDK may be configured against a proxy or hosting platform that transparently relays requests to a different provider. The `gen_ai.provider.name` attribute acts as a discriminator that identifies the GenAI telemetry format flavor specific to that provider within GenAI semantic conventions. It SHOULD be set consistently with provider-specific attributes and signals. For example, GenAI spans, metrics, and events related to AWS Bedrock should have the `gen_ai.provider.name` set to `aws.bedrock` and include applicable `aws.bedrock.*` attributes and are not expected to include `openai.*` attributes. | +| `gen_ai.request.model` | string | Recommended | The name of the GenAI model a request is being made to. | +| `gen_ai.request.stream` | boolean | Recommended | Indicates whether the GenAI request was made in streaming mode. | +| `gen_ai.response.finish_reasons` | string[] | Recommended | Array of reasons the model stopped generating tokens, corresponding to each generation received. Values correspond to generations in the same order as the returned choices/candidates. Each position SHOULD contain the finish reason for the corresponding choice/candidate. If a finish reason was expected but not received, for example because generation failed, was cancelled, or a stream ended before the final event, instrumentations SHOULD report `error` for that position instead of omitting it. `error` indicates that the generation ended abnormally, whether reported by the provider or inferred by the instrumentation. | +| `gen_ai.response.id` | string | Recommended | The unique identifier for the completion. | +| `gen_ai.response.model` | string | Recommended | The name of the model that generated the response. | +| `gen_ai.response.time_to_first_chunk` | double | Recommended | Time to first chunk in a streaming response, measured from request issuance, in seconds. The value is measured from when the client issues the generation request to when the first chunk is received in the response stream. | +| `gen_ai.usage.input_tokens` | int | Recommended | The number of tokens used in the GenAI input (prompt). This value SHOULD include all types of input tokens, including cached tokens. Instrumentations SHOULD make a best effort to populate this value, using a total provided by the provider when available or, depending on the provider API, by summing different token types parsed from the provider output. When the provider reports both billed token counts and model-consumed token counts (for example, Cohere exposes both `usage.billed_units` and `usage.tokens`), instrumentations SHOULD report the billed count so the value matches the units the customer is charged for. Detailed usage attributes are subsets of total and aggregate counts. For example, if a request has 100 text tokens (40 cached) and 200 image tokens: - `gen_ai.usage.input_tokens`: 300 - `gen_ai.usage.cache_read.input_tokens`: 40 - `gen_ai.usage.text.input_tokens`: 100 - `gen_ai.usage.text.cache_read.input_tokens`: 40 - `gen_ai.usage.image.input_tokens`: 200 | +| `gen_ai.usage.output_tokens` | int | Recommended | The number of tokens used in the GenAI response (completion). When the provider reports both billed token counts and model-consumed token counts (for example, Cohere exposes both `usage.billed_units` and `usage.tokens`), instrumentations SHOULD report the billed count so the value matches the units the customer is charged for. | +| `lk.retry_count` | int | Recommended | Retries before the operation succeeded or gave up. | +| `gen_ai.input.messages` | any | Opt-in | The chat history provided to the model as an input. Messages MUST be provided in the order they were sent to the model. Instrumentations MAY provide a way for users to filter or truncate input messages. > [!Warning] > This attribute is likely to contain sensitive information including user/PII data. See [Recording content on attributes](/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) section for more details. | +| `gen_ai.output.messages` | any | Opt-in | Messages returned by the model where each message represents a specific model response (choice, candidate). Each message represents a single output choice/candidate generated by the model. Each message corresponds to exactly one generation (choice/candidate) and vice versa - one choice cannot be split across multiple messages or one message cannot contain parts from multiple choices. Instrumentations MAY provide a way for users to filter or truncate output messages. `gen_ai.response.finish_reasons` remains aligned with the generations returned by the provider, not with a filtered or truncated `gen_ai.output.messages` value. When an operation ends in an error, this attribute SHOULD only be set when the operation produced output, including partial streamed output. Instrumentation SHOULD NOT synthesize output content for a failed operation. > [!Warning] > This attribute is likely to contain sensitive information including user/PII data. See [Recording content on attributes](/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) section for more details. | +| `gen_ai.system_instructions` | any | Opt-in | The system message or instructions provided to the GenAI model separately from the chat history. This attribute SHOULD be used when the corresponding provider or API allows to provide system instructions or messages separately from the chat history. Instructions that are part of the chat history SHOULD be recorded in `gen_ai.input.messages` attribute instead. Instrumentations MAY provide a way for users to filter or truncate system instructions. > [!Warning] > This attribute may contain sensitive information. See [Recording content on attributes](/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) section for more details. | +| `gen_ai.tool.definitions` | any | Opt-in | The list of tool definitions available to the GenAI agent or model. > [!WARNING] > This attribute may contain sensitive information. Since this attribute could be large, it's NOT RECOMMENDED to populate non-required properties by default. Instrumentations MAY provide a way to enable populating optional properties. | +| `gen_ai.usage.audio.cache_read.input_tokens` | int | Opt-in | The number of audio input tokens served from a provider-managed cache. The value SHOULD be included in `gen_ai.usage.cache_read.input_tokens` and in `gen_ai.usage.audio.input_tokens`. | +| `gen_ai.usage.audio.input_tokens` | int | Opt-in | The number of audio input tokens. The value SHOULD be included in `gen_ai.usage.input_tokens`. | +| `gen_ai.usage.audio.output_tokens` | int | Opt-in | The number of audio output tokens. The value SHOULD be included in `gen_ai.usage.output_tokens`. | +| `gen_ai.usage.cache_read.input_tokens` | int | Opt-in | The number of input tokens served from a provider-managed cache. The value SHOULD be included in `gen_ai.usage.input_tokens`. | +| `gen_ai.usage.cache_write.input_tokens` | int | Opt-in | The number of input tokens written to a provider-managed cache. The value SHOULD be included in `gen_ai.usage.input_tokens`. | +| `gen_ai.usage.image.cache_read.input_tokens` | int | Opt-in | The number of image input tokens served from a provider-managed cache. The value SHOULD be included in `gen_ai.usage.cache_read.input_tokens` and in `gen_ai.usage.image.input_tokens`. | +| `gen_ai.usage.image.input_tokens` | int | Opt-in | The number of image input tokens. The value SHOULD be included in `gen_ai.usage.input_tokens`. | +| `gen_ai.usage.reasoning.output_tokens` | int | Opt-in | The number of output tokens used for reasoning (e.g. chain-of-thought, extended thinking). The value SHOULD be included in `gen_ai.usage.output_tokens`. | +| `gen_ai.usage.text.cache_read.input_tokens` | int | Opt-in | The number of text input tokens served from a provider-managed cache. The value SHOULD be included in `gen_ai.usage.cache_read.input_tokens` and in `gen_ai.usage.text.input_tokens`. | +| `gen_ai.usage.text.input_tokens` | int | Opt-in | The number of text input tokens. The value SHOULD be included in `gen_ai.usage.input_tokens`. | +| `gen_ai.usage.text.output_tokens` | int | Opt-in | The number of text output tokens. The value SHOULD be included in `gen_ai.usage.output_tokens`. | +| `lk.fallback.index` | int | Opt-in | Index of the fallback instance that served the request. | +| `lk.fallback.label` | string | Opt-in | Label of the fallback instance that served the request. | +| `lk.fallback.recovery` | boolean | Opt-in | The request was a recovery probe of a failed instance. | +| `lk.input.base_span_id` | string | Opt-in | Span whose input record this one continues. | +| `lk.input.delta` | boolean | Opt-in | The span's input record is a delta over its base span. | +| `lk.input.dropped_from_base` | int | Opt-in | Items of the base record not carried into this one. | +| `lk.provider_request_ids` | string[] | Opt-in | Provider request ids a span covers, for joining against provider logs. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `gen_ai.client.inference.operation.details` | Best effort | OTel GenAI inference details event (the GenAI registry defines it; events cannot be referenced | + +No limits declared. + +### `llm_request_run` + +Kind `internal`. The LLM request's stream being consumed. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `metrics` + +Kind `internal`. A plugin-task metric noted through the Node binding, stamped with its resolved frame. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.frame.resolution` | enum | Required | How the inbound frame of a plugin-task metric was resolved (Node binding). | +| `lk.metric` | string | Required | Metric type noted from a plugin task (Node binding `metrics` span). | +| `lk.speech_id` | string | Conditionally required: When the frame resolves to a speech. | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | + +No checkpoints. + +No limits declared. + +### `on_enter` + +Kind `internal`. The agent's `on_enter` hook. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `on_exit` + +Kind `internal`. The agent's `on_exit` hook. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `on_session_end` + +Kind `internal`. The session-end hook. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `on_user_turn_completed` + +Kind `internal`. The `on_user_turn_completed` hook. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.on_user_turn_completed_delay` | double, `s` | Recommended | Seconds the `on_user_turn_completed` hook took. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `stop_response` | Best effort | The hook stopped the response. | + +No limits declared. + +### `pause_agent_activity` + +Kind `internal`. Pausing an agent activity. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `publish_audio_output` + +Kind `internal`. Publishing the agent's audio track. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.track_sid` | string | Recommended | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | +| `lk.pre_connect_audio.duration` | double, `s` | Opt-in | Seconds of pre-connect audio replayed. | + +No checkpoints. + +No limits declared. + +### `realtime_inference` + +Kind `client`. One realtime-model generation. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `gen_ai.provider.name` | enum | Recommended | The Generative AI provider as identified by the client or server instrumentation. Semantic conventions for individual GenAI operations SHOULD clarify which kinds of providers (e.g. inference, embeddings, retrieval, memory, hosted agent providers) apply when it is not clear from context. The attribute SHOULD be set based on the instrumentation's best knowledge and may differ from the actual upstream provider. For example, a client SDK may be configured against a proxy or hosting platform that transparently relays requests to a different provider. The `gen_ai.provider.name` attribute acts as a discriminator that identifies the GenAI telemetry format flavor specific to that provider within GenAI semantic conventions. It SHOULD be set consistently with provider-specific attributes and signals. For example, GenAI spans, metrics, and events related to AWS Bedrock should have the `gen_ai.provider.name` set to `aws.bedrock` and include applicable `aws.bedrock.*` attributes and are not expected to include `openai.*` attributes. | +| `gen_ai.request.model` | string | Recommended | The name of the GenAI model a request is being made to. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | + +No checkpoints. + +No limits declared. + +### `realtime_metrics` + +Kind `internal`. Realtime-model metrics attached to a turn. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.realtime_model_metrics` | string | Recommended | Realtime model metrics of the turn, serialized. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | + +No checkpoints. + +No limits declared. + +### `resume_agent_activity` + +Kind `internal`. Resuming an agent activity. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `room_connect` + +Kind `client`. Connecting the job to its room. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.connection_state` | string | Recommended | Room connection state. | +| `lk.room.auto_subscribe` | boolean | Recommended | The room was joined with auto-subscribe. | +| `lk.room.e2ee` | boolean | Recommended | End-to-end encryption is on. | +| `lk.room.remote_participant_count` | int | Recommended | Remote participants when the agent connected. | + +No checkpoints. + +No limits declared. + +### `room_disconnect` + +Kind `client`. Disconnecting from the room. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `rpc_call` + +Kind `client`. An outgoing RPC. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `rpc.method` | string | Required | The fully-qualified logical name of the method from the RPC interface perspective. The LiveKit RPC method name. | +| `lk.rpc.error_code` | int | Conditionally required: On failure. | RPC error code. | +| `lk.rpc.destination_identity` | string | Recommended | Identity the RPC is sent to. | +| `lk.rpc.payload_size` | int, `By` | Recommended | RPC request payload size in bytes. | +| `lk.rpc.request_id` | string | Recommended | RPC request id. | +| `lk.rpc.response_size` | int, `By` | Recommended | RPC response size in bytes. | +| `lk.rpc.response_timeout` | double, `s` | Recommended | RPC response timeout in seconds. | +| `lk.pii.rpc.payload` | string | Opt-in | RPC request payload. | +| `lk.pii.rpc.response` | string | Opt-in | RPC response payload. | + +No checkpoints. + +No limits declared. + +### `rpc_handler` + +Kind `server`. An incoming RPC handled by the agent. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `rpc.method` | string | Required | The fully-qualified logical name of the method from the RPC interface perspective. The LiveKit RPC method name. | +| `lk.rpc.error_code` | int | Conditionally required: On failure. | RPC error code. | +| `lk.rpc.caller_identity` | string | Recommended | Identity of the RPC caller. A participant identity outside `lk.pii.*`, as Agents 2.0 ships it; the registry records it rather than renaming it. | +| `lk.rpc.handler_registered` | boolean | Recommended | A handler was registered for the method. | +| `lk.rpc.payload_size` | int, `By` | Recommended | RPC request payload size in bytes. | +| `lk.rpc.request_id` | string | Recommended | RPC request id. | +| `lk.rpc.response_size` | int, `By` | Recommended | RPC response size in bytes. | +| `lk.rpc.response_timeout` | double, `s` | Recommended | RPC response timeout in seconds. | +| `lk.pii.rpc.payload` | string | Opt-in | RPC request payload. | +| `lk.pii.rpc.response` | string | Opt-in | RPC response payload. | + +No checkpoints. + +No limits declared. + +### `session_close` + +Kind `internal`. Closing the session. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.close.drain` | boolean | Recommended | The close drained pending speech first. | +| `lk.close_reason` | string | Recommended | Why the session closed. | + +No checkpoints. + +No limits declared. + +### `session_end_upload` + +Kind `internal`. Uploading the session report. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `session_start` + +Kind `internal`. Starting the session (room I/O, first activity). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `setup_toolsets` + +Kind `internal`. Initializing the agent's tool sets. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.tool_sets` | string[] | Recommended | Tool set names offered. | + +No checkpoints. + +No limits declared. + +### `shutdown_callback` + +Kind `internal`. One shutdown callback. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.callback.name` | string | Required | Name of the shutdown callback. | + +No checkpoints. + +No limits declared. + +### `start_agent_activity` + +Kind `internal`. Starting an agent activity. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | + +No checkpoints. + +No limits declared. + +### `tts_fallback_adapter` + +Kind `internal`. The TTS fallback adapter choosing an instance. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.fallback.index` | int | Recommended | Index of the fallback instance that served the request. | +| `lk.fallback.label` | string | Recommended | Label of the fallback instance that served the request. | +| `lk.fallback.recovery` | boolean | Recommended | The request was a recovery probe of a failed instance. | + +No checkpoints. + +No limits declared. + +### `tts_node` + +Kind `internal`. The agent's TTS node for one generation. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | +| `lk.response.ttfb` | double, `s` | Recommended | Seconds to the first TTS audio byte. | +| `lk.speech_id` | string | Recommended | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | +| `lk.tts.label` | string | Recommended | Label of the TTS instance. | +| `lk.tts.streaming` | boolean | Recommended | The TTS instance streams (duplex path). | +| `lk.pii.input_text` | string | Opt-in | Text sent to the TTS. | + +No checkpoints. + +No limits declared. + +### `tts_request` + +Kind `client`. One TTS request. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `error.type` | enum | Conditionally required: On failure. | Describes a class of error the operation ended with. The `error.type` SHOULD be predictable, and SHOULD have low cardinality. When `error.type` is set to a type (e.g., an exception type), its canonical class name identifying the type within the artifact SHOULD be used. If the recorded error type is a wrapper that is not meaningful for failure classification, instrumentation MAY use the type of the inner error instead. For example, in Go, errors created with `fmt.Errorf` using `%w` MAY be unwrapped when the wrapper type does not help classify the failure. Instrumentations SHOULD document the list of errors they report. The cardinality of `error.type` within one instrumentation library SHOULD be low. Telemetry consumers that aggregate data from multiple instrumentation libraries and applications should be prepared for `error.type` to have high cardinality at query time when no additional filters are applied. If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), it's RECOMMENDED to: - Use a domain-specific attribute - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. | +| `lk.retry_count` | int | Recommended | Retries before the operation succeeded or gave up. | +| `lk.tts.label` | string | Recommended | Label of the TTS instance. | +| `gen_ai.provider.name` | enum | Opt-in | The Generative AI provider as identified by the client or server instrumentation. Semantic conventions for individual GenAI operations SHOULD clarify which kinds of providers (e.g. inference, embeddings, retrieval, memory, hosted agent providers) apply when it is not clear from context. The attribute SHOULD be set based on the instrumentation's best knowledge and may differ from the actual upstream provider. For example, a client SDK may be configured against a proxy or hosting platform that transparently relays requests to a different provider. The `gen_ai.provider.name` attribute acts as a discriminator that identifies the GenAI telemetry format flavor specific to that provider within GenAI semantic conventions. It SHOULD be set consistently with provider-specific attributes and signals. For example, GenAI spans, metrics, and events related to AWS Bedrock should have the `gen_ai.provider.name` set to `aws.bedrock` and include applicable `aws.bedrock.*` attributes and are not expected to include `openai.*` attributes. | +| `gen_ai.request.model` | string | Opt-in | The name of the GenAI model a request is being made to. | +| `lk.provider_request_ids` | string[] | Opt-in | Provider request ids a span covers, for joining against provider logs. | + +No checkpoints. + +No limits declared. + +### `tts_request_run` + +Kind `internal`. The TTS request's stream being consumed. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `tts_stream` + +Kind `internal`. The Node binding driving `Tts::stream` (the duplex path). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.tts.label` | string | Required | Label of the TTS instance. | +| `lk.error.retryable` | boolean | Conditionally required: On failure. | The provider error is retryable (Node binding). | +| `lk.tts.bytes` | int, `By` | Conditionally required: On success. | Audio bytes the TTS driver collected (Node binding). | +| `lk.tts.frames` | int | Conditionally required: On success. | Audio frames the TTS driver collected (Node binding). | +| `lk.tts.no_duplex_path` | boolean | Conditionally required: When the instance has no duplex path. | The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure). | + +No checkpoints. + +No limits declared. + +### `tts_stream_adapter` + +Kind `internal`. The sentence-splitting adapter over a non-streaming TTS. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.tts.label` | string | Recommended | Label of the TTS instance. | + +No checkpoints. + +No limits declared. + +### `tts_synthesize` + +Kind `internal`. The Node binding driving `Tts::synthesize` (the simplex path). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.tts.label` | string | Required | Label of the TTS instance. | +| `lk.tts.streaming` | boolean | Required | The TTS instance streams (duplex path). | +| `lk.error.retryable` | boolean | Conditionally required: On failure. | The provider error is retryable (Node binding). | +| `lk.tts.bytes` | int, `By` | Conditionally required: On success. | Audio bytes the TTS driver collected (Node binding). | +| `lk.tts.frames` | int | Conditionally required: On success. | Audio frames the TTS driver collected (Node binding). | + +No checkpoints. + +No limits declared. + +### `update_agent` + +Kind `internal`. Replacing the session's agent. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.agent_label` | string | Recommended | Label of the agent running the activity. | +| `lk.previous_agent_label` | string | Recommended | Label of the agent handed off from. | + +No checkpoints. + +No limits declared. + +### `user_speaking` + +Kind `internal`. The user speaking (VAD). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | + +No checkpoints. + +No limits declared. + +### `user_turn` + +Kind `internal`. One user turn, from speech to the turn decision. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.end_of_turn_delay` | double, `s` | Recommended | Seconds from the end of speech to the turn decision. | +| `lk.eou.resume_count` | int | Recommended | Endpointing waits the user cut short by speaking again. | +| `lk.eou.source` | string | Recommended | What decided the end of the turn. | +| `lk.is_interruption` | boolean | Recommended | The user turn was classified as an interruption. | +| `lk.stt.events` | string | Recommended | JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length). | +| `lk.transcript_confidence` | double | Recommended | Confidence of the final transcript. | +| `lk.transcription_delay` | double, `s` | Recommended | Seconds from the end of speech to the final transcript. | +| `lk.interruption.detection_delay` | double, `s` | Opt-in | Seconds from the user's speech to the interruption decision. | +| `lk.interruption.prediction_duration` | double, `s` | Opt-in | Seconds the interruption prediction took. | +| `lk.interruption.probability` | double | Opt-in | Interruption probability. | +| `lk.interruption.total_duration` | double, `s` | Opt-in | Seconds of the interruption from detection to resolution. | +| `lk.pii.interim_transcript` | string | Opt-in | An interim (non-final) transcript, on a log record. On the interim-transcript log records of the turn, not on the span. | +| `lk.pii.user_transcript` | string | Opt-in | Transcript the detector scored. | + +No checkpoints. + +No limits declared. + +### `wait_for_audio_track` + +Kind `internal`. Waiting for the participant's audio track. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.first_frame_delay` | double, `s` | Recommended | Seconds from waiting for a track to its first frame. | +| `lk.track_sid` | string | Recommended | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | +| `lk.track_source` | enum | Recommended | Source of the track, as the Agents RTC FFI `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `track_subscribed` | Best effort | The track was subscribed, with `lk.track_sid` and `lk.track_source`. | +| `first_frame` | Best effort | The first frame arrived. | +| `pre_connect_audio` | Best effort | Pre-connect audio was replayed, with `lk.pre_connect_audio.duration`. | + +No limits declared. + +### `wait_for_participant` + +Kind `internal`. Waiting for the participant to link to. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.room_io.participant_filter` | string | Recommended | How room I/O picked its participant. | + +No checkpoints. + +No limits declared. + +### `wait_for_video_track` + +Kind `internal`. Waiting for the participant's video track. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.first_frame_delay` | double, `s` | Recommended | Seconds from waiting for a track to its first frame. | +| `lk.track_sid` | string | Recommended | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | +| `lk.track_source` | enum | Recommended | Source of the track, as the Agents RTC FFI `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | + +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `track_subscribed` | Best effort | The track was subscribed, with `lk.track_sid` and `lk.track_source`. | +| `first_frame` | Best effort | The first frame arrived. | + +No limits declared. + +## Attribute reference + +Every key this registry defines, with its meaning; the OTel keys used above are defined by OTel. +"Used on" names the attribute sets, events and spans that reference the key. + +| Key | Type | Owner | Sensitivity | Description | Examples | Used on | +| --- | --- | --- | --- | --- | --- | --- | +| `agent_name` | string | agents | none | The agent's name. | `support-agent` | | +| `browser.name` | string | client | none | Browser name. Resource attribute, JS port only. | `Chrome`, `Safari`, `Firefox` | resource | +| `browser.version` | string | client | none | Browser version. Resource attribute, JS port only. | `124.0` | resource | +| `chat.item` | any | agents | pii | One chat history item, as a map. Carries conversation content. With redaction on, the collector drops the whole `chat.item` record. | `{"type": "message", "role": "user"}` | | +| `connection_reused` | string | agents | none | Whether a pooled connection was reused, as the string `true`/`false`. | `true` | | +| `evaluation` | any | agents | none | One evaluation result, as a map of string fields. | `{"name": "task_completion", "verdict": "pass"}` | | +| `gen_ai.token.type` | string | agents | none | Token type (`input` or `output`); deprecated upstream, a dimension of `gen_ai.client.token.usage`. | `input`, `output` | | +| `gen_ai.usage.input_audio_tokens` | int | agents | none | LangFuse spelling of audio input tokens. | `800` | | +| `gen_ai.usage.input_cached_tokens` | int | agents | none | LangFuse spelling of cached input tokens. | `64` | | +| `gen_ai.usage.input_text_tokens` | int | agents | none | LangFuse spelling of text input tokens. | `120` | | +| `gen_ai.usage.output_audio_tokens` | int | agents | none | LangFuse spelling of audio output tokens. | `300` | | +| `gen_ai.usage.output_text_tokens` | int | agents | none | LangFuse spelling of text output tokens. | `40` | | +| `gen_ai.usage.reasoning_tokens` | int | agents | none | LangFuse spelling of reasoning tokens. | `12` | | +| `langfuse.observation.completion_start_time` | string | agents | none | LangFuse's completion start time (ISO 8601), set next to `gen_ai.response.time_to_first_chunk`. | `2026-10-19T12:00:00.000Z` | | +| `lk.agent_label` | string | agents | none | Label of the agent running the activity. | `Assistant` | `drain_agent_activity` `llm_node` `on_enter` `on_exit` `pause_agent_activity` `resume_agent_activity` `start_agent_activity` `tts_node` `update_agent` | +| `lk.agent_name` | string | shared | none | Name of the agent a job was dispatched to. On the Agents Resource and the `job_entrypoint` and `agent_session` spans, set when the job has an agent name (explicit dispatch). A client that reports the agent it talks to uses this key. The participant attribute `lk.agent.name` carries the same name on the room; it is not a telemetry key. | `my-agent` | `agent_session` `job_entrypoint` | +| `lk.amd.category` | string | agents | none | AMD classification. | `human`, `machine` | `amd` | +| `lk.amd.delay` | double, `s` | agents | none | Seconds from the call start to the AMD decision. | `3.0` | `amd` | +| `lk.amd.reason` | string | agents | none | Why the AMD classifier decided. | `greeting_length` | `amd` | +| `lk.amd.speech_duration` | double, `s` | agents | none | Seconds of speech the AMD classifier heard. | `2.1` | `amd` | +| `lk.blocking.count` | int | agents | none | Blocks in the session. | `2` | `agent_session` | +| `lk.blocking.cpu_time` | double, `s` | agents | none | CPU seconds the loop thread consumed during the block. | `0.25` | `event_loop_blocked` | +| `lk.blocking.duration` | double, `s` | agents | none | Seconds the loop was blocked. | `0.3` | `event_loop_blocked` | +| `lk.blocking.gc_time` | double, `s` | agents | none | Seconds of garbage collection on the loop thread during the block. | `0.02` | `event_loop_blocked` | +| `lk.blocking.import` | string | agents | none | Module a lazy import was loading when sampled. | `numpy` | `event_loop_blocked` | +| `lk.blocking.max_duration` | double, `s` | agents | none | Seconds of the longest block in the session. | `0.4` | `agent_session` | +| `lk.blocking.severity` | string | agents | none | Severity bucket of the block. | `warning` | `event_loop_blocked` | +| `lk.blocking.stack` | string | agents | none | Loop-thread stack sampled while blocked (source locations only). | `agent.py:42` | `event_loop_blocked` | +| `lk.blocking.suppressed` | int | agents | none | Reports dropped by rate limiting since the previous emitted span. | `0` | `event_loop_blocked` | +| `lk.blocking.task` | string | agents | none | Name of the task that was running. | `Task-12` | `event_loop_blocked` | +| `lk.blocking.threshold` | double, `s` | agents | none | Seconds above which a block is reported. | `0.1` | `event_loop_blocked` | +| `lk.blocking.total_duration` | double, `s` | agents | none | Seconds blocked in total over the session. | `0.6` | `agent_session` | +| `lk.callback.name` | string | agents | none | Name of the shutdown callback. | `flush_transcript` | `shutdown_callback` | +| `lk.close.drain` | boolean | agents | none | The close drained pending speech first. | `true` | `session_close` | +| `lk.close_reason` | string | agents | none | Why the session closed. | `user_initiated` | `session_close` | +| `lk.cloud_agent_id` | string | agents | none | LiveKit Cloud agent id. | `CA_abc123` | `job_entrypoint` | +| `lk.connect.attempt` | int | client | none | 1-based connect attempt. 1 for the user-initiated connect; a platform that retries sets the next number on the next `lk.connect` span. | `1` | `lk.connect` | +| `lk.connection_state` | string | agents | none | Room connection state. | `connected` | `room_connect` | +| `lk.deployment_id` | string | agents | none | LiveKit Cloud deployment id. | `DP_abc123` | `job_entrypoint` | +| `lk.device.app_state` | enum | client | none | Whether the app is in front of the user. All platforms; the web maps page visibility. | | `lk.device.app_state.changed` | +| `lk.device.audio.interruption` | enum | client | none | Phase of an audio session interruption. iOS audio session interruptions; Android audio focus loss and gain. | | `lk.device.audio.interruption` | +| `lk.device.audio_route.outputs` | string | client | none | The active outputs, comma-separated. Each item is one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. A string, not an array: the vocabulary is closed but the list is joined. | `bluetooth`, `speaker,usb` | `lk.device.audio_route.changed` | +| `lk.device.audio_route.reason` | enum | client | none | Why the audio route changed. AVAudioSession route change reasons on iOS; Android maps its audio device callbacks; `unknown` where the platform gives none. | | `lk.device.audio_route.changed` | +| `lk.device.battery.charging` | boolean | client | none | The device is charging. Sent together with `lk.device.battery.level`. | `false` | `lk.device.battery.changed` | +| `lk.device.battery.level` | int, `%` | client | none | Battery charge in percent, 0-100. Only where the level is known (iOS, Android; not desktops or tvOS); never from the JS port. An integer percentage, not a 0-1 ratio. | `42` | `lk.device.battery.changed` | +| `lk.device.capture.device` | enum | client | none | Capture device that failed. All platforms. | | `lk.device.capture.failed` | +| `lk.device.capture.reason` | enum | client | none | Why capture failed, in the getUserMedia failure taxonomy. iOS: authorization status and capture interruptions; Android: permission checks and camera callbacks; web: DOMException names. | | `lk.device.capture.failed` | +| `lk.device.low_power.enabled` | boolean | client | none | Low Power Mode / Battery Saver is on. Absent where the platform has no source. iOS, macOS and Android; optional elsewhere. | `true` | `lk.device.low_power.changed` | +| `lk.device.memory.pressure` | enum | client | none | Memory pressure as the OS reports it. Apple `DispatchSource` memory-pressure levels; Android `onTrimMemory`: RUNNING_LOW and BACKGROUND map to `warning`, RUNNING_CRITICAL and COMPLETE to `critical`. iOS, macOS and Android; optional elsewhere. | | `lk.device.memory.changed` | +| `lk.device.network.constrained` | boolean | client | none | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver; on the web `navigator.connection.saveData`, only where the Network Information API exists. | `false` | `lk.device.network.changed` | +| `lk.device.network.expensive` | boolean | client | none | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; on the web only where the Network Information API exists. | `false` | `lk.device.network.changed` | +| `lk.device.thermal.state` | enum | client | none | Thermal pressure as the OS reports it. Apple `ProcessInfo.thermalState`, Android `PowerManager` thermal status, web Compute Pressure API, all mapped to this vocabulary. The platform's `unknown` (no thermal source) is no reading and is never emitted. iOS, macOS and Android; optional elsewhere. | | `lk.device.thermal.changed` | +| `lk.disconnect_reason` | enum | shared | none | Why a participant left the room, as the protobuf `DisconnectReason` enum name. On the client `lk.room.disconnected` event (the local participant) and on the Agents `participant_disconnected` checkpoint (the linked participant). `RECONNECT_FAILED` is the one value outside the protobuf enum: a client that ran out of reconnect attempts sends it. | | `lk.room.disconnected` `agent_session` | +| `lk.dispatch_id` | string | agents | none | Agent dispatch id. | `AD_abc123` | `job_entrypoint` | +| `lk.e2e_latency` | double, `s` | agents | none | Seconds from the end of the user's speech to the agent's first audio. | `1.1` | `agent_turn` | +| `lk.end_of_turn_delay` | double, `s` | agents | none | Seconds from the end of speech to the turn decision. | `0.7` | `agent_turn` `user_turn` | +| `lk.eou.detection_delay` | double, `s` | agents | none | Seconds the detector inference took. | `0.08` | `eou_detection` | +| `lk.eou.endpointing_delay` | double, `s` | agents | none | Seconds of endpointing delay applied. | `0.5` | `eou_wait` | +| `lk.eou.from_cache` | boolean | agents | none | The detector answer came from the cache. | `false` | `eou_detection` | +| `lk.eou.language` | string | agents | none | Language the detector ran for. | `en` | `eou_detection` | +| `lk.eou.not_committed_count` | int | agents | none | Turn decisions the wait rejected before it ended. | `0` | `eou_wait` | +| `lk.eou.outcome` | enum | agents | none | How the endpointing wait ended. | | `eou_wait` | +| `lk.eou.probability` | double | agents | none | End-of-utterance probability. | `0.93` | `eou_detection` | +| `lk.eou.rearm_count` | int | agents | none | Times the endpointing wait restarted on a later trigger. | `0` | `eou_wait` | +| `lk.eou.resume_count` | int | agents | none | Endpointing waits the user cut short by speaking again. | `1` | `user_turn` | +| `lk.eou.source` | string | agents | none | What decided the end of the turn. | `turn_detector`, `vad` | `user_turn` | +| `lk.eou.unlikely_threshold` | double | agents | none | Threshold under which the turn is unlikely to be over. | `0.15` | `eou_detection` | +| `lk.eou.wait_duration` | double, `s` | agents | none | Seconds from the end of the user's speech to the turn decision. | `0.6` | `eou_wait` | +| `lk.error.retryable` | boolean | agents | none | The provider error is retryable (Node binding). | `true` | `tts_stream` `tts_synthesize` | +| `lk.fail` | boolean | agents | none | The session report is tagged failed. See `lk.success`. | `true` | `agent_session` | +| `lk.fallback.index` | int | agents | none | Index of the fallback instance that served the request. | `1` | `llm_fallback_adapter` `llm_request` `tts_fallback_adapter` | +| `lk.fallback.label` | string | agents | none | Label of the fallback instance that served the request. | `openai.LLM` | `llm_fallback_adapter` `llm_request` `tts_fallback_adapter` | +| `lk.fallback.recovery` | boolean | agents | none | The request was a recovery probe of a failed instance. | `false` | `llm_fallback_adapter` `llm_request` `tts_fallback_adapter` | +| `lk.first_frame_delay` | double, `s` | agents | none | Seconds from waiting for a track to its first frame. | `0.25` | `wait_for_audio_track` `wait_for_video_track` | +| `lk.frame.resolution` | enum | agents | none | How the inbound frame of a plugin-task metric was resolved (Node binding). | | `metrics` | +| `lk.function_tool.id` | string | agents | none | Tool call id. | `call_7` | `function_tool` | +| `lk.function_tool.is_error` | boolean | agents | none | The tool returned an error. | `false` | `function_tool` | +| `lk.function_tool.name` | string | agents | none | Tool name. | `lookup_weather` | `function_tool` | +| `lk.function_tools` | string[] | agents | none | Function tool names offered. | `["lookup_weather"]` | `llm_node` | +| `lk.generation_count` | int | agents | none | Generations in the turn. | `1` | `agent_turn` | +| `lk.generation_id` | string | agents | none | Id of the agent turn (generation). | `gen_abc123` | `agent_turn` | +| `lk.input.base_span_id` | string | agents | none | Span whose input record this one continues. | `7f3c2a1b9e8d4c5f` | `llm_node` `llm_request` | +| `lk.input.delta` | boolean | agents | none | The span's input record is a delta over its base span. | `true` | `llm_node` `llm_request` | +| `lk.input.dropped_from_base` | int | agents | none | Items of the base record not carried into this one. | `0` | `llm_node` `llm_request` | +| `lk.interrupted` | boolean | agents | none | The speech was interrupted. | `true` | `agent_speaking` `agent_turn` | +| `lk.interruption.detection_delay` | double, `s` | agents | none | Seconds from the user's speech to the interruption decision. | `0.2` | `user_turn` | +| `lk.interruption.prediction_duration` | double, `s` | agents | none | Seconds the interruption prediction took. | `0.05` | `user_turn` | +| `lk.interruption.probability` | double | agents | none | Interruption probability. | `0.8` | `user_turn` | +| `lk.interruption.source` | string | agents | none | What triggered the interruption. | `vad`, `manual` | `agent_speaking` | +| `lk.interruption.total_duration` | double, `s` | agents | none | Seconds of the interruption from detection to resolution. | `0.5` | `user_turn` | +| `lk.is_interruption` | boolean | agents | none | The user turn was classified as an interruption. | `true` | `user_turn` | +| `lk.job.accept_latency` | double, `s` | agents | none | Seconds from the job request to its acceptance. | `0.012` | `job_entrypoint` | +| `lk.job.agent_id` | string | agents | none | Agent id of the job. | `AG_abc123` | `job_entrypoint` | +| `lk.job.assignment_latency` | double, `s` | agents | none | Seconds from acceptance to the job assignment. | `0.3` | `job_entrypoint` | +| `lk.job.dispatch_latency` | double, `s` | agents | none | Seconds from the dispatch request to the entrypoint running. | `0.9` | `job_entrypoint` | +| `lk.job.entrypoint_latency` | double, `s` | agents | none | Seconds from launch to the entrypoint running. | `0.1` | `job_entrypoint` | +| `lk.job.launch_latency` | double, `s` | agents | none | Seconds from assignment to the job process launch. | `0.5` | `job_entrypoint` | +| `lk.job.worker_id` | string | agents | none | Worker id the job runs on. | `AW_abc123` | `job_entrypoint` | +| `lk.job_id` | string | shared | none | Agent job ID (`AJ_...`). Sent by Agents, not by clients. High cardinality; Agents' job-scoped metrics are the one sanctioned metric-dimension exception. | `AJ_kVbMBWtnVHvT` | `job_entrypoint` | +| `lk.keyterms.added` | int | agents | none | Keyterms added by the update. | `3` | `keyterm_detection` | +| `lk.keyterms.count` | int | agents | none | Number of keyterms configured. | `12` | `keyterm_detection` | +| `lk.keyterms.removed` | int | agents | none | Keyterms removed by the update. | `1` | `keyterm_detection` | +| `lk.llm_metrics` | string | agents | none | LLM metrics of the turn, serialized. | `{}` | `agent_turn` | +| `lk.log.logger` | string | client | none | Logger, type, module or file that wrote the line. Free text from the platform's logger. | `Room`, `livekit::rtc_engine` | log | +| `lk.log.source` | enum | client | none | Which layer wrote the log line. Only WARN and ERROR records are sent; WebRTC lines only at ERROR. | | log | +| `lk.metric` | string | agents | none | Metric type noted from a plugin task (Node binding `metrics` span). | `tts_metrics` | `metrics` | +| `lk.new_state` | string | agents | none | State after a transition. | `speaking` | | +| `lk.old_state` | string | agents | none | State before a transition. | `listening` | | +| `lk.on_user_turn_completed_delay` | double, `s` | agents | none | Seconds the `on_user_turn_completed` hook took. | `0.01` | `agent_turn` `on_user_turn_completed` | +| `lk.outcome` | enum | client | none | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer must drop an attribute of this name set any other way. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | | `lk.connect` `lk.publish` `lk.reconnect` `lk.subscribe` | +| `lk.parent_generation_id` | string | agents | none | Generation this one continues (a tool-call follow-up). | `gen_abc122` | `agent_turn` | +| `lk.participant_id` | string | shared | none | Server-assigned SID of the local participant (`PA_...`). On every record and span of a session once the client has joined. High cardinality, never a default metric dimension. | `PA_9kLm2NpQ4rTs` | record `agent_session` | +| `lk.participant_kind` | string | agents | none | Kind of the linked participant (protocol `ParticipantInfo.Kind`). | `standard`, `sip` | `agent_session` | +| `lk.pii.amd.transcript` | string | agents | pii | Transcript the AMD classifier used. | `hello?` | `amd` | +| `lk.pii.arguments` | string | agents | pii | Tool-call arguments on a log record. | `{}` | | +| `lk.pii.chat_ctx` | string | agents | pii | Serialized chat context sent to the LLM. | `[]` | `llm_node` | +| `lk.pii.dtmf_inputs` | string | agents | pii | DTMF digits received. | `1234` | | +| `lk.pii.error` | string | agents | pii | Error text that may quote user content. | `expected ','` | | +| `lk.pii.event` | string | agents | pii | Raw provider event on a debug log record. | `{}` | | +| `lk.pii.function_tool.arguments` | string | agents | pii | Tool call arguments. | `{"city":"Paris"}` | `function_tool` | +| `lk.pii.function_tool.output` | string | agents | pii | Tool output. | `sunny` | `function_tool` | +| `lk.pii.input_text` | string | agents | pii | Text sent to the TTS. | `Hello` | `tts_node` | +| `lk.pii.instructions` | string | agents | pii | Agent instructions in force. | `You are a helpful assistant.` | `agent_turn` | +| `lk.pii.interim_transcript` | string | agents | pii | An interim (non-final) transcript, on a log record. | `book a` | `user_turn` | +| `lk.pii.keyterms` | string | agents | pii | The keyterms themselves (customer vocabulary). | `LiveKit, WebRTC` | `keyterm_detection` | +| `lk.pii.output` | string | agents | pii | Tool-call output on a log record. | `ok` | | +| `lk.pii.participant_identity` | string | shared | pii | Customer-supplied identity of the local participant. On every record and span of a session once the client has joined. | `identity-0` | record `agent_session` | +| `lk.pii.progress_message` | string | agents | pii | MCP tool progress message. | `fetching` | | +| `lk.pii.raw_arguments` | string | agents | pii | Malformed tool-call arguments as received. | `{'x':1}` | | +| `lk.pii.remote_participant_identity` | string | shared | pii | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). On subscribe spans: the identity of the participant publishing the track. | `identity-1` | `lk.subscribe` | +| `lk.pii.repaired` | string | agents | pii | Tool-call arguments after repair. | `{"x":1}` | | +| `lk.pii.response.function_calls` | string | agents | pii | Function calls the LLM produced, serialized. | `[]` | `llm_node` | +| `lk.pii.response.text` | string | agents | pii | Text the LLM produced. | `Sure.` | `llm_node` | +| `lk.pii.room_name` | string | shared | pii | Customer-supplied room name. On every record and span of a session once the client has joined. | `my-room` | record `job_entrypoint` | +| `lk.pii.rpc.payload` | string | agents | pii | RPC request payload. | `{"q":1}` | `rpc_call` `rpc_handler` | +| `lk.pii.rpc.response` | string | agents | pii | RPC response payload. | `ok` | `rpc_call` `rpc_handler` | +| `lk.pii.sip.phoneNumber` | string | agents | pii | Phone number of a SIP participant. Camel case as the SIP participant attribute it is copied from; registered as emitted. | `+15550100` | `agent_session` | +| `lk.pii.text` | string | agents | pii | Message text on a log record. | `hello` | | +| `lk.pii.transcript` | string | agents | pii | Transcript text on a log record. | `hello` | | +| `lk.pii.user_input` | string | agents | pii | The user's transcript for the turn. | `book a table` | `agent_turn` | +| `lk.pii.user_transcript` | string | agents | pii | Transcript the detector scored. | `book a table` | `eou_detection` `user_turn` | +| `lk.ping.seq` | int | client | none | Monotonically increasing sequence number per pipeline. Optional; on `lk.ping` only. | `1` | `lk.ping` | +| `lk.playout.position` | double, `s` | agents | none | Seconds of the agent's speech played out when it was interrupted. | `1.2` | `agent_speaking` | +| `lk.pre_connect_audio.duration` | double, `s` | agents | none | Seconds of pre-connect audio replayed. | `1.5` | `publish_audio_output` | +| `lk.previous_agent_label` | string | agents | none | Label of the agent handed off from. | `Greeter` | `update_agent` | +| `lk.provider_request_ids` | string[] | agents | none | Provider request ids a span covers, for joining against provider logs. | `["req_abc123"]` | `agent_turn` `llm_request` `tts_request` | +| `lk.provider_tools` | string[] | agents | none | Provider-side tool names offered. | `["web_search"]` | `llm_node` | +| `lk.realtime_model_metrics` | string | agents | none | Realtime model metrics of the turn, serialized. | `{}` | `agent_turn` `realtime_metrics` | +| `lk.reconnect.attempts` | int | client | none | Reconnect attempts made in the cycle. Equals `` of the last `attempt ` checkpoint. | `2` | `lk.reconnect` | +| `lk.reconnect.mode` | enum | client | none | Mode of the last reconnect attempt. Updated with each `attempt ` checkpoint; matches the last one. | | `lk.reconnect` | +| `lk.reconnect.reason` | enum | client | none | What triggered the reconnect cycle. The protocol's `ReconnectReason` (`RR_*`), plus reasons only a client knows. | | `lk.reconnect` | +| `lk.redaction.enabled` | boolean | agents | none | PII redaction was requested for the session. Telemetry metadata key; the producer's own `pii` filter reads it from the span attributes. | `true` | `job_entrypoint` | +| `lk.response.ttfb` | double, `s` | agents | none | Seconds to the first TTS audio byte. | `0.3` | `tts_node` | +| `lk.response.ttft` | double, `s` | agents | none | Seconds to the first LLM token. | `0.42` | `llm_node` | +| `lk.retry_count` | int | agents | none | Retries before the operation succeeded or gave up. | `0`, `2` | `llm_request` `tts_request` | +| `lk.room.auto_subscribe` | boolean | agents | none | The room was joined with auto-subscribe. | `true` | `room_connect` | +| `lk.room.e2ee` | boolean | agents | none | End-to-end encryption is on. | `false` | `room_connect` | +| `lk.room.remote_participant_count` | int | agents | none | Remote participants when the agent connected. | `1` | `room_connect` | +| `lk.room_io.participant_filter` | string | agents | none | How room I/O picked its participant. | `identity`, `kind` | `wait_for_participant` | +| `lk.room_sid` | string | shared | none | Server-assigned room session SID (`RM_...`). On every record and span of a session once the client has joined; absent before join. High cardinality, never a default metric dimension. | `RM_5Fq8xQ2ZbXwN` | record `job_entrypoint` | +| `lk.rpc.caller_identity` | string | agents | none | Identity of the RPC caller. A participant identity outside `lk.pii.*`, as Agents 2.0 ships it; the registry records it rather than renaming it. | `identity-0` | `rpc_handler` | +| `lk.rpc.destination_identity` | string | agents | none | Identity the RPC is sent to. | `identity-1` | `rpc_call` | +| `lk.rpc.error_code` | int | agents | none | RPC error code. | `1400` | `rpc_call` `rpc_handler` | +| `lk.rpc.handler_registered` | boolean | agents | none | A handler was registered for the method. | `true` | `rpc_handler` | +| `lk.rpc.payload_size` | int, `By` | agents | none | RPC request payload size in bytes. | `42` | `rpc_call` `rpc_handler` | +| `lk.rpc.request_id` | string | agents | none | RPC request id. | `rpc_abc123` | `rpc_call` `rpc_handler` | +| `lk.rpc.response_size` | int, `By` | agents | none | RPC response size in bytes. | `2` | `rpc_call` `rpc_handler` | +| `lk.rpc.response_timeout` | double, `s` | agents | none | RPC response timeout in seconds. | `10.0` | `rpc_call` `rpc_handler` | +| `lk.rtc.audio_level.avg` | double, `1` | client | none | Average audio level in the window, 0-1, inbound audio. Inbound `audioLevel`, averaged over the window's readings that report it. | `0.05` | `lk.rtc.stats.sample` | +| `lk.rtc.audio_level.max` | double, `1` | client | none | Maximum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | `0.3` | `lk.rtc.stats.sample` | +| `lk.rtc.audio_level.min` | double, `1` | client | none | Minimum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | `0.0` | `lk.rtc.stats.sample` | +| `lk.rtc.bytes` | int, `By` | client | none | Bytes sent or received (cumulative). Outbound `bytesSent`, inbound `bytesReceived`. | `1048576` | `lk.rtc.stats.sample` | +| `lk.rtc.codec` | string | client | none | Codec mimeType, when known. The `codec` stats entry the RTP stream's `codecId` names. | `audio/opus`, `video/VP8` | `lk.rtc.stats.sample` | +| `lk.rtc.concealed_samples` | int, `{sample}` | client | none | Concealed audio samples, inbound audio (cumulative). Inbound `concealedSamples`. | `480` | `lk.rtc.stats.sample` | +| `lk.rtc.concealment_events` | int, `{event}` | client | none | Concealment events, inbound audio (cumulative). Inbound `concealmentEvents`. | `1` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.avg` | double, `{frame}/s` | client | none | Average frame rate in the window, video. `framesPerSecond`, either direction, averaged over the window's readings that report it. | `29.7` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.max` | double, `{frame}/s` | client | none | Maximum frame rate in the window, video. `framesPerSecond`, either direction. | `30.0` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.min` | double, `{frame}/s` | client | none | Minimum frame rate in the window, video. `framesPerSecond`, either direction. | `24.0` | `lk.rtc.stats.sample` | +| `lk.rtc.freeze_count` | int, `{freeze}` | client | none | Video freezes, inbound video (cumulative). Inbound `freezeCount`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.freezes_duration_ms` | int, `ms` | client | none | Total freeze time, inbound video (cumulative). Inbound `totalFreezesDuration`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.interruption_count` | int, `{interruption}` | client | none | Audio interruptions, inbound audio (cumulative). Inbound `interruptionCount`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.interruptions_duration_ms` | int, `ms` | client | none | Total interruption time, inbound audio (cumulative). Inbound `totalInterruptionDuration`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_buffer_delay_ms` | int, `ms` | client | none | Jitter buffer delay, inbound (cumulative). Inbound `jitterBufferDelay`. Average delay = jitter_buffer_delay_ms / jitter_buffer_emitted_count. | `1200` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_buffer_emitted_count` | int, `{sample}` | client | none | Samples or frames emitted by the jitter buffer, inbound (cumulative). Inbound `jitterBufferEmittedCount`. | `48000` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.avg` | double, `ms` | client | none | Average jitter in the window, inbound. Inbound `jitter` (seconds × 1000), averaged over the window's readings that report it. | `1.8` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.max` | double, `ms` | client | none | Maximum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | `4.2` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.min` | double, `ms` | client | none | Minimum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | `0.5` | `lk.rtc.stats.sample` | +| `lk.rtc.packets` | int, `{packet}` | client | none | Packets sent or received (cumulative). Outbound `packetsSent`, inbound `packetsReceived`. | `3000` | `lk.rtc.stats.sample` | +| `lk.rtc.packets_lost` | int, `{packet}` | client | none | Packets lost, inbound (cumulative). Inbound `packetsLost`. Loss ratio = packets_lost / (packets + packets_lost). | `3` | `lk.rtc.stats.sample` | +| `lk.rtc.pause_count` | int, `{pause}` | client | none | Video pauses, inbound video (cumulative). Inbound `pauseCount`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.pauses_duration_ms` | int, `ms` | client | none | Total pause time, inbound video (cumulative). Inbound `totalPausesDuration`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.bandwidth_ms` | int, `ms` | client | none | Time the encoder was limited by bandwidth, outbound video (cumulative). Outbound `qualityLimitationDurations.bandwidth`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.cpu_ms` | int, `ms` | client | none | Time the encoder was limited by CPU, outbound video (cumulative). Outbound `qualityLimitationDurations.cpu`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.other_ms` | int, `ms` | client | none | Time the encoder was limited for other reasons, outbound video (cumulative). Outbound `qualityLimitationDurations.other`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.avg` | double, `ms` | client | none | Average round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`, averaged over the window's readings that report it. | `25.5` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.max` | double, `ms` | client | none | Maximum round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`. | `80.0` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.min` | double, `ms` | client | none | Minimum round-trip time in the window. Outbound: the `remote-inbound-rtp` entry's `roundTripTime`; inbound: the nominated candidate pair's `currentRoundTripTime` (seconds × 1000). | `12.0` | `lk.rtc.stats.sample` | +| `lk.rtc.samples` | int, `{reading}` | client | none | Stats readings folded into the window. A gauge's `.avg` divides by the readings that reported that gauge, which can be fewer. | `2` | `lk.rtc.stats.sample` | +| `lk.rtc.silent_concealed_samples` | int, `{sample}` | client | none | Concealed samples that were silent, inbound audio (cumulative). Inbound `silentConcealedSamples`. | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.window_ms` | int, `ms` | client | none | Actual length of the stats window. From the window's start to its close, on a monotonic clock. | `60000` | `lk.rtc.stats.sample` | +| `lk.sdk.name` | string | shared | none | LiveKit SDK name. Resource attribute. A low-cardinality metric dimension. Matches the `` part of `service.name = livekit-client-`. | `swift`, `android`, `flutter`, `react-native`, `unity`, `rust`, `js` | resource | +| `lk.sdk.version` | string | shared | none | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. | `2.17.0` | resource | +| `lk.shutdown.reason` | string | agents | none | Why the job shut down. | `room_disconnected` | `job_shutdown` | +| `lk.shutdown.user_initiated` | boolean | agents | none | The shutdown was requested by user code. | `false` | `job_shutdown` | +| `lk.simulation.enabled` | boolean | agents | none | The session is a simulation. Telemetry metadata key set by the job context; a span attribute on the session trace. | `true` | `job_entrypoint` | +| `lk.simulation.job_id` | string | agents | none | Simulation run job (one scenario) behind the session. | `srj_abc123` | `job_entrypoint` | +| `lk.simulation.run_id` | string | agents | none | Simulation run the session belongs to. | `run_abc123` | `job_entrypoint` | +| `lk.speech.queue_wait` | double, `s` | agents | none | Seconds a speech handle waited in the queue before generation was authorized. | `0.05` | `agent_turn` | +| `lk.speech_id` | string | agents | none | Id of the speech handle a span belongs to. The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model. | `speech_3f2a9c` | `agent_speaking` `agent_turn` `llm_node` `metrics` `realtime_inference` `realtime_metrics` `tts_node` | +| `lk.stt.events` | string | agents | none | JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length). | `[]` | `user_turn` | +| `lk.success` | boolean | agents | none | The session report is tagged successful. A tag name in the session report (`observability.py` / `report.rs`); the outcome is derived from which of `lk.success` and `lk.fail` is present. | `true` | `agent_session` | +| `lk.telemetry.cache.batches` | int, `{batch}` | client | none | Batches waiting in the cache now. A gauge, not a count since the previous report. Always present on a report. | `0` | `lk.telemetry.report` | +| `lk.telemetry.cache.delete_errors` | int, `{batch}` | client | none | Sent batches the cache could not delete; never sent twice from the same launch. Only when non-zero. JS port only. | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.list_errors` | int, `{attempt}` | client | none | Times the cache could not be listed, so nothing was reconciled or sent. Only when non-zero. JS port only. | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.read_errors` | int, `{batch}` | client | none | Cached batches that could not be read right now, kept for the next pass. Only when non-zero. JS port only. | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.write_errors` | int, `{batch}` | client | none | Batches the disk refused, kept in memory instead. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.cache_error` | int, `{record}` | client | none | Records no cache, not even memory, could take. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.cache_full` | int, `{record}` | client | none | Records evicted by the cache's size or file-count bound. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.corrupt` | int, `{record}` | client | none | Records in cached batches that failed their CRC. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.expired` | int, `{record}` | client | none | Records in batches past the cache's age limit. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.invalid` | int, `{record}` | client | none | Custom events, attributes or SDK strings over the limits, rejected. Only when non-zero. Over-limit values are rejected, never truncated (a truncated id collides). | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.oversized` | int, `{record}` | client | none | Single records larger than the collector accepts (413). Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.queue_full` | int, `{record}` | client | none | Records evicted from the in-memory queue. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.rate_limited` | int, `{record}` | client | none | Discrete events dropped by the flood guard. Only when non-zero. RTC stats windows and reports are exempt from the guard. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.rejected` | int, `{record}` | client | none | Records the collector rejected (final 4xx/5xx, OTLP partial success). Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.throttled` | int, `{record}` | client | none | Records evicted from the cache during a server-directed pause. Only when non-zero. Rust core only; the JS port never sends it. | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.unattributed` | int, `{record}` | client | none | Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. Only when non-zero. JS port only. | `1` | `lk.telemetry.report` | +| `lk.telemetry.holds.capped` | int, `{hold}` | client | none | Soft upload holds that reached their cap. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.uploads.bytes` | int, `By` | client | none | Compressed bytes the collector accepted. What telemetry cost the uplink. Always present on a report, even when 0. | `20480` | `lk.telemetry.report` | +| `lk.telemetry.uploads.failed` | int, `{attempt}` | client | none | Upload attempts that failed transiently (no answer, 429, 5xx). Always present on a report, even when 0. | `0` | `lk.telemetry.report` | +| `lk.telemetry.uploads.sent` | int, `{batch}` | client | none | Batches the collector accepted. Always present on a report, even when 0. | `4` | `lk.telemetry.report` | +| `lk.telemetry.uploads.timeouts` | int, `{attempt}` | client | none | Upload attempts that hit the export timeout. Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.telemetry.uploads.unauthorized` | int, `{attempt}` | client | none | 401/403 answers (tokens the collector refused). Only when non-zero. | `1` | `lk.telemetry.report` | +| `lk.tool_sets` | string[] | agents | none | Tool set names offered. | `["mcp:docs"]` | `llm_node` `setup_toolsets` | +| `lk.track.direction` | enum | client | none | Whether the stats describe sent or received media. On RTC stats windows only. | | `lk.rtc.stats.sample` | +| `lk.track.kind` | enum | client | none | Media kind of the track. On publish and subscribe spans and RTC stats windows. | | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` | +| `lk.track_sid` | string | shared | none | Server-assigned track SID (`TR_...`). On publish and subscribe spans and RTC stats windows. A publish learns it only on success. High cardinality, never a default metric dimension. | `TR_AMkTyV3Zx8Qp` | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` `publish_audio_output` `wait_for_audio_track` `wait_for_video_track` | +| `lk.track_source` | enum | shared | none | Source of the track, as the Agents RTC FFI `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the RTC FFI enum name exactly, e.g. `SOURCE_SCREENSHARE`. The signal protocol's `TrackSource` names differ (`CAMERA`, `SCREEN_SHARE`), so clients map their track source to these names explicitly. | | `lk.publish` `lk.subscribe` `wait_for_audio_track` `wait_for_video_track` | +| `lk.transcript_confidence` | double | agents | none | Confidence of the final transcript. | `0.98` | `user_turn` | +| `lk.transcription_delay` | double, `s` | agents | none | Seconds from the end of speech to the final transcript. | `0.4` | `agent_turn` `user_turn` | +| `lk.tts.bytes` | int, `By` | agents | none | Audio bytes the TTS driver collected (Node binding). | `230400` | `tts_stream` `tts_synthesize` | +| `lk.tts.frames` | int | agents | none | Audio frames the TTS driver collected (Node binding). | `120` | `tts_stream` `tts_synthesize` | +| `lk.tts.label` | string | agents | none | Label of the TTS instance. | `cartesia.TTS` | `tts_node` `tts_request` `tts_stream` `tts_stream_adapter` `tts_synthesize` | +| `lk.tts.no_duplex_path` | boolean | agents | none | The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure). | `true` | `tts_stream` | +| `lk.tts.streaming` | boolean | agents | none | The TTS instance streams (duplex path). | `true` | `tts_node` `tts_synthesize` | +| `lk.tts_metrics` | string | agents | none | TTS metrics of the turn, serialized. | `{}` | `agent_turn` | +| `model_name` | string | agents | none | Model name the plugin reported. | `gpt-4o-mini` | | +| `model_provider` | string | agents | none | Provider the plugin reported. | `openai` | | +| `outcome` | any | agents | none | The session outcome, as a map with the field `outcome` (`success` or `fail`) and an optional `reason`. `reason` is a field of this map, not an attribute key. | `{"outcome": "fail", "reason": "user hung up"}` | | +| `sdk_version` | string | agents | none | The Agents SDK version. | `2.0.0` | | +| `session.options` | any | agents | none | The session's options, as a map of option name to value. In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it. | `{"allow_interruptions": true}` | | +| `session.report_timestamp` | double | agents | none | When the report was made, in Unix seconds. In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it. | `1760870400.5` | | +| `session.tags` | string[] | agents | none | The session's tags, sorted. Absent when the session has none. In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it. | `["support", "escalated"]` | | +| `tag` | any | agents | none | One tag with metadata, as a map with the fields `name` and `metadata`. | `{"name": "escalated", "metadata": {}}` | | +| `usage` | any | agents | none | Model usage, as a list of maps, one per model. Absent when there was none. | `[{"model": "gpt-4o", "input_tokens": 120}]` | | + +## Enum values + +### `lk.device.app_state` + +| Value | Meaning | +| --- | --- | +| `foreground` | In front of the user. | +| `background` | In the background. | + +### `lk.device.audio.interruption` + +| Value | Meaning | +| --- | --- | +| `began` | Interruption began (another app or a call took the audio session). | +| `ended` | Interruption ended. | + +### `lk.device.audio_route.reason` + +| Value | Meaning | +| --- | --- | +| `new_device` | A new output became available. | +| `old_device_unavailable` | The previous output went away. | +| `category_change` | The audio session category changed. | +| `override` | The route was overridden. | +| `wake_from_sleep` | The device woke from sleep. | +| `no_suitable_route` | No route fits the category. | +| `route_configuration_change` | The route's configuration changed. | +| `unknown` | The platform gave no reason. | + +### `lk.device.capture.device` + +| Value | Meaning | +| --- | --- | +| `camera` | Camera. | +| `microphone` | Microphone. | +| `screen_share` | Screen capture. | + +### `lk.device.capture.reason` + +| Value | Meaning | +| --- | --- | +| `permission_denied` | The user or OS denied access. | +| `not_found` | No such device. | +| `in_use` | Another app holds the device. | +| `disconnected` | The device went away. | +| `other` | Anything else. | + +### `lk.device.memory.pressure` + +| Value | Meaning | +| --- | --- | +| `normal` | No pressure. | +| `warning` | Low memory. | +| `critical` | Critically low memory. | + +### `lk.device.thermal.state` + +| Value | Meaning | +| --- | --- | +| `nominal` | Within normal limits. | +| `fair` | Slightly elevated. | +| `serious` | High; the system reduces performance. | +| `critical` | Critical; the device needs to cool down. | + +### `lk.disconnect_reason` + +| Value | Meaning | +| --- | --- | +| `UNKNOWN_REASON` | No reason given, or a number the producer does not know. | +| `CLIENT_INITIATED` | The participant hung up. | +| `DUPLICATE_IDENTITY` | Another participant joined with the same identity. | +| `SERVER_SHUTDOWN` | The server instance shut down. | +| `PARTICIPANT_REMOVED` | RoomService.RemoveParticipant was called. | +| `ROOM_DELETED` | RoomService.DeleteRoom was called. | +| `STATE_MISMATCH` | The server did not know the session the client tried to resume. | +| `JOIN_FAILURE` | The client could not connect fully. | +| `MIGRATION` | The server asked the participant to migrate (Cloud only). | +| `SIGNAL_CLOSE` | The signal WebSocket closed unexpectedly. | +| `ROOM_CLOSED` | The room closed after its participants left. | +| `USER_UNAVAILABLE` | The SIP callee did not answer in time. | +| `USER_REJECTED` | The SIP callee rejected the call. | +| `SIP_TRUNK_FAILURE` | SIP protocol failure. | +| `CONNECTION_TIMEOUT` | The server timed out the session. | +| `MEDIA_FAILURE` | Media failed or timed out. | +| `AGENT_ERROR` | The agent failed. | +| `RECONNECT_FAILED` | The client ran out of reconnect attempts (client only, not a protobuf value). | + +### `lk.eou.outcome` + +| Value | Meaning | +| --- | --- | +| `committed` | The turn was committed. | +| `user_resumed` | The user spoke again. | +| `dropped` | The turn was dropped. | + +### `lk.frame.resolution` + +| Value | Meaning | +| --- | --- | +| `root` | The root frame. | +| `live` | A live speech frame. | +| `stale` | A frame whose speech already ended. | +| `unknown` | Not a frame the slab knows. | + +### `lk.log.source` + +| Value | Meaning | +| --- | --- | +| `sdk` | The platform SDK. | +| `ffi` | The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``. | +| `webrtc` | WebRTC. | + +### `lk.outcome` + +| Value | Meaning | +| --- | --- | +| `ok` | Succeeded; span status Unset. | +| `error` | Failed; span status Error, with `error.type` when the failure has a type. | +| `cancelled` | Cancelled by the caller or superseded; span status Unset. | + +### `lk.reconnect.mode` + +| Value | Meaning | +| --- | --- | +| `quick` | Resume the session (ICE restart). | +| `full` | Reconnect from scratch. | + +### `lk.reconnect.reason` + +| Value | Meaning | +| --- | --- | +| `signal_disconnected` | The signaling socket closed (RR_SIGNAL_DISCONNECTED). | +| `publisher_failed` | The publisher peer connection failed (RR_PUBLISHER_FAILED). | +| `subscriber_failed` | The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). | +| `transport_failed` | A peer connection failed and the platform did not say which (client only). | +| `switch_candidate` | The server asked for a candidate switch (RR_SWITCH_CANDIDATE). | +| `network_changed` | The device moved to another network (client only). | +| `debug` | A test or a debug menu asked for it (client only). | +| `unknown` | RR_UNKNOWN or a number the client does not know. | + +### `lk.track.direction` + +| Value | Meaning | +| --- | --- | +| `inbound` | Received media (inbound-rtp). | +| `outbound` | Sent media (outbound-rtp). | + +### `lk.track.kind` + +| Value | Meaning | +| --- | --- | +| `audio` | Audio. | +| `video` | Video. | + +### `lk.track_source` + +| Value | Meaning | +| --- | --- | +| `SOURCE_UNKNOWN` | Unknown source. | +| `SOURCE_CAMERA` | Camera. | +| `SOURCE_MICROPHONE` | Microphone. | +| `SOURCE_SCREENSHARE` | Screen share video. | +| `SOURCE_SCREENSHARE_AUDIO` | Screen share audio. | + +## Bindings + +The physical name of a concept in other producers and stores. Carried for reference; nothing +validates it. + +| Key | Producer or store | Name there | +| --- | --- | --- | +| `lk.agent_name` | agents_participant_attribute | `lk.agent.name` | +| `lk.agent_name` | agents_session_report | `agent_name` | +| `lk.job_id` | agents_otlp | `lk.job_id`, `job_id` | +| `lk.job_id` | collector_column | `none` | +| `lk.room_sid` | agents_otlp | `lk.room_sid`, `room_id` | +| `lk.room_sid` | collector_column | `room_id` | +| `lk.room_sid` | roomobs | `room_session.id` | + +`agents_otlp` lists Agents' span key first, then its Resource and metric-point key. +`agents_participant_attribute` is the room participant attribute, which is not an OTLP key. +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID`. + +## Old spellings + +Keys and values that producers sent before this registry, and the key and value to send instead. +Each row comes from `renamed_from` on the shared key. Live-check reports an old spelling as +`livekit_shared_rename` and names the replacement. + +| Old key | Old value | Key to send | Value to send | +| --- | --- | --- | --- | +| `lk.disconnect.reason` | `agent_error` | `lk.disconnect_reason` | `AGENT_ERROR` | +| `lk.disconnect.reason` | `client_initiated` | `lk.disconnect_reason` | `CLIENT_INITIATED` | +| `lk.disconnect.reason` | `connection_timeout` | `lk.disconnect_reason` | `CONNECTION_TIMEOUT` | +| `lk.disconnect.reason` | `duplicate_identity` | `lk.disconnect_reason` | `DUPLICATE_IDENTITY` | +| `lk.disconnect.reason` | `join_failure` | `lk.disconnect_reason` | `JOIN_FAILURE` | +| `lk.disconnect.reason` | `media_failure` | `lk.disconnect_reason` | `MEDIA_FAILURE` | +| `lk.disconnect.reason` | `migration` | `lk.disconnect_reason` | `MIGRATION` | +| `lk.disconnect.reason` | `participant_removed` | `lk.disconnect_reason` | `PARTICIPANT_REMOVED` | +| `lk.disconnect.reason` | `reconnect_failed` | `lk.disconnect_reason` | `RECONNECT_FAILED` | +| `lk.disconnect.reason` | `room_closed` | `lk.disconnect_reason` | `ROOM_CLOSED` | +| `lk.disconnect.reason` | `room_deleted` | `lk.disconnect_reason` | `ROOM_DELETED` | +| `lk.disconnect.reason` | `server_shutdown` | `lk.disconnect_reason` | `SERVER_SHUTDOWN` | +| `lk.disconnect.reason` | `signal_close` | `lk.disconnect_reason` | `SIGNAL_CLOSE` | +| `lk.disconnect.reason` | `sip_trunk_failure` | `lk.disconnect_reason` | `SIP_TRUNK_FAILURE` | +| `lk.disconnect.reason` | `state_mismatch` | `lk.disconnect_reason` | `STATE_MISMATCH` | +| `lk.disconnect.reason` | `unknown` | `lk.disconnect_reason` | `UNKNOWN_REASON` | +| `lk.disconnect.reason` | `user_rejected` | `lk.disconnect_reason` | `USER_REJECTED` | +| `lk.disconnect.reason` | `user_unavailable` | `lk.disconnect_reason` | `USER_UNAVAILABLE` | +| `lk.participant.sid` | any | `lk.participant_id` | unchanged | +| `lk.participant.identity` | any | `lk.pii.participant_identity` | unchanged | +| `lk.participant.remote_identity` | any | `lk.pii.remote_participant_identity` | unchanged | +| `lk.room.name` | any | `lk.pii.room_name` | unchanged | +| `lk.room.sid` | any | `lk.room_sid` | unchanged | +| `lk.track.sid` | any | `lk.track_sid` | unchanged | +| `lk.track.source` | `camera` | `lk.track_source` | `SOURCE_CAMERA` | +| `lk.track.source` | `microphone` | `lk.track_source` | `SOURCE_MICROPHONE` | +| `lk.track.source` | `screen_share` | `lk.track_source` | `SOURCE_SCREENSHARE` | +| `lk.track.source` | `screen_share_audio` | `lk.track_source` | `SOURCE_SCREENSHARE_AUDIO` | +| `lk.track.source` | `unknown` | `lk.track_source` | `SOURCE_UNKNOWN` | diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py new file mode 100644 index 000000000..fd61a50c0 --- /dev/null +++ b/otel/gen/python/semconv.py @@ -0,0 +1,1568 @@ +# Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +"""LiveKit telemetry semantic conventions.""" + +#: The agent's name. +AGENT_NAME = "agent_name" + +#: Browser name. +BROWSER_NAME = "browser.name" + +#: Browser version. +BROWSER_VERSION = "browser.version" + +#: One chat history item, as a map. Carries conversation content. PII: redacted by the collector. +CHAT_ITEM = "chat.item" + +#: Whether a pooled connection was reused, as the string `true`/`false`. +CONNECTION_REUSED = "connection_reused" + +#: One evaluation result, as a map of string fields. +EVALUATION = "evaluation" + +#: Token type (`input` or `output`); deprecated upstream, a dimension of `gen_ai.client.token.usage`. +GEN_AI_TOKEN_TYPE = "gen_ai.token.type" + +#: LangFuse spelling of audio input tokens. +GEN_AI_USAGE_INPUT_AUDIO_TOKENS = "gen_ai.usage.input_audio_tokens" + +#: LangFuse spelling of cached input tokens. +GEN_AI_USAGE_INPUT_CACHED_TOKENS = "gen_ai.usage.input_cached_tokens" + +#: LangFuse spelling of text input tokens. +GEN_AI_USAGE_INPUT_TEXT_TOKENS = "gen_ai.usage.input_text_tokens" + +#: LangFuse spelling of audio output tokens. +GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS = "gen_ai.usage.output_audio_tokens" + +#: LangFuse spelling of text output tokens. +GEN_AI_USAGE_OUTPUT_TEXT_TOKENS = "gen_ai.usage.output_text_tokens" + +#: LangFuse spelling of reasoning tokens. +GEN_AI_USAGE_REASONING_TOKENS = "gen_ai.usage.reasoning_tokens" + +#: LangFuse's completion start time (ISO 8601), set next to `gen_ai.response.time_to_first_chunk`. +LANGFUSE_OBSERVATION_COMPLETION_START_TIME = "langfuse.observation.completion_start_time" + +#: Label of the agent running the activity. +LK_AGENT_LABEL = "lk.agent_label" + +#: Name of the agent a job was dispatched to. +LK_AGENT_NAME = "lk.agent_name" + +#: AMD classification. +LK_AMD_CATEGORY = "lk.amd.category" + +#: Seconds from the call start to the AMD decision. Unit: `s`. +LK_AMD_DELAY = "lk.amd.delay" + +#: Why the AMD classifier decided. +LK_AMD_REASON = "lk.amd.reason" + +#: Seconds of speech the AMD classifier heard. Unit: `s`. +LK_AMD_SPEECH_DURATION = "lk.amd.speech_duration" + +#: Blocks in the session. +LK_BLOCKING_COUNT = "lk.blocking.count" + +#: CPU seconds the loop thread consumed during the block. Unit: `s`. +LK_BLOCKING_CPU_TIME = "lk.blocking.cpu_time" + +#: Seconds the loop was blocked. Unit: `s`. +LK_BLOCKING_DURATION = "lk.blocking.duration" + +#: Seconds of garbage collection on the loop thread during the block. Unit: `s`. +LK_BLOCKING_GC_TIME = "lk.blocking.gc_time" + +#: Module a lazy import was loading when sampled. +LK_BLOCKING_IMPORT = "lk.blocking.import" + +#: Seconds of the longest block in the session. Unit: `s`. +LK_BLOCKING_MAX_DURATION = "lk.blocking.max_duration" + +#: Severity bucket of the block. +LK_BLOCKING_SEVERITY = "lk.blocking.severity" + +#: Loop-thread stack sampled while blocked (source locations only). +LK_BLOCKING_STACK = "lk.blocking.stack" + +#: Reports dropped by rate limiting since the previous emitted span. +LK_BLOCKING_SUPPRESSED = "lk.blocking.suppressed" + +#: Name of the task that was running. +LK_BLOCKING_TASK = "lk.blocking.task" + +#: Seconds above which a block is reported. Unit: `s`. +LK_BLOCKING_THRESHOLD = "lk.blocking.threshold" + +#: Seconds blocked in total over the session. Unit: `s`. +LK_BLOCKING_TOTAL_DURATION = "lk.blocking.total_duration" + +#: Name of the shutdown callback. +LK_CALLBACK_NAME = "lk.callback.name" + +#: The close drained pending speech first. +LK_CLOSE_DRAIN = "lk.close.drain" + +#: Why the session closed. +LK_CLOSE_REASON = "lk.close_reason" + +#: LiveKit Cloud agent id. +LK_CLOUD_AGENT_ID = "lk.cloud_agent_id" + +#: 1-based connect attempt. +LK_CONNECT_ATTEMPT = "lk.connect.attempt" + +#: Room connection state. +LK_CONNECTION_STATE = "lk.connection_state" + +#: LiveKit Cloud deployment id. +LK_DEPLOYMENT_ID = "lk.deployment_id" + +#: Whether the app is in front of the user. +LK_DEVICE_APP_STATE = "lk.device.app_state" +#: In front of the user. +LK_DEVICE_APP_STATE_FOREGROUND = "foreground" +#: In the background. +LK_DEVICE_APP_STATE_BACKGROUND = "background" + +#: Phase of an audio session interruption. +LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" +#: Interruption began (another app or a call took the audio session). +LK_DEVICE_AUDIO_INTERRUPTION_BEGAN = "began" +#: Interruption ended. +LK_DEVICE_AUDIO_INTERRUPTION_ENDED = "ended" + +#: The active outputs, comma-separated. +LK_DEVICE_AUDIO_ROUTE_OUTPUTS = "lk.device.audio_route.outputs" + +#: Why the audio route changed. +LK_DEVICE_AUDIO_ROUTE_REASON = "lk.device.audio_route.reason" +#: A new output became available. +LK_DEVICE_AUDIO_ROUTE_REASON_NEW_DEVICE = "new_device" +#: The previous output went away. +LK_DEVICE_AUDIO_ROUTE_REASON_OLD_DEVICE_UNAVAILABLE = "old_device_unavailable" +#: The audio session category changed. +LK_DEVICE_AUDIO_ROUTE_REASON_CATEGORY_CHANGE = "category_change" +#: The route was overridden. +LK_DEVICE_AUDIO_ROUTE_REASON_OVERRIDE = "override" +#: The device woke from sleep. +LK_DEVICE_AUDIO_ROUTE_REASON_WAKE_FROM_SLEEP = "wake_from_sleep" +#: No route fits the category. +LK_DEVICE_AUDIO_ROUTE_REASON_NO_SUITABLE_ROUTE = "no_suitable_route" +#: The route's configuration changed. +LK_DEVICE_AUDIO_ROUTE_REASON_ROUTE_CONFIGURATION_CHANGE = "route_configuration_change" +#: The platform gave no reason. +LK_DEVICE_AUDIO_ROUTE_REASON_UNKNOWN = "unknown" + +#: The device is charging. +LK_DEVICE_BATTERY_CHARGING = "lk.device.battery.charging" + +#: Battery charge in percent, 0-100. Unit: `%`. +LK_DEVICE_BATTERY_LEVEL = "lk.device.battery.level" + +#: Capture device that failed. +LK_DEVICE_CAPTURE_DEVICE = "lk.device.capture.device" +#: Camera. +LK_DEVICE_CAPTURE_DEVICE_CAMERA = "camera" +#: Microphone. +LK_DEVICE_CAPTURE_DEVICE_MICROPHONE = "microphone" +#: Screen capture. +LK_DEVICE_CAPTURE_DEVICE_SCREEN_SHARE = "screen_share" + +#: Why capture failed, in the getUserMedia failure taxonomy. +LK_DEVICE_CAPTURE_REASON = "lk.device.capture.reason" +#: The user or OS denied access. +LK_DEVICE_CAPTURE_REASON_PERMISSION_DENIED = "permission_denied" +#: No such device. +LK_DEVICE_CAPTURE_REASON_NOT_FOUND = "not_found" +#: Another app holds the device. +LK_DEVICE_CAPTURE_REASON_IN_USE = "in_use" +#: The device went away. +LK_DEVICE_CAPTURE_REASON_DISCONNECTED = "disconnected" +#: Anything else. +LK_DEVICE_CAPTURE_REASON_OTHER = "other" + +#: Low Power Mode / Battery Saver is on. +LK_DEVICE_LOW_POWER_ENABLED = "lk.device.low_power.enabled" + +#: Memory pressure as the OS reports it. +LK_DEVICE_MEMORY_PRESSURE = "lk.device.memory.pressure" +#: No pressure. +LK_DEVICE_MEMORY_PRESSURE_NORMAL = "normal" +#: Low memory. +LK_DEVICE_MEMORY_PRESSURE_WARNING = "warning" +#: Critically low memory. +LK_DEVICE_MEMORY_PRESSURE_CRITICAL = "critical" + +#: The user asked for less traffic (Low Data Mode, Data Saver). +LK_DEVICE_NETWORK_CONSTRAINED = "lk.device.network.constrained" + +#: The network is metered (cellular, personal hotspot). +LK_DEVICE_NETWORK_EXPENSIVE = "lk.device.network.expensive" + +#: Thermal pressure as the OS reports it. +LK_DEVICE_THERMAL_STATE = "lk.device.thermal.state" +#: Within normal limits. +LK_DEVICE_THERMAL_STATE_NOMINAL = "nominal" +#: Slightly elevated. +LK_DEVICE_THERMAL_STATE_FAIR = "fair" +#: High; the system reduces performance. +LK_DEVICE_THERMAL_STATE_SERIOUS = "serious" +#: Critical; the device needs to cool down. +LK_DEVICE_THERMAL_STATE_CRITICAL = "critical" + +#: Why a participant left the room, as the protobuf `DisconnectReason` enum name. +LK_DISCONNECT_REASON = "lk.disconnect_reason" +#: No reason given, or a number the producer does not know. +LK_DISCONNECT_REASON_UNKNOWN_REASON = "UNKNOWN_REASON" +#: The participant hung up. +LK_DISCONNECT_REASON_CLIENT_INITIATED = "CLIENT_INITIATED" +#: Another participant joined with the same identity. +LK_DISCONNECT_REASON_DUPLICATE_IDENTITY = "DUPLICATE_IDENTITY" +#: The server instance shut down. +LK_DISCONNECT_REASON_SERVER_SHUTDOWN = "SERVER_SHUTDOWN" +#: RoomService.RemoveParticipant was called. +LK_DISCONNECT_REASON_PARTICIPANT_REMOVED = "PARTICIPANT_REMOVED" +#: RoomService.DeleteRoom was called. +LK_DISCONNECT_REASON_ROOM_DELETED = "ROOM_DELETED" +#: The server did not know the session the client tried to resume. +LK_DISCONNECT_REASON_STATE_MISMATCH = "STATE_MISMATCH" +#: The client could not connect fully. +LK_DISCONNECT_REASON_JOIN_FAILURE = "JOIN_FAILURE" +#: The server asked the participant to migrate (Cloud only). +LK_DISCONNECT_REASON_MIGRATION = "MIGRATION" +#: The signal WebSocket closed unexpectedly. +LK_DISCONNECT_REASON_SIGNAL_CLOSE = "SIGNAL_CLOSE" +#: The room closed after its participants left. +LK_DISCONNECT_REASON_ROOM_CLOSED = "ROOM_CLOSED" +#: The SIP callee did not answer in time. +LK_DISCONNECT_REASON_USER_UNAVAILABLE = "USER_UNAVAILABLE" +#: The SIP callee rejected the call. +LK_DISCONNECT_REASON_USER_REJECTED = "USER_REJECTED" +#: SIP protocol failure. +LK_DISCONNECT_REASON_SIP_TRUNK_FAILURE = "SIP_TRUNK_FAILURE" +#: The server timed out the session. +LK_DISCONNECT_REASON_CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT" +#: Media failed or timed out. +LK_DISCONNECT_REASON_MEDIA_FAILURE = "MEDIA_FAILURE" +#: The agent failed. +LK_DISCONNECT_REASON_AGENT_ERROR = "AGENT_ERROR" +#: The client ran out of reconnect attempts (client only, not a protobuf value). +LK_DISCONNECT_REASON_RECONNECT_FAILED = "RECONNECT_FAILED" + +#: Agent dispatch id. +LK_DISPATCH_ID = "lk.dispatch_id" + +#: Seconds from the end of the user's speech to the agent's first audio. Unit: `s`. +LK_E2E_LATENCY = "lk.e2e_latency" + +#: Seconds from the end of speech to the turn decision. Unit: `s`. +LK_END_OF_TURN_DELAY = "lk.end_of_turn_delay" + +#: Seconds the detector inference took. Unit: `s`. +LK_EOU_DETECTION_DELAY = "lk.eou.detection_delay" + +#: Seconds of endpointing delay applied. Unit: `s`. +LK_EOU_ENDPOINTING_DELAY = "lk.eou.endpointing_delay" + +#: The detector answer came from the cache. +LK_EOU_FROM_CACHE = "lk.eou.from_cache" + +#: Language the detector ran for. +LK_EOU_LANGUAGE = "lk.eou.language" + +#: Turn decisions the wait rejected before it ended. +LK_EOU_NOT_COMMITTED_COUNT = "lk.eou.not_committed_count" + +#: How the endpointing wait ended. +LK_EOU_OUTCOME = "lk.eou.outcome" +#: The turn was committed. +LK_EOU_OUTCOME_COMMITTED = "committed" +#: The user spoke again. +LK_EOU_OUTCOME_USER_RESUMED = "user_resumed" +#: The turn was dropped. +LK_EOU_OUTCOME_DROPPED = "dropped" + +#: End-of-utterance probability. +LK_EOU_PROBABILITY = "lk.eou.probability" + +#: Times the endpointing wait restarted on a later trigger. +LK_EOU_REARM_COUNT = "lk.eou.rearm_count" + +#: Endpointing waits the user cut short by speaking again. +LK_EOU_RESUME_COUNT = "lk.eou.resume_count" + +#: What decided the end of the turn. +LK_EOU_SOURCE = "lk.eou.source" + +#: Threshold under which the turn is unlikely to be over. +LK_EOU_UNLIKELY_THRESHOLD = "lk.eou.unlikely_threshold" + +#: Seconds from the end of the user's speech to the turn decision. Unit: `s`. +LK_EOU_WAIT_DURATION = "lk.eou.wait_duration" + +#: The provider error is retryable (Node binding). +LK_ERROR_RETRYABLE = "lk.error.retryable" + +#: The session report is tagged failed. +LK_FAIL = "lk.fail" + +#: Index of the fallback instance that served the request. +LK_FALLBACK_INDEX = "lk.fallback.index" + +#: Label of the fallback instance that served the request. +LK_FALLBACK_LABEL = "lk.fallback.label" + +#: The request was a recovery probe of a failed instance. +LK_FALLBACK_RECOVERY = "lk.fallback.recovery" + +#: Seconds from waiting for a track to its first frame. Unit: `s`. +LK_FIRST_FRAME_DELAY = "lk.first_frame_delay" + +#: How the inbound frame of a plugin-task metric was resolved (Node binding). +LK_FRAME_RESOLUTION = "lk.frame.resolution" +#: The root frame. +LK_FRAME_RESOLUTION_ROOT = "root" +#: A live speech frame. +LK_FRAME_RESOLUTION_LIVE = "live" +#: A frame whose speech already ended. +LK_FRAME_RESOLUTION_STALE = "stale" +#: Not a frame the slab knows. +LK_FRAME_RESOLUTION_UNKNOWN = "unknown" + +#: Tool call id. +LK_FUNCTION_TOOL_ID = "lk.function_tool.id" + +#: The tool returned an error. +LK_FUNCTION_TOOL_IS_ERROR = "lk.function_tool.is_error" + +#: Tool name. +LK_FUNCTION_TOOL_NAME = "lk.function_tool.name" + +#: Function tool names offered. +LK_FUNCTION_TOOLS = "lk.function_tools" + +#: Generations in the turn. +LK_GENERATION_COUNT = "lk.generation_count" + +#: Id of the agent turn (generation). +LK_GENERATION_ID = "lk.generation_id" + +#: Span whose input record this one continues. +LK_INPUT_BASE_SPAN_ID = "lk.input.base_span_id" + +#: The span's input record is a delta over its base span. +LK_INPUT_DELTA = "lk.input.delta" + +#: Items of the base record not carried into this one. +LK_INPUT_DROPPED_FROM_BASE = "lk.input.dropped_from_base" + +#: The speech was interrupted. +LK_INTERRUPTED = "lk.interrupted" + +#: Seconds from the user's speech to the interruption decision. Unit: `s`. +LK_INTERRUPTION_DETECTION_DELAY = "lk.interruption.detection_delay" + +#: Seconds the interruption prediction took. Unit: `s`. +LK_INTERRUPTION_PREDICTION_DURATION = "lk.interruption.prediction_duration" + +#: Interruption probability. +LK_INTERRUPTION_PROBABILITY = "lk.interruption.probability" + +#: What triggered the interruption. +LK_INTERRUPTION_SOURCE = "lk.interruption.source" + +#: Seconds of the interruption from detection to resolution. Unit: `s`. +LK_INTERRUPTION_TOTAL_DURATION = "lk.interruption.total_duration" + +#: The user turn was classified as an interruption. +LK_IS_INTERRUPTION = "lk.is_interruption" + +#: Seconds from the job request to its acceptance. Unit: `s`. +LK_JOB_ACCEPT_LATENCY = "lk.job.accept_latency" + +#: Agent id of the job. +LK_JOB_AGENT_ID = "lk.job.agent_id" + +#: Seconds from acceptance to the job assignment. Unit: `s`. +LK_JOB_ASSIGNMENT_LATENCY = "lk.job.assignment_latency" + +#: Seconds from the dispatch request to the entrypoint running. Unit: `s`. +LK_JOB_DISPATCH_LATENCY = "lk.job.dispatch_latency" + +#: Seconds from launch to the entrypoint running. Unit: `s`. +LK_JOB_ENTRYPOINT_LATENCY = "lk.job.entrypoint_latency" + +#: Seconds from assignment to the job process launch. Unit: `s`. +LK_JOB_LAUNCH_LATENCY = "lk.job.launch_latency" + +#: Worker id the job runs on. +LK_JOB_WORKER_ID = "lk.job.worker_id" + +#: Agent job ID (`AJ_...`). +LK_JOB_ID = "lk.job_id" + +#: Keyterms added by the update. +LK_KEYTERMS_ADDED = "lk.keyterms.added" + +#: Number of keyterms configured. +LK_KEYTERMS_COUNT = "lk.keyterms.count" + +#: Keyterms removed by the update. +LK_KEYTERMS_REMOVED = "lk.keyterms.removed" + +#: LLM metrics of the turn, serialized. +LK_LLM_METRICS = "lk.llm_metrics" + +#: Logger, type, module or file that wrote the line. +LK_LOG_LOGGER = "lk.log.logger" + +#: Which layer wrote the log line. +LK_LOG_SOURCE = "lk.log.source" +#: The platform SDK. +LK_LOG_SOURCE_SDK = "sdk" +#: The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``. +LK_LOG_SOURCE_FFI = "ffi" +#: WebRTC. +LK_LOG_SOURCE_WEBRTC = "webrtc" + +#: Metric type noted from a plugin task (Node binding `metrics` span). +LK_METRIC = "lk.metric" + +#: State after a transition. +LK_NEW_STATE = "lk.new_state" + +#: State before a transition. +LK_OLD_STATE = "lk.old_state" + +#: Seconds the `on_user_turn_completed` hook took. Unit: `s`. +LK_ON_USER_TURN_COMPLETED_DELAY = "lk.on_user_turn_completed_delay" + +#: How the attempt ended. Rollups read this, never the span status. +LK_OUTCOME = "lk.outcome" +#: Succeeded; span status Unset. +LK_OUTCOME_OK = "ok" +#: Failed; span status Error, with `error.type` when the failure has a type. +LK_OUTCOME_ERROR = "error" +#: Cancelled by the caller or superseded; span status Unset. +LK_OUTCOME_CANCELLED = "cancelled" + +#: Generation this one continues (a tool-call follow-up). +LK_PARENT_GENERATION_ID = "lk.parent_generation_id" + +#: Server-assigned SID of the local participant (`PA_...`). +LK_PARTICIPANT_ID = "lk.participant_id" + +#: Kind of the linked participant (protocol `ParticipantInfo.Kind`). +LK_PARTICIPANT_KIND = "lk.participant_kind" + +#: Transcript the AMD classifier used. PII: redacted by the collector. +LK_PII_AMD_TRANSCRIPT = "lk.pii.amd.transcript" + +#: Tool-call arguments on a log record. PII: redacted by the collector. +LK_PII_ARGUMENTS = "lk.pii.arguments" + +#: Serialized chat context sent to the LLM. PII: redacted by the collector. +LK_PII_CHAT_CTX = "lk.pii.chat_ctx" + +#: DTMF digits received. PII: redacted by the collector. +LK_PII_DTMF_INPUTS = "lk.pii.dtmf_inputs" + +#: Error text that may quote user content. PII: redacted by the collector. +LK_PII_ERROR = "lk.pii.error" + +#: Raw provider event on a debug log record. PII: redacted by the collector. +LK_PII_EVENT = "lk.pii.event" + +#: Tool call arguments. PII: redacted by the collector. +LK_PII_FUNCTION_TOOL_ARGUMENTS = "lk.pii.function_tool.arguments" + +#: Tool output. PII: redacted by the collector. +LK_PII_FUNCTION_TOOL_OUTPUT = "lk.pii.function_tool.output" + +#: Text sent to the TTS. PII: redacted by the collector. +LK_PII_INPUT_TEXT = "lk.pii.input_text" + +#: Agent instructions in force. PII: redacted by the collector. +LK_PII_INSTRUCTIONS = "lk.pii.instructions" + +#: An interim (non-final) transcript, on a log record. PII: redacted by the collector. +LK_PII_INTERIM_TRANSCRIPT = "lk.pii.interim_transcript" + +#: The keyterms themselves (customer vocabulary). PII: redacted by the collector. +LK_PII_KEYTERMS = "lk.pii.keyterms" + +#: Tool-call output on a log record. PII: redacted by the collector. +LK_PII_OUTPUT = "lk.pii.output" + +#: Customer-supplied identity of the local participant. PII: redacted by the collector. +LK_PII_PARTICIPANT_IDENTITY = "lk.pii.participant_identity" + +#: MCP tool progress message. PII: redacted by the collector. +LK_PII_PROGRESS_MESSAGE = "lk.pii.progress_message" + +#: Malformed tool-call arguments as received. PII: redacted by the collector. +LK_PII_RAW_ARGUMENTS = "lk.pii.raw_arguments" + +#: Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). PII: redacted by the collector. +LK_PII_REMOTE_PARTICIPANT_IDENTITY = "lk.pii.remote_participant_identity" + +#: Tool-call arguments after repair. PII: redacted by the collector. +LK_PII_REPAIRED = "lk.pii.repaired" + +#: Function calls the LLM produced, serialized. PII: redacted by the collector. +LK_PII_RESPONSE_FUNCTION_CALLS = "lk.pii.response.function_calls" + +#: Text the LLM produced. PII: redacted by the collector. +LK_PII_RESPONSE_TEXT = "lk.pii.response.text" + +#: Customer-supplied room name. PII: redacted by the collector. +LK_PII_ROOM_NAME = "lk.pii.room_name" + +#: RPC request payload. PII: redacted by the collector. +LK_PII_RPC_PAYLOAD = "lk.pii.rpc.payload" + +#: RPC response payload. PII: redacted by the collector. +LK_PII_RPC_RESPONSE = "lk.pii.rpc.response" + +#: Phone number of a SIP participant. PII: redacted by the collector. +LK_PII_SIP_PHONE_NUMBER = "lk.pii.sip.phoneNumber" + +#: Message text on a log record. PII: redacted by the collector. +LK_PII_TEXT = "lk.pii.text" + +#: Transcript text on a log record. PII: redacted by the collector. +LK_PII_TRANSCRIPT = "lk.pii.transcript" + +#: The user's transcript for the turn. PII: redacted by the collector. +LK_PII_USER_INPUT = "lk.pii.user_input" + +#: Transcript the detector scored. PII: redacted by the collector. +LK_PII_USER_TRANSCRIPT = "lk.pii.user_transcript" + +#: Monotonically increasing sequence number per pipeline. +LK_PING_SEQ = "lk.ping.seq" + +#: Seconds of the agent's speech played out when it was interrupted. Unit: `s`. +LK_PLAYOUT_POSITION = "lk.playout.position" + +#: Seconds of pre-connect audio replayed. Unit: `s`. +LK_PRE_CONNECT_AUDIO_DURATION = "lk.pre_connect_audio.duration" + +#: Label of the agent handed off from. +LK_PREVIOUS_AGENT_LABEL = "lk.previous_agent_label" + +#: Provider request ids a span covers, for joining against provider logs. +LK_PROVIDER_REQUEST_IDS = "lk.provider_request_ids" + +#: Provider-side tool names offered. +LK_PROVIDER_TOOLS = "lk.provider_tools" + +#: Realtime model metrics of the turn, serialized. +LK_REALTIME_MODEL_METRICS = "lk.realtime_model_metrics" + +#: Reconnect attempts made in the cycle. +LK_RECONNECT_ATTEMPTS = "lk.reconnect.attempts" + +#: Mode of the last reconnect attempt. +LK_RECONNECT_MODE = "lk.reconnect.mode" +#: Resume the session (ICE restart). +LK_RECONNECT_MODE_QUICK = "quick" +#: Reconnect from scratch. +LK_RECONNECT_MODE_FULL = "full" + +#: What triggered the reconnect cycle. +LK_RECONNECT_REASON = "lk.reconnect.reason" +#: The signaling socket closed (RR_SIGNAL_DISCONNECTED). +LK_RECONNECT_REASON_SIGNAL_DISCONNECTED = "signal_disconnected" +#: The publisher peer connection failed (RR_PUBLISHER_FAILED). +LK_RECONNECT_REASON_PUBLISHER_FAILED = "publisher_failed" +#: The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). +LK_RECONNECT_REASON_SUBSCRIBER_FAILED = "subscriber_failed" +#: A peer connection failed and the platform did not say which (client only). +LK_RECONNECT_REASON_TRANSPORT_FAILED = "transport_failed" +#: The server asked for a candidate switch (RR_SWITCH_CANDIDATE). +LK_RECONNECT_REASON_SWITCH_CANDIDATE = "switch_candidate" +#: The device moved to another network (client only). +LK_RECONNECT_REASON_NETWORK_CHANGED = "network_changed" +#: A test or a debug menu asked for it (client only). +LK_RECONNECT_REASON_DEBUG = "debug" +#: RR_UNKNOWN or a number the client does not know. +LK_RECONNECT_REASON_UNKNOWN = "unknown" + +#: PII redaction was requested for the session. +LK_REDACTION_ENABLED = "lk.redaction.enabled" + +#: Seconds to the first TTS audio byte. Unit: `s`. +LK_RESPONSE_TTFB = "lk.response.ttfb" + +#: Seconds to the first LLM token. Unit: `s`. +LK_RESPONSE_TTFT = "lk.response.ttft" + +#: Retries before the operation succeeded or gave up. +LK_RETRY_COUNT = "lk.retry_count" + +#: The room was joined with auto-subscribe. +LK_ROOM_AUTO_SUBSCRIBE = "lk.room.auto_subscribe" + +#: End-to-end encryption is on. +LK_ROOM_E2EE = "lk.room.e2ee" + +#: Remote participants when the agent connected. +LK_ROOM_REMOTE_PARTICIPANT_COUNT = "lk.room.remote_participant_count" + +#: How room I/O picked its participant. +LK_ROOM_IO_PARTICIPANT_FILTER = "lk.room_io.participant_filter" + +#: Server-assigned room session SID (`RM_...`). +LK_ROOM_SID = "lk.room_sid" + +#: Identity of the RPC caller. +LK_RPC_CALLER_IDENTITY = "lk.rpc.caller_identity" + +#: Identity the RPC is sent to. +LK_RPC_DESTINATION_IDENTITY = "lk.rpc.destination_identity" + +#: RPC error code. +LK_RPC_ERROR_CODE = "lk.rpc.error_code" + +#: A handler was registered for the method. +LK_RPC_HANDLER_REGISTERED = "lk.rpc.handler_registered" + +#: RPC request payload size in bytes. Unit: `By`. +LK_RPC_PAYLOAD_SIZE = "lk.rpc.payload_size" + +#: RPC request id. +LK_RPC_REQUEST_ID = "lk.rpc.request_id" + +#: RPC response size in bytes. Unit: `By`. +LK_RPC_RESPONSE_SIZE = "lk.rpc.response_size" + +#: RPC response timeout in seconds. Unit: `s`. +LK_RPC_RESPONSE_TIMEOUT = "lk.rpc.response_timeout" + +#: Average audio level in the window, 0-1, inbound audio. Unit: `1`. +LK_RTC_AUDIO_LEVEL_AVG = "lk.rtc.audio_level.avg" + +#: Maximum audio level in the window, 0-1, inbound audio. Unit: `1`. +LK_RTC_AUDIO_LEVEL_MAX = "lk.rtc.audio_level.max" + +#: Minimum audio level in the window, 0-1, inbound audio. Unit: `1`. +LK_RTC_AUDIO_LEVEL_MIN = "lk.rtc.audio_level.min" + +#: Bytes sent or received (cumulative). Unit: `By`. +LK_RTC_BYTES = "lk.rtc.bytes" + +#: Codec mimeType, when known. +LK_RTC_CODEC = "lk.rtc.codec" + +#: Concealed audio samples, inbound audio (cumulative). Unit: `{sample}`. +LK_RTC_CONCEALED_SAMPLES = "lk.rtc.concealed_samples" + +#: Concealment events, inbound audio (cumulative). Unit: `{event}`. +LK_RTC_CONCEALMENT_EVENTS = "lk.rtc.concealment_events" + +#: Average frame rate in the window, video. Unit: `{frame}/s`. +LK_RTC_FPS_AVG = "lk.rtc.fps.avg" + +#: Maximum frame rate in the window, video. Unit: `{frame}/s`. +LK_RTC_FPS_MAX = "lk.rtc.fps.max" + +#: Minimum frame rate in the window, video. Unit: `{frame}/s`. +LK_RTC_FPS_MIN = "lk.rtc.fps.min" + +#: Video freezes, inbound video (cumulative). Unit: `{freeze}`. +LK_RTC_FREEZE_COUNT = "lk.rtc.freeze_count" + +#: Total freeze time, inbound video (cumulative). Unit: `ms`. +LK_RTC_FREEZES_DURATION_MS = "lk.rtc.freezes_duration_ms" + +#: Audio interruptions, inbound audio (cumulative). Unit: `{interruption}`. +LK_RTC_INTERRUPTION_COUNT = "lk.rtc.interruption_count" + +#: Total interruption time, inbound audio (cumulative). Unit: `ms`. +LK_RTC_INTERRUPTIONS_DURATION_MS = "lk.rtc.interruptions_duration_ms" + +#: Jitter buffer delay, inbound (cumulative). Unit: `ms`. +LK_RTC_JITTER_BUFFER_DELAY_MS = "lk.rtc.jitter_buffer_delay_ms" + +#: Samples or frames emitted by the jitter buffer, inbound (cumulative). Unit: `{sample}`. +LK_RTC_JITTER_BUFFER_EMITTED_COUNT = "lk.rtc.jitter_buffer_emitted_count" + +#: Average jitter in the window, inbound. Unit: `ms`. +LK_RTC_JITTER_MS_AVG = "lk.rtc.jitter_ms.avg" + +#: Maximum jitter in the window, inbound. Unit: `ms`. +LK_RTC_JITTER_MS_MAX = "lk.rtc.jitter_ms.max" + +#: Minimum jitter in the window, inbound. Unit: `ms`. +LK_RTC_JITTER_MS_MIN = "lk.rtc.jitter_ms.min" + +#: Packets sent or received (cumulative). Unit: `{packet}`. +LK_RTC_PACKETS = "lk.rtc.packets" + +#: Packets lost, inbound (cumulative). Unit: `{packet}`. +LK_RTC_PACKETS_LOST = "lk.rtc.packets_lost" + +#: Video pauses, inbound video (cumulative). Unit: `{pause}`. +LK_RTC_PAUSE_COUNT = "lk.rtc.pause_count" + +#: Total pause time, inbound video (cumulative). Unit: `ms`. +LK_RTC_PAUSES_DURATION_MS = "lk.rtc.pauses_duration_ms" + +#: Time the encoder was limited by bandwidth, outbound video (cumulative). Unit: `ms`. +LK_RTC_QUALITY_LIMITATION_BANDWIDTH_MS = "lk.rtc.quality_limitation.bandwidth_ms" + +#: Time the encoder was limited by CPU, outbound video (cumulative). Unit: `ms`. +LK_RTC_QUALITY_LIMITATION_CPU_MS = "lk.rtc.quality_limitation.cpu_ms" + +#: Time the encoder was limited for other reasons, outbound video (cumulative). Unit: `ms`. +LK_RTC_QUALITY_LIMITATION_OTHER_MS = "lk.rtc.quality_limitation.other_ms" + +#: Average round-trip time in the window. Unit: `ms`. +LK_RTC_RTT_MS_AVG = "lk.rtc.rtt_ms.avg" + +#: Maximum round-trip time in the window. Unit: `ms`. +LK_RTC_RTT_MS_MAX = "lk.rtc.rtt_ms.max" + +#: Minimum round-trip time in the window. Unit: `ms`. +LK_RTC_RTT_MS_MIN = "lk.rtc.rtt_ms.min" + +#: Stats readings folded into the window. Unit: `{reading}`. +LK_RTC_SAMPLES = "lk.rtc.samples" + +#: Concealed samples that were silent, inbound audio (cumulative). Unit: `{sample}`. +LK_RTC_SILENT_CONCEALED_SAMPLES = "lk.rtc.silent_concealed_samples" + +#: Actual length of the stats window. Unit: `ms`. +LK_RTC_WINDOW_MS = "lk.rtc.window_ms" + +#: LiveKit SDK name. +LK_SDK_NAME = "lk.sdk.name" + +#: LiveKit SDK version. +LK_SDK_VERSION = "lk.sdk.version" + +#: Why the job shut down. +LK_SHUTDOWN_REASON = "lk.shutdown.reason" + +#: The shutdown was requested by user code. +LK_SHUTDOWN_USER_INITIATED = "lk.shutdown.user_initiated" + +#: The session is a simulation. +LK_SIMULATION_ENABLED = "lk.simulation.enabled" + +#: Simulation run job (one scenario) behind the session. +LK_SIMULATION_JOB_ID = "lk.simulation.job_id" + +#: Simulation run the session belongs to. +LK_SIMULATION_RUN_ID = "lk.simulation.run_id" + +#: Seconds a speech handle waited in the queue before generation was authorized. Unit: `s`. +LK_SPEECH_QUEUE_WAIT = "lk.speech.queue_wait" + +#: Id of the speech handle a span belongs to. +LK_SPEECH_ID = "lk.speech_id" + +#: JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length). +LK_STT_EVENTS = "lk.stt.events" + +#: The session report is tagged successful. +LK_SUCCESS = "lk.success" + +#: Batches waiting in the cache now. Unit: `{batch}`. +LK_TELEMETRY_CACHE_BATCHES = "lk.telemetry.cache.batches" + +#: Sent batches the cache could not delete; never sent twice from the same launch. Unit: `{batch}`. +LK_TELEMETRY_CACHE_DELETE_ERRORS = "lk.telemetry.cache.delete_errors" + +#: Times the cache could not be listed, so nothing was reconciled or sent. Unit: `{attempt}`. +LK_TELEMETRY_CACHE_LIST_ERRORS = "lk.telemetry.cache.list_errors" + +#: Cached batches that could not be read right now, kept for the next pass. Unit: `{batch}`. +LK_TELEMETRY_CACHE_READ_ERRORS = "lk.telemetry.cache.read_errors" + +#: Batches the disk refused, kept in memory instead. Unit: `{batch}`. +LK_TELEMETRY_CACHE_WRITE_ERRORS = "lk.telemetry.cache.write_errors" + +#: Records no cache, not even memory, could take. Unit: `{record}`. +LK_TELEMETRY_DROPPED_CACHE_ERROR = "lk.telemetry.dropped.cache_error" + +#: Records evicted by the cache's size or file-count bound. Unit: `{record}`. +LK_TELEMETRY_DROPPED_CACHE_FULL = "lk.telemetry.dropped.cache_full" + +#: Records in cached batches that failed their CRC. Unit: `{record}`. +LK_TELEMETRY_DROPPED_CORRUPT = "lk.telemetry.dropped.corrupt" + +#: Records in batches past the cache's age limit. Unit: `{record}`. +LK_TELEMETRY_DROPPED_EXPIRED = "lk.telemetry.dropped.expired" + +#: Custom events, attributes or SDK strings over the limits, rejected. Unit: `{record}`. +LK_TELEMETRY_DROPPED_INVALID = "lk.telemetry.dropped.invalid" + +#: Single records larger than the collector accepts (413). Unit: `{record}`. +LK_TELEMETRY_DROPPED_OVERSIZED = "lk.telemetry.dropped.oversized" + +#: Records evicted from the in-memory queue. Unit: `{record}`. +LK_TELEMETRY_DROPPED_QUEUE_FULL = "lk.telemetry.dropped.queue_full" + +#: Discrete events dropped by the flood guard. Unit: `{record}`. +LK_TELEMETRY_DROPPED_RATE_LIMITED = "lk.telemetry.dropped.rate_limited" + +#: Records the collector rejected (final 4xx/5xx, OTLP partial success). Unit: `{record}`. +LK_TELEMETRY_DROPPED_REJECTED = "lk.telemetry.dropped.rejected" + +#: Records evicted from the cache during a server-directed pause. Unit: `{record}`. +LK_TELEMETRY_DROPPED_THROTTLED = "lk.telemetry.dropped.throttled" + +#: Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. Unit: `{record}`. +LK_TELEMETRY_DROPPED_UNATTRIBUTED = "lk.telemetry.dropped.unattributed" + +#: Soft upload holds that reached their cap. Unit: `{hold}`. +LK_TELEMETRY_HOLDS_CAPPED = "lk.telemetry.holds.capped" + +#: Compressed bytes the collector accepted. Unit: `By`. +LK_TELEMETRY_UPLOADS_BYTES = "lk.telemetry.uploads.bytes" + +#: Upload attempts that failed transiently (no answer, 429, 5xx). Unit: `{attempt}`. +LK_TELEMETRY_UPLOADS_FAILED = "lk.telemetry.uploads.failed" + +#: Batches the collector accepted. Unit: `{batch}`. +LK_TELEMETRY_UPLOADS_SENT = "lk.telemetry.uploads.sent" + +#: Upload attempts that hit the export timeout. Unit: `{attempt}`. +LK_TELEMETRY_UPLOADS_TIMEOUTS = "lk.telemetry.uploads.timeouts" + +#: 401/403 answers (tokens the collector refused). Unit: `{attempt}`. +LK_TELEMETRY_UPLOADS_UNAUTHORIZED = "lk.telemetry.uploads.unauthorized" + +#: Tool set names offered. +LK_TOOL_SETS = "lk.tool_sets" + +#: Whether the stats describe sent or received media. +LK_TRACK_DIRECTION = "lk.track.direction" +#: Received media (inbound-rtp). +LK_TRACK_DIRECTION_INBOUND = "inbound" +#: Sent media (outbound-rtp). +LK_TRACK_DIRECTION_OUTBOUND = "outbound" + +#: Media kind of the track. +LK_TRACK_KIND = "lk.track.kind" +#: Audio. +LK_TRACK_KIND_AUDIO = "audio" +#: Video. +LK_TRACK_KIND_VIDEO = "video" + +#: Server-assigned track SID (`TR_...`). +LK_TRACK_SID = "lk.track_sid" + +#: Source of the track, as the Agents RTC FFI `TrackSource` enum name. +LK_TRACK_SOURCE = "lk.track_source" +#: Unknown source. +LK_TRACK_SOURCE_SOURCE_UNKNOWN = "SOURCE_UNKNOWN" +#: Camera. +LK_TRACK_SOURCE_SOURCE_CAMERA = "SOURCE_CAMERA" +#: Microphone. +LK_TRACK_SOURCE_SOURCE_MICROPHONE = "SOURCE_MICROPHONE" +#: Screen share video. +LK_TRACK_SOURCE_SOURCE_SCREENSHARE = "SOURCE_SCREENSHARE" +#: Screen share audio. +LK_TRACK_SOURCE_SOURCE_SCREENSHARE_AUDIO = "SOURCE_SCREENSHARE_AUDIO" + +#: Confidence of the final transcript. +LK_TRANSCRIPT_CONFIDENCE = "lk.transcript_confidence" + +#: Seconds from the end of speech to the final transcript. Unit: `s`. +LK_TRANSCRIPTION_DELAY = "lk.transcription_delay" + +#: Audio bytes the TTS driver collected (Node binding). Unit: `By`. +LK_TTS_BYTES = "lk.tts.bytes" + +#: Audio frames the TTS driver collected (Node binding). +LK_TTS_FRAMES = "lk.tts.frames" + +#: Label of the TTS instance. +LK_TTS_LABEL = "lk.tts.label" + +#: The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure). +LK_TTS_NO_DUPLEX_PATH = "lk.tts.no_duplex_path" + +#: The TTS instance streams (duplex path). +LK_TTS_STREAMING = "lk.tts.streaming" + +#: TTS metrics of the turn, serialized. +LK_TTS_METRICS = "lk.tts_metrics" + +#: Model name the plugin reported. +MODEL_NAME = "model_name" + +#: Provider the plugin reported. +MODEL_PROVIDER = "model_provider" + +#: The session outcome, as a map with the field `outcome` (`success` or `fail`) and an optional `reason`. +OUTCOME = "outcome" + +#: The Agents SDK version. +SDK_VERSION = "sdk_version" + +#: The session's options, as a map of option name to value. +SESSION_OPTIONS = "session.options" + +#: When the report was made, in Unix seconds. +SESSION_REPORT_TIMESTAMP = "session.report_timestamp" + +#: The session's tags, sorted. Absent when the session has none. +SESSION_TAGS = "session.tags" + +#: One tag with metadata, as a map with the fields `name` and `metadata`. +TAG = "tag" + +#: Model usage, as a list of maps, one per model. Absent when there was none. +USAGE = "usage" + + +# Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, +# never here), so a producer can name them from one place. +#: The source code filename that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. +CODE_FILE_PATH = "code.file.path" + +#: The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. +CODE_FUNCTION_NAME = "code.function.name" + +#: The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. +CODE_LINE_NUMBER = "code.line.number" + +#: The model identifier for the device +DEVICE_MODEL_IDENTIFIER = "device.model.identifier" + +#: Describes a class of error the operation ended with. +ERROR_TYPE = "error.type" +#: A fallback error value to be used when the instrumentation doesn't define a custom value. +ERROR_TYPE_OTHER = "_OTHER" + +#: The exception message. +EXCEPTION_MESSAGE = "exception.message" + +#: A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. +EXCEPTION_STACKTRACE = "exception.stacktrace" + +#: The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. +EXCEPTION_TYPE = "exception.type" + +#: Human-readable name of the GenAI agent provided by the application. +GEN_AI_AGENT_NAME = "gen_ai.agent.name" + +#: The unique identifier for a conversation (session, thread), used to store and correlate messages within this conversation. +GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" + +#: A free-form explanation for the assigned score provided by the evaluator. +GEN_AI_EVALUATION_EXPLANATION = "gen_ai.evaluation.explanation" + +#: The name of the evaluation metric used for the GenAI response. +GEN_AI_EVALUATION_NAME = "gen_ai.evaluation.name" + +#: Human readable label for evaluation. +GEN_AI_EVALUATION_SCORE_LABEL = "gen_ai.evaluation.score.label" + +#: The chat history provided to the model as an input. +GEN_AI_INPUT_MESSAGES = "gen_ai.input.messages" + +#: The search query used to retrieve memories. +GEN_AI_MEMORY_QUERY_TEXT = "gen_ai.memory.query.text" + +#: The memory records stored or retrieved in a memory operation. +GEN_AI_MEMORY_RECORDS = "gen_ai.memory.records" + +#: The name of the operation being performed. +GEN_AI_OPERATION_NAME = "gen_ai.operation.name" +#: Chat completion operation such as [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) +GEN_AI_OPERATION_NAME_CHAT = "chat" +#: Multimodal content generation operation such as [Gemini Generate Content](https://ai.google.dev/api/generate-content) +GEN_AI_OPERATION_NAME_GENERATE_CONTENT = "generate_content" +#: Text completions operation such as [OpenAI Completions API (Legacy)](https://platform.openai.com/docs/api-reference/completions) +GEN_AI_OPERATION_NAME_TEXT_COMPLETION = "text_completion" +#: Embeddings operation such as [OpenAI Create embeddings API](https://platform.openai.com/docs/api-reference/embeddings/create) +GEN_AI_OPERATION_NAME_EMBEDDINGS = "embeddings" +#: Retrieval operation such as [OpenAI Search Vector Store API](https://platform.openai.com/docs/api-reference/vector-stores/search) +GEN_AI_OPERATION_NAME_RETRIEVAL = "retrieval" +#: Fetch a previously generated model response by its identifier, without performing inference, such as [OpenAI Get a model response](https://platform.openai.com/docs/api-reference/responses/get) +GEN_AI_OPERATION_NAME_FETCH_RESPONSE = "fetch_response" +#: Create GenAI agent +GEN_AI_OPERATION_NAME_CREATE_AGENT = "create_agent" +#: Invoke GenAI agent +GEN_AI_OPERATION_NAME_INVOKE_AGENT = "invoke_agent" +#: Execute a tool +GEN_AI_OPERATION_NAME_EXECUTE_TOOL = "execute_tool" +#: Invoke GenAI workflow +GEN_AI_OPERATION_NAME_INVOKE_WORKFLOW = "invoke_workflow" +#: Agent planning or task decomposition phase +GEN_AI_OPERATION_NAME_PLAN = "plan" +#: Search/query memories from a memory store +GEN_AI_OPERATION_NAME_SEARCH_MEMORY = "search_memory" +#: Create new memory records +GEN_AI_OPERATION_NAME_CREATE_MEMORY = "create_memory" +#: Update existing memory records +GEN_AI_OPERATION_NAME_UPDATE_MEMORY = "update_memory" +#: Create or update memory records without the caller choosing which +GEN_AI_OPERATION_NAME_UPSERT_MEMORY = "upsert_memory" +#: Delete memory records +GEN_AI_OPERATION_NAME_DELETE_MEMORY = "delete_memory" +#: Create or initialize a memory store +GEN_AI_OPERATION_NAME_CREATE_MEMORY_STORE = "create_memory_store" +#: Delete or deprovision a memory store +GEN_AI_OPERATION_NAME_DELETE_MEMORY_STORE = "delete_memory_store" + +#: Messages returned by the model where each message represents a specific model response (choice, candidate). +GEN_AI_OUTPUT_MESSAGES = "gen_ai.output.messages" + +#: Represents the content type requested by the client. +GEN_AI_OUTPUT_TYPE = "gen_ai.output.type" +#: Plain text +GEN_AI_OUTPUT_TYPE_TEXT = "text" +#: JSON object with known or unknown schema +GEN_AI_OUTPUT_TYPE_JSON = "json" +#: Image +GEN_AI_OUTPUT_TYPE_IMAGE = "image" +#: Speech +GEN_AI_OUTPUT_TYPE_SPEECH = "speech" + +#: The variables supplied to the prompt template, the `` being the variable name, the value being the variable value. +GEN_AI_PROMPT_VARIABLE = "gen_ai.prompt.variable" + +#: The Generative AI provider as identified by the client or server instrumentation. +GEN_AI_PROVIDER_NAME = "gen_ai.provider.name" +#: [OpenAI](https://openai.com/) +GEN_AI_PROVIDER_NAME_OPENAI = "openai" +#: Any Google generative AI endpoint +GEN_AI_PROVIDER_NAME_GCP_GEN_AI = "gcp.gen_ai" +#: [Vertex AI](https://cloud.google.com/vertex-ai) +GEN_AI_PROVIDER_NAME_GCP_VERTEX_AI = "gcp.vertex_ai" +#: [Gemini](https://cloud.google.com/products/gemini) +GEN_AI_PROVIDER_NAME_GCP_GEMINI = "gcp.gemini" +#: [Anthropic](https://www.anthropic.com/) +GEN_AI_PROVIDER_NAME_ANTHROPIC = "anthropic" +#: [Cohere](https://cohere.com/) +GEN_AI_PROVIDER_NAME_COHERE = "cohere" +#: Azure AI Inference +GEN_AI_PROVIDER_NAME_AZURE_AI_INFERENCE = "azure.ai.inference" +#: [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) +GEN_AI_PROVIDER_NAME_AZURE_AI_OPENAI = "azure.ai.openai" +#: [IBM Watsonx AI](https://www.ibm.com/products/watsonx-ai) +GEN_AI_PROVIDER_NAME_IBM_WATSONX_AI = "ibm.watsonx.ai" +#: [AWS Bedrock](https://aws.amazon.com/bedrock) +GEN_AI_PROVIDER_NAME_AWS_BEDROCK = "aws.bedrock" +#: [Perplexity](https://www.perplexity.ai/) +GEN_AI_PROVIDER_NAME_PERPLEXITY = "perplexity" +#: [xAI](https://x.ai/) +GEN_AI_PROVIDER_NAME_X_AI = "x_ai" +#: [DeepSeek](https://www.deepseek.com/) +GEN_AI_PROVIDER_NAME_DEEPSEEK = "deepseek" +#: [Groq](https://groq.com/) +GEN_AI_PROVIDER_NAME_GROQ = "groq" +#: [Mistral](https://mistral.ai/) +GEN_AI_PROVIDER_NAME_MISTRAL = "mistral" +#: [Moonshot AI](https://www.moonshot.ai/) +GEN_AI_PROVIDER_NAME_MOONSHOT_AI = "moonshot_ai" +#: Mistral, as Agents sends it. The upstream member is `mistral`. Sent by LiveKit, not an upstream value. +GEN_AI_PROVIDER_NAME_MISTRAL_AI = "mistral_ai" + +#: The name of the GenAI model a request is being made to. +GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + +#: Indicates whether the GenAI request was made in streaming mode. +GEN_AI_REQUEST_STREAM = "gen_ai.request.stream" + +#: Array of reasons the model stopped generating tokens, corresponding to each generation received. +GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" +#: Declared in Agents' `GenAIFinishReason`. No Agents code path sends it today. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_COMPACTION = "compaction" +#: A content filter stopped the output. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_CONTENT_FILTER = "content_filter" +#: The generation ended abnormally. The upstream note names this value. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_ERROR = "error" +#: The output reached the token limit. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_LENGTH = "length" +#: The generation finished normally. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_STOP = "stop" +#: The generation ended with a tool call. Sent by LiveKit, not an upstream value. +GEN_AI_RESPONSE_FINISH_REASONS_TOOL_CALL = "tool_call" + +#: The unique identifier for the completion. +GEN_AI_RESPONSE_ID = "gen_ai.response.id" + +#: The name of the model that generated the response. +GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + +#: Time to first chunk in a streaming response, measured from request issuance, in seconds. The value is measured from when the client issues the generation request to when the first chunk is received in the response stream. +GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK = "gen_ai.response.time_to_first_chunk" + +#: The documents retrieved. +GEN_AI_RETRIEVAL_DOCUMENTS = "gen_ai.retrieval.documents" + +#: The query text used for retrieval. +GEN_AI_RETRIEVAL_QUERY_TEXT = "gen_ai.retrieval.query.text" + +#: The system message or instructions provided to the GenAI model separately from the chat history. +GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions" + +#: Parameters passed to the tool call. +GEN_AI_TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments" + +#: The tool call identifier supplied by the model or provider. +GEN_AI_TOOL_CALL_ID = "gen_ai.tool.call.id" + +#: The result returned by the tool call (if any and if execution was successful). +GEN_AI_TOOL_CALL_RESULT = "gen_ai.tool.call.result" + +#: The list of tool definitions available to the GenAI agent or model. +GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + +#: The tool description. +GEN_AI_TOOL_DESCRIPTION = "gen_ai.tool.description" + +#: Name of the tool utilized by the agent. +GEN_AI_TOOL_NAME = "gen_ai.tool.name" + +#: Type of the tool utilized by the agent +GEN_AI_TOOL_TYPE = "gen_ai.tool.type" + +#: The number of audio input tokens served from a provider-managed cache. +GEN_AI_USAGE_AUDIO_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.audio.cache_read.input_tokens" + +#: The number of audio input tokens. +GEN_AI_USAGE_AUDIO_INPUT_TOKENS = "gen_ai.usage.audio.input_tokens" + +#: The number of audio output tokens. +GEN_AI_USAGE_AUDIO_OUTPUT_TOKENS = "gen_ai.usage.audio.output_tokens" + +#: The number of input tokens served from a provider-managed cache. +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" + +#: The number of input tokens written to a provider-managed cache. +GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS = "gen_ai.usage.cache_write.input_tokens" + +#: The number of image input tokens served from a provider-managed cache. +GEN_AI_USAGE_IMAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.image.cache_read.input_tokens" + +#: The number of image input tokens. +GEN_AI_USAGE_IMAGE_INPUT_TOKENS = "gen_ai.usage.image.input_tokens" + +#: The number of tokens used in the GenAI input (prompt). +GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + +#: The number of tokens used in the GenAI response (completion). +GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + +#: The number of output tokens used for reasoning (e.g. chain-of-thought, extended thinking). +GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = "gen_ai.usage.reasoning.output_tokens" + +#: The number of text input tokens served from a provider-managed cache. +GEN_AI_USAGE_TEXT_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.text.cache_read.input_tokens" + +#: The number of text input tokens. +GEN_AI_USAGE_TEXT_INPUT_TOKENS = "gen_ai.usage.text.input_tokens" + +#: The number of text output tokens. +GEN_AI_USAGE_TEXT_OUTPUT_TOKENS = "gen_ai.usage.text.output_tokens" + +#: Human-readable name of the GenAI workflow provided by the application. +GEN_AI_WORKFLOW_NAME = "gen_ai.workflow.name" + +#: The internet connection type. +NETWORK_CONNECTION_TYPE = "network.connection.type" +#: Wi-Fi +NETWORK_CONNECTION_TYPE_WIFI = "wifi" +#: `wired`. +NETWORK_CONNECTION_TYPE_WIRED = "wired" +#: `cell`. +NETWORK_CONNECTION_TYPE_CELL = "cell" +#: `unavailable`. +NETWORK_CONNECTION_TYPE_UNAVAILABLE = "unavailable" +#: `unknown`. +NETWORK_CONNECTION_TYPE_UNKNOWN = "unknown" + +#: Human readable operating system name. +OS_NAME = "os.name" + +#: The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). +OS_VERSION = "os.version" + +#: Identifies the class / type of event. +OTEL_EVENT_NAME = "otel.event.name" + +#: The fully-qualified logical name of the method from the RPC interface perspective. +RPC_METHOD = "rpc.method" + +#: Logical name of the service. +SERVICE_NAME = "service.name" + +#: The version string of the service component. The format is not defined by these conventions. +SERVICE_VERSION = "service.version" + +#: A unique ID to identify a session. +SESSION_ID = "session.id" + +#: The language of the telemetry SDK. +TELEMETRY_SDK_LANGUAGE = "telemetry.sdk.language" +#: [C++](https://opentelemetry.io/docs/languages/cpp/) +TELEMETRY_SDK_LANGUAGE_CPP = "cpp" +#: [.NET](https://opentelemetry.io/docs/languages/dotnet/) +TELEMETRY_SDK_LANGUAGE_DOTNET = "dotnet" +#: [Erlang/Elixir](https://opentelemetry.io/docs/languages/erlang/) +TELEMETRY_SDK_LANGUAGE_ERLANG = "erlang" +#: [Go](https://opentelemetry.io/docs/languages/go/) +TELEMETRY_SDK_LANGUAGE_GO = "go" +#: [Java](https://opentelemetry.io/docs/languages/java/) +TELEMETRY_SDK_LANGUAGE_JAVA = "java" +#: [Kotlin](https://opentelemetry.io/docs/languages/kotlin/) +TELEMETRY_SDK_LANGUAGE_KOTLIN = "kotlin" +#: [Node.js](https://opentelemetry.io/docs/languages/js/) +TELEMETRY_SDK_LANGUAGE_NODEJS = "nodejs" +#: [PHP](https://opentelemetry.io/docs/languages/php/) +TELEMETRY_SDK_LANGUAGE_PHP = "php" +#: [Python](https://opentelemetry.io/docs/languages/python/) +TELEMETRY_SDK_LANGUAGE_PYTHON = "python" +#: [Ruby](https://opentelemetry.io/docs/languages/ruby/) +TELEMETRY_SDK_LANGUAGE_RUBY = "ruby" +#: [Rust](https://opentelemetry.io/docs/languages/rust/) +TELEMETRY_SDK_LANGUAGE_RUST = "rust" +#: [Swift](https://opentelemetry.io/docs/languages/swift/) +TELEMETRY_SDK_LANGUAGE_SWIFT = "swift" +#: [Browser](https://opentelemetry.io/docs/languages/js/) +TELEMETRY_SDK_LANGUAGE_WEBJS = "webjs" + +#: The name of the telemetry SDK as defined above. +TELEMETRY_SDK_NAME = "telemetry.sdk.name" + +#: The version string of the telemetry SDK. +TELEMETRY_SDK_VERSION = "telemetry.sdk.version" + + +# Upstream metrics that LiveKit producers create, imported from the registry's dependencies. +#: The duration of a single tool execution. Instrument: histogram, unit: `s`. +METRIC_GEN_AI_EXECUTE_TOOL_DURATION = "gen_ai.execute_tool.duration" +#: Unit of the instrument above, as the upstream definition gives it (empty = none). +METRIC_GEN_AI_EXECUTE_TOOL_DURATION_UNIT = "s" +#: The end-to-end duration of a single in-process agent invocation, from the moment the invocation starts until the agent emits the last chunk of its final response or terminates with an error. Instrument: histogram, unit: `s`. +METRIC_GEN_AI_INVOKE_AGENT_DURATION = "gen_ai.invoke_agent.duration" +#: Unit of the instrument above, as the upstream definition gives it (empty = none). +METRIC_GEN_AI_INVOKE_AGENT_DURATION_UNIT = "s" + +# Event names (OTLP `LogRecord.event_name`). +#: Namespace of app-defined events, named `custom.` and carrying the app's own attributes. +EVENT_CUSTOM = "custom" +#: OTel GenAI assistant message (also a function call), a span event on `llm_node`. +EVENT_GEN_AI_ASSISTANT_MESSAGE = "gen_ai.assistant.message" +#: OTel GenAI choice, a span event on `llm_node`. +EVENT_GEN_AI_CHOICE = "gen_ai.choice" +#: OTel GenAI system (or developer) message, a span event on `llm_node`. +EVENT_GEN_AI_SYSTEM_MESSAGE = "gen_ai.system.message" +#: OTel GenAI tool message (a function call output), a span event on `llm_node`. +EVENT_GEN_AI_TOOL_MESSAGE = "gen_ai.tool.message" +#: OTel GenAI user message, a span event on `llm_node`. +EVENT_GEN_AI_USER_MESSAGE = "gen_ai.user.message" +#: The app moved to the foreground or background. +EVENT_LK_DEVICE_APP_STATE_CHANGED = "lk.device.app_state.changed" +#: An audio session interruption began or ended. +EVENT_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" +#: The audio route changed. +EVENT_LK_DEVICE_AUDIO_ROUTE_CHANGED = "lk.device.audio_route.changed" +#: Charging changed, or the level crossed 20 % or 10 % unplugged. +EVENT_LK_DEVICE_BATTERY_CHANGED = "lk.device.battery.changed" +#: A capture device failed. +EVENT_LK_DEVICE_CAPTURE_FAILED = "lk.device.capture.failed" +#: Low Power Mode changed. +EVENT_LK_DEVICE_LOW_POWER_CHANGED = "lk.device.low_power.changed" +#: Memory pressure changed. +EVENT_LK_DEVICE_MEMORY_CHANGED = "lk.device.memory.changed" +#: The network path changed. +EVENT_LK_DEVICE_NETWORK_CHANGED = "lk.device.network.changed" +#: Thermal state changed. +EVENT_LK_DEVICE_THERMAL_CHANGED = "lk.device.thermal.changed" +#: Pipeline smoke test. +EVENT_LK_PING = "lk.ping" +#: The Room left connected for good. +EVENT_LK_ROOM_DISCONNECTED = "lk.room.disconnected" +#: One RTC stats window for one track and direction. +EVENT_LK_RTC_STATS_SAMPLE = "lk.rtc.stats.sample" +#: Self-telemetry; counts since the previous report. +EVENT_LK_TELEMETRY_REPORT = "lk.telemetry.report" + +# Span names. +#: The agent session, parent of every turn. Kind: internal. +SPAN_AGENT_SESSION = "agent_session" +#: The agent's audio playing out. Kind: internal. +SPAN_AGENT_SPEAKING = "agent_speaking" +#: One agent turn (generation), parent of the LLM and TTS nodes. Kind: internal. +SPAN_AGENT_TURN = "agent_turn" +#: Answering-machine detection on an outbound call. Kind: internal. +SPAN_AMD = "amd" +#: Draining an agent activity before it ends. Kind: internal. +SPAN_DRAIN_AGENT_ACTIVITY = "drain_agent_activity" +#: One end-of-utterance inference. Kind: internal. +SPAN_EOU_DETECTION = "eou_detection" +#: The endpointing wait, from the user's last speech to the turn decision. Kind: internal. +SPAN_EOU_WAIT = "eou_wait" +#: A synchronous block on the agent event loop. Kind: internal. +SPAN_EVENT_LOOP_BLOCKED = "event_loop_blocked" +#: One function tool execution, the OTel GenAI execute-tool span. Kind: internal. +SPAN_FUNCTION_TOOL = "function_tool" +#: The job's entrypoint, root of a job's trace. Kind: internal. +SPAN_JOB_ENTRYPOINT = "job_entrypoint" +#: The job shutting down. Kind: internal. +SPAN_JOB_SHUTDOWN = "job_shutdown" +#: One LLM-judge evaluation of the session. Kind: internal. +SPAN_JUDGE_EVALUATION = "judge_evaluation" +#: A keyterm update to the STT. Kind: internal. +SPAN_KEYTERM_DETECTION = "keyterm_detection" +#: One connect attempt. Kind: client. +SPAN_LK_CONNECT = "lk.connect" +#: One track publish. Kind: internal. +SPAN_LK_PUBLISH = "lk.publish" +#: One reconnect cycle. Kind: client. +SPAN_LK_RECONNECT = "lk.reconnect" +#: One subscription, from the intent to subscribe to the first media. Kind: internal. +SPAN_LK_SUBSCRIBE = "lk.subscribe" +#: The LLM fallback adapter choosing an instance. Kind: internal. +SPAN_LLM_FALLBACK_ADAPTER = "llm_fallback_adapter" +#: The agent's LLM node for one generation. Kind: internal. +SPAN_LLM_NODE = "llm_node" +#: One LLM inference request, the OTel GenAI inference span. Kind: client. +SPAN_LLM_REQUEST = "llm_request" +#: The LLM request's stream being consumed. Kind: internal. +SPAN_LLM_REQUEST_RUN = "llm_request_run" +#: A plugin-task metric noted through the Node binding, stamped with its resolved frame. Kind: internal. +SPAN_METRICS = "metrics" +#: The agent's `on_enter` hook. Kind: internal. +SPAN_ON_ENTER = "on_enter" +#: The agent's `on_exit` hook. Kind: internal. +SPAN_ON_EXIT = "on_exit" +#: The session-end hook. Kind: internal. +SPAN_ON_SESSION_END = "on_session_end" +#: The `on_user_turn_completed` hook. Kind: internal. +SPAN_ON_USER_TURN_COMPLETED = "on_user_turn_completed" +#: Pausing an agent activity. Kind: internal. +SPAN_PAUSE_AGENT_ACTIVITY = "pause_agent_activity" +#: Publishing the agent's audio track. Kind: internal. +SPAN_PUBLISH_AUDIO_OUTPUT = "publish_audio_output" +#: One realtime-model generation. Kind: client. +SPAN_REALTIME_INFERENCE = "realtime_inference" +#: Realtime-model metrics attached to a turn. Kind: internal. +SPAN_REALTIME_METRICS = "realtime_metrics" +#: Resuming an agent activity. Kind: internal. +SPAN_RESUME_AGENT_ACTIVITY = "resume_agent_activity" +#: Connecting the job to its room. Kind: client. +SPAN_ROOM_CONNECT = "room_connect" +#: Disconnecting from the room. Kind: client. +SPAN_ROOM_DISCONNECT = "room_disconnect" +#: An outgoing RPC. Kind: client. +SPAN_RPC_CALL = "rpc_call" +#: An incoming RPC handled by the agent. Kind: server. +SPAN_RPC_HANDLER = "rpc_handler" +#: Closing the session. Kind: internal. +SPAN_SESSION_CLOSE = "session_close" +#: Uploading the session report. Kind: internal. +SPAN_SESSION_END_UPLOAD = "session_end_upload" +#: Starting the session (room I/O, first activity). Kind: internal. +SPAN_SESSION_START = "session_start" +#: Initializing the agent's tool sets. Kind: internal. +SPAN_SETUP_TOOLSETS = "setup_toolsets" +#: One shutdown callback. Kind: internal. +SPAN_SHUTDOWN_CALLBACK = "shutdown_callback" +#: Starting an agent activity. Kind: internal. +SPAN_START_AGENT_ACTIVITY = "start_agent_activity" +#: The TTS fallback adapter choosing an instance. Kind: internal. +SPAN_TTS_FALLBACK_ADAPTER = "tts_fallback_adapter" +#: The agent's TTS node for one generation. Kind: internal. +SPAN_TTS_NODE = "tts_node" +#: One TTS request. Kind: client. +SPAN_TTS_REQUEST = "tts_request" +#: The TTS request's stream being consumed. Kind: internal. +SPAN_TTS_REQUEST_RUN = "tts_request_run" +#: The Node binding driving `Tts::stream` (the duplex path). Kind: internal. +SPAN_TTS_STREAM = "tts_stream" +#: The sentence-splitting adapter over a non-streaming TTS. Kind: internal. +SPAN_TTS_STREAM_ADAPTER = "tts_stream_adapter" +#: The Node binding driving `Tts::synthesize` (the simplex path). Kind: internal. +SPAN_TTS_SYNTHESIZE = "tts_synthesize" +#: Replacing the session's agent. Kind: internal. +SPAN_UPDATE_AGENT = "update_agent" +#: The user speaking (VAD). Kind: internal. +SPAN_USER_SPEAKING = "user_speaking" +#: One user turn, from speech to the turn decision. Kind: internal. +SPAN_USER_TURN = "user_turn" +#: Waiting for the participant's audio track. Kind: internal. +SPAN_WAIT_FOR_AUDIO_TRACK = "wait_for_audio_track" +#: Waiting for the participant to link to. Kind: internal. +SPAN_WAIT_FOR_PARTICIPANT = "wait_for_participant" +#: Waiting for the participant's video track. Kind: internal. +SPAN_WAIT_FOR_VIDEO_TRACK = "wait_for_video_track" + +# Checkpoint names: span events of the spans above. `lk.reconnect` names its attempts +# `attempt ` instead. +#: The agent state changed (`lk.old_state` -> `lk.new_state`). +CHECKPOINT_AGENT_STATE_CHANGED = "agent_state_changed" +#: The subscriber's SDP answer went out. +CHECKPOINT_ANSWER_SENT = "answer_sent" +#: A peer connection was created before the join response (JS port only). +CHECKPOINT_EARLY_PC_CREATED = "early_pc_created" +#: The RTC engine is ready (Swift stamps it with pc_connected). +CHECKPOINT_ENGINE = "engine" +#: The entrypoint was cancelled. +CHECKPOINT_ENTRYPOINT_CANCELLED = "entrypoint_cancelled" +#: The entrypoint returned. +CHECKPOINT_ENTRYPOINT_RETURNED = "entrypoint_returned" +#: The entrypoint started running. +CHECKPOINT_ENTRYPOINT_STARTED = "entrypoint_started" +#: A synchronous block on the event loop, with `lk.blocking.duration`; mirrors the span of the same name. +CHECKPOINT_EVENT_LOOP_BLOCKED = "event_loop_blocked" +#: The OTel exception event (`exception.*`), recorded on whichever span the exception was caught on; declared here once. +CHECKPOINT_EXCEPTION = "exception" +#: The first frame arrived. +CHECKPOINT_FIRST_FRAME = "first_frame" +#: The first inbound stats reading with bytes; the span ends ok here. +CHECKPOINT_FIRST_MEDIA = "first_media" +#: OTel GenAI inference details event (the GenAI registry defines it; events cannot be referenced +CHECKPOINT_GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS = "gen_ai.client.inference.operation.details" +#: One generation started (the generation attributes). +CHECKPOINT_GENERATION = "generation" +#: The worker accepted the job. +CHECKPOINT_JOB_ACCEPTED = "job_accepted" +#: The server assigned the job. +CHECKPOINT_JOB_ASSIGNED = "job_assigned" +#: The worker received the job request. +CHECKPOINT_JOB_RECEIVED = "job_received" +#: The server-side job start time, as the worker learned it. +CHECKPOINT_JOB_STARTED_ON_SERVER = "job_started_on_server" +#: The join response arrived. +CHECKPOINT_JOIN_RECV = "join_recv" +#: The publisher's SDP offer went out. +CHECKPOINT_OFFER_SENT = "offer_sent" +#: The linked participant left the room, with `lk.disconnect_reason`. +CHECKPOINT_PARTICIPANT_DISCONNECTED = "participant_disconnected" +#: The session linked to its participant. +CHECKPOINT_PARTICIPANT_LINKED = "participant_linked" +#: The primary peer connection connected. +CHECKPOINT_PC_CONNECTED = "pc_connected" +#: The peer connections exist (stamped once the join is adopted). +CHECKPOINT_PC_CREATED = "pc_created" +#: Pre-connect audio was replayed, with `lk.pre_connect_audio.duration`. +CHECKPOINT_PRE_CONNECT_AUDIO = "pre_connect_audio" +#: A preemptive generation was discarded in favour of this one (`lk.speech_id` of the discarded handle). +CHECKPOINT_PREEMPTIVE_GENERATION_DISCARDED = "preemptive_generation_discarded" +#: A job process was launched for it. +CHECKPOINT_PROCESS_ASSIGNED = "process_assigned" +#: A later trigger re-armed the wait. +CHECKPOINT_REARMED = "rearmed" +#: The Room reached the connected state. +CHECKPOINT_ROOM_CONNECTED = "room_connected" +#: Signaling is up (stamped with ws_open on the web and with join_recv on Swift). +CHECKPOINT_SIGNAL = "signal" +#: The hook stopped the response. +CHECKPOINT_STOP_RESPONSE = "stop_response" +#: The platform reported the remote track subscribed. +CHECKPOINT_SUBSCRIBED = "subscribed" +#: A newer detection superseded this one. +CHECKPOINT_SUPERSEDED = "superseded" +#: The track was subscribed, with `lk.track_sid` and `lk.track_source`. +CHECKPOINT_TRACK_SUBSCRIBED = "track_subscribed" +#: The user spoke again during the wait. +CHECKPOINT_USER_RESUMED = "user_resumed" +#: The user state changed (`lk.old_state` -> `lk.new_state`). +CHECKPOINT_USER_STATE_CHANGED = "user_state_changed" +#: The signaling WebSocket opened. +CHECKPOINT_WS_OPEN = "ws_open" + +# Metric names. +#: GenAI operation duration, as the GenAI registry defines it (declared here because weaver cannot import it). Instrument: histogram, unit: `s`. +METRIC_GEN_AI_CLIENT_OPERATION_DURATION = "gen_ai.client.operation.duration" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_GEN_AI_CLIENT_OPERATION_DURATION_UNIT = "s" +#: Time to the first chunk of a streaming GenAI response (deprecated upstream; declared as emitted). Instrument: histogram, unit: `s`. +METRIC_GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK = "gen_ai.client.operation.time_to_first_chunk" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK_UNIT = "s" +#: Input and output tokens used by the GenAI operation (deprecated upstream; declared as emitted). Instrument: histogram, unit: `{token}`. +METRIC_GEN_AI_CLIENT_TOKEN_USAGE = "gen_ai.client.token.usage" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_GEN_AI_CLIENT_TOKEN_USAGE_UNIT = "{token}" +#: Seconds to acquire a provider connection. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_CONNECTION_ACQUIRE_TIME = "lk.agents.connection.acquire_time" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_CONNECTION_ACQUIRE_TIME_UNIT = "s" +#: Duration of synchronous blocks detected on an agent event loop. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION = "lk.agents.event_loop.blocked_duration" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION_UNIT = "s" +#: End-to-end latency of a turn. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_E2E_LATENCY = "lk.agents.turn.e2e_latency" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_E2E_LATENCY_UNIT = "s" +#: Delay from the end of speech to the turn decision. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_END_OF_TURN_DELAY = "lk.agents.turn.end_of_turn_delay" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_END_OF_TURN_DELAY_UNIT = "s" +#: Time to the first LLM token. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_LLM_TTFT = "lk.agents.turn.llm_ttft" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_LLM_TTFT_UNIT = "s" +#: Duration of the `on_user_turn_completed` hook. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY = "lk.agents.turn.on_user_turn_completed_delay" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY_UNIT = "s" +#: Delay from the end of speech to the final transcript. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_TRANSCRIPTION_DELAY = "lk.agents.turn.transcription_delay" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_TRANSCRIPTION_DELAY_UNIT = "s" +#: Time to the first TTS byte. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_TTS_TTFB = "lk.agents.turn.tts_ttfb" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_TURN_TTS_TTFB_UNIT = "s" +#: Interruption-detector requests. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS = "lk.agents.usage.interruption_num_requests" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS_UNIT = "" +#: Realtime-model audio input tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS = "lk.agents.usage.llm_input_audio_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS_UNIT = "" +#: LLM cached input tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS = "lk.agents.usage.llm_input_cached_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS_UNIT = "" +#: Realtime-model text input tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS = "lk.agents.usage.llm_input_text_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS_UNIT = "" +#: LLM input tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TOKENS = "lk.agents.usage.llm_input_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TOKENS_UNIT = "" +#: Realtime-model audio output tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS = "lk.agents.usage.llm_output_audio_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS_UNIT = "" +#: Realtime-model text output tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS = "lk.agents.usage.llm_output_text_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS_UNIT = "" +#: LLM output tokens. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS = "lk.agents.usage.llm_output_tokens" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS_UNIT = "" +#: Realtime-model session seconds. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_LLM_SESSION_DURATION = "lk.agents.usage.llm_session_duration" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_LLM_SESSION_DURATION_UNIT = "s" +#: Seconds of audio transcribed. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_STT_AUDIO_DURATION = "lk.agents.usage.stt_audio_duration" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_STT_AUDIO_DURATION_UNIT = "s" +#: Seconds of audio synthesized. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_TTS_AUDIO_DURATION = "lk.agents.usage.tts_audio_duration" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_TTS_AUDIO_DURATION_UNIT = "s" +#: Characters synthesized. Instrument: counter, unit: ``. +METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS = "lk.agents.usage.tts_characters" +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS_UNIT = "" diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs new file mode 100644 index 000000000..2d7f3e16e --- /dev/null +++ b/otel/gen/rust/semconv.rs @@ -0,0 +1,1615 @@ +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +//! LiveKit telemetry semantic conventions. +//! +//! In a `log` key-value list, write a constant in parentheses, `(LK_PII_ERROR) = value`: `log` +//! stringifies a bare identifier, so `LK_PII_ERROR = value` sends the key `LK_PII_ERROR`. + +/// The agent's name. +pub const AGENT_NAME: &str = "agent_name"; + +/// Browser name. +pub const BROWSER_NAME: &str = "browser.name"; + +/// Browser version. +pub const BROWSER_VERSION: &str = "browser.version"; + +/// One chat history item, as a map. Carries conversation content. PII: redacted by the collector. +pub const CHAT_ITEM: &str = "chat.item"; + +/// Whether a pooled connection was reused, as the string `true`/`false`. +pub const CONNECTION_REUSED: &str = "connection_reused"; + +/// One evaluation result, as a map of string fields. +pub const EVALUATION: &str = "evaluation"; + +/// Token type (`input` or `output`); deprecated upstream, a dimension of `gen_ai.client.token.usage`. +pub const GEN_AI_TOKEN_TYPE: &str = "gen_ai.token.type"; + +/// LangFuse spelling of audio input tokens. +pub const GEN_AI_USAGE_INPUT_AUDIO_TOKENS: &str = "gen_ai.usage.input_audio_tokens"; + +/// LangFuse spelling of cached input tokens. +pub const GEN_AI_USAGE_INPUT_CACHED_TOKENS: &str = "gen_ai.usage.input_cached_tokens"; + +/// LangFuse spelling of text input tokens. +pub const GEN_AI_USAGE_INPUT_TEXT_TOKENS: &str = "gen_ai.usage.input_text_tokens"; + +/// LangFuse spelling of audio output tokens. +pub const GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS: &str = "gen_ai.usage.output_audio_tokens"; + +/// LangFuse spelling of text output tokens. +pub const GEN_AI_USAGE_OUTPUT_TEXT_TOKENS: &str = "gen_ai.usage.output_text_tokens"; + +/// LangFuse spelling of reasoning tokens. +pub const GEN_AI_USAGE_REASONING_TOKENS: &str = "gen_ai.usage.reasoning_tokens"; + +/// LangFuse's completion start time (ISO 8601), set next to `gen_ai.response.time_to_first_chunk`. +pub const LANGFUSE_OBSERVATION_COMPLETION_START_TIME: &str = "langfuse.observation.completion_start_time"; + +/// Label of the agent running the activity. +pub const LK_AGENT_LABEL: &str = "lk.agent_label"; + +/// Name of the agent a job was dispatched to. +pub const LK_AGENT_NAME: &str = "lk.agent_name"; + +/// AMD classification. +pub const LK_AMD_CATEGORY: &str = "lk.amd.category"; + +/// Seconds from the call start to the AMD decision. Unit: `s`. +pub const LK_AMD_DELAY: &str = "lk.amd.delay"; + +/// Why the AMD classifier decided. +pub const LK_AMD_REASON: &str = "lk.amd.reason"; + +/// Seconds of speech the AMD classifier heard. Unit: `s`. +pub const LK_AMD_SPEECH_DURATION: &str = "lk.amd.speech_duration"; + +/// Blocks in the session. +pub const LK_BLOCKING_COUNT: &str = "lk.blocking.count"; + +/// CPU seconds the loop thread consumed during the block. Unit: `s`. +pub const LK_BLOCKING_CPU_TIME: &str = "lk.blocking.cpu_time"; + +/// Seconds the loop was blocked. Unit: `s`. +pub const LK_BLOCKING_DURATION: &str = "lk.blocking.duration"; + +/// Seconds of garbage collection on the loop thread during the block. Unit: `s`. +pub const LK_BLOCKING_GC_TIME: &str = "lk.blocking.gc_time"; + +/// Module a lazy import was loading when sampled. +pub const LK_BLOCKING_IMPORT: &str = "lk.blocking.import"; + +/// Seconds of the longest block in the session. Unit: `s`. +pub const LK_BLOCKING_MAX_DURATION: &str = "lk.blocking.max_duration"; + +/// Severity bucket of the block. +pub const LK_BLOCKING_SEVERITY: &str = "lk.blocking.severity"; + +/// Loop-thread stack sampled while blocked (source locations only). +pub const LK_BLOCKING_STACK: &str = "lk.blocking.stack"; + +/// Reports dropped by rate limiting since the previous emitted span. +pub const LK_BLOCKING_SUPPRESSED: &str = "lk.blocking.suppressed"; + +/// Name of the task that was running. +pub const LK_BLOCKING_TASK: &str = "lk.blocking.task"; + +/// Seconds above which a block is reported. Unit: `s`. +pub const LK_BLOCKING_THRESHOLD: &str = "lk.blocking.threshold"; + +/// Seconds blocked in total over the session. Unit: `s`. +pub const LK_BLOCKING_TOTAL_DURATION: &str = "lk.blocking.total_duration"; + +/// Name of the shutdown callback. +pub const LK_CALLBACK_NAME: &str = "lk.callback.name"; + +/// The close drained pending speech first. +pub const LK_CLOSE_DRAIN: &str = "lk.close.drain"; + +/// Why the session closed. +pub const LK_CLOSE_REASON: &str = "lk.close_reason"; + +/// LiveKit Cloud agent id. +pub const LK_CLOUD_AGENT_ID: &str = "lk.cloud_agent_id"; + +/// 1-based connect attempt. +pub const LK_CONNECT_ATTEMPT: &str = "lk.connect.attempt"; + +/// Room connection state. +pub const LK_CONNECTION_STATE: &str = "lk.connection_state"; + +/// LiveKit Cloud deployment id. +pub const LK_DEPLOYMENT_ID: &str = "lk.deployment_id"; + +/// Whether the app is in front of the user. +pub const LK_DEVICE_APP_STATE: &str = "lk.device.app_state"; + +/// Values of [`LK_DEVICE_APP_STATE`]. +pub mod lk_device_app_state { + /// In front of the user. + pub const FOREGROUND: &str = "foreground"; + /// In the background. + pub const BACKGROUND: &str = "background"; +} + +/// Phase of an audio session interruption. +pub const LK_DEVICE_AUDIO_INTERRUPTION: &str = "lk.device.audio.interruption"; + +/// Values of [`LK_DEVICE_AUDIO_INTERRUPTION`]. +pub mod lk_device_audio_interruption { + /// Interruption began (another app or a call took the audio session). + pub const BEGAN: &str = "began"; + /// Interruption ended. + pub const ENDED: &str = "ended"; +} + +/// The active outputs, comma-separated. +pub const LK_DEVICE_AUDIO_ROUTE_OUTPUTS: &str = "lk.device.audio_route.outputs"; + +/// Why the audio route changed. +pub const LK_DEVICE_AUDIO_ROUTE_REASON: &str = "lk.device.audio_route.reason"; + +/// Values of [`LK_DEVICE_AUDIO_ROUTE_REASON`]. +pub mod lk_device_audio_route_reason { + /// A new output became available. + pub const NEW_DEVICE: &str = "new_device"; + /// The previous output went away. + pub const OLD_DEVICE_UNAVAILABLE: &str = "old_device_unavailable"; + /// The audio session category changed. + pub const CATEGORY_CHANGE: &str = "category_change"; + /// The route was overridden. + pub const OVERRIDE: &str = "override"; + /// The device woke from sleep. + pub const WAKE_FROM_SLEEP: &str = "wake_from_sleep"; + /// No route fits the category. + pub const NO_SUITABLE_ROUTE: &str = "no_suitable_route"; + /// The route's configuration changed. + pub const ROUTE_CONFIGURATION_CHANGE: &str = "route_configuration_change"; + /// The platform gave no reason. + pub const UNKNOWN: &str = "unknown"; +} + +/// The device is charging. +pub const LK_DEVICE_BATTERY_CHARGING: &str = "lk.device.battery.charging"; + +/// Battery charge in percent, 0-100. Unit: `%`. +pub const LK_DEVICE_BATTERY_LEVEL: &str = "lk.device.battery.level"; + +/// Capture device that failed. +pub const LK_DEVICE_CAPTURE_DEVICE: &str = "lk.device.capture.device"; + +/// Values of [`LK_DEVICE_CAPTURE_DEVICE`]. +pub mod lk_device_capture_device { + /// Camera. + pub const CAMERA: &str = "camera"; + /// Microphone. + pub const MICROPHONE: &str = "microphone"; + /// Screen capture. + pub const SCREEN_SHARE: &str = "screen_share"; +} + +/// Why capture failed, in the getUserMedia failure taxonomy. +pub const LK_DEVICE_CAPTURE_REASON: &str = "lk.device.capture.reason"; + +/// Values of [`LK_DEVICE_CAPTURE_REASON`]. +pub mod lk_device_capture_reason { + /// The user or OS denied access. + pub const PERMISSION_DENIED: &str = "permission_denied"; + /// No such device. + pub const NOT_FOUND: &str = "not_found"; + /// Another app holds the device. + pub const IN_USE: &str = "in_use"; + /// The device went away. + pub const DISCONNECTED: &str = "disconnected"; + /// Anything else. + pub const OTHER: &str = "other"; +} + +/// Low Power Mode / Battery Saver is on. +pub const LK_DEVICE_LOW_POWER_ENABLED: &str = "lk.device.low_power.enabled"; + +/// Memory pressure as the OS reports it. +pub const LK_DEVICE_MEMORY_PRESSURE: &str = "lk.device.memory.pressure"; + +/// Values of [`LK_DEVICE_MEMORY_PRESSURE`]. +pub mod lk_device_memory_pressure { + /// No pressure. + pub const NORMAL: &str = "normal"; + /// Low memory. + pub const WARNING: &str = "warning"; + /// Critically low memory. + pub const CRITICAL: &str = "critical"; +} + +/// The user asked for less traffic (Low Data Mode, Data Saver). +pub const LK_DEVICE_NETWORK_CONSTRAINED: &str = "lk.device.network.constrained"; + +/// The network is metered (cellular, personal hotspot). +pub const LK_DEVICE_NETWORK_EXPENSIVE: &str = "lk.device.network.expensive"; + +/// Thermal pressure as the OS reports it. +pub const LK_DEVICE_THERMAL_STATE: &str = "lk.device.thermal.state"; + +/// Values of [`LK_DEVICE_THERMAL_STATE`]. +pub mod lk_device_thermal_state { + /// Within normal limits. + pub const NOMINAL: &str = "nominal"; + /// Slightly elevated. + pub const FAIR: &str = "fair"; + /// High; the system reduces performance. + pub const SERIOUS: &str = "serious"; + /// Critical; the device needs to cool down. + pub const CRITICAL: &str = "critical"; +} + +/// Why a participant left the room, as the protobuf `DisconnectReason` enum name. +pub const LK_DISCONNECT_REASON: &str = "lk.disconnect_reason"; + +/// Values of [`LK_DISCONNECT_REASON`]. +pub mod lk_disconnect_reason { + /// No reason given, or a number the producer does not know. + pub const UNKNOWN_REASON: &str = "UNKNOWN_REASON"; + /// The participant hung up. + pub const CLIENT_INITIATED: &str = "CLIENT_INITIATED"; + /// Another participant joined with the same identity. + pub const DUPLICATE_IDENTITY: &str = "DUPLICATE_IDENTITY"; + /// The server instance shut down. + pub const SERVER_SHUTDOWN: &str = "SERVER_SHUTDOWN"; + /// RoomService.RemoveParticipant was called. + pub const PARTICIPANT_REMOVED: &str = "PARTICIPANT_REMOVED"; + /// RoomService.DeleteRoom was called. + pub const ROOM_DELETED: &str = "ROOM_DELETED"; + /// The server did not know the session the client tried to resume. + pub const STATE_MISMATCH: &str = "STATE_MISMATCH"; + /// The client could not connect fully. + pub const JOIN_FAILURE: &str = "JOIN_FAILURE"; + /// The server asked the participant to migrate (Cloud only). + pub const MIGRATION: &str = "MIGRATION"; + /// The signal WebSocket closed unexpectedly. + pub const SIGNAL_CLOSE: &str = "SIGNAL_CLOSE"; + /// The room closed after its participants left. + pub const ROOM_CLOSED: &str = "ROOM_CLOSED"; + /// The SIP callee did not answer in time. + pub const USER_UNAVAILABLE: &str = "USER_UNAVAILABLE"; + /// The SIP callee rejected the call. + pub const USER_REJECTED: &str = "USER_REJECTED"; + /// SIP protocol failure. + pub const SIP_TRUNK_FAILURE: &str = "SIP_TRUNK_FAILURE"; + /// The server timed out the session. + pub const CONNECTION_TIMEOUT: &str = "CONNECTION_TIMEOUT"; + /// Media failed or timed out. + pub const MEDIA_FAILURE: &str = "MEDIA_FAILURE"; + /// The agent failed. + pub const AGENT_ERROR: &str = "AGENT_ERROR"; + /// The client ran out of reconnect attempts (client only, not a protobuf value). + pub const RECONNECT_FAILED: &str = "RECONNECT_FAILED"; +} + +/// Agent dispatch id. +pub const LK_DISPATCH_ID: &str = "lk.dispatch_id"; + +/// Seconds from the end of the user's speech to the agent's first audio. Unit: `s`. +pub const LK_E2E_LATENCY: &str = "lk.e2e_latency"; + +/// Seconds from the end of speech to the turn decision. Unit: `s`. +pub const LK_END_OF_TURN_DELAY: &str = "lk.end_of_turn_delay"; + +/// Seconds the detector inference took. Unit: `s`. +pub const LK_EOU_DETECTION_DELAY: &str = "lk.eou.detection_delay"; + +/// Seconds of endpointing delay applied. Unit: `s`. +pub const LK_EOU_ENDPOINTING_DELAY: &str = "lk.eou.endpointing_delay"; + +/// The detector answer came from the cache. +pub const LK_EOU_FROM_CACHE: &str = "lk.eou.from_cache"; + +/// Language the detector ran for. +pub const LK_EOU_LANGUAGE: &str = "lk.eou.language"; + +/// Turn decisions the wait rejected before it ended. +pub const LK_EOU_NOT_COMMITTED_COUNT: &str = "lk.eou.not_committed_count"; + +/// How the endpointing wait ended. +pub const LK_EOU_OUTCOME: &str = "lk.eou.outcome"; + +/// Values of [`LK_EOU_OUTCOME`]. +pub mod lk_eou_outcome { + /// The turn was committed. + pub const COMMITTED: &str = "committed"; + /// The user spoke again. + pub const USER_RESUMED: &str = "user_resumed"; + /// The turn was dropped. + pub const DROPPED: &str = "dropped"; +} + +/// End-of-utterance probability. +pub const LK_EOU_PROBABILITY: &str = "lk.eou.probability"; + +/// Times the endpointing wait restarted on a later trigger. +pub const LK_EOU_REARM_COUNT: &str = "lk.eou.rearm_count"; + +/// Endpointing waits the user cut short by speaking again. +pub const LK_EOU_RESUME_COUNT: &str = "lk.eou.resume_count"; + +/// What decided the end of the turn. +pub const LK_EOU_SOURCE: &str = "lk.eou.source"; + +/// Threshold under which the turn is unlikely to be over. +pub const LK_EOU_UNLIKELY_THRESHOLD: &str = "lk.eou.unlikely_threshold"; + +/// Seconds from the end of the user's speech to the turn decision. Unit: `s`. +pub const LK_EOU_WAIT_DURATION: &str = "lk.eou.wait_duration"; + +/// The provider error is retryable (Node binding). +pub const LK_ERROR_RETRYABLE: &str = "lk.error.retryable"; + +/// The session report is tagged failed. +pub const LK_FAIL: &str = "lk.fail"; + +/// Index of the fallback instance that served the request. +pub const LK_FALLBACK_INDEX: &str = "lk.fallback.index"; + +/// Label of the fallback instance that served the request. +pub const LK_FALLBACK_LABEL: &str = "lk.fallback.label"; + +/// The request was a recovery probe of a failed instance. +pub const LK_FALLBACK_RECOVERY: &str = "lk.fallback.recovery"; + +/// Seconds from waiting for a track to its first frame. Unit: `s`. +pub const LK_FIRST_FRAME_DELAY: &str = "lk.first_frame_delay"; + +/// How the inbound frame of a plugin-task metric was resolved (Node binding). +pub const LK_FRAME_RESOLUTION: &str = "lk.frame.resolution"; + +/// Values of [`LK_FRAME_RESOLUTION`]. +pub mod lk_frame_resolution { + /// The root frame. + pub const ROOT: &str = "root"; + /// A live speech frame. + pub const LIVE: &str = "live"; + /// A frame whose speech already ended. + pub const STALE: &str = "stale"; + /// Not a frame the slab knows. + pub const UNKNOWN: &str = "unknown"; +} + +/// Tool call id. +pub const LK_FUNCTION_TOOL_ID: &str = "lk.function_tool.id"; + +/// The tool returned an error. +pub const LK_FUNCTION_TOOL_IS_ERROR: &str = "lk.function_tool.is_error"; + +/// Tool name. +pub const LK_FUNCTION_TOOL_NAME: &str = "lk.function_tool.name"; + +/// Function tool names offered. +pub const LK_FUNCTION_TOOLS: &str = "lk.function_tools"; + +/// Generations in the turn. +pub const LK_GENERATION_COUNT: &str = "lk.generation_count"; + +/// Id of the agent turn (generation). +pub const LK_GENERATION_ID: &str = "lk.generation_id"; + +/// Span whose input record this one continues. +pub const LK_INPUT_BASE_SPAN_ID: &str = "lk.input.base_span_id"; + +/// The span's input record is a delta over its base span. +pub const LK_INPUT_DELTA: &str = "lk.input.delta"; + +/// Items of the base record not carried into this one. +pub const LK_INPUT_DROPPED_FROM_BASE: &str = "lk.input.dropped_from_base"; + +/// The speech was interrupted. +pub const LK_INTERRUPTED: &str = "lk.interrupted"; + +/// Seconds from the user's speech to the interruption decision. Unit: `s`. +pub const LK_INTERRUPTION_DETECTION_DELAY: &str = "lk.interruption.detection_delay"; + +/// Seconds the interruption prediction took. Unit: `s`. +pub const LK_INTERRUPTION_PREDICTION_DURATION: &str = "lk.interruption.prediction_duration"; + +/// Interruption probability. +pub const LK_INTERRUPTION_PROBABILITY: &str = "lk.interruption.probability"; + +/// What triggered the interruption. +pub const LK_INTERRUPTION_SOURCE: &str = "lk.interruption.source"; + +/// Seconds of the interruption from detection to resolution. Unit: `s`. +pub const LK_INTERRUPTION_TOTAL_DURATION: &str = "lk.interruption.total_duration"; + +/// The user turn was classified as an interruption. +pub const LK_IS_INTERRUPTION: &str = "lk.is_interruption"; + +/// Seconds from the job request to its acceptance. Unit: `s`. +pub const LK_JOB_ACCEPT_LATENCY: &str = "lk.job.accept_latency"; + +/// Agent id of the job. +pub const LK_JOB_AGENT_ID: &str = "lk.job.agent_id"; + +/// Seconds from acceptance to the job assignment. Unit: `s`. +pub const LK_JOB_ASSIGNMENT_LATENCY: &str = "lk.job.assignment_latency"; + +/// Seconds from the dispatch request to the entrypoint running. Unit: `s`. +pub const LK_JOB_DISPATCH_LATENCY: &str = "lk.job.dispatch_latency"; + +/// Seconds from launch to the entrypoint running. Unit: `s`. +pub const LK_JOB_ENTRYPOINT_LATENCY: &str = "lk.job.entrypoint_latency"; + +/// Seconds from assignment to the job process launch. Unit: `s`. +pub const LK_JOB_LAUNCH_LATENCY: &str = "lk.job.launch_latency"; + +/// Worker id the job runs on. +pub const LK_JOB_WORKER_ID: &str = "lk.job.worker_id"; + +/// Agent job ID (`AJ_...`). +pub const LK_JOB_ID: &str = "lk.job_id"; + +/// Keyterms added by the update. +pub const LK_KEYTERMS_ADDED: &str = "lk.keyterms.added"; + +/// Number of keyterms configured. +pub const LK_KEYTERMS_COUNT: &str = "lk.keyterms.count"; + +/// Keyterms removed by the update. +pub const LK_KEYTERMS_REMOVED: &str = "lk.keyterms.removed"; + +/// LLM metrics of the turn, serialized. +pub const LK_LLM_METRICS: &str = "lk.llm_metrics"; + +/// Logger, type, module or file that wrote the line. +pub const LK_LOG_LOGGER: &str = "lk.log.logger"; + +/// Which layer wrote the log line. +pub const LK_LOG_SOURCE: &str = "lk.log.source"; + +/// Values of [`LK_LOG_SOURCE`]. +pub mod lk_log_source { + /// The platform SDK. + pub const SDK: &str = "sdk"; + /// The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``. + pub const FFI: &str = "ffi"; + /// WebRTC. + pub const WEBRTC: &str = "webrtc"; +} + +/// Metric type noted from a plugin task (Node binding `metrics` span). +pub const LK_METRIC: &str = "lk.metric"; + +/// State after a transition. +pub const LK_NEW_STATE: &str = "lk.new_state"; + +/// State before a transition. +pub const LK_OLD_STATE: &str = "lk.old_state"; + +/// Seconds the `on_user_turn_completed` hook took. Unit: `s`. +pub const LK_ON_USER_TURN_COMPLETED_DELAY: &str = "lk.on_user_turn_completed_delay"; + +/// How the attempt ended. Rollups read this, never the span status. +pub const LK_OUTCOME: &str = "lk.outcome"; + +/// Values of [`LK_OUTCOME`]. +pub mod lk_outcome { + /// Succeeded; span status Unset. + pub const OK: &str = "ok"; + /// Failed; span status Error, with `error.type` when the failure has a type. + pub const ERROR: &str = "error"; + /// Cancelled by the caller or superseded; span status Unset. + pub const CANCELLED: &str = "cancelled"; +} + +/// Generation this one continues (a tool-call follow-up). +pub const LK_PARENT_GENERATION_ID: &str = "lk.parent_generation_id"; + +/// Server-assigned SID of the local participant (`PA_...`). +pub const LK_PARTICIPANT_ID: &str = "lk.participant_id"; + +/// Kind of the linked participant (protocol `ParticipantInfo.Kind`). +pub const LK_PARTICIPANT_KIND: &str = "lk.participant_kind"; + +/// Transcript the AMD classifier used. PII: redacted by the collector. +pub const LK_PII_AMD_TRANSCRIPT: &str = "lk.pii.amd.transcript"; + +/// Tool-call arguments on a log record. PII: redacted by the collector. +pub const LK_PII_ARGUMENTS: &str = "lk.pii.arguments"; + +/// Serialized chat context sent to the LLM. PII: redacted by the collector. +pub const LK_PII_CHAT_CTX: &str = "lk.pii.chat_ctx"; + +/// DTMF digits received. PII: redacted by the collector. +pub const LK_PII_DTMF_INPUTS: &str = "lk.pii.dtmf_inputs"; + +/// Error text that may quote user content. PII: redacted by the collector. +pub const LK_PII_ERROR: &str = "lk.pii.error"; + +/// Raw provider event on a debug log record. PII: redacted by the collector. +pub const LK_PII_EVENT: &str = "lk.pii.event"; + +/// Tool call arguments. PII: redacted by the collector. +pub const LK_PII_FUNCTION_TOOL_ARGUMENTS: &str = "lk.pii.function_tool.arguments"; + +/// Tool output. PII: redacted by the collector. +pub const LK_PII_FUNCTION_TOOL_OUTPUT: &str = "lk.pii.function_tool.output"; + +/// Text sent to the TTS. PII: redacted by the collector. +pub const LK_PII_INPUT_TEXT: &str = "lk.pii.input_text"; + +/// Agent instructions in force. PII: redacted by the collector. +pub const LK_PII_INSTRUCTIONS: &str = "lk.pii.instructions"; + +/// An interim (non-final) transcript, on a log record. PII: redacted by the collector. +pub const LK_PII_INTERIM_TRANSCRIPT: &str = "lk.pii.interim_transcript"; + +/// The keyterms themselves (customer vocabulary). PII: redacted by the collector. +pub const LK_PII_KEYTERMS: &str = "lk.pii.keyterms"; + +/// Tool-call output on a log record. PII: redacted by the collector. +pub const LK_PII_OUTPUT: &str = "lk.pii.output"; + +/// Customer-supplied identity of the local participant. PII: redacted by the collector. +pub const LK_PII_PARTICIPANT_IDENTITY: &str = "lk.pii.participant_identity"; + +/// MCP tool progress message. PII: redacted by the collector. +pub const LK_PII_PROGRESS_MESSAGE: &str = "lk.pii.progress_message"; + +/// Malformed tool-call arguments as received. PII: redacted by the collector. +pub const LK_PII_RAW_ARGUMENTS: &str = "lk.pii.raw_arguments"; + +/// Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). PII: redacted by the collector. +pub const LK_PII_REMOTE_PARTICIPANT_IDENTITY: &str = "lk.pii.remote_participant_identity"; + +/// Tool-call arguments after repair. PII: redacted by the collector. +pub const LK_PII_REPAIRED: &str = "lk.pii.repaired"; + +/// Function calls the LLM produced, serialized. PII: redacted by the collector. +pub const LK_PII_RESPONSE_FUNCTION_CALLS: &str = "lk.pii.response.function_calls"; + +/// Text the LLM produced. PII: redacted by the collector. +pub const LK_PII_RESPONSE_TEXT: &str = "lk.pii.response.text"; + +/// Customer-supplied room name. PII: redacted by the collector. +pub const LK_PII_ROOM_NAME: &str = "lk.pii.room_name"; + +/// RPC request payload. PII: redacted by the collector. +pub const LK_PII_RPC_PAYLOAD: &str = "lk.pii.rpc.payload"; + +/// RPC response payload. PII: redacted by the collector. +pub const LK_PII_RPC_RESPONSE: &str = "lk.pii.rpc.response"; + +/// Phone number of a SIP participant. PII: redacted by the collector. +pub const LK_PII_SIP_PHONE_NUMBER: &str = "lk.pii.sip.phoneNumber"; + +/// Message text on a log record. PII: redacted by the collector. +pub const LK_PII_TEXT: &str = "lk.pii.text"; + +/// Transcript text on a log record. PII: redacted by the collector. +pub const LK_PII_TRANSCRIPT: &str = "lk.pii.transcript"; + +/// The user's transcript for the turn. PII: redacted by the collector. +pub const LK_PII_USER_INPUT: &str = "lk.pii.user_input"; + +/// Transcript the detector scored. PII: redacted by the collector. +pub const LK_PII_USER_TRANSCRIPT: &str = "lk.pii.user_transcript"; + +/// Monotonically increasing sequence number per pipeline. +pub const LK_PING_SEQ: &str = "lk.ping.seq"; + +/// Seconds of the agent's speech played out when it was interrupted. Unit: `s`. +pub const LK_PLAYOUT_POSITION: &str = "lk.playout.position"; + +/// Seconds of pre-connect audio replayed. Unit: `s`. +pub const LK_PRE_CONNECT_AUDIO_DURATION: &str = "lk.pre_connect_audio.duration"; + +/// Label of the agent handed off from. +pub const LK_PREVIOUS_AGENT_LABEL: &str = "lk.previous_agent_label"; + +/// Provider request ids a span covers, for joining against provider logs. +pub const LK_PROVIDER_REQUEST_IDS: &str = "lk.provider_request_ids"; + +/// Provider-side tool names offered. +pub const LK_PROVIDER_TOOLS: &str = "lk.provider_tools"; + +/// Realtime model metrics of the turn, serialized. +pub const LK_REALTIME_MODEL_METRICS: &str = "lk.realtime_model_metrics"; + +/// Reconnect attempts made in the cycle. +pub const LK_RECONNECT_ATTEMPTS: &str = "lk.reconnect.attempts"; + +/// Mode of the last reconnect attempt. +pub const LK_RECONNECT_MODE: &str = "lk.reconnect.mode"; + +/// Values of [`LK_RECONNECT_MODE`]. +pub mod lk_reconnect_mode { + /// Resume the session (ICE restart). + pub const QUICK: &str = "quick"; + /// Reconnect from scratch. + pub const FULL: &str = "full"; +} + +/// What triggered the reconnect cycle. +pub const LK_RECONNECT_REASON: &str = "lk.reconnect.reason"; + +/// Values of [`LK_RECONNECT_REASON`]. +pub mod lk_reconnect_reason { + /// The signaling socket closed (RR_SIGNAL_DISCONNECTED). + pub const SIGNAL_DISCONNECTED: &str = "signal_disconnected"; + /// The publisher peer connection failed (RR_PUBLISHER_FAILED). + pub const PUBLISHER_FAILED: &str = "publisher_failed"; + /// The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). + pub const SUBSCRIBER_FAILED: &str = "subscriber_failed"; + /// A peer connection failed and the platform did not say which (client only). + pub const TRANSPORT_FAILED: &str = "transport_failed"; + /// The server asked for a candidate switch (RR_SWITCH_CANDIDATE). + pub const SWITCH_CANDIDATE: &str = "switch_candidate"; + /// The device moved to another network (client only). + pub const NETWORK_CHANGED: &str = "network_changed"; + /// A test or a debug menu asked for it (client only). + pub const DEBUG: &str = "debug"; + /// RR_UNKNOWN or a number the client does not know. + pub const UNKNOWN: &str = "unknown"; +} + +/// PII redaction was requested for the session. +pub const LK_REDACTION_ENABLED: &str = "lk.redaction.enabled"; + +/// Seconds to the first TTS audio byte. Unit: `s`. +pub const LK_RESPONSE_TTFB: &str = "lk.response.ttfb"; + +/// Seconds to the first LLM token. Unit: `s`. +pub const LK_RESPONSE_TTFT: &str = "lk.response.ttft"; + +/// Retries before the operation succeeded or gave up. +pub const LK_RETRY_COUNT: &str = "lk.retry_count"; + +/// The room was joined with auto-subscribe. +pub const LK_ROOM_AUTO_SUBSCRIBE: &str = "lk.room.auto_subscribe"; + +/// End-to-end encryption is on. +pub const LK_ROOM_E2EE: &str = "lk.room.e2ee"; + +/// Remote participants when the agent connected. +pub const LK_ROOM_REMOTE_PARTICIPANT_COUNT: &str = "lk.room.remote_participant_count"; + +/// How room I/O picked its participant. +pub const LK_ROOM_IO_PARTICIPANT_FILTER: &str = "lk.room_io.participant_filter"; + +/// Server-assigned room session SID (`RM_...`). +pub const LK_ROOM_SID: &str = "lk.room_sid"; + +/// Identity of the RPC caller. +pub const LK_RPC_CALLER_IDENTITY: &str = "lk.rpc.caller_identity"; + +/// Identity the RPC is sent to. +pub const LK_RPC_DESTINATION_IDENTITY: &str = "lk.rpc.destination_identity"; + +/// RPC error code. +pub const LK_RPC_ERROR_CODE: &str = "lk.rpc.error_code"; + +/// A handler was registered for the method. +pub const LK_RPC_HANDLER_REGISTERED: &str = "lk.rpc.handler_registered"; + +/// RPC request payload size in bytes. Unit: `By`. +pub const LK_RPC_PAYLOAD_SIZE: &str = "lk.rpc.payload_size"; + +/// RPC request id. +pub const LK_RPC_REQUEST_ID: &str = "lk.rpc.request_id"; + +/// RPC response size in bytes. Unit: `By`. +pub const LK_RPC_RESPONSE_SIZE: &str = "lk.rpc.response_size"; + +/// RPC response timeout in seconds. Unit: `s`. +pub const LK_RPC_RESPONSE_TIMEOUT: &str = "lk.rpc.response_timeout"; + +/// Average audio level in the window, 0-1, inbound audio. Unit: `1`. +pub const LK_RTC_AUDIO_LEVEL_AVG: &str = "lk.rtc.audio_level.avg"; + +/// Maximum audio level in the window, 0-1, inbound audio. Unit: `1`. +pub const LK_RTC_AUDIO_LEVEL_MAX: &str = "lk.rtc.audio_level.max"; + +/// Minimum audio level in the window, 0-1, inbound audio. Unit: `1`. +pub const LK_RTC_AUDIO_LEVEL_MIN: &str = "lk.rtc.audio_level.min"; + +/// Bytes sent or received (cumulative). Unit: `By`. +pub const LK_RTC_BYTES: &str = "lk.rtc.bytes"; + +/// Codec mimeType, when known. +pub const LK_RTC_CODEC: &str = "lk.rtc.codec"; + +/// Concealed audio samples, inbound audio (cumulative). Unit: `{sample}`. +pub const LK_RTC_CONCEALED_SAMPLES: &str = "lk.rtc.concealed_samples"; + +/// Concealment events, inbound audio (cumulative). Unit: `{event}`. +pub const LK_RTC_CONCEALMENT_EVENTS: &str = "lk.rtc.concealment_events"; + +/// Average frame rate in the window, video. Unit: `{frame}/s`. +pub const LK_RTC_FPS_AVG: &str = "lk.rtc.fps.avg"; + +/// Maximum frame rate in the window, video. Unit: `{frame}/s`. +pub const LK_RTC_FPS_MAX: &str = "lk.rtc.fps.max"; + +/// Minimum frame rate in the window, video. Unit: `{frame}/s`. +pub const LK_RTC_FPS_MIN: &str = "lk.rtc.fps.min"; + +/// Video freezes, inbound video (cumulative). Unit: `{freeze}`. +pub const LK_RTC_FREEZE_COUNT: &str = "lk.rtc.freeze_count"; + +/// Total freeze time, inbound video (cumulative). Unit: `ms`. +pub const LK_RTC_FREEZES_DURATION_MS: &str = "lk.rtc.freezes_duration_ms"; + +/// Audio interruptions, inbound audio (cumulative). Unit: `{interruption}`. +pub const LK_RTC_INTERRUPTION_COUNT: &str = "lk.rtc.interruption_count"; + +/// Total interruption time, inbound audio (cumulative). Unit: `ms`. +pub const LK_RTC_INTERRUPTIONS_DURATION_MS: &str = "lk.rtc.interruptions_duration_ms"; + +/// Jitter buffer delay, inbound (cumulative). Unit: `ms`. +pub const LK_RTC_JITTER_BUFFER_DELAY_MS: &str = "lk.rtc.jitter_buffer_delay_ms"; + +/// Samples or frames emitted by the jitter buffer, inbound (cumulative). Unit: `{sample}`. +pub const LK_RTC_JITTER_BUFFER_EMITTED_COUNT: &str = "lk.rtc.jitter_buffer_emitted_count"; + +/// Average jitter in the window, inbound. Unit: `ms`. +pub const LK_RTC_JITTER_MS_AVG: &str = "lk.rtc.jitter_ms.avg"; + +/// Maximum jitter in the window, inbound. Unit: `ms`. +pub const LK_RTC_JITTER_MS_MAX: &str = "lk.rtc.jitter_ms.max"; + +/// Minimum jitter in the window, inbound. Unit: `ms`. +pub const LK_RTC_JITTER_MS_MIN: &str = "lk.rtc.jitter_ms.min"; + +/// Packets sent or received (cumulative). Unit: `{packet}`. +pub const LK_RTC_PACKETS: &str = "lk.rtc.packets"; + +/// Packets lost, inbound (cumulative). Unit: `{packet}`. +pub const LK_RTC_PACKETS_LOST: &str = "lk.rtc.packets_lost"; + +/// Video pauses, inbound video (cumulative). Unit: `{pause}`. +pub const LK_RTC_PAUSE_COUNT: &str = "lk.rtc.pause_count"; + +/// Total pause time, inbound video (cumulative). Unit: `ms`. +pub const LK_RTC_PAUSES_DURATION_MS: &str = "lk.rtc.pauses_duration_ms"; + +/// Time the encoder was limited by bandwidth, outbound video (cumulative). Unit: `ms`. +pub const LK_RTC_QUALITY_LIMITATION_BANDWIDTH_MS: &str = "lk.rtc.quality_limitation.bandwidth_ms"; + +/// Time the encoder was limited by CPU, outbound video (cumulative). Unit: `ms`. +pub const LK_RTC_QUALITY_LIMITATION_CPU_MS: &str = "lk.rtc.quality_limitation.cpu_ms"; + +/// Time the encoder was limited for other reasons, outbound video (cumulative). Unit: `ms`. +pub const LK_RTC_QUALITY_LIMITATION_OTHER_MS: &str = "lk.rtc.quality_limitation.other_ms"; + +/// Average round-trip time in the window. Unit: `ms`. +pub const LK_RTC_RTT_MS_AVG: &str = "lk.rtc.rtt_ms.avg"; + +/// Maximum round-trip time in the window. Unit: `ms`. +pub const LK_RTC_RTT_MS_MAX: &str = "lk.rtc.rtt_ms.max"; + +/// Minimum round-trip time in the window. Unit: `ms`. +pub const LK_RTC_RTT_MS_MIN: &str = "lk.rtc.rtt_ms.min"; + +/// Stats readings folded into the window. Unit: `{reading}`. +pub const LK_RTC_SAMPLES: &str = "lk.rtc.samples"; + +/// Concealed samples that were silent, inbound audio (cumulative). Unit: `{sample}`. +pub const LK_RTC_SILENT_CONCEALED_SAMPLES: &str = "lk.rtc.silent_concealed_samples"; + +/// Actual length of the stats window. Unit: `ms`. +pub const LK_RTC_WINDOW_MS: &str = "lk.rtc.window_ms"; + +/// LiveKit SDK name. +pub const LK_SDK_NAME: &str = "lk.sdk.name"; + +/// LiveKit SDK version. +pub const LK_SDK_VERSION: &str = "lk.sdk.version"; + +/// Why the job shut down. +pub const LK_SHUTDOWN_REASON: &str = "lk.shutdown.reason"; + +/// The shutdown was requested by user code. +pub const LK_SHUTDOWN_USER_INITIATED: &str = "lk.shutdown.user_initiated"; + +/// The session is a simulation. +pub const LK_SIMULATION_ENABLED: &str = "lk.simulation.enabled"; + +/// Simulation run job (one scenario) behind the session. +pub const LK_SIMULATION_JOB_ID: &str = "lk.simulation.job_id"; + +/// Simulation run the session belongs to. +pub const LK_SIMULATION_RUN_ID: &str = "lk.simulation.run_id"; + +/// Seconds a speech handle waited in the queue before generation was authorized. Unit: `s`. +pub const LK_SPEECH_QUEUE_WAIT: &str = "lk.speech.queue_wait"; + +/// Id of the speech handle a span belongs to. +pub const LK_SPEECH_ID: &str = "lk.speech_id"; + +/// JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length). +pub const LK_STT_EVENTS: &str = "lk.stt.events"; + +/// The session report is tagged successful. +pub const LK_SUCCESS: &str = "lk.success"; + +/// Batches waiting in the cache now. Unit: `{batch}`. +pub const LK_TELEMETRY_CACHE_BATCHES: &str = "lk.telemetry.cache.batches"; + +/// Sent batches the cache could not delete; never sent twice from the same launch. Unit: `{batch}`. +pub const LK_TELEMETRY_CACHE_DELETE_ERRORS: &str = "lk.telemetry.cache.delete_errors"; + +/// Times the cache could not be listed, so nothing was reconciled or sent. Unit: `{attempt}`. +pub const LK_TELEMETRY_CACHE_LIST_ERRORS: &str = "lk.telemetry.cache.list_errors"; + +/// Cached batches that could not be read right now, kept for the next pass. Unit: `{batch}`. +pub const LK_TELEMETRY_CACHE_READ_ERRORS: &str = "lk.telemetry.cache.read_errors"; + +/// Batches the disk refused, kept in memory instead. Unit: `{batch}`. +pub const LK_TELEMETRY_CACHE_WRITE_ERRORS: &str = "lk.telemetry.cache.write_errors"; + +/// Records no cache, not even memory, could take. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_CACHE_ERROR: &str = "lk.telemetry.dropped.cache_error"; + +/// Records evicted by the cache's size or file-count bound. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_CACHE_FULL: &str = "lk.telemetry.dropped.cache_full"; + +/// Records in cached batches that failed their CRC. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_CORRUPT: &str = "lk.telemetry.dropped.corrupt"; + +/// Records in batches past the cache's age limit. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_EXPIRED: &str = "lk.telemetry.dropped.expired"; + +/// Custom events, attributes or SDK strings over the limits, rejected. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_INVALID: &str = "lk.telemetry.dropped.invalid"; + +/// Single records larger than the collector accepts (413). Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_OVERSIZED: &str = "lk.telemetry.dropped.oversized"; + +/// Records evicted from the in-memory queue. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_QUEUE_FULL: &str = "lk.telemetry.dropped.queue_full"; + +/// Discrete events dropped by the flood guard. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_RATE_LIMITED: &str = "lk.telemetry.dropped.rate_limited"; + +/// Records the collector rejected (final 4xx/5xx, OTLP partial success). Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_REJECTED: &str = "lk.telemetry.dropped.rejected"; + +/// Records evicted from the cache during a server-directed pause. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_THROTTLED: &str = "lk.telemetry.dropped.throttled"; + +/// Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_UNATTRIBUTED: &str = "lk.telemetry.dropped.unattributed"; + +/// Soft upload holds that reached their cap. Unit: `{hold}`. +pub const LK_TELEMETRY_HOLDS_CAPPED: &str = "lk.telemetry.holds.capped"; + +/// Compressed bytes the collector accepted. Unit: `By`. +pub const LK_TELEMETRY_UPLOADS_BYTES: &str = "lk.telemetry.uploads.bytes"; + +/// Upload attempts that failed transiently (no answer, 429, 5xx). Unit: `{attempt}`. +pub const LK_TELEMETRY_UPLOADS_FAILED: &str = "lk.telemetry.uploads.failed"; + +/// Batches the collector accepted. Unit: `{batch}`. +pub const LK_TELEMETRY_UPLOADS_SENT: &str = "lk.telemetry.uploads.sent"; + +/// Upload attempts that hit the export timeout. Unit: `{attempt}`. +pub const LK_TELEMETRY_UPLOADS_TIMEOUTS: &str = "lk.telemetry.uploads.timeouts"; + +/// 401/403 answers (tokens the collector refused). Unit: `{attempt}`. +pub const LK_TELEMETRY_UPLOADS_UNAUTHORIZED: &str = "lk.telemetry.uploads.unauthorized"; + +/// Tool set names offered. +pub const LK_TOOL_SETS: &str = "lk.tool_sets"; + +/// Whether the stats describe sent or received media. +pub const LK_TRACK_DIRECTION: &str = "lk.track.direction"; + +/// Values of [`LK_TRACK_DIRECTION`]. +pub mod lk_track_direction { + /// Received media (inbound-rtp). + pub const INBOUND: &str = "inbound"; + /// Sent media (outbound-rtp). + pub const OUTBOUND: &str = "outbound"; +} + +/// Media kind of the track. +pub const LK_TRACK_KIND: &str = "lk.track.kind"; + +/// Values of [`LK_TRACK_KIND`]. +pub mod lk_track_kind { + /// Audio. + pub const AUDIO: &str = "audio"; + /// Video. + pub const VIDEO: &str = "video"; +} + +/// Server-assigned track SID (`TR_...`). +pub const LK_TRACK_SID: &str = "lk.track_sid"; + +/// Source of the track, as the Agents RTC FFI `TrackSource` enum name. +pub const LK_TRACK_SOURCE: &str = "lk.track_source"; + +/// Values of [`LK_TRACK_SOURCE`]. +pub mod lk_track_source { + /// Unknown source. + pub const SOURCE_UNKNOWN: &str = "SOURCE_UNKNOWN"; + /// Camera. + pub const SOURCE_CAMERA: &str = "SOURCE_CAMERA"; + /// Microphone. + pub const SOURCE_MICROPHONE: &str = "SOURCE_MICROPHONE"; + /// Screen share video. + pub const SOURCE_SCREENSHARE: &str = "SOURCE_SCREENSHARE"; + /// Screen share audio. + pub const SOURCE_SCREENSHARE_AUDIO: &str = "SOURCE_SCREENSHARE_AUDIO"; +} + +/// Confidence of the final transcript. +pub const LK_TRANSCRIPT_CONFIDENCE: &str = "lk.transcript_confidence"; + +/// Seconds from the end of speech to the final transcript. Unit: `s`. +pub const LK_TRANSCRIPTION_DELAY: &str = "lk.transcription_delay"; + +/// Audio bytes the TTS driver collected (Node binding). Unit: `By`. +pub const LK_TTS_BYTES: &str = "lk.tts.bytes"; + +/// Audio frames the TTS driver collected (Node binding). +pub const LK_TTS_FRAMES: &str = "lk.tts.frames"; + +/// Label of the TTS instance. +pub const LK_TTS_LABEL: &str = "lk.tts.label"; + +/// The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure). +pub const LK_TTS_NO_DUPLEX_PATH: &str = "lk.tts.no_duplex_path"; + +/// The TTS instance streams (duplex path). +pub const LK_TTS_STREAMING: &str = "lk.tts.streaming"; + +/// TTS metrics of the turn, serialized. +pub const LK_TTS_METRICS: &str = "lk.tts_metrics"; + +/// Model name the plugin reported. +pub const MODEL_NAME: &str = "model_name"; + +/// Provider the plugin reported. +pub const MODEL_PROVIDER: &str = "model_provider"; + +/// The session outcome, as a map with the field `outcome` (`success` or `fail`) and an optional `reason`. +pub const OUTCOME: &str = "outcome"; + +/// The Agents SDK version. +pub const SDK_VERSION: &str = "sdk_version"; + +/// The session's options, as a map of option name to value. +pub const SESSION_OPTIONS: &str = "session.options"; + +/// When the report was made, in Unix seconds. +pub const SESSION_REPORT_TIMESTAMP: &str = "session.report_timestamp"; + +/// The session's tags, sorted. Absent when the session has none. +pub const SESSION_TAGS: &str = "session.tags"; + +/// One tag with metadata, as a map with the fields `name` and `metadata`. +pub const TAG: &str = "tag"; + +/// Model usage, as a list of maps, one per model. Absent when there was none. +pub const USAGE: &str = "usage"; + +/// Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, +/// never here), so a producer can name them from one place. +pub mod upstream { + /// The source code filename that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. + pub const CODE_FILE_PATH: &str = "code.file.path"; + /// The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. + pub const CODE_FUNCTION_NAME: &str = "code.function.name"; + /// The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. + pub const CODE_LINE_NUMBER: &str = "code.line.number"; + /// The model identifier for the device + pub const DEVICE_MODEL_IDENTIFIER: &str = "device.model.identifier"; + /// Describes a class of error the operation ended with. + pub const ERROR_TYPE: &str = "error.type"; + + /// Values of [`ERROR_TYPE`]. + pub mod error_type { + /// A fallback error value to be used when the instrumentation doesn't define a custom value. + pub const OTHER: &str = "_OTHER"; + } + /// The exception message. + pub const EXCEPTION_MESSAGE: &str = "exception.message"; + /// A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. + pub const EXCEPTION_STACKTRACE: &str = "exception.stacktrace"; + /// The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. + pub const EXCEPTION_TYPE: &str = "exception.type"; + /// Human-readable name of the GenAI agent provided by the application. + pub const GEN_AI_AGENT_NAME: &str = "gen_ai.agent.name"; + /// The unique identifier for a conversation (session, thread), used to store and correlate messages within this conversation. + pub const GEN_AI_CONVERSATION_ID: &str = "gen_ai.conversation.id"; + /// A free-form explanation for the assigned score provided by the evaluator. + pub const GEN_AI_EVALUATION_EXPLANATION: &str = "gen_ai.evaluation.explanation"; + /// The name of the evaluation metric used for the GenAI response. + pub const GEN_AI_EVALUATION_NAME: &str = "gen_ai.evaluation.name"; + /// Human readable label for evaluation. + pub const GEN_AI_EVALUATION_SCORE_LABEL: &str = "gen_ai.evaluation.score.label"; + /// The chat history provided to the model as an input. + pub const GEN_AI_INPUT_MESSAGES: &str = "gen_ai.input.messages"; + /// The search query used to retrieve memories. + pub const GEN_AI_MEMORY_QUERY_TEXT: &str = "gen_ai.memory.query.text"; + /// The memory records stored or retrieved in a memory operation. + pub const GEN_AI_MEMORY_RECORDS: &str = "gen_ai.memory.records"; + /// The name of the operation being performed. + pub const GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name"; + + /// Values of [`GEN_AI_OPERATION_NAME`]. + pub mod gen_ai_operation_name { + /// Chat completion operation such as [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) + pub const CHAT: &str = "chat"; + /// Multimodal content generation operation such as [Gemini Generate Content](https://ai.google.dev/api/generate-content) + pub const GENERATE_CONTENT: &str = "generate_content"; + /// Text completions operation such as [OpenAI Completions API (Legacy)](https://platform.openai.com/docs/api-reference/completions) + pub const TEXT_COMPLETION: &str = "text_completion"; + /// Embeddings operation such as [OpenAI Create embeddings API](https://platform.openai.com/docs/api-reference/embeddings/create) + pub const EMBEDDINGS: &str = "embeddings"; + /// Retrieval operation such as [OpenAI Search Vector Store API](https://platform.openai.com/docs/api-reference/vector-stores/search) + pub const RETRIEVAL: &str = "retrieval"; + /// Fetch a previously generated model response by its identifier, without performing inference, such as [OpenAI Get a model response](https://platform.openai.com/docs/api-reference/responses/get) + pub const FETCH_RESPONSE: &str = "fetch_response"; + /// Create GenAI agent + pub const CREATE_AGENT: &str = "create_agent"; + /// Invoke GenAI agent + pub const INVOKE_AGENT: &str = "invoke_agent"; + /// Execute a tool + pub const EXECUTE_TOOL: &str = "execute_tool"; + /// Invoke GenAI workflow + pub const INVOKE_WORKFLOW: &str = "invoke_workflow"; + /// Agent planning or task decomposition phase + pub const PLAN: &str = "plan"; + /// Search/query memories from a memory store + pub const SEARCH_MEMORY: &str = "search_memory"; + /// Create new memory records + pub const CREATE_MEMORY: &str = "create_memory"; + /// Update existing memory records + pub const UPDATE_MEMORY: &str = "update_memory"; + /// Create or update memory records without the caller choosing which + pub const UPSERT_MEMORY: &str = "upsert_memory"; + /// Delete memory records + pub const DELETE_MEMORY: &str = "delete_memory"; + /// Create or initialize a memory store + pub const CREATE_MEMORY_STORE: &str = "create_memory_store"; + /// Delete or deprovision a memory store + pub const DELETE_MEMORY_STORE: &str = "delete_memory_store"; + } + /// Messages returned by the model where each message represents a specific model response (choice, candidate). + pub const GEN_AI_OUTPUT_MESSAGES: &str = "gen_ai.output.messages"; + /// Represents the content type requested by the client. + pub const GEN_AI_OUTPUT_TYPE: &str = "gen_ai.output.type"; + + /// Values of [`GEN_AI_OUTPUT_TYPE`]. + pub mod gen_ai_output_type { + /// Plain text + pub const TEXT: &str = "text"; + /// JSON object with known or unknown schema + pub const JSON: &str = "json"; + /// Image + pub const IMAGE: &str = "image"; + /// Speech + pub const SPEECH: &str = "speech"; + } + /// The variables supplied to the prompt template, the `` being the variable name, the value being the variable value. + pub const GEN_AI_PROMPT_VARIABLE: &str = "gen_ai.prompt.variable"; + /// The Generative AI provider as identified by the client or server instrumentation. + pub const GEN_AI_PROVIDER_NAME: &str = "gen_ai.provider.name"; + + /// Values of [`GEN_AI_PROVIDER_NAME`]. + pub mod gen_ai_provider_name { + /// [OpenAI](https://openai.com/) + pub const OPENAI: &str = "openai"; + /// Any Google generative AI endpoint + pub const GCP_GEN_AI: &str = "gcp.gen_ai"; + /// [Vertex AI](https://cloud.google.com/vertex-ai) + pub const GCP_VERTEX_AI: &str = "gcp.vertex_ai"; + /// [Gemini](https://cloud.google.com/products/gemini) + pub const GCP_GEMINI: &str = "gcp.gemini"; + /// [Anthropic](https://www.anthropic.com/) + pub const ANTHROPIC: &str = "anthropic"; + /// [Cohere](https://cohere.com/) + pub const COHERE: &str = "cohere"; + /// Azure AI Inference + pub const AZURE_AI_INFERENCE: &str = "azure.ai.inference"; + /// [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) + pub const AZURE_AI_OPENAI: &str = "azure.ai.openai"; + /// [IBM Watsonx AI](https://www.ibm.com/products/watsonx-ai) + pub const IBM_WATSONX_AI: &str = "ibm.watsonx.ai"; + /// [AWS Bedrock](https://aws.amazon.com/bedrock) + pub const AWS_BEDROCK: &str = "aws.bedrock"; + /// [Perplexity](https://www.perplexity.ai/) + pub const PERPLEXITY: &str = "perplexity"; + /// [xAI](https://x.ai/) + pub const X_AI: &str = "x_ai"; + /// [DeepSeek](https://www.deepseek.com/) + pub const DEEPSEEK: &str = "deepseek"; + /// [Groq](https://groq.com/) + pub const GROQ: &str = "groq"; + /// [Mistral](https://mistral.ai/) + pub const MISTRAL: &str = "mistral"; + /// [Moonshot AI](https://www.moonshot.ai/) + pub const MOONSHOT_AI: &str = "moonshot_ai"; + /// Mistral, as Agents sends it. The upstream member is `mistral`. Sent by LiveKit, not an upstream value. + pub const MISTRAL_AI: &str = "mistral_ai"; + } + /// The name of the GenAI model a request is being made to. + pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model"; + /// Indicates whether the GenAI request was made in streaming mode. + pub const GEN_AI_REQUEST_STREAM: &str = "gen_ai.request.stream"; + /// Array of reasons the model stopped generating tokens, corresponding to each generation received. + pub const GEN_AI_RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons"; + + /// Values of [`GEN_AI_RESPONSE_FINISH_REASONS`]. + pub mod gen_ai_response_finish_reasons { + /// Declared in Agents' `GenAIFinishReason`. No Agents code path sends it today. Sent by LiveKit, not an upstream value. + pub const COMPACTION: &str = "compaction"; + /// A content filter stopped the output. Sent by LiveKit, not an upstream value. + pub const CONTENT_FILTER: &str = "content_filter"; + /// The generation ended abnormally. The upstream note names this value. Sent by LiveKit, not an upstream value. + pub const ERROR: &str = "error"; + /// The output reached the token limit. Sent by LiveKit, not an upstream value. + pub const LENGTH: &str = "length"; + /// The generation finished normally. Sent by LiveKit, not an upstream value. + pub const STOP: &str = "stop"; + /// The generation ended with a tool call. Sent by LiveKit, not an upstream value. + pub const TOOL_CALL: &str = "tool_call"; + } + /// The unique identifier for the completion. + pub const GEN_AI_RESPONSE_ID: &str = "gen_ai.response.id"; + /// The name of the model that generated the response. + pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model"; + /// Time to first chunk in a streaming response, measured from request issuance, in seconds. The value is measured from when the client issues the generation request to when the first chunk is received in the response stream. + pub const GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK: &str = "gen_ai.response.time_to_first_chunk"; + /// The documents retrieved. + pub const GEN_AI_RETRIEVAL_DOCUMENTS: &str = "gen_ai.retrieval.documents"; + /// The query text used for retrieval. + pub const GEN_AI_RETRIEVAL_QUERY_TEXT: &str = "gen_ai.retrieval.query.text"; + /// The system message or instructions provided to the GenAI model separately from the chat history. + pub const GEN_AI_SYSTEM_INSTRUCTIONS: &str = "gen_ai.system_instructions"; + /// Parameters passed to the tool call. + pub const GEN_AI_TOOL_CALL_ARGUMENTS: &str = "gen_ai.tool.call.arguments"; + /// The tool call identifier supplied by the model or provider. + pub const GEN_AI_TOOL_CALL_ID: &str = "gen_ai.tool.call.id"; + /// The result returned by the tool call (if any and if execution was successful). + pub const GEN_AI_TOOL_CALL_RESULT: &str = "gen_ai.tool.call.result"; + /// The list of tool definitions available to the GenAI agent or model. + pub const GEN_AI_TOOL_DEFINITIONS: &str = "gen_ai.tool.definitions"; + /// The tool description. + pub const GEN_AI_TOOL_DESCRIPTION: &str = "gen_ai.tool.description"; + /// Name of the tool utilized by the agent. + pub const GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name"; + /// Type of the tool utilized by the agent + pub const GEN_AI_TOOL_TYPE: &str = "gen_ai.tool.type"; + /// The number of audio input tokens served from a provider-managed cache. + pub const GEN_AI_USAGE_AUDIO_CACHE_READ_INPUT_TOKENS: &str = "gen_ai.usage.audio.cache_read.input_tokens"; + /// The number of audio input tokens. + pub const GEN_AI_USAGE_AUDIO_INPUT_TOKENS: &str = "gen_ai.usage.audio.input_tokens"; + /// The number of audio output tokens. + pub const GEN_AI_USAGE_AUDIO_OUTPUT_TOKENS: &str = "gen_ai.usage.audio.output_tokens"; + /// The number of input tokens served from a provider-managed cache. + pub const GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: &str = "gen_ai.usage.cache_read.input_tokens"; + /// The number of input tokens written to a provider-managed cache. + pub const GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS: &str = "gen_ai.usage.cache_write.input_tokens"; + /// The number of image input tokens served from a provider-managed cache. + pub const GEN_AI_USAGE_IMAGE_CACHE_READ_INPUT_TOKENS: &str = "gen_ai.usage.image.cache_read.input_tokens"; + /// The number of image input tokens. + pub const GEN_AI_USAGE_IMAGE_INPUT_TOKENS: &str = "gen_ai.usage.image.input_tokens"; + /// The number of tokens used in the GenAI input (prompt). + pub const GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens"; + /// The number of tokens used in the GenAI response (completion). + pub const GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens"; + /// The number of output tokens used for reasoning (e.g. chain-of-thought, extended thinking). + pub const GEN_AI_USAGE_REASONING_OUTPUT_TOKENS: &str = "gen_ai.usage.reasoning.output_tokens"; + /// The number of text input tokens served from a provider-managed cache. + pub const GEN_AI_USAGE_TEXT_CACHE_READ_INPUT_TOKENS: &str = "gen_ai.usage.text.cache_read.input_tokens"; + /// The number of text input tokens. + pub const GEN_AI_USAGE_TEXT_INPUT_TOKENS: &str = "gen_ai.usage.text.input_tokens"; + /// The number of text output tokens. + pub const GEN_AI_USAGE_TEXT_OUTPUT_TOKENS: &str = "gen_ai.usage.text.output_tokens"; + /// Human-readable name of the GenAI workflow provided by the application. + pub const GEN_AI_WORKFLOW_NAME: &str = "gen_ai.workflow.name"; + /// The internet connection type. + pub const NETWORK_CONNECTION_TYPE: &str = "network.connection.type"; + + /// Values of [`NETWORK_CONNECTION_TYPE`]. + pub mod network_connection_type { + /// Wi-Fi + pub const WIFI: &str = "wifi"; + /// `wired`. + pub const WIRED: &str = "wired"; + /// `cell`. + pub const CELL: &str = "cell"; + /// `unavailable`. + pub const UNAVAILABLE: &str = "unavailable"; + /// `unknown`. + pub const UNKNOWN: &str = "unknown"; + } + /// Human readable operating system name. + pub const OS_NAME: &str = "os.name"; + /// The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). + pub const OS_VERSION: &str = "os.version"; + /// Identifies the class / type of event. + pub const OTEL_EVENT_NAME: &str = "otel.event.name"; + /// The fully-qualified logical name of the method from the RPC interface perspective. + pub const RPC_METHOD: &str = "rpc.method"; + /// Logical name of the service. + pub const SERVICE_NAME: &str = "service.name"; + /// The version string of the service component. The format is not defined by these conventions. + pub const SERVICE_VERSION: &str = "service.version"; + /// A unique ID to identify a session. + pub const SESSION_ID: &str = "session.id"; + /// The language of the telemetry SDK. + pub const TELEMETRY_SDK_LANGUAGE: &str = "telemetry.sdk.language"; + + /// Values of [`TELEMETRY_SDK_LANGUAGE`]. + pub mod telemetry_sdk_language { + /// [C++](https://opentelemetry.io/docs/languages/cpp/) + pub const CPP: &str = "cpp"; + /// [.NET](https://opentelemetry.io/docs/languages/dotnet/) + pub const DOTNET: &str = "dotnet"; + /// [Erlang/Elixir](https://opentelemetry.io/docs/languages/erlang/) + pub const ERLANG: &str = "erlang"; + /// [Go](https://opentelemetry.io/docs/languages/go/) + pub const GO: &str = "go"; + /// [Java](https://opentelemetry.io/docs/languages/java/) + pub const JAVA: &str = "java"; + /// [Kotlin](https://opentelemetry.io/docs/languages/kotlin/) + pub const KOTLIN: &str = "kotlin"; + /// [Node.js](https://opentelemetry.io/docs/languages/js/) + pub const NODEJS: &str = "nodejs"; + /// [PHP](https://opentelemetry.io/docs/languages/php/) + pub const PHP: &str = "php"; + /// [Python](https://opentelemetry.io/docs/languages/python/) + pub const PYTHON: &str = "python"; + /// [Ruby](https://opentelemetry.io/docs/languages/ruby/) + pub const RUBY: &str = "ruby"; + /// [Rust](https://opentelemetry.io/docs/languages/rust/) + pub const RUST: &str = "rust"; + /// [Swift](https://opentelemetry.io/docs/languages/swift/) + pub const SWIFT: &str = "swift"; + /// [Browser](https://opentelemetry.io/docs/languages/js/) + pub const WEBJS: &str = "webjs"; + } + /// The name of the telemetry SDK as defined above. + pub const TELEMETRY_SDK_NAME: &str = "telemetry.sdk.name"; + /// The version string of the telemetry SDK. + pub const TELEMETRY_SDK_VERSION: &str = "telemetry.sdk.version"; + + /// Upstream metrics that LiveKit producers create, imported from the registry's dependencies. + pub mod metric { + /// The duration of a single tool execution. Instrument: histogram, unit: `s`. + pub const GEN_AI_EXECUTE_TOOL_DURATION: &str = "gen_ai.execute_tool.duration"; + /// Unit of [`GEN_AI_EXECUTE_TOOL_DURATION`], as the upstream definition gives it (empty = none). + pub const GEN_AI_EXECUTE_TOOL_DURATION_UNIT: &str = "s"; + /// The end-to-end duration of a single in-process agent invocation, from the moment the invocation starts until the agent emits the last chunk of its final response or terminates with an error. Instrument: histogram, unit: `s`. + pub const GEN_AI_INVOKE_AGENT_DURATION: &str = "gen_ai.invoke_agent.duration"; + /// Unit of [`GEN_AI_INVOKE_AGENT_DURATION`], as the upstream definition gives it (empty = none). + pub const GEN_AI_INVOKE_AGENT_DURATION_UNIT: &str = "s"; + } +} +/// Event names (OTLP `LogRecord.event_name`). +pub mod event { + /// Namespace of app-defined events, named `custom.` and carrying the app's own attributes. + pub const CUSTOM: &str = "custom"; + /// OTel GenAI assistant message (also a function call), a span event on `llm_node`. + pub const GEN_AI_ASSISTANT_MESSAGE: &str = "gen_ai.assistant.message"; + /// OTel GenAI choice, a span event on `llm_node`. + pub const GEN_AI_CHOICE: &str = "gen_ai.choice"; + /// OTel GenAI system (or developer) message, a span event on `llm_node`. + pub const GEN_AI_SYSTEM_MESSAGE: &str = "gen_ai.system.message"; + /// OTel GenAI tool message (a function call output), a span event on `llm_node`. + pub const GEN_AI_TOOL_MESSAGE: &str = "gen_ai.tool.message"; + /// OTel GenAI user message, a span event on `llm_node`. + pub const GEN_AI_USER_MESSAGE: &str = "gen_ai.user.message"; + /// The app moved to the foreground or background. + pub const LK_DEVICE_APP_STATE_CHANGED: &str = "lk.device.app_state.changed"; + /// An audio session interruption began or ended. + pub const LK_DEVICE_AUDIO_INTERRUPTION: &str = "lk.device.audio.interruption"; + /// The audio route changed. + pub const LK_DEVICE_AUDIO_ROUTE_CHANGED: &str = "lk.device.audio_route.changed"; + /// Charging changed, or the level crossed 20 % or 10 % unplugged. + pub const LK_DEVICE_BATTERY_CHANGED: &str = "lk.device.battery.changed"; + /// A capture device failed. + pub const LK_DEVICE_CAPTURE_FAILED: &str = "lk.device.capture.failed"; + /// Low Power Mode changed. + pub const LK_DEVICE_LOW_POWER_CHANGED: &str = "lk.device.low_power.changed"; + /// Memory pressure changed. + pub const LK_DEVICE_MEMORY_CHANGED: &str = "lk.device.memory.changed"; + /// The network path changed. + pub const LK_DEVICE_NETWORK_CHANGED: &str = "lk.device.network.changed"; + /// Thermal state changed. + pub const LK_DEVICE_THERMAL_CHANGED: &str = "lk.device.thermal.changed"; + /// Pipeline smoke test. + pub const LK_PING: &str = "lk.ping"; + /// The Room left connected for good. + pub const LK_ROOM_DISCONNECTED: &str = "lk.room.disconnected"; + /// One RTC stats window for one track and direction. + pub const LK_RTC_STATS_SAMPLE: &str = "lk.rtc.stats.sample"; + /// Self-telemetry; counts since the previous report. + pub const LK_TELEMETRY_REPORT: &str = "lk.telemetry.report"; +} + +/// Span names. +pub mod span { + /// The agent session, parent of every turn. Kind: internal. + pub const AGENT_SESSION: &str = "agent_session"; + /// The agent's audio playing out. Kind: internal. + pub const AGENT_SPEAKING: &str = "agent_speaking"; + /// One agent turn (generation), parent of the LLM and TTS nodes. Kind: internal. + pub const AGENT_TURN: &str = "agent_turn"; + /// Answering-machine detection on an outbound call. Kind: internal. + pub const AMD: &str = "amd"; + /// Draining an agent activity before it ends. Kind: internal. + pub const DRAIN_AGENT_ACTIVITY: &str = "drain_agent_activity"; + /// One end-of-utterance inference. Kind: internal. + pub const EOU_DETECTION: &str = "eou_detection"; + /// The endpointing wait, from the user's last speech to the turn decision. Kind: internal. + pub const EOU_WAIT: &str = "eou_wait"; + /// A synchronous block on the agent event loop. Kind: internal. + pub const EVENT_LOOP_BLOCKED: &str = "event_loop_blocked"; + /// One function tool execution, the OTel GenAI execute-tool span. Kind: internal. + pub const FUNCTION_TOOL: &str = "function_tool"; + /// The job's entrypoint, root of a job's trace. Kind: internal. + pub const JOB_ENTRYPOINT: &str = "job_entrypoint"; + /// The job shutting down. Kind: internal. + pub const JOB_SHUTDOWN: &str = "job_shutdown"; + /// One LLM-judge evaluation of the session. Kind: internal. + pub const JUDGE_EVALUATION: &str = "judge_evaluation"; + /// A keyterm update to the STT. Kind: internal. + pub const KEYTERM_DETECTION: &str = "keyterm_detection"; + /// One connect attempt. Kind: client. + pub const LK_CONNECT: &str = "lk.connect"; + /// One track publish. Kind: internal. + pub const LK_PUBLISH: &str = "lk.publish"; + /// One reconnect cycle. Kind: client. + pub const LK_RECONNECT: &str = "lk.reconnect"; + /// One subscription, from the intent to subscribe to the first media. Kind: internal. + pub const LK_SUBSCRIBE: &str = "lk.subscribe"; + /// The LLM fallback adapter choosing an instance. Kind: internal. + pub const LLM_FALLBACK_ADAPTER: &str = "llm_fallback_adapter"; + /// The agent's LLM node for one generation. Kind: internal. + pub const LLM_NODE: &str = "llm_node"; + /// One LLM inference request, the OTel GenAI inference span. Kind: client. + pub const LLM_REQUEST: &str = "llm_request"; + /// The LLM request's stream being consumed. Kind: internal. + pub const LLM_REQUEST_RUN: &str = "llm_request_run"; + /// A plugin-task metric noted through the Node binding, stamped with its resolved frame. Kind: internal. + pub const METRICS: &str = "metrics"; + /// The agent's `on_enter` hook. Kind: internal. + pub const ON_ENTER: &str = "on_enter"; + /// The agent's `on_exit` hook. Kind: internal. + pub const ON_EXIT: &str = "on_exit"; + /// The session-end hook. Kind: internal. + pub const ON_SESSION_END: &str = "on_session_end"; + /// The `on_user_turn_completed` hook. Kind: internal. + pub const ON_USER_TURN_COMPLETED: &str = "on_user_turn_completed"; + /// Pausing an agent activity. Kind: internal. + pub const PAUSE_AGENT_ACTIVITY: &str = "pause_agent_activity"; + /// Publishing the agent's audio track. Kind: internal. + pub const PUBLISH_AUDIO_OUTPUT: &str = "publish_audio_output"; + /// One realtime-model generation. Kind: client. + pub const REALTIME_INFERENCE: &str = "realtime_inference"; + /// Realtime-model metrics attached to a turn. Kind: internal. + pub const REALTIME_METRICS: &str = "realtime_metrics"; + /// Resuming an agent activity. Kind: internal. + pub const RESUME_AGENT_ACTIVITY: &str = "resume_agent_activity"; + /// Connecting the job to its room. Kind: client. + pub const ROOM_CONNECT: &str = "room_connect"; + /// Disconnecting from the room. Kind: client. + pub const ROOM_DISCONNECT: &str = "room_disconnect"; + /// An outgoing RPC. Kind: client. + pub const RPC_CALL: &str = "rpc_call"; + /// An incoming RPC handled by the agent. Kind: server. + pub const RPC_HANDLER: &str = "rpc_handler"; + /// Closing the session. Kind: internal. + pub const SESSION_CLOSE: &str = "session_close"; + /// Uploading the session report. Kind: internal. + pub const SESSION_END_UPLOAD: &str = "session_end_upload"; + /// Starting the session (room I/O, first activity). Kind: internal. + pub const SESSION_START: &str = "session_start"; + /// Initializing the agent's tool sets. Kind: internal. + pub const SETUP_TOOLSETS: &str = "setup_toolsets"; + /// One shutdown callback. Kind: internal. + pub const SHUTDOWN_CALLBACK: &str = "shutdown_callback"; + /// Starting an agent activity. Kind: internal. + pub const START_AGENT_ACTIVITY: &str = "start_agent_activity"; + /// The TTS fallback adapter choosing an instance. Kind: internal. + pub const TTS_FALLBACK_ADAPTER: &str = "tts_fallback_adapter"; + /// The agent's TTS node for one generation. Kind: internal. + pub const TTS_NODE: &str = "tts_node"; + /// One TTS request. Kind: client. + pub const TTS_REQUEST: &str = "tts_request"; + /// The TTS request's stream being consumed. Kind: internal. + pub const TTS_REQUEST_RUN: &str = "tts_request_run"; + /// The Node binding driving `Tts::stream` (the duplex path). Kind: internal. + pub const TTS_STREAM: &str = "tts_stream"; + /// The sentence-splitting adapter over a non-streaming TTS. Kind: internal. + pub const TTS_STREAM_ADAPTER: &str = "tts_stream_adapter"; + /// The Node binding driving `Tts::synthesize` (the simplex path). Kind: internal. + pub const TTS_SYNTHESIZE: &str = "tts_synthesize"; + /// Replacing the session's agent. Kind: internal. + pub const UPDATE_AGENT: &str = "update_agent"; + /// The user speaking (VAD). Kind: internal. + pub const USER_SPEAKING: &str = "user_speaking"; + /// One user turn, from speech to the turn decision. Kind: internal. + pub const USER_TURN: &str = "user_turn"; + /// Waiting for the participant's audio track. Kind: internal. + pub const WAIT_FOR_AUDIO_TRACK: &str = "wait_for_audio_track"; + /// Waiting for the participant to link to. Kind: internal. + pub const WAIT_FOR_PARTICIPANT: &str = "wait_for_participant"; + /// Waiting for the participant's video track. Kind: internal. + pub const WAIT_FOR_VIDEO_TRACK: &str = "wait_for_video_track"; +} + +/// Checkpoint names: span events of the spans above. `lk.reconnect` names its attempts +/// `attempt ` instead. +pub mod checkpoint { + /// The agent state changed (`lk.old_state` -> `lk.new_state`). + pub const AGENT_STATE_CHANGED: &str = "agent_state_changed"; + /// The subscriber's SDP answer went out. + pub const ANSWER_SENT: &str = "answer_sent"; + /// A peer connection was created before the join response (JS port only). + pub const EARLY_PC_CREATED: &str = "early_pc_created"; + /// The RTC engine is ready (Swift stamps it with pc_connected). + pub const ENGINE: &str = "engine"; + /// The entrypoint was cancelled. + pub const ENTRYPOINT_CANCELLED: &str = "entrypoint_cancelled"; + /// The entrypoint returned. + pub const ENTRYPOINT_RETURNED: &str = "entrypoint_returned"; + /// The entrypoint started running. + pub const ENTRYPOINT_STARTED: &str = "entrypoint_started"; + /// A synchronous block on the event loop, with `lk.blocking.duration`; mirrors the span of the same name. + pub const EVENT_LOOP_BLOCKED: &str = "event_loop_blocked"; + /// The OTel exception event (`exception.*`), recorded on whichever span the exception was caught on; declared here once. + pub const EXCEPTION: &str = "exception"; + /// The first frame arrived. + pub const FIRST_FRAME: &str = "first_frame"; + /// The first inbound stats reading with bytes; the span ends ok here. + pub const FIRST_MEDIA: &str = "first_media"; + /// OTel GenAI inference details event (the GenAI registry defines it; events cannot be referenced + pub const GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS: &str = "gen_ai.client.inference.operation.details"; + /// One generation started (the generation attributes). + pub const GENERATION: &str = "generation"; + /// The worker accepted the job. + pub const JOB_ACCEPTED: &str = "job_accepted"; + /// The server assigned the job. + pub const JOB_ASSIGNED: &str = "job_assigned"; + /// The worker received the job request. + pub const JOB_RECEIVED: &str = "job_received"; + /// The server-side job start time, as the worker learned it. + pub const JOB_STARTED_ON_SERVER: &str = "job_started_on_server"; + /// The join response arrived. + pub const JOIN_RECV: &str = "join_recv"; + /// The publisher's SDP offer went out. + pub const OFFER_SENT: &str = "offer_sent"; + /// The linked participant left the room, with `lk.disconnect_reason`. + pub const PARTICIPANT_DISCONNECTED: &str = "participant_disconnected"; + /// The session linked to its participant. + pub const PARTICIPANT_LINKED: &str = "participant_linked"; + /// The primary peer connection connected. + pub const PC_CONNECTED: &str = "pc_connected"; + /// The peer connections exist (stamped once the join is adopted). + pub const PC_CREATED: &str = "pc_created"; + /// Pre-connect audio was replayed, with `lk.pre_connect_audio.duration`. + pub const PRE_CONNECT_AUDIO: &str = "pre_connect_audio"; + /// A preemptive generation was discarded in favour of this one (`lk.speech_id` of the discarded handle). + pub const PREEMPTIVE_GENERATION_DISCARDED: &str = "preemptive_generation_discarded"; + /// A job process was launched for it. + pub const PROCESS_ASSIGNED: &str = "process_assigned"; + /// A later trigger re-armed the wait. + pub const REARMED: &str = "rearmed"; + /// The Room reached the connected state. + pub const ROOM_CONNECTED: &str = "room_connected"; + /// Signaling is up (stamped with ws_open on the web and with join_recv on Swift). + pub const SIGNAL: &str = "signal"; + /// The hook stopped the response. + pub const STOP_RESPONSE: &str = "stop_response"; + /// The platform reported the remote track subscribed. + pub const SUBSCRIBED: &str = "subscribed"; + /// A newer detection superseded this one. + pub const SUPERSEDED: &str = "superseded"; + /// The track was subscribed, with `lk.track_sid` and `lk.track_source`. + pub const TRACK_SUBSCRIBED: &str = "track_subscribed"; + /// The user spoke again during the wait. + pub const USER_RESUMED: &str = "user_resumed"; + /// The user state changed (`lk.old_state` -> `lk.new_state`). + pub const USER_STATE_CHANGED: &str = "user_state_changed"; + /// The signaling WebSocket opened. + pub const WS_OPEN: &str = "ws_open"; +} + +/// Metric names. +pub mod metric { + /// GenAI operation duration, as the GenAI registry defines it (declared here because weaver cannot import it). Instrument: histogram, unit: `s`. + pub const GEN_AI_CLIENT_OPERATION_DURATION: &str = "gen_ai.client.operation.duration"; + /// Unit of [`GEN_AI_CLIENT_OPERATION_DURATION`], exactly as the producer creates the instrument (empty = none). + pub const GEN_AI_CLIENT_OPERATION_DURATION_UNIT: &str = "s"; + /// Time to the first chunk of a streaming GenAI response (deprecated upstream; declared as emitted). Instrument: histogram, unit: `s`. + pub const GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK: &str = "gen_ai.client.operation.time_to_first_chunk"; + /// Unit of [`GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK`], exactly as the producer creates the instrument (empty = none). + pub const GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK_UNIT: &str = "s"; + /// Input and output tokens used by the GenAI operation (deprecated upstream; declared as emitted). Instrument: histogram, unit: `{token}`. + pub const GEN_AI_CLIENT_TOKEN_USAGE: &str = "gen_ai.client.token.usage"; + /// Unit of [`GEN_AI_CLIENT_TOKEN_USAGE`], exactly as the producer creates the instrument (empty = none). + pub const GEN_AI_CLIENT_TOKEN_USAGE_UNIT: &str = "{token}"; + /// Seconds to acquire a provider connection. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_CONNECTION_ACQUIRE_TIME: &str = "lk.agents.connection.acquire_time"; + /// Unit of [`LK_AGENTS_CONNECTION_ACQUIRE_TIME`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_CONNECTION_ACQUIRE_TIME_UNIT: &str = "s"; + /// Duration of synchronous blocks detected on an agent event loop. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION: &str = "lk.agents.event_loop.blocked_duration"; + /// Unit of [`LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION_UNIT: &str = "s"; + /// End-to-end latency of a turn. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_E2E_LATENCY: &str = "lk.agents.turn.e2e_latency"; + /// Unit of [`LK_AGENTS_TURN_E2E_LATENCY`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_E2E_LATENCY_UNIT: &str = "s"; + /// Delay from the end of speech to the turn decision. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_END_OF_TURN_DELAY: &str = "lk.agents.turn.end_of_turn_delay"; + /// Unit of [`LK_AGENTS_TURN_END_OF_TURN_DELAY`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_END_OF_TURN_DELAY_UNIT: &str = "s"; + /// Time to the first LLM token. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_LLM_TTFT: &str = "lk.agents.turn.llm_ttft"; + /// Unit of [`LK_AGENTS_TURN_LLM_TTFT`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_LLM_TTFT_UNIT: &str = "s"; + /// Duration of the `on_user_turn_completed` hook. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY: &str = "lk.agents.turn.on_user_turn_completed_delay"; + /// Unit of [`LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY_UNIT: &str = "s"; + /// Delay from the end of speech to the final transcript. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_TRANSCRIPTION_DELAY: &str = "lk.agents.turn.transcription_delay"; + /// Unit of [`LK_AGENTS_TURN_TRANSCRIPTION_DELAY`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_TRANSCRIPTION_DELAY_UNIT: &str = "s"; + /// Time to the first TTS byte. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_TTS_TTFB: &str = "lk.agents.turn.tts_ttfb"; + /// Unit of [`LK_AGENTS_TURN_TTS_TTFB`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_TURN_TTS_TTFB_UNIT: &str = "s"; + /// Interruption-detector requests. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS: &str = "lk.agents.usage.interruption_num_requests"; + /// Unit of [`LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS_UNIT: &str = ""; + /// Realtime-model audio input tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS: &str = "lk.agents.usage.llm_input_audio_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS_UNIT: &str = ""; + /// LLM cached input tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS: &str = "lk.agents.usage.llm_input_cached_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS_UNIT: &str = ""; + /// Realtime-model text input tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS: &str = "lk.agents.usage.llm_input_text_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS_UNIT: &str = ""; + /// LLM input tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_INPUT_TOKENS: &str = "lk.agents.usage.llm_input_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_INPUT_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_INPUT_TOKENS_UNIT: &str = ""; + /// Realtime-model audio output tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS: &str = "lk.agents.usage.llm_output_audio_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS_UNIT: &str = ""; + /// Realtime-model text output tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS: &str = "lk.agents.usage.llm_output_text_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS_UNIT: &str = ""; + /// LLM output tokens. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS: &str = "lk.agents.usage.llm_output_tokens"; + /// Unit of [`LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS_UNIT: &str = ""; + /// Realtime-model session seconds. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_LLM_SESSION_DURATION: &str = "lk.agents.usage.llm_session_duration"; + /// Unit of [`LK_AGENTS_USAGE_LLM_SESSION_DURATION`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_LLM_SESSION_DURATION_UNIT: &str = "s"; + /// Seconds of audio transcribed. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_STT_AUDIO_DURATION: &str = "lk.agents.usage.stt_audio_duration"; + /// Unit of [`LK_AGENTS_USAGE_STT_AUDIO_DURATION`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_STT_AUDIO_DURATION_UNIT: &str = "s"; + /// Seconds of audio synthesized. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_TTS_AUDIO_DURATION: &str = "lk.agents.usage.tts_audio_duration"; + /// Unit of [`LK_AGENTS_USAGE_TTS_AUDIO_DURATION`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_TTS_AUDIO_DURATION_UNIT: &str = "s"; + /// Characters synthesized. Instrument: counter, unit: ``. + pub const LK_AGENTS_USAGE_TTS_CHARACTERS: &str = "lk.agents.usage.tts_characters"; + /// Unit of [`LK_AGENTS_USAGE_TTS_CHARACTERS`], exactly as the producer creates the instrument (empty = none). + pub const LK_AGENTS_USAGE_TTS_CHARACTERS_UNIT: &str = ""; +} diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts new file mode 100644 index 000000000..fa3d74856 --- /dev/null +++ b/otel/gen/ts/semconv.ts @@ -0,0 +1,1216 @@ +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. + +/** The agent's name. */ +export const ATTR_AGENT_NAME = "agent_name" as const; + +/** Browser name. */ +export const ATTR_BROWSER_NAME = "browser.name" as const; + +/** Browser version. */ +export const ATTR_BROWSER_VERSION = "browser.version" as const; + +/** One chat history item, as a map. Carries conversation content. PII: redacted by the collector. */ +export const ATTR_CHAT_ITEM = "chat.item" as const; + +/** Whether a pooled connection was reused, as the string `true`/`false`. */ +export const ATTR_CONNECTION_REUSED = "connection_reused" as const; + +/** One evaluation result, as a map of string fields. */ +export const ATTR_EVALUATION = "evaluation" as const; + +/** Token type (`input` or `output`); deprecated upstream, a dimension of `gen_ai.client.token.usage`. */ +export const ATTR_GEN_AI_TOKEN_TYPE = "gen_ai.token.type" as const; + +/** LangFuse spelling of audio input tokens. */ +export const ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS = "gen_ai.usage.input_audio_tokens" as const; + +/** LangFuse spelling of cached input tokens. */ +export const ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS = "gen_ai.usage.input_cached_tokens" as const; + +/** LangFuse spelling of text input tokens. */ +export const ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS = "gen_ai.usage.input_text_tokens" as const; + +/** LangFuse spelling of audio output tokens. */ +export const ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS = "gen_ai.usage.output_audio_tokens" as const; + +/** LangFuse spelling of text output tokens. */ +export const ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS = "gen_ai.usage.output_text_tokens" as const; + +/** LangFuse spelling of reasoning tokens. */ +export const ATTR_GEN_AI_USAGE_REASONING_TOKENS = "gen_ai.usage.reasoning_tokens" as const; + +/** LangFuse's completion start time (ISO 8601), set next to `gen_ai.response.time_to_first_chunk`. */ +export const ATTR_LANGFUSE_OBSERVATION_COMPLETION_START_TIME = "langfuse.observation.completion_start_time" as const; + +/** Label of the agent running the activity. */ +export const ATTR_LK_AGENT_LABEL = "lk.agent_label" as const; + +/** Name of the agent a job was dispatched to. */ +export const ATTR_LK_AGENT_NAME = "lk.agent_name" as const; + +/** AMD classification. */ +export const ATTR_LK_AMD_CATEGORY = "lk.amd.category" as const; + +/** Seconds from the call start to the AMD decision. Unit: `s`. */ +export const ATTR_LK_AMD_DELAY = "lk.amd.delay" as const; + +/** Why the AMD classifier decided. */ +export const ATTR_LK_AMD_REASON = "lk.amd.reason" as const; + +/** Seconds of speech the AMD classifier heard. Unit: `s`. */ +export const ATTR_LK_AMD_SPEECH_DURATION = "lk.amd.speech_duration" as const; + +/** Blocks in the session. */ +export const ATTR_LK_BLOCKING_COUNT = "lk.blocking.count" as const; + +/** CPU seconds the loop thread consumed during the block. Unit: `s`. */ +export const ATTR_LK_BLOCKING_CPU_TIME = "lk.blocking.cpu_time" as const; + +/** Seconds the loop was blocked. Unit: `s`. */ +export const ATTR_LK_BLOCKING_DURATION = "lk.blocking.duration" as const; + +/** Seconds of garbage collection on the loop thread during the block. Unit: `s`. */ +export const ATTR_LK_BLOCKING_GC_TIME = "lk.blocking.gc_time" as const; + +/** Module a lazy import was loading when sampled. */ +export const ATTR_LK_BLOCKING_IMPORT = "lk.blocking.import" as const; + +/** Seconds of the longest block in the session. Unit: `s`. */ +export const ATTR_LK_BLOCKING_MAX_DURATION = "lk.blocking.max_duration" as const; + +/** Severity bucket of the block. */ +export const ATTR_LK_BLOCKING_SEVERITY = "lk.blocking.severity" as const; + +/** Loop-thread stack sampled while blocked (source locations only). */ +export const ATTR_LK_BLOCKING_STACK = "lk.blocking.stack" as const; + +/** Reports dropped by rate limiting since the previous emitted span. */ +export const ATTR_LK_BLOCKING_SUPPRESSED = "lk.blocking.suppressed" as const; + +/** Name of the task that was running. */ +export const ATTR_LK_BLOCKING_TASK = "lk.blocking.task" as const; + +/** Seconds above which a block is reported. Unit: `s`. */ +export const ATTR_LK_BLOCKING_THRESHOLD = "lk.blocking.threshold" as const; + +/** Seconds blocked in total over the session. Unit: `s`. */ +export const ATTR_LK_BLOCKING_TOTAL_DURATION = "lk.blocking.total_duration" as const; + +/** Name of the shutdown callback. */ +export const ATTR_LK_CALLBACK_NAME = "lk.callback.name" as const; + +/** The close drained pending speech first. */ +export const ATTR_LK_CLOSE_DRAIN = "lk.close.drain" as const; + +/** Why the session closed. */ +export const ATTR_LK_CLOSE_REASON = "lk.close_reason" as const; + +/** LiveKit Cloud agent id. */ +export const ATTR_LK_CLOUD_AGENT_ID = "lk.cloud_agent_id" as const; + +/** 1-based connect attempt. */ +export const ATTR_LK_CONNECT_ATTEMPT = "lk.connect.attempt" as const; + +/** Room connection state. */ +export const ATTR_LK_CONNECTION_STATE = "lk.connection_state" as const; + +/** LiveKit Cloud deployment id. */ +export const ATTR_LK_DEPLOYMENT_ID = "lk.deployment_id" as const; + +/** Whether the app is in front of the user. */ +export const ATTR_LK_DEVICE_APP_STATE = "lk.device.app_state" as const; +/** In front of the user. */ +export const LK_DEVICE_APP_STATE_VALUE_FOREGROUND = "foreground" as const; +/** In the background. */ +export const LK_DEVICE_APP_STATE_VALUE_BACKGROUND = "background" as const; + +/** Phase of an audio session interruption. */ +export const ATTR_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" as const; +/** Interruption began (another app or a call took the audio session). */ +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_BEGAN = "began" as const; +/** Interruption ended. */ +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_ENDED = "ended" as const; + +/** The active outputs, comma-separated. */ +export const ATTR_LK_DEVICE_AUDIO_ROUTE_OUTPUTS = "lk.device.audio_route.outputs" as const; + +/** Why the audio route changed. */ +export const ATTR_LK_DEVICE_AUDIO_ROUTE_REASON = "lk.device.audio_route.reason" as const; +/** A new output became available. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_NEW_DEVICE = "new_device" as const; +/** The previous output went away. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_OLD_DEVICE_UNAVAILABLE = "old_device_unavailable" as const; +/** The audio session category changed. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_CATEGORY_CHANGE = "category_change" as const; +/** The route was overridden. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_OVERRIDE = "override" as const; +/** The device woke from sleep. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_WAKE_FROM_SLEEP = "wake_from_sleep" as const; +/** No route fits the category. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_NO_SUITABLE_ROUTE = "no_suitable_route" as const; +/** The route's configuration changed. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_ROUTE_CONFIGURATION_CHANGE = "route_configuration_change" as const; +/** The platform gave no reason. */ +export const LK_DEVICE_AUDIO_ROUTE_REASON_VALUE_UNKNOWN = "unknown" as const; + +/** The device is charging. */ +export const ATTR_LK_DEVICE_BATTERY_CHARGING = "lk.device.battery.charging" as const; + +/** Battery charge in percent, 0-100. Unit: `%`. */ +export const ATTR_LK_DEVICE_BATTERY_LEVEL = "lk.device.battery.level" as const; + +/** Capture device that failed. */ +export const ATTR_LK_DEVICE_CAPTURE_DEVICE = "lk.device.capture.device" as const; +/** Camera. */ +export const LK_DEVICE_CAPTURE_DEVICE_VALUE_CAMERA = "camera" as const; +/** Microphone. */ +export const LK_DEVICE_CAPTURE_DEVICE_VALUE_MICROPHONE = "microphone" as const; +/** Screen capture. */ +export const LK_DEVICE_CAPTURE_DEVICE_VALUE_SCREEN_SHARE = "screen_share" as const; + +/** Why capture failed, in the getUserMedia failure taxonomy. */ +export const ATTR_LK_DEVICE_CAPTURE_REASON = "lk.device.capture.reason" as const; +/** The user or OS denied access. */ +export const LK_DEVICE_CAPTURE_REASON_VALUE_PERMISSION_DENIED = "permission_denied" as const; +/** No such device. */ +export const LK_DEVICE_CAPTURE_REASON_VALUE_NOT_FOUND = "not_found" as const; +/** Another app holds the device. */ +export const LK_DEVICE_CAPTURE_REASON_VALUE_IN_USE = "in_use" as const; +/** The device went away. */ +export const LK_DEVICE_CAPTURE_REASON_VALUE_DISCONNECTED = "disconnected" as const; +/** Anything else. */ +export const LK_DEVICE_CAPTURE_REASON_VALUE_OTHER = "other" as const; + +/** Low Power Mode / Battery Saver is on. */ +export const ATTR_LK_DEVICE_LOW_POWER_ENABLED = "lk.device.low_power.enabled" as const; + +/** Memory pressure as the OS reports it. */ +export const ATTR_LK_DEVICE_MEMORY_PRESSURE = "lk.device.memory.pressure" as const; +/** No pressure. */ +export const LK_DEVICE_MEMORY_PRESSURE_VALUE_NORMAL = "normal" as const; +/** Low memory. */ +export const LK_DEVICE_MEMORY_PRESSURE_VALUE_WARNING = "warning" as const; +/** Critically low memory. */ +export const LK_DEVICE_MEMORY_PRESSURE_VALUE_CRITICAL = "critical" as const; + +/** The user asked for less traffic (Low Data Mode, Data Saver). */ +export const ATTR_LK_DEVICE_NETWORK_CONSTRAINED = "lk.device.network.constrained" as const; + +/** The network is metered (cellular, personal hotspot). */ +export const ATTR_LK_DEVICE_NETWORK_EXPENSIVE = "lk.device.network.expensive" as const; + +/** Thermal pressure as the OS reports it. */ +export const ATTR_LK_DEVICE_THERMAL_STATE = "lk.device.thermal.state" as const; +/** Within normal limits. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_NOMINAL = "nominal" as const; +/** Slightly elevated. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_FAIR = "fair" as const; +/** High; the system reduces performance. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_SERIOUS = "serious" as const; +/** Critical; the device needs to cool down. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_CRITICAL = "critical" as const; + +/** Why a participant left the room, as the protobuf `DisconnectReason` enum name. */ +export const ATTR_LK_DISCONNECT_REASON = "lk.disconnect_reason" as const; +/** No reason given, or a number the producer does not know. */ +export const LK_DISCONNECT_REASON_VALUE_UNKNOWN_REASON = "UNKNOWN_REASON" as const; +/** The participant hung up. */ +export const LK_DISCONNECT_REASON_VALUE_CLIENT_INITIATED = "CLIENT_INITIATED" as const; +/** Another participant joined with the same identity. */ +export const LK_DISCONNECT_REASON_VALUE_DUPLICATE_IDENTITY = "DUPLICATE_IDENTITY" as const; +/** The server instance shut down. */ +export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = "SERVER_SHUTDOWN" as const; +/** RoomService.RemoveParticipant was called. */ +export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = "PARTICIPANT_REMOVED" as const; +/** RoomService.DeleteRoom was called. */ +export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = "ROOM_DELETED" as const; +/** The server did not know the session the client tried to resume. */ +export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = "STATE_MISMATCH" as const; +/** The client could not connect fully. */ +export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = "JOIN_FAILURE" as const; +/** The server asked the participant to migrate (Cloud only). */ +export const LK_DISCONNECT_REASON_VALUE_MIGRATION = "MIGRATION" as const; +/** The signal WebSocket closed unexpectedly. */ +export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = "SIGNAL_CLOSE" as const; +/** The room closed after its participants left. */ +export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = "ROOM_CLOSED" as const; +/** The SIP callee did not answer in time. */ +export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = "USER_UNAVAILABLE" as const; +/** The SIP callee rejected the call. */ +export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = "USER_REJECTED" as const; +/** SIP protocol failure. */ +export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = "SIP_TRUNK_FAILURE" as const; +/** The server timed out the session. */ +export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT" as const; +/** Media failed or timed out. */ +export const LK_DISCONNECT_REASON_VALUE_MEDIA_FAILURE = "MEDIA_FAILURE" as const; +/** The agent failed. */ +export const LK_DISCONNECT_REASON_VALUE_AGENT_ERROR = "AGENT_ERROR" as const; +/** The client ran out of reconnect attempts (client only, not a protobuf value). */ +export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = "RECONNECT_FAILED" as const; + +/** Agent dispatch id. */ +export const ATTR_LK_DISPATCH_ID = "lk.dispatch_id" as const; + +/** Seconds from the end of the user's speech to the agent's first audio. Unit: `s`. */ +export const ATTR_LK_E2E_LATENCY = "lk.e2e_latency" as const; + +/** Seconds from the end of speech to the turn decision. Unit: `s`. */ +export const ATTR_LK_END_OF_TURN_DELAY = "lk.end_of_turn_delay" as const; + +/** Seconds the detector inference took. Unit: `s`. */ +export const ATTR_LK_EOU_DETECTION_DELAY = "lk.eou.detection_delay" as const; + +/** Seconds of endpointing delay applied. Unit: `s`. */ +export const ATTR_LK_EOU_ENDPOINTING_DELAY = "lk.eou.endpointing_delay" as const; + +/** The detector answer came from the cache. */ +export const ATTR_LK_EOU_FROM_CACHE = "lk.eou.from_cache" as const; + +/** Language the detector ran for. */ +export const ATTR_LK_EOU_LANGUAGE = "lk.eou.language" as const; + +/** Turn decisions the wait rejected before it ended. */ +export const ATTR_LK_EOU_NOT_COMMITTED_COUNT = "lk.eou.not_committed_count" as const; + +/** How the endpointing wait ended. */ +export const ATTR_LK_EOU_OUTCOME = "lk.eou.outcome" as const; +/** The turn was committed. */ +export const LK_EOU_OUTCOME_VALUE_COMMITTED = "committed" as const; +/** The user spoke again. */ +export const LK_EOU_OUTCOME_VALUE_USER_RESUMED = "user_resumed" as const; +/** The turn was dropped. */ +export const LK_EOU_OUTCOME_VALUE_DROPPED = "dropped" as const; + +/** End-of-utterance probability. */ +export const ATTR_LK_EOU_PROBABILITY = "lk.eou.probability" as const; + +/** Times the endpointing wait restarted on a later trigger. */ +export const ATTR_LK_EOU_REARM_COUNT = "lk.eou.rearm_count" as const; + +/** Endpointing waits the user cut short by speaking again. */ +export const ATTR_LK_EOU_RESUME_COUNT = "lk.eou.resume_count" as const; + +/** What decided the end of the turn. */ +export const ATTR_LK_EOU_SOURCE = "lk.eou.source" as const; + +/** Threshold under which the turn is unlikely to be over. */ +export const ATTR_LK_EOU_UNLIKELY_THRESHOLD = "lk.eou.unlikely_threshold" as const; + +/** Seconds from the end of the user's speech to the turn decision. Unit: `s`. */ +export const ATTR_LK_EOU_WAIT_DURATION = "lk.eou.wait_duration" as const; + +/** The provider error is retryable (Node binding). */ +export const ATTR_LK_ERROR_RETRYABLE = "lk.error.retryable" as const; + +/** The session report is tagged failed. */ +export const ATTR_LK_FAIL = "lk.fail" as const; + +/** Index of the fallback instance that served the request. */ +export const ATTR_LK_FALLBACK_INDEX = "lk.fallback.index" as const; + +/** Label of the fallback instance that served the request. */ +export const ATTR_LK_FALLBACK_LABEL = "lk.fallback.label" as const; + +/** The request was a recovery probe of a failed instance. */ +export const ATTR_LK_FALLBACK_RECOVERY = "lk.fallback.recovery" as const; + +/** Seconds from waiting for a track to its first frame. Unit: `s`. */ +export const ATTR_LK_FIRST_FRAME_DELAY = "lk.first_frame_delay" as const; + +/** How the inbound frame of a plugin-task metric was resolved (Node binding). */ +export const ATTR_LK_FRAME_RESOLUTION = "lk.frame.resolution" as const; +/** The root frame. */ +export const LK_FRAME_RESOLUTION_VALUE_ROOT = "root" as const; +/** A live speech frame. */ +export const LK_FRAME_RESOLUTION_VALUE_LIVE = "live" as const; +/** A frame whose speech already ended. */ +export const LK_FRAME_RESOLUTION_VALUE_STALE = "stale" as const; +/** Not a frame the slab knows. */ +export const LK_FRAME_RESOLUTION_VALUE_UNKNOWN = "unknown" as const; + +/** Tool call id. */ +export const ATTR_LK_FUNCTION_TOOL_ID = "lk.function_tool.id" as const; + +/** The tool returned an error. */ +export const ATTR_LK_FUNCTION_TOOL_IS_ERROR = "lk.function_tool.is_error" as const; + +/** Tool name. */ +export const ATTR_LK_FUNCTION_TOOL_NAME = "lk.function_tool.name" as const; + +/** Function tool names offered. */ +export const ATTR_LK_FUNCTION_TOOLS = "lk.function_tools" as const; + +/** Generations in the turn. */ +export const ATTR_LK_GENERATION_COUNT = "lk.generation_count" as const; + +/** Id of the agent turn (generation). */ +export const ATTR_LK_GENERATION_ID = "lk.generation_id" as const; + +/** Span whose input record this one continues. */ +export const ATTR_LK_INPUT_BASE_SPAN_ID = "lk.input.base_span_id" as const; + +/** The span's input record is a delta over its base span. */ +export const ATTR_LK_INPUT_DELTA = "lk.input.delta" as const; + +/** Items of the base record not carried into this one. */ +export const ATTR_LK_INPUT_DROPPED_FROM_BASE = "lk.input.dropped_from_base" as const; + +/** The speech was interrupted. */ +export const ATTR_LK_INTERRUPTED = "lk.interrupted" as const; + +/** Seconds from the user's speech to the interruption decision. Unit: `s`. */ +export const ATTR_LK_INTERRUPTION_DETECTION_DELAY = "lk.interruption.detection_delay" as const; + +/** Seconds the interruption prediction took. Unit: `s`. */ +export const ATTR_LK_INTERRUPTION_PREDICTION_DURATION = "lk.interruption.prediction_duration" as const; + +/** Interruption probability. */ +export const ATTR_LK_INTERRUPTION_PROBABILITY = "lk.interruption.probability" as const; + +/** What triggered the interruption. */ +export const ATTR_LK_INTERRUPTION_SOURCE = "lk.interruption.source" as const; + +/** Seconds of the interruption from detection to resolution. Unit: `s`. */ +export const ATTR_LK_INTERRUPTION_TOTAL_DURATION = "lk.interruption.total_duration" as const; + +/** The user turn was classified as an interruption. */ +export const ATTR_LK_IS_INTERRUPTION = "lk.is_interruption" as const; + +/** Seconds from the job request to its acceptance. Unit: `s`. */ +export const ATTR_LK_JOB_ACCEPT_LATENCY = "lk.job.accept_latency" as const; + +/** Agent id of the job. */ +export const ATTR_LK_JOB_AGENT_ID = "lk.job.agent_id" as const; + +/** Seconds from acceptance to the job assignment. Unit: `s`. */ +export const ATTR_LK_JOB_ASSIGNMENT_LATENCY = "lk.job.assignment_latency" as const; + +/** Seconds from the dispatch request to the entrypoint running. Unit: `s`. */ +export const ATTR_LK_JOB_DISPATCH_LATENCY = "lk.job.dispatch_latency" as const; + +/** Seconds from launch to the entrypoint running. Unit: `s`. */ +export const ATTR_LK_JOB_ENTRYPOINT_LATENCY = "lk.job.entrypoint_latency" as const; + +/** Seconds from assignment to the job process launch. Unit: `s`. */ +export const ATTR_LK_JOB_LAUNCH_LATENCY = "lk.job.launch_latency" as const; + +/** Worker id the job runs on. */ +export const ATTR_LK_JOB_WORKER_ID = "lk.job.worker_id" as const; + +/** Agent job ID (`AJ_...`). */ +export const ATTR_LK_JOB_ID = "lk.job_id" as const; + +/** Keyterms added by the update. */ +export const ATTR_LK_KEYTERMS_ADDED = "lk.keyterms.added" as const; + +/** Number of keyterms configured. */ +export const ATTR_LK_KEYTERMS_COUNT = "lk.keyterms.count" as const; + +/** Keyterms removed by the update. */ +export const ATTR_LK_KEYTERMS_REMOVED = "lk.keyterms.removed" as const; + +/** LLM metrics of the turn, serialized. */ +export const ATTR_LK_LLM_METRICS = "lk.llm_metrics" as const; + +/** Logger, type, module or file that wrote the line. */ +export const ATTR_LK_LOG_LOGGER = "lk.log.logger" as const; + +/** Which layer wrote the log line. */ +export const ATTR_LK_LOG_SOURCE = "lk.log.source" as const; +/** The platform SDK. */ +export const LK_LOG_SOURCE_VALUE_SDK = "sdk" as const; +/** The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``. */ +export const LK_LOG_SOURCE_VALUE_FFI = "ffi" as const; +/** WebRTC. */ +export const LK_LOG_SOURCE_VALUE_WEBRTC = "webrtc" as const; + +/** Metric type noted from a plugin task (Node binding `metrics` span). */ +export const ATTR_LK_METRIC = "lk.metric" as const; + +/** State after a transition. */ +export const ATTR_LK_NEW_STATE = "lk.new_state" as const; + +/** State before a transition. */ +export const ATTR_LK_OLD_STATE = "lk.old_state" as const; + +/** Seconds the `on_user_turn_completed` hook took. Unit: `s`. */ +export const ATTR_LK_ON_USER_TURN_COMPLETED_DELAY = "lk.on_user_turn_completed_delay" as const; + +/** How the attempt ended. Rollups read this, never the span status. */ +export const ATTR_LK_OUTCOME = "lk.outcome" as const; +/** Succeeded; span status Unset. */ +export const LK_OUTCOME_VALUE_OK = "ok" as const; +/** Failed; span status Error, with `error.type` when the failure has a type. */ +export const LK_OUTCOME_VALUE_ERROR = "error" as const; +/** Cancelled by the caller or superseded; span status Unset. */ +export const LK_OUTCOME_VALUE_CANCELLED = "cancelled" as const; + +/** Generation this one continues (a tool-call follow-up). */ +export const ATTR_LK_PARENT_GENERATION_ID = "lk.parent_generation_id" as const; + +/** Server-assigned SID of the local participant (`PA_...`). */ +export const ATTR_LK_PARTICIPANT_ID = "lk.participant_id" as const; + +/** Kind of the linked participant (protocol `ParticipantInfo.Kind`). */ +export const ATTR_LK_PARTICIPANT_KIND = "lk.participant_kind" as const; + +/** Transcript the AMD classifier used. PII: redacted by the collector. */ +export const ATTR_LK_PII_AMD_TRANSCRIPT = "lk.pii.amd.transcript" as const; + +/** Tool-call arguments on a log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_ARGUMENTS = "lk.pii.arguments" as const; + +/** Serialized chat context sent to the LLM. PII: redacted by the collector. */ +export const ATTR_LK_PII_CHAT_CTX = "lk.pii.chat_ctx" as const; + +/** DTMF digits received. PII: redacted by the collector. */ +export const ATTR_LK_PII_DTMF_INPUTS = "lk.pii.dtmf_inputs" as const; + +/** Error text that may quote user content. PII: redacted by the collector. */ +export const ATTR_LK_PII_ERROR = "lk.pii.error" as const; + +/** Raw provider event on a debug log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_EVENT = "lk.pii.event" as const; + +/** Tool call arguments. PII: redacted by the collector. */ +export const ATTR_LK_PII_FUNCTION_TOOL_ARGUMENTS = "lk.pii.function_tool.arguments" as const; + +/** Tool output. PII: redacted by the collector. */ +export const ATTR_LK_PII_FUNCTION_TOOL_OUTPUT = "lk.pii.function_tool.output" as const; + +/** Text sent to the TTS. PII: redacted by the collector. */ +export const ATTR_LK_PII_INPUT_TEXT = "lk.pii.input_text" as const; + +/** Agent instructions in force. PII: redacted by the collector. */ +export const ATTR_LK_PII_INSTRUCTIONS = "lk.pii.instructions" as const; + +/** An interim (non-final) transcript, on a log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_INTERIM_TRANSCRIPT = "lk.pii.interim_transcript" as const; + +/** The keyterms themselves (customer vocabulary). PII: redacted by the collector. */ +export const ATTR_LK_PII_KEYTERMS = "lk.pii.keyterms" as const; + +/** Tool-call output on a log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_OUTPUT = "lk.pii.output" as const; + +/** Customer-supplied identity of the local participant. PII: redacted by the collector. */ +export const ATTR_LK_PII_PARTICIPANT_IDENTITY = "lk.pii.participant_identity" as const; + +/** MCP tool progress message. PII: redacted by the collector. */ +export const ATTR_LK_PII_PROGRESS_MESSAGE = "lk.pii.progress_message" as const; + +/** Malformed tool-call arguments as received. PII: redacted by the collector. */ +export const ATTR_LK_PII_RAW_ARGUMENTS = "lk.pii.raw_arguments" as const; + +/** Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). PII: redacted by the collector. */ +export const ATTR_LK_PII_REMOTE_PARTICIPANT_IDENTITY = "lk.pii.remote_participant_identity" as const; + +/** Tool-call arguments after repair. PII: redacted by the collector. */ +export const ATTR_LK_PII_REPAIRED = "lk.pii.repaired" as const; + +/** Function calls the LLM produced, serialized. PII: redacted by the collector. */ +export const ATTR_LK_PII_RESPONSE_FUNCTION_CALLS = "lk.pii.response.function_calls" as const; + +/** Text the LLM produced. PII: redacted by the collector. */ +export const ATTR_LK_PII_RESPONSE_TEXT = "lk.pii.response.text" as const; + +/** Customer-supplied room name. PII: redacted by the collector. */ +export const ATTR_LK_PII_ROOM_NAME = "lk.pii.room_name" as const; + +/** RPC request payload. PII: redacted by the collector. */ +export const ATTR_LK_PII_RPC_PAYLOAD = "lk.pii.rpc.payload" as const; + +/** RPC response payload. PII: redacted by the collector. */ +export const ATTR_LK_PII_RPC_RESPONSE = "lk.pii.rpc.response" as const; + +/** Phone number of a SIP participant. PII: redacted by the collector. */ +export const ATTR_LK_PII_SIP_PHONE_NUMBER = "lk.pii.sip.phoneNumber" as const; + +/** Message text on a log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_TEXT = "lk.pii.text" as const; + +/** Transcript text on a log record. PII: redacted by the collector. */ +export const ATTR_LK_PII_TRANSCRIPT = "lk.pii.transcript" as const; + +/** The user's transcript for the turn. PII: redacted by the collector. */ +export const ATTR_LK_PII_USER_INPUT = "lk.pii.user_input" as const; + +/** Transcript the detector scored. PII: redacted by the collector. */ +export const ATTR_LK_PII_USER_TRANSCRIPT = "lk.pii.user_transcript" as const; + +/** Monotonically increasing sequence number per pipeline. */ +export const ATTR_LK_PING_SEQ = "lk.ping.seq" as const; + +/** Seconds of the agent's speech played out when it was interrupted. Unit: `s`. */ +export const ATTR_LK_PLAYOUT_POSITION = "lk.playout.position" as const; + +/** Seconds of pre-connect audio replayed. Unit: `s`. */ +export const ATTR_LK_PRE_CONNECT_AUDIO_DURATION = "lk.pre_connect_audio.duration" as const; + +/** Label of the agent handed off from. */ +export const ATTR_LK_PREVIOUS_AGENT_LABEL = "lk.previous_agent_label" as const; + +/** Provider request ids a span covers, for joining against provider logs. */ +export const ATTR_LK_PROVIDER_REQUEST_IDS = "lk.provider_request_ids" as const; + +/** Provider-side tool names offered. */ +export const ATTR_LK_PROVIDER_TOOLS = "lk.provider_tools" as const; + +/** Realtime model metrics of the turn, serialized. */ +export const ATTR_LK_REALTIME_MODEL_METRICS = "lk.realtime_model_metrics" as const; + +/** Reconnect attempts made in the cycle. */ +export const ATTR_LK_RECONNECT_ATTEMPTS = "lk.reconnect.attempts" as const; + +/** Mode of the last reconnect attempt. */ +export const ATTR_LK_RECONNECT_MODE = "lk.reconnect.mode" as const; +/** Resume the session (ICE restart). */ +export const LK_RECONNECT_MODE_VALUE_QUICK = "quick" as const; +/** Reconnect from scratch. */ +export const LK_RECONNECT_MODE_VALUE_FULL = "full" as const; + +/** What triggered the reconnect cycle. */ +export const ATTR_LK_RECONNECT_REASON = "lk.reconnect.reason" as const; +/** The signaling socket closed (RR_SIGNAL_DISCONNECTED). */ +export const LK_RECONNECT_REASON_VALUE_SIGNAL_DISCONNECTED = "signal_disconnected" as const; +/** The publisher peer connection failed (RR_PUBLISHER_FAILED). */ +export const LK_RECONNECT_REASON_VALUE_PUBLISHER_FAILED = "publisher_failed" as const; +/** The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). */ +export const LK_RECONNECT_REASON_VALUE_SUBSCRIBER_FAILED = "subscriber_failed" as const; +/** A peer connection failed and the platform did not say which (client only). */ +export const LK_RECONNECT_REASON_VALUE_TRANSPORT_FAILED = "transport_failed" as const; +/** The server asked for a candidate switch (RR_SWITCH_CANDIDATE). */ +export const LK_RECONNECT_REASON_VALUE_SWITCH_CANDIDATE = "switch_candidate" as const; +/** The device moved to another network (client only). */ +export const LK_RECONNECT_REASON_VALUE_NETWORK_CHANGED = "network_changed" as const; +/** A test or a debug menu asked for it (client only). */ +export const LK_RECONNECT_REASON_VALUE_DEBUG = "debug" as const; +/** RR_UNKNOWN or a number the client does not know. */ +export const LK_RECONNECT_REASON_VALUE_UNKNOWN = "unknown" as const; + +/** PII redaction was requested for the session. */ +export const ATTR_LK_REDACTION_ENABLED = "lk.redaction.enabled" as const; + +/** Seconds to the first TTS audio byte. Unit: `s`. */ +export const ATTR_LK_RESPONSE_TTFB = "lk.response.ttfb" as const; + +/** Seconds to the first LLM token. Unit: `s`. */ +export const ATTR_LK_RESPONSE_TTFT = "lk.response.ttft" as const; + +/** Retries before the operation succeeded or gave up. */ +export const ATTR_LK_RETRY_COUNT = "lk.retry_count" as const; + +/** The room was joined with auto-subscribe. */ +export const ATTR_LK_ROOM_AUTO_SUBSCRIBE = "lk.room.auto_subscribe" as const; + +/** End-to-end encryption is on. */ +export const ATTR_LK_ROOM_E2EE = "lk.room.e2ee" as const; + +/** Remote participants when the agent connected. */ +export const ATTR_LK_ROOM_REMOTE_PARTICIPANT_COUNT = "lk.room.remote_participant_count" as const; + +/** How room I/O picked its participant. */ +export const ATTR_LK_ROOM_IO_PARTICIPANT_FILTER = "lk.room_io.participant_filter" as const; + +/** Server-assigned room session SID (`RM_...`). */ +export const ATTR_LK_ROOM_SID = "lk.room_sid" as const; + +/** Identity of the RPC caller. */ +export const ATTR_LK_RPC_CALLER_IDENTITY = "lk.rpc.caller_identity" as const; + +/** Identity the RPC is sent to. */ +export const ATTR_LK_RPC_DESTINATION_IDENTITY = "lk.rpc.destination_identity" as const; + +/** RPC error code. */ +export const ATTR_LK_RPC_ERROR_CODE = "lk.rpc.error_code" as const; + +/** A handler was registered for the method. */ +export const ATTR_LK_RPC_HANDLER_REGISTERED = "lk.rpc.handler_registered" as const; + +/** RPC request payload size in bytes. Unit: `By`. */ +export const ATTR_LK_RPC_PAYLOAD_SIZE = "lk.rpc.payload_size" as const; + +/** RPC request id. */ +export const ATTR_LK_RPC_REQUEST_ID = "lk.rpc.request_id" as const; + +/** RPC response size in bytes. Unit: `By`. */ +export const ATTR_LK_RPC_RESPONSE_SIZE = "lk.rpc.response_size" as const; + +/** RPC response timeout in seconds. Unit: `s`. */ +export const ATTR_LK_RPC_RESPONSE_TIMEOUT = "lk.rpc.response_timeout" as const; + +/** Average audio level in the window, 0-1, inbound audio. Unit: `1`. */ +export const ATTR_LK_RTC_AUDIO_LEVEL_AVG = "lk.rtc.audio_level.avg" as const; + +/** Maximum audio level in the window, 0-1, inbound audio. Unit: `1`. */ +export const ATTR_LK_RTC_AUDIO_LEVEL_MAX = "lk.rtc.audio_level.max" as const; + +/** Minimum audio level in the window, 0-1, inbound audio. Unit: `1`. */ +export const ATTR_LK_RTC_AUDIO_LEVEL_MIN = "lk.rtc.audio_level.min" as const; + +/** Bytes sent or received (cumulative). Unit: `By`. */ +export const ATTR_LK_RTC_BYTES = "lk.rtc.bytes" as const; + +/** Codec mimeType, when known. */ +export const ATTR_LK_RTC_CODEC = "lk.rtc.codec" as const; + +/** Concealed audio samples, inbound audio (cumulative). Unit: `{sample}`. */ +export const ATTR_LK_RTC_CONCEALED_SAMPLES = "lk.rtc.concealed_samples" as const; + +/** Concealment events, inbound audio (cumulative). Unit: `{event}`. */ +export const ATTR_LK_RTC_CONCEALMENT_EVENTS = "lk.rtc.concealment_events" as const; + +/** Average frame rate in the window, video. Unit: `{frame}/s`. */ +export const ATTR_LK_RTC_FPS_AVG = "lk.rtc.fps.avg" as const; + +/** Maximum frame rate in the window, video. Unit: `{frame}/s`. */ +export const ATTR_LK_RTC_FPS_MAX = "lk.rtc.fps.max" as const; + +/** Minimum frame rate in the window, video. Unit: `{frame}/s`. */ +export const ATTR_LK_RTC_FPS_MIN = "lk.rtc.fps.min" as const; + +/** Video freezes, inbound video (cumulative). Unit: `{freeze}`. */ +export const ATTR_LK_RTC_FREEZE_COUNT = "lk.rtc.freeze_count" as const; + +/** Total freeze time, inbound video (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_FREEZES_DURATION_MS = "lk.rtc.freezes_duration_ms" as const; + +/** Audio interruptions, inbound audio (cumulative). Unit: `{interruption}`. */ +export const ATTR_LK_RTC_INTERRUPTION_COUNT = "lk.rtc.interruption_count" as const; + +/** Total interruption time, inbound audio (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_INTERRUPTIONS_DURATION_MS = "lk.rtc.interruptions_duration_ms" as const; + +/** Jitter buffer delay, inbound (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_BUFFER_DELAY_MS = "lk.rtc.jitter_buffer_delay_ms" as const; + +/** Samples or frames emitted by the jitter buffer, inbound (cumulative). Unit: `{sample}`. */ +export const ATTR_LK_RTC_JITTER_BUFFER_EMITTED_COUNT = "lk.rtc.jitter_buffer_emitted_count" as const; + +/** Average jitter in the window, inbound. Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_MS_AVG = "lk.rtc.jitter_ms.avg" as const; + +/** Maximum jitter in the window, inbound. Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_MS_MAX = "lk.rtc.jitter_ms.max" as const; + +/** Minimum jitter in the window, inbound. Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_MS_MIN = "lk.rtc.jitter_ms.min" as const; + +/** Packets sent or received (cumulative). Unit: `{packet}`. */ +export const ATTR_LK_RTC_PACKETS = "lk.rtc.packets" as const; + +/** Packets lost, inbound (cumulative). Unit: `{packet}`. */ +export const ATTR_LK_RTC_PACKETS_LOST = "lk.rtc.packets_lost" as const; + +/** Video pauses, inbound video (cumulative). Unit: `{pause}`. */ +export const ATTR_LK_RTC_PAUSE_COUNT = "lk.rtc.pause_count" as const; + +/** Total pause time, inbound video (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_PAUSES_DURATION_MS = "lk.rtc.pauses_duration_ms" as const; + +/** Time the encoder was limited by bandwidth, outbound video (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_QUALITY_LIMITATION_BANDWIDTH_MS = "lk.rtc.quality_limitation.bandwidth_ms" as const; + +/** Time the encoder was limited by CPU, outbound video (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_QUALITY_LIMITATION_CPU_MS = "lk.rtc.quality_limitation.cpu_ms" as const; + +/** Time the encoder was limited for other reasons, outbound video (cumulative). Unit: `ms`. */ +export const ATTR_LK_RTC_QUALITY_LIMITATION_OTHER_MS = "lk.rtc.quality_limitation.other_ms" as const; + +/** Average round-trip time in the window. Unit: `ms`. */ +export const ATTR_LK_RTC_RTT_MS_AVG = "lk.rtc.rtt_ms.avg" as const; + +/** Maximum round-trip time in the window. Unit: `ms`. */ +export const ATTR_LK_RTC_RTT_MS_MAX = "lk.rtc.rtt_ms.max" as const; + +/** Minimum round-trip time in the window. Unit: `ms`. */ +export const ATTR_LK_RTC_RTT_MS_MIN = "lk.rtc.rtt_ms.min" as const; + +/** Stats readings folded into the window. Unit: `{reading}`. */ +export const ATTR_LK_RTC_SAMPLES = "lk.rtc.samples" as const; + +/** Concealed samples that were silent, inbound audio (cumulative). Unit: `{sample}`. */ +export const ATTR_LK_RTC_SILENT_CONCEALED_SAMPLES = "lk.rtc.silent_concealed_samples" as const; + +/** Actual length of the stats window. Unit: `ms`. */ +export const ATTR_LK_RTC_WINDOW_MS = "lk.rtc.window_ms" as const; + +/** LiveKit SDK name. */ +export const ATTR_LK_SDK_NAME = "lk.sdk.name" as const; + +/** LiveKit SDK version. */ +export const ATTR_LK_SDK_VERSION = "lk.sdk.version" as const; + +/** Why the job shut down. */ +export const ATTR_LK_SHUTDOWN_REASON = "lk.shutdown.reason" as const; + +/** The shutdown was requested by user code. */ +export const ATTR_LK_SHUTDOWN_USER_INITIATED = "lk.shutdown.user_initiated" as const; + +/** The session is a simulation. */ +export const ATTR_LK_SIMULATION_ENABLED = "lk.simulation.enabled" as const; + +/** Simulation run job (one scenario) behind the session. */ +export const ATTR_LK_SIMULATION_JOB_ID = "lk.simulation.job_id" as const; + +/** Simulation run the session belongs to. */ +export const ATTR_LK_SIMULATION_RUN_ID = "lk.simulation.run_id" as const; + +/** Seconds a speech handle waited in the queue before generation was authorized. Unit: `s`. */ +export const ATTR_LK_SPEECH_QUEUE_WAIT = "lk.speech.queue_wait" as const; + +/** Id of the speech handle a span belongs to. */ +export const ATTR_LK_SPEECH_ID = "lk.speech_id" as const; + +/** JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length). */ +export const ATTR_LK_STT_EVENTS = "lk.stt.events" as const; + +/** The session report is tagged successful. */ +export const ATTR_LK_SUCCESS = "lk.success" as const; + +/** Batches waiting in the cache now. Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_CACHE_BATCHES = "lk.telemetry.cache.batches" as const; + +/** Sent batches the cache could not delete; never sent twice from the same launch. Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_CACHE_DELETE_ERRORS = "lk.telemetry.cache.delete_errors" as const; + +/** Times the cache could not be listed, so nothing was reconciled or sent. Unit: `{attempt}`. */ +export const ATTR_LK_TELEMETRY_CACHE_LIST_ERRORS = "lk.telemetry.cache.list_errors" as const; + +/** Cached batches that could not be read right now, kept for the next pass. Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_CACHE_READ_ERRORS = "lk.telemetry.cache.read_errors" as const; + +/** Batches the disk refused, kept in memory instead. Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_CACHE_WRITE_ERRORS = "lk.telemetry.cache.write_errors" as const; + +/** Records no cache, not even memory, could take. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_CACHE_ERROR = "lk.telemetry.dropped.cache_error" as const; + +/** Records evicted by the cache's size or file-count bound. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_CACHE_FULL = "lk.telemetry.dropped.cache_full" as const; + +/** Records in cached batches that failed their CRC. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_CORRUPT = "lk.telemetry.dropped.corrupt" as const; + +/** Records in batches past the cache's age limit. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_EXPIRED = "lk.telemetry.dropped.expired" as const; + +/** Custom events, attributes or SDK strings over the limits, rejected. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_INVALID = "lk.telemetry.dropped.invalid" as const; + +/** Single records larger than the collector accepts (413). Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_OVERSIZED = "lk.telemetry.dropped.oversized" as const; + +/** Records evicted from the in-memory queue. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_QUEUE_FULL = "lk.telemetry.dropped.queue_full" as const; + +/** Discrete events dropped by the flood guard. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_RATE_LIMITED = "lk.telemetry.dropped.rate_limited" as const; + +/** Records the collector rejected (final 4xx/5xx, OTLP partial success). Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_REJECTED = "lk.telemetry.dropped.rejected" as const; + +/** Records evicted from the cache during a server-directed pause. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_THROTTLED = "lk.telemetry.dropped.throttled" as const; + +/** Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_UNATTRIBUTED = "lk.telemetry.dropped.unattributed" as const; + +/** Soft upload holds that reached their cap. Unit: `{hold}`. */ +export const ATTR_LK_TELEMETRY_HOLDS_CAPPED = "lk.telemetry.holds.capped" as const; + +/** Compressed bytes the collector accepted. Unit: `By`. */ +export const ATTR_LK_TELEMETRY_UPLOADS_BYTES = "lk.telemetry.uploads.bytes" as const; + +/** Upload attempts that failed transiently (no answer, 429, 5xx). Unit: `{attempt}`. */ +export const ATTR_LK_TELEMETRY_UPLOADS_FAILED = "lk.telemetry.uploads.failed" as const; + +/** Batches the collector accepted. Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_UPLOADS_SENT = "lk.telemetry.uploads.sent" as const; + +/** Upload attempts that hit the export timeout. Unit: `{attempt}`. */ +export const ATTR_LK_TELEMETRY_UPLOADS_TIMEOUTS = "lk.telemetry.uploads.timeouts" as const; + +/** 401/403 answers (tokens the collector refused). Unit: `{attempt}`. */ +export const ATTR_LK_TELEMETRY_UPLOADS_UNAUTHORIZED = "lk.telemetry.uploads.unauthorized" as const; + +/** Tool set names offered. */ +export const ATTR_LK_TOOL_SETS = "lk.tool_sets" as const; + +/** Whether the stats describe sent or received media. */ +export const ATTR_LK_TRACK_DIRECTION = "lk.track.direction" as const; +/** Received media (inbound-rtp). */ +export const LK_TRACK_DIRECTION_VALUE_INBOUND = "inbound" as const; +/** Sent media (outbound-rtp). */ +export const LK_TRACK_DIRECTION_VALUE_OUTBOUND = "outbound" as const; + +/** Media kind of the track. */ +export const ATTR_LK_TRACK_KIND = "lk.track.kind" as const; +/** Audio. */ +export const LK_TRACK_KIND_VALUE_AUDIO = "audio" as const; +/** Video. */ +export const LK_TRACK_KIND_VALUE_VIDEO = "video" as const; + +/** Server-assigned track SID (`TR_...`). */ +export const ATTR_LK_TRACK_SID = "lk.track_sid" as const; + +/** Source of the track, as the Agents RTC FFI `TrackSource` enum name. */ +export const ATTR_LK_TRACK_SOURCE = "lk.track_source" as const; +/** Unknown source. */ +export const LK_TRACK_SOURCE_VALUE_SOURCE_UNKNOWN = "SOURCE_UNKNOWN" as const; +/** Camera. */ +export const LK_TRACK_SOURCE_VALUE_SOURCE_CAMERA = "SOURCE_CAMERA" as const; +/** Microphone. */ +export const LK_TRACK_SOURCE_VALUE_SOURCE_MICROPHONE = "SOURCE_MICROPHONE" as const; +/** Screen share video. */ +export const LK_TRACK_SOURCE_VALUE_SOURCE_SCREENSHARE = "SOURCE_SCREENSHARE" as const; +/** Screen share audio. */ +export const LK_TRACK_SOURCE_VALUE_SOURCE_SCREENSHARE_AUDIO = "SOURCE_SCREENSHARE_AUDIO" as const; + +/** Confidence of the final transcript. */ +export const ATTR_LK_TRANSCRIPT_CONFIDENCE = "lk.transcript_confidence" as const; + +/** Seconds from the end of speech to the final transcript. Unit: `s`. */ +export const ATTR_LK_TRANSCRIPTION_DELAY = "lk.transcription_delay" as const; + +/** Audio bytes the TTS driver collected (Node binding). Unit: `By`. */ +export const ATTR_LK_TTS_BYTES = "lk.tts.bytes" as const; + +/** Audio frames the TTS driver collected (Node binding). */ +export const ATTR_LK_TTS_FRAMES = "lk.tts.frames" as const; + +/** Label of the TTS instance. */ +export const ATTR_LK_TTS_LABEL = "lk.tts.label" as const; + +/** The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure). */ +export const ATTR_LK_TTS_NO_DUPLEX_PATH = "lk.tts.no_duplex_path" as const; + +/** The TTS instance streams (duplex path). */ +export const ATTR_LK_TTS_STREAMING = "lk.tts.streaming" as const; + +/** TTS metrics of the turn, serialized. */ +export const ATTR_LK_TTS_METRICS = "lk.tts_metrics" as const; + +/** Model name the plugin reported. */ +export const ATTR_MODEL_NAME = "model_name" as const; + +/** Provider the plugin reported. */ +export const ATTR_MODEL_PROVIDER = "model_provider" as const; + +/** The session outcome, as a map with the field `outcome` (`success` or `fail`) and an optional `reason`. */ +export const ATTR_OUTCOME = "outcome" as const; + +/** The Agents SDK version. */ +export const ATTR_SDK_VERSION = "sdk_version" as const; + +/** The session's options, as a map of option name to value. */ +export const ATTR_SESSION_OPTIONS = "session.options" as const; + +/** When the report was made, in Unix seconds. */ +export const ATTR_SESSION_REPORT_TIMESTAMP = "session.report_timestamp" as const; + +/** The session's tags, sorted. Absent when the session has none. */ +export const ATTR_SESSION_TAGS = "session.tags" as const; + +/** One tag with metadata, as a map with the fields `name` and `metadata`. */ +export const ATTR_TAG = "tag" as const; + +/** Model usage, as a list of maps, one per model. Absent when there was none. */ +export const ATTR_USAGE = "usage" as const; + +/** Namespace of app-defined events, named `custom.` and carrying the app's own attributes. */ +export const EVENT_CUSTOM = "custom" as const; +/** OTel GenAI assistant message (also a function call), a span event on `llm_node`. */ +export const EVENT_GEN_AI_ASSISTANT_MESSAGE = "gen_ai.assistant.message" as const; +/** OTel GenAI choice, a span event on `llm_node`. */ +export const EVENT_GEN_AI_CHOICE = "gen_ai.choice" as const; +/** OTel GenAI system (or developer) message, a span event on `llm_node`. */ +export const EVENT_GEN_AI_SYSTEM_MESSAGE = "gen_ai.system.message" as const; +/** OTel GenAI tool message (a function call output), a span event on `llm_node`. */ +export const EVENT_GEN_AI_TOOL_MESSAGE = "gen_ai.tool.message" as const; +/** OTel GenAI user message, a span event on `llm_node`. */ +export const EVENT_GEN_AI_USER_MESSAGE = "gen_ai.user.message" as const; +/** The app moved to the foreground or background. */ +export const EVENT_LK_DEVICE_APP_STATE_CHANGED = "lk.device.app_state.changed" as const; +/** An audio session interruption began or ended. */ +export const EVENT_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" as const; +/** The audio route changed. */ +export const EVENT_LK_DEVICE_AUDIO_ROUTE_CHANGED = "lk.device.audio_route.changed" as const; +/** Charging changed, or the level crossed 20 % or 10 % unplugged. */ +export const EVENT_LK_DEVICE_BATTERY_CHANGED = "lk.device.battery.changed" as const; +/** A capture device failed. */ +export const EVENT_LK_DEVICE_CAPTURE_FAILED = "lk.device.capture.failed" as const; +/** Low Power Mode changed. */ +export const EVENT_LK_DEVICE_LOW_POWER_CHANGED = "lk.device.low_power.changed" as const; +/** Memory pressure changed. */ +export const EVENT_LK_DEVICE_MEMORY_CHANGED = "lk.device.memory.changed" as const; +/** The network path changed. */ +export const EVENT_LK_DEVICE_NETWORK_CHANGED = "lk.device.network.changed" as const; +/** Thermal state changed. */ +export const EVENT_LK_DEVICE_THERMAL_CHANGED = "lk.device.thermal.changed" as const; +/** Pipeline smoke test. */ +export const EVENT_LK_PING = "lk.ping" as const; +/** The Room left connected for good. */ +export const EVENT_LK_ROOM_DISCONNECTED = "lk.room.disconnected" as const; +/** One RTC stats window for one track and direction. */ +export const EVENT_LK_RTC_STATS_SAMPLE = "lk.rtc.stats.sample" as const; +/** Self-telemetry; counts since the previous report. */ +export const EVENT_LK_TELEMETRY_REPORT = "lk.telemetry.report" as const; + +/** The agent session, parent of every turn. Kind: internal. */ +export const SPAN_AGENT_SESSION = "agent_session" as const; +/** The agent's audio playing out. Kind: internal. */ +export const SPAN_AGENT_SPEAKING = "agent_speaking" as const; +/** One agent turn (generation), parent of the LLM and TTS nodes. Kind: internal. */ +export const SPAN_AGENT_TURN = "agent_turn" as const; +/** Answering-machine detection on an outbound call. Kind: internal. */ +export const SPAN_AMD = "amd" as const; +/** Draining an agent activity before it ends. Kind: internal. */ +export const SPAN_DRAIN_AGENT_ACTIVITY = "drain_agent_activity" as const; +/** One end-of-utterance inference. Kind: internal. */ +export const SPAN_EOU_DETECTION = "eou_detection" as const; +/** The endpointing wait, from the user's last speech to the turn decision. Kind: internal. */ +export const SPAN_EOU_WAIT = "eou_wait" as const; +/** A synchronous block on the agent event loop. Kind: internal. */ +export const SPAN_EVENT_LOOP_BLOCKED = "event_loop_blocked" as const; +/** One function tool execution, the OTel GenAI execute-tool span. Kind: internal. */ +export const SPAN_FUNCTION_TOOL = "function_tool" as const; +/** The job's entrypoint, root of a job's trace. Kind: internal. */ +export const SPAN_JOB_ENTRYPOINT = "job_entrypoint" as const; +/** The job shutting down. Kind: internal. */ +export const SPAN_JOB_SHUTDOWN = "job_shutdown" as const; +/** One LLM-judge evaluation of the session. Kind: internal. */ +export const SPAN_JUDGE_EVALUATION = "judge_evaluation" as const; +/** A keyterm update to the STT. Kind: internal. */ +export const SPAN_KEYTERM_DETECTION = "keyterm_detection" as const; +/** One connect attempt. Kind: client. */ +export const SPAN_LK_CONNECT = "lk.connect" as const; +/** One track publish. Kind: internal. */ +export const SPAN_LK_PUBLISH = "lk.publish" as const; +/** One reconnect cycle. Kind: client. */ +export const SPAN_LK_RECONNECT = "lk.reconnect" as const; +/** One subscription, from the intent to subscribe to the first media. Kind: internal. */ +export const SPAN_LK_SUBSCRIBE = "lk.subscribe" as const; +/** The LLM fallback adapter choosing an instance. Kind: internal. */ +export const SPAN_LLM_FALLBACK_ADAPTER = "llm_fallback_adapter" as const; +/** The agent's LLM node for one generation. Kind: internal. */ +export const SPAN_LLM_NODE = "llm_node" as const; +/** One LLM inference request, the OTel GenAI inference span. Kind: client. */ +export const SPAN_LLM_REQUEST = "llm_request" as const; +/** The LLM request's stream being consumed. Kind: internal. */ +export const SPAN_LLM_REQUEST_RUN = "llm_request_run" as const; +/** A plugin-task metric noted through the Node binding, stamped with its resolved frame. Kind: internal. */ +export const SPAN_METRICS = "metrics" as const; +/** The agent's `on_enter` hook. Kind: internal. */ +export const SPAN_ON_ENTER = "on_enter" as const; +/** The agent's `on_exit` hook. Kind: internal. */ +export const SPAN_ON_EXIT = "on_exit" as const; +/** The session-end hook. Kind: internal. */ +export const SPAN_ON_SESSION_END = "on_session_end" as const; +/** The `on_user_turn_completed` hook. Kind: internal. */ +export const SPAN_ON_USER_TURN_COMPLETED = "on_user_turn_completed" as const; +/** Pausing an agent activity. Kind: internal. */ +export const SPAN_PAUSE_AGENT_ACTIVITY = "pause_agent_activity" as const; +/** Publishing the agent's audio track. Kind: internal. */ +export const SPAN_PUBLISH_AUDIO_OUTPUT = "publish_audio_output" as const; +/** One realtime-model generation. Kind: client. */ +export const SPAN_REALTIME_INFERENCE = "realtime_inference" as const; +/** Realtime-model metrics attached to a turn. Kind: internal. */ +export const SPAN_REALTIME_METRICS = "realtime_metrics" as const; +/** Resuming an agent activity. Kind: internal. */ +export const SPAN_RESUME_AGENT_ACTIVITY = "resume_agent_activity" as const; +/** Connecting the job to its room. Kind: client. */ +export const SPAN_ROOM_CONNECT = "room_connect" as const; +/** Disconnecting from the room. Kind: client. */ +export const SPAN_ROOM_DISCONNECT = "room_disconnect" as const; +/** An outgoing RPC. Kind: client. */ +export const SPAN_RPC_CALL = "rpc_call" as const; +/** An incoming RPC handled by the agent. Kind: server. */ +export const SPAN_RPC_HANDLER = "rpc_handler" as const; +/** Closing the session. Kind: internal. */ +export const SPAN_SESSION_CLOSE = "session_close" as const; +/** Uploading the session report. Kind: internal. */ +export const SPAN_SESSION_END_UPLOAD = "session_end_upload" as const; +/** Starting the session (room I/O, first activity). Kind: internal. */ +export const SPAN_SESSION_START = "session_start" as const; +/** Initializing the agent's tool sets. Kind: internal. */ +export const SPAN_SETUP_TOOLSETS = "setup_toolsets" as const; +/** One shutdown callback. Kind: internal. */ +export const SPAN_SHUTDOWN_CALLBACK = "shutdown_callback" as const; +/** Starting an agent activity. Kind: internal. */ +export const SPAN_START_AGENT_ACTIVITY = "start_agent_activity" as const; +/** The TTS fallback adapter choosing an instance. Kind: internal. */ +export const SPAN_TTS_FALLBACK_ADAPTER = "tts_fallback_adapter" as const; +/** The agent's TTS node for one generation. Kind: internal. */ +export const SPAN_TTS_NODE = "tts_node" as const; +/** One TTS request. Kind: client. */ +export const SPAN_TTS_REQUEST = "tts_request" as const; +/** The TTS request's stream being consumed. Kind: internal. */ +export const SPAN_TTS_REQUEST_RUN = "tts_request_run" as const; +/** The Node binding driving `Tts::stream` (the duplex path). Kind: internal. */ +export const SPAN_TTS_STREAM = "tts_stream" as const; +/** The sentence-splitting adapter over a non-streaming TTS. Kind: internal. */ +export const SPAN_TTS_STREAM_ADAPTER = "tts_stream_adapter" as const; +/** The Node binding driving `Tts::synthesize` (the simplex path). Kind: internal. */ +export const SPAN_TTS_SYNTHESIZE = "tts_synthesize" as const; +/** Replacing the session's agent. Kind: internal. */ +export const SPAN_UPDATE_AGENT = "update_agent" as const; +/** The user speaking (VAD). Kind: internal. */ +export const SPAN_USER_SPEAKING = "user_speaking" as const; +/** One user turn, from speech to the turn decision. Kind: internal. */ +export const SPAN_USER_TURN = "user_turn" as const; +/** Waiting for the participant's audio track. Kind: internal. */ +export const SPAN_WAIT_FOR_AUDIO_TRACK = "wait_for_audio_track" as const; +/** Waiting for the participant to link to. Kind: internal. */ +export const SPAN_WAIT_FOR_PARTICIPANT = "wait_for_participant" as const; +/** Waiting for the participant's video track. Kind: internal. */ +export const SPAN_WAIT_FOR_VIDEO_TRACK = "wait_for_video_track" as const; + +/** Checkpoint (span event): The agent state changed (`lk.old_state` -> `lk.new_state`). */ +export const CHECKPOINT_AGENT_STATE_CHANGED = "agent_state_changed" as const; +/** Checkpoint (span event): The subscriber's SDP answer went out. */ +export const CHECKPOINT_ANSWER_SENT = "answer_sent" as const; +/** Checkpoint (span event): A peer connection was created before the join response (JS port only). */ +export const CHECKPOINT_EARLY_PC_CREATED = "early_pc_created" as const; +/** Checkpoint (span event): The RTC engine is ready (Swift stamps it with pc_connected). */ +export const CHECKPOINT_ENGINE = "engine" as const; +/** Checkpoint (span event): The entrypoint was cancelled. */ +export const CHECKPOINT_ENTRYPOINT_CANCELLED = "entrypoint_cancelled" as const; +/** Checkpoint (span event): The entrypoint returned. */ +export const CHECKPOINT_ENTRYPOINT_RETURNED = "entrypoint_returned" as const; +/** Checkpoint (span event): The entrypoint started running. */ +export const CHECKPOINT_ENTRYPOINT_STARTED = "entrypoint_started" as const; +/** Checkpoint (span event): A synchronous block on the event loop, with `lk.blocking.duration`; mirrors the span of the same name. */ +export const CHECKPOINT_EVENT_LOOP_BLOCKED = "event_loop_blocked" as const; +/** Checkpoint (span event): The OTel exception event (`exception.*`), recorded on whichever span the exception was caught on; declared here once. */ +export const CHECKPOINT_EXCEPTION = "exception" as const; +/** Checkpoint (span event): The first frame arrived. */ +export const CHECKPOINT_FIRST_FRAME = "first_frame" as const; +/** Checkpoint (span event): The first inbound stats reading with bytes; the span ends ok here. */ +export const CHECKPOINT_FIRST_MEDIA = "first_media" as const; +/** Checkpoint (span event): OTel GenAI inference details event (the GenAI registry defines it; events cannot be referenced */ +export const CHECKPOINT_GEN_AI_CLIENT_INFERENCE_OPERATION_DETAILS = "gen_ai.client.inference.operation.details" as const; +/** Checkpoint (span event): One generation started (the generation attributes). */ +export const CHECKPOINT_GENERATION = "generation" as const; +/** Checkpoint (span event): The worker accepted the job. */ +export const CHECKPOINT_JOB_ACCEPTED = "job_accepted" as const; +/** Checkpoint (span event): The server assigned the job. */ +export const CHECKPOINT_JOB_ASSIGNED = "job_assigned" as const; +/** Checkpoint (span event): The worker received the job request. */ +export const CHECKPOINT_JOB_RECEIVED = "job_received" as const; +/** Checkpoint (span event): The server-side job start time, as the worker learned it. */ +export const CHECKPOINT_JOB_STARTED_ON_SERVER = "job_started_on_server" as const; +/** Checkpoint (span event): The join response arrived. */ +export const CHECKPOINT_JOIN_RECV = "join_recv" as const; +/** Checkpoint (span event): The publisher's SDP offer went out. */ +export const CHECKPOINT_OFFER_SENT = "offer_sent" as const; +/** Checkpoint (span event): The linked participant left the room, with `lk.disconnect_reason`. */ +export const CHECKPOINT_PARTICIPANT_DISCONNECTED = "participant_disconnected" as const; +/** Checkpoint (span event): The session linked to its participant. */ +export const CHECKPOINT_PARTICIPANT_LINKED = "participant_linked" as const; +/** Checkpoint (span event): The primary peer connection connected. */ +export const CHECKPOINT_PC_CONNECTED = "pc_connected" as const; +/** Checkpoint (span event): The peer connections exist (stamped once the join is adopted). */ +export const CHECKPOINT_PC_CREATED = "pc_created" as const; +/** Checkpoint (span event): Pre-connect audio was replayed, with `lk.pre_connect_audio.duration`. */ +export const CHECKPOINT_PRE_CONNECT_AUDIO = "pre_connect_audio" as const; +/** Checkpoint (span event): A preemptive generation was discarded in favour of this one (`lk.speech_id` of the discarded handle). */ +export const CHECKPOINT_PREEMPTIVE_GENERATION_DISCARDED = "preemptive_generation_discarded" as const; +/** Checkpoint (span event): A job process was launched for it. */ +export const CHECKPOINT_PROCESS_ASSIGNED = "process_assigned" as const; +/** Checkpoint (span event): A later trigger re-armed the wait. */ +export const CHECKPOINT_REARMED = "rearmed" as const; +/** Checkpoint (span event): The Room reached the connected state. */ +export const CHECKPOINT_ROOM_CONNECTED = "room_connected" as const; +/** Checkpoint (span event): Signaling is up (stamped with ws_open on the web and with join_recv on Swift). */ +export const CHECKPOINT_SIGNAL = "signal" as const; +/** Checkpoint (span event): The hook stopped the response. */ +export const CHECKPOINT_STOP_RESPONSE = "stop_response" as const; +/** Checkpoint (span event): The platform reported the remote track subscribed. */ +export const CHECKPOINT_SUBSCRIBED = "subscribed" as const; +/** Checkpoint (span event): A newer detection superseded this one. */ +export const CHECKPOINT_SUPERSEDED = "superseded" as const; +/** Checkpoint (span event): The track was subscribed, with `lk.track_sid` and `lk.track_source`. */ +export const CHECKPOINT_TRACK_SUBSCRIBED = "track_subscribed" as const; +/** Checkpoint (span event): The user spoke again during the wait. */ +export const CHECKPOINT_USER_RESUMED = "user_resumed" as const; +/** Checkpoint (span event): The user state changed (`lk.old_state` -> `lk.new_state`). */ +export const CHECKPOINT_USER_STATE_CHANGED = "user_state_changed" as const; +/** Checkpoint (span event): The signaling WebSocket opened. */ +export const CHECKPOINT_WS_OPEN = "ws_open" as const; + +/** GenAI operation duration, as the GenAI registry defines it (declared here because weaver cannot import it). Instrument: histogram, unit: `s`. */ +export const METRIC_GEN_AI_CLIENT_OPERATION_DURATION = "gen_ai.client.operation.duration" as const; + +/** Time to the first chunk of a streaming GenAI response (deprecated upstream; declared as emitted). Instrument: histogram, unit: `s`. */ +export const METRIC_GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK = "gen_ai.client.operation.time_to_first_chunk" as const; + +/** Input and output tokens used by the GenAI operation (deprecated upstream; declared as emitted). Instrument: histogram, unit: `{token}`. */ +export const METRIC_GEN_AI_CLIENT_TOKEN_USAGE = "gen_ai.client.token.usage" as const; + +/** The duration of a single tool execution. Instrument: histogram, unit: `s`. */ +export const METRIC_GEN_AI_EXECUTE_TOOL_DURATION = "gen_ai.execute_tool.duration" as const; + +/** The end-to-end duration of a single in-process agent invocation, from the moment the invocation starts until the agent emits the last chunk of its final response or terminates with an error. Instrument: histogram, unit: `s`. */ +export const METRIC_GEN_AI_INVOKE_AGENT_DURATION = "gen_ai.invoke_agent.duration" as const; + +/** Seconds to acquire a provider connection. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_CONNECTION_ACQUIRE_TIME = "lk.agents.connection.acquire_time" as const; + +/** Duration of synchronous blocks detected on an agent event loop. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_EVENT_LOOP_BLOCKED_DURATION = "lk.agents.event_loop.blocked_duration" as const; + +/** End-to-end latency of a turn. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_E2E_LATENCY = "lk.agents.turn.e2e_latency" as const; + +/** Delay from the end of speech to the turn decision. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_END_OF_TURN_DELAY = "lk.agents.turn.end_of_turn_delay" as const; + +/** Time to the first LLM token. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_LLM_TTFT = "lk.agents.turn.llm_ttft" as const; + +/** Duration of the `on_user_turn_completed` hook. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_ON_USER_TURN_COMPLETED_DELAY = "lk.agents.turn.on_user_turn_completed_delay" as const; + +/** Delay from the end of speech to the final transcript. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_TRANSCRIPTION_DELAY = "lk.agents.turn.transcription_delay" as const; + +/** Time to the first TTS byte. Instrument: histogram, unit: `s`. */ +export const METRIC_LK_AGENTS_TURN_TTS_TTFB = "lk.agents.turn.tts_ttfb" as const; + +/** Interruption-detector requests. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS = "lk.agents.usage.interruption_num_requests" as const; + +/** Realtime-model audio input tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS = "lk.agents.usage.llm_input_audio_tokens" as const; + +/** LLM cached input tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS = "lk.agents.usage.llm_input_cached_tokens" as const; + +/** Realtime-model text input tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS = "lk.agents.usage.llm_input_text_tokens" as const; + +/** LLM input tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_INPUT_TOKENS = "lk.agents.usage.llm_input_tokens" as const; + +/** Realtime-model audio output tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS = "lk.agents.usage.llm_output_audio_tokens" as const; + +/** Realtime-model text output tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS = "lk.agents.usage.llm_output_text_tokens" as const; + +/** LLM output tokens. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS = "lk.agents.usage.llm_output_tokens" as const; + +/** Realtime-model session seconds. Instrument: counter, unit: `s`. */ +export const METRIC_LK_AGENTS_USAGE_LLM_SESSION_DURATION = "lk.agents.usage.llm_session_duration" as const; + +/** Seconds of audio transcribed. Instrument: counter, unit: `s`. */ +export const METRIC_LK_AGENTS_USAGE_STT_AUDIO_DURATION = "lk.agents.usage.stt_audio_duration" as const; + +/** Seconds of audio synthesized. Instrument: counter, unit: `s`. */ +export const METRIC_LK_AGENTS_USAGE_TTS_AUDIO_DURATION = "lk.agents.usage.tts_audio_duration" as const; + +/** Characters synthesized. Instrument: counter, unit: ``. */ +export const METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS = "lk.agents.usage.tts_characters" as const; diff --git a/otel/generate.sh b/otel/generate.sh new file mode 100755 index 000000000..2eb56c336 --- /dev/null +++ b/otel/generate.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Regenerates otel/gen from otel/model (Rust, TypeScript, Python, markdown docs). Generation runs +# the LiveKit policies (--v2 mode). gen/ is replaced whole, and only when every target passes, so +# an output that no target writes any more disappears. CI fails when the committed outputs differ +# from a fresh run. otel/tests/generate.sh tests the replacement. +set -euo pipefail +cd "$(dirname "$0")" +W=${WEAVER:-weaver} +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT +for target in rust:rust typescript:ts python:python markdown:docs; do + "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "$tmp/gen/${target#*:}" +done +rm -rf gen && mv "$tmp/gen" gen diff --git a/otel/live-check.sh b/otel/live-check.sh new file mode 100755 index 000000000..9dea94439 --- /dev/null +++ b/otel/live-check.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Checks captured client telemetry against otel/model. +# usage: otel/live-check.sh [out-dir] [extra `weaver registry live-check` args] +# capture: OTLP/JSON lines from an otelcol file exporter (*.jsonl), or weaver samples (*.json) +# out-dir: where live_check.json lands (default: a new temporary directory) +# Exits like weaver: 1 when there is a violation (pass `--fail-on none` to only report). When weaver +# writes no report, prints the weaver log and exits with weaver's status, or 1 if that was 0. +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +W=${WEAVER:-weaver} +capture=$1 out=${2:-$(mktemp -d)} +mkdir -p "$out" +# Only the previous report goes: out-dir can be the caller's and hold other files. +rm -f "$out/live_check.json" + +samples=$capture +if [[ $capture == *.jsonl ]]; then + samples=$out/samples.json + python3 -I "$here/live-check/otlp2samples.py" "$capture" > "$samples" +fi + +status=0 +"$W" registry live-check --config "$here/live-check/weaver.toml" -r "$here/model" --v2 \ + --advice-policies "$here/live-check" --advice-preprocessor "$here/live-check/renames.jq" \ + --input-source "$samples" --input-format json \ + --format json -o "$out" "${@:3}" > "$out/weaver.log" 2>&1 || status=$? +if [[ ! -f $out/live_check.json ]]; then + cat "$out/weaver.log" >&2 + echo "live-check: weaver wrote no report (exit $status)" >&2 + exit "$(( status ? status : 1 ))" +fi +python3 -I "$here/live-check/summarize.py" "$out/live_check.json" +echo "report: $out/live_check.json" +exit "$status" diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego new file mode 100644 index 000000000..ba5f1cfac --- /dev/null +++ b/otel/live-check/livekit.rego @@ -0,0 +1,123 @@ +package live_check_advice + +# Same contract as otel/policies/livekit.rego, applied to emitted telemetry. A finding on an old +# spelling names the registered key, and value, a producer must send instead. + +import rego.v1 + +pii_segment := `(^|\.)pii(\.|$)` + +pii_looking := `(identity|email|phone|ip_address|user_agent|(room|participant)[._]name)` + +# The renames recorded in otel/model (`renamed_from` on each shared key), keyed by the old +# spelling: `data.renames[old] = {key, values}`. Built by live-check/renames.jq. +renames := object.get(data, "renames", {}) + +lk_advice(type, ctx, msg) := { + "type": "advice", + "advice_type": type, + "advice_level": "violation", + "advice_context": ctx, + "message": msg, +} + +# A PII-looking key that is not an old spelling (those get livekit_shared_rename). +deny contains lk_advice("livekit_pii_unmarked", {"attribute_key": key}, sprintf("'%s' looks like PII but has no `pii` key segment, so the collector will not redact it", [key])) if { + key := input.sample.attribute.name + startswith(key, "lk.") + regex.match(pii_looking, key) + not regex.match(pii_segment, key) + not renames[key] +} + +# An old spelling: the model names the key to send and, for an enum, the value. +deny contains lk_advice("livekit_shared_rename", {"attribute_key": key, "expected": to.key, "expected_value": value}, sprintf("'%s' is renamed: send '%s'%s", [key, to.key, hint])) if { + key := input.sample.attribute.name + to := renames[key] + value := object.get(to.values, input.sample.attribute.value, null) + hint := rename_value_hint(to, value) +} + +rename_value_hint(to, value) := "" if count(to.values) == 0 + +rename_value_hint(to, value) := sprintf(" = '%s'", [value]) if { + count(to.values) > 0 + value != null +} + +rename_value_hint(to, value) := sprintf(" (no value mapped for '%v')", [input.sample.attribute.value]) if { + count(to.values) > 0 + value == null +} + +# Spans have no name-based match of their own (otel/live-check/weaver.toml matches the registered +# ones), so weaver says nothing about an lk.* span it cannot place. Like an unknown event, flag it. +deny contains lk_advice("livekit_unknown_span", {"span_name": name}, sprintf("span '%s' is in the LiveKit namespace but not registered in otel/model", [name])) if { + name := input.sample.span.name + startswith(name, "lk.") + object.get(input, "registry_group", null) == null # always present, null when unmatched +} + +# What otel/model/client/signals.yaml declares in annotations.livekit, checked on matched samples. +declared(field) := object.get(input, ["registry_group", "annotations", "livekit", field], null) + +span_attr(key) := [a.value | some a in input.sample.span.attributes; a.name == key] + +# A span event must be one of the span's checkpoints, or match a pattern the span declares. +matches_declared_pattern(name) if { + pattern := object.get(input, ["registry_group", "annotations", "livekit", "checkpoint_pattern", "pattern"], null) + is_string(pattern) + regex.match(pattern, name) +} + +deny contains lk_advice("livekit_checkpoint", {"span_name": span.name, "checkpoint": name}, sprintf("span '%s' has checkpoint '%s', which otel/model does not list for it", [span.name, name])) if { + span := input.sample.span + declared("checkpoints") != null + some event in span.span_events + name := event.name + not name in {c.name | some c in declared("checkpoints")} + not matches_declared_pattern(name) +} + +# OTLP severity numbers per severity text (logs data model: INFO 9-12, WARN 13-16, ERROR 17-20). +severity_range := {"INFO": [9, 12], "WARN": [13, 16], "ERROR": [17, 20]} + +number_declared(n) if { + some text in declared("severity") + severity_range[text][0] <= n + n <= severity_range[text][1] +} + +# The number decides; a text, when present, must agree with the declaration too. +deny contains lk_advice("livekit_severity", {"event_name": log.event_name, "severity_number": log.severity_number}, sprintf("event '%s' is sent at severity number %v; otel/model allows %v", [log.event_name, log.severity_number, declared("severity")])) if { + log := input.sample.log + declared("severity") != null + not number_declared(object.get(log, "severity_number", 0)) +} + +deny contains lk_advice("livekit_severity", {"event_name": log.event_name, "severity_text": log.severity_text}, sprintf("event '%s' is sent at %v; otel/model allows %v", [log.event_name, log.severity_text, declared("severity")])) if { + log := input.sample.log + declared("severity") != null + text := object.get(log, "severity_text", null) # optional in OTLP; omitted arrives as null + is_string(text) + text != "" + not text in declared("severity") +} + +# The span status lk.outcome implies: Error for `error`, Unset for `ok` and `cancelled`. +status_for := {"ok": "unset", "cancelled": "unset", "error": "error"} + +deny contains lk_advice("livekit_span_outcome", {"span_name": span.name, "outcome": outcome, "status": span.status.code}, sprintf("span '%s': lk.outcome=%s requires status %s, got %s", [span.name, outcome, status_for[outcome], span.status.code])) if { + span := input.sample.span + startswith(span.name, "lk.") + some outcome in span_attr("lk.outcome") + span.status.code != status_for[outcome] +} + +deny contains lk_advice("livekit_span_outcome", {"span_name": span.name, "outcome": outcome}, sprintf("span '%s': error.type without lk.outcome=error (lk.outcome=%s)", [span.name, outcome])) if { + span := input.sample.span + startswith(span.name, "lk.") + count(span_attr("error.type")) > 0 + some outcome in span_attr("lk.outcome") + outcome != "error" +} diff --git a/otel/live-check/otlp2samples.py b/otel/live-check/otlp2samples.py new file mode 100644 index 000000000..ec3cf1ebd --- /dev/null +++ b/otel/live-check/otlp2samples.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""OTLP/JSON lines (otelcol file exporter) -> weaver live-check samples JSON array. + +usage: otel/live-check/otlp2samples.py session.jsonl > samples.json +Lets `weaver registry live-check --input-source --input-format json` check a +capture without a gRPC replay. Stdlib only. +""" +import json, sys + +KIND = {0: "internal", 1: "internal", 2: "server", 3: "client", 4: "producer", 5: "consumer"} +STATUS = {0: "unset", 1: "ok", 2: "error"} + + +def value(v): + if "stringValue" in v: return v["stringValue"] + if "intValue" in v: return int(v["intValue"]) # OTLP/JSON encodes int64 as a string + if "doubleValue" in v: return float(v["doubleValue"]) + if "boolValue" in v: return v["boolValue"] + if "arrayValue" in v: return [value(x) for x in v["arrayValue"].get("values", [])] + raise ValueError(f"unsupported AnyValue {v}") # ponytail: kvlist/bytes unused by the core today + + +def attrs(kvs): + return [{"name": kv["key"], "value": value(kv["value"])} for kv in kvs or []] + + +def samples(row): + for rs in row.get("resourceSpans", []) + row.get("resourceLogs", []): + yield {"resource": {"attributes": attrs(rs.get("resource", {}).get("attributes"))}} + for ss in rs.get("scopeSpans", []): + for s in ss.get("spans", []): + yield {"span": { + "name": s["name"], + "kind": KIND[s.get("kind", 0)], + "status": {"code": STATUS[s.get("status", {}).get("code", 0)], "message": s.get("status", {}).get("message", "")}, + "attributes": attrs(s.get("attributes")), + "span_events": [{"name": e["name"], "attributes": attrs(e.get("attributes"))} for e in s.get("events", [])], + "span_links": [], + }} + for sl in rs.get("scopeLogs", []): + for r in sl.get("logRecords", []): + yield {"log": { + "event_name": r.get("eventName", ""), # plain logs have none; weaver wants a string + "severity_number": r.get("severityNumber"), + "severity_text": r.get("severityText"), + "body": r.get("body", {}).get("stringValue"), + "attributes": attrs(r.get("attributes")), + }} + + +if __name__ == "__main__": + out = [s for line in open(sys.argv[1]) if line.strip() for s in samples(json.loads(line))] + assert out, "no OTLP records found" + json.dump(out, sys.stdout, indent=1) diff --git a/otel/live-check/renames.jq b/otel/live-check/renames.jq new file mode 100644 index 000000000..1a6890773 --- /dev/null +++ b/otel/live-check/renames.jq @@ -0,0 +1,6 @@ +# Live-check advice preprocessor: the renames recorded in otel/model as `renamed_from`, keyed by +# the old spelling, for live-check/livekit.rego (`data.renames`). +{renames: ([.registry.registry.attributes[] + | .annotations.livekit.renamed_from as $from + | select($from) + | {($from.key): {key: .key, values: ($from.values // {})}}] | add // {})} diff --git a/otel/live-check/summarize.py b/otel/live-check/summarize.py new file mode 100644 index 000000000..957c2c7ca --- /dev/null +++ b/otel/live-check/summarize.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Summarize a weaver live-check JSON report: findings by level and id, with the keys involved. + +usage: otel/live-check/summarize.py live_check.json +""" +import collections, json, sys + +report = json.load(open(sys.argv[1])) +by_type, keys = collections.Counter(), collections.defaultdict(collections.Counter) + + +def walk(x): + if isinstance(x, dict): + for adv in x.get("live_check_result", {}).get("all_advice", []) if isinstance(x.get("live_check_result"), dict) else []: + t = f'{adv["level"]:<11} {adv["id"]}' + by_type[t] += 1 + ctx = adv.get("context") or {} + keys[t][ctx.get("attribute_key") or ctx.get("event_name") or ctx.get("span_name") or adv.get("signal_name") or "-"] += 1 + for v in x.values(): walk(v) + elif isinstance(x, list): + for v in x: walk(v) + + +walk(report["samples"]) +for t, n in by_type.most_common(): + top = ", ".join(f"{k}×{c}" for k, c in keys[t].most_common(8)) + print(f"{n:4} {t}: {top}") +st = report.get("statistics", {}) +print("totals:", json.dumps({k: st.get(k) for k in ("total_entities_by_type", "advice_level_counts")})) diff --git a/otel/live-check/weaver.toml b/otel/live-check/weaver.toml new file mode 100644 index 000000000..6d322ecc3 --- /dev/null +++ b/otel/live-check/weaver.toml @@ -0,0 +1,65 @@ +# Live-check config for client captures; otel/live-check.sh passes it with --config. +# Matchers say what each sample is checked against. Filters drop what is not a finding for a +# registry that is all `development`. + +# Spans carry no name-based match of their own: one matcher per span name. +[[live-check.matchers]] +id = "match.lk.connect" +sample_type = "span" +when = 'name == "lk.connect"' +signal = "lk.connect" +attribute_groups = ["livekit.client.record"] + +[[live-check.matchers]] +id = "match.lk.reconnect" +sample_type = "span" +when = 'name == "lk.reconnect"' +signal = "lk.reconnect" +attribute_groups = ["livekit.client.record"] + +[[live-check.matchers]] +id = "match.lk.publish" +sample_type = "span" +when = 'name == "lk.publish"' +signal = "lk.publish" +attribute_groups = ["livekit.client.record"] + +[[live-check.matchers]] +id = "match.lk.subscribe" +sample_type = "span" +when = 'name == "lk.subscribe"' +signal = "lk.subscribe" +attribute_groups = ["livekit.client.record"] + +# App-defined events: the core names them custom.. +[[live-check.matchers]] +id = "match.custom" +sample_type = "log" +when = 'event_name.startsWith("custom.")' +signal = "custom" + +# Plain log lines, then the session attributes every record carries. +[[live-check.matchers]] +id = "match.log.plain" +sample_type = "log" +when = 'event_name == ""' +attribute_groups = ["livekit.client.log"] + +[[live-check.matchers]] +id = "match.log.record" +sample_type = "log" +attribute_groups = ["livekit.client.record"] + +[[live-check.matchers]] +id = "match.resource" +sample_type = "resource" +when = '"service.name" in attributes && attributes["service.name"].startsWith("livekit-client")' +attribute_groups = ["livekit.client.resource"] + +# Every key here is `development`, ours and the upstream ones we use alike. +[[live-check.finding_filters]] +exclude = ["not_stable"] + +# Conditions are prose (`Inbound video.`); weaver cannot evaluate them, so absence is no finding. +[[live-check.finding_filters]] +exclude = ["conditionally_required_attribute_not_present"] diff --git a/otel/model/agents/groups.yaml b/otel/model/agents/groups.yaml new file mode 100644 index 000000000..64e2396bf --- /dev/null +++ b/otel/model/agents/groups.yaml @@ -0,0 +1,37 @@ +# Agents' use of upstream GenAI definitions that a plain `ref` cannot express: metrics imported +# from the GenAI registry, and values Agents sends for upstream keys that the upstream definition +# does not list. Definition/2, because only a public attribute group reaches the generated code. +file_format: definition/2 +imports: + # Generated in the `upstream` section of the Rust and Python output. gen_ai.client.operation.duration + # cannot be imported (see the comment on it in signals.yaml). + metrics: + - gen_ai.invoke_agent.duration + - gen_ai.execute_tool.duration +attribute_groups: + - id: livekit.agents.upstream_values + visibility: public + stability: development + brief: Values Agents sends for upstream GenAI keys that the upstream definitions do not list. + note: > + Each `annotations.livekit.shipped_values` entry is a value Agents emits for the referenced + upstream key. The generated code gives it a constant next to the upstream values. The policy + livekit_shipped_value rejects an entry that repeats an upstream value. + attributes: + - ref: gen_ai.provider.name + requirement_level: recommended + annotations: + livekit: + shipped_values: + - {id: mistral_ai, value: mistral_ai, brief: "Mistral, as Agents sends it. The upstream member is `mistral`."} + - ref: gen_ai.response.finish_reasons + requirement_level: recommended + annotations: + livekit: + shipped_values: + - {id: stop, value: stop, brief: The generation finished normally.} + - {id: length, value: length, brief: The output reached the token limit.} + - {id: content_filter, value: content_filter, brief: A content filter stopped the output.} + - {id: tool_call, value: tool_call, brief: The generation ended with a tool call.} + - {id: compaction, value: compaction, brief: "Declared in Agents' `GenAIFinishReason`. No Agents code path sends it today."} + - {id: error, value: error, brief: "The generation ended abnormally. The upstream note names this value."} diff --git a/otel/model/agents/registry.yaml b/otel/model/agents/registry.yaml new file mode 100644 index 000000000..2ecb57e3a --- /dev/null +++ b/otel/model/agents/registry.yaml @@ -0,0 +1,273 @@ +# Agents-owned attributes, under the names LiveKit Agents 2.0 emits today (livekit-agents +# `telemetry/trace_types.py`, `types.py`, `otel_metrics.py`, `observability.py`, and the Rust +# core `lk-agents-abi`, `lk-agents-node`). Registered as emitted: no key is renamed here, and +# where a current name trips a LiveKit rule the exception is annotated and reasoned, so the +# registry documents the vocabulary rather than fixing it. Shared identity keys (`lk.room_sid`, +# `lk.participant_id`, `lk.job_id`, `lk.track_sid`, `lk.pii.room_name`, +# `lk.pii.participant_identity`) live in otel/model/shared and are referenced, never redefined. +# Upstream keys (`gen_ai.*`, `error.type`, `exception.*`, `rpc.method`) are referenced from the +# spans in signals.yaml. Types follow the Python producer; the Node binding's deviations are +# noted on the key. +groups: + - id: registry.lk.agents.job + type: attribute_group + display_name: LiveKit Agents job and session identity + brief: Which worker, job, agent and participant a span is about. + attributes: + - {id: lk.speech_id, type: string, stability: development, brief: "Id of the speech handle a span belongs to.", note: "The Python producer sends the speech handle id as a string. The Node binding stamps it on its `metrics` span as a number (the frame's speech slot), which this registry does not model.", examples: ["speech_3f2a9c"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.agent_label, type: string, stability: development, brief: "Label of the agent running the activity.", examples: ["Assistant"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.previous_agent_label, type: string, stability: development, brief: "Label of the agent handed off from.", examples: ["Greeter"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.retry_count, type: int, stability: development, brief: "Retries before the operation succeeded or gave up.", examples: [0, 2], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.provider_request_ids, type: "string[]", stability: development, brief: "Provider request ids a span covers, for joining against provider logs.", examples: [["req_abc123"]], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.participant_kind, type: string, stability: development, brief: "Kind of the linked participant (protocol `ParticipantInfo.Kind`).", examples: ["standard", "sip"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.cloud_agent_id, type: string, stability: development, brief: "LiveKit Cloud agent id.", examples: ["CA_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.deployment_id, type: string, stability: development, brief: "LiveKit Cloud deployment id.", examples: ["DP_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.dispatch_id, type: string, stability: development, brief: "Agent dispatch id.", examples: ["AD_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.job.worker_id, type: string, stability: development, brief: "Worker id the job runs on.", examples: ["AW_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.job.agent_id, type: string, stability: development, brief: "Agent id of the job.", examples: ["AG_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.sip.phoneNumber, type: string, stability: development, brief: "Phone number of a SIP participant.", note: "Camel case as the SIP participant attribute it is copied from; registered as emitted.", examples: ["+15550100"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.job.accept_latency, type: double, stability: development, brief: "Seconds from the job request to its acceptance.", examples: [0.012], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.job.assignment_latency, type: double, stability: development, brief: "Seconds from acceptance to the job assignment.", examples: [0.3], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.job.launch_latency, type: double, stability: development, brief: "Seconds from assignment to the job process launch.", examples: [0.5], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.job.entrypoint_latency, type: double, stability: development, brief: "Seconds from launch to the entrypoint running.", examples: [0.1], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.job.dispatch_latency, type: double, stability: development, brief: "Seconds from the dispatch request to the entrypoint running.", examples: [0.9], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.simulation.enabled, type: boolean, stability: development, brief: "The session is a simulation.", note: "Telemetry metadata key set by the job context; a span attribute on the session trace.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.simulation.run_id, type: string, stability: development, brief: "Simulation run the session belongs to.", examples: ["run_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.simulation.job_id, type: string, stability: development, brief: "Simulation run job (one scenario) behind the session.", examples: ["srj_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.redaction.enabled, type: boolean, stability: development, brief: "PII redaction was requested for the session.", note: "Telemetry metadata key; the producer's own `pii` filter reads it from the span attributes.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.success, type: boolean, stability: development, brief: "The session report is tagged successful.", note: "A tag name in the session report (`observability.py` / `report.rs`); the outcome is derived from which of `lk.success` and `lk.fail` is present.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.fail, type: boolean, stability: development, brief: "The session report is tagged failed.", note: "See `lk.success`.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.lk.agents.room + type: attribute_group + display_name: LiveKit Agents room I/O + brief: Room connection, tracks and RPC as the agent sees them. + attributes: + - {id: lk.keyterms.count, type: int, stability: development, brief: "Number of keyterms configured.", examples: [12], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.keyterms.added, type: int, stability: development, brief: "Keyterms added by the update.", examples: [3], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.keyterms.removed, type: int, stability: development, brief: "Keyterms removed by the update.", examples: [1], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.keyterms, type: string, stability: development, brief: "The keyterms themselves (customer vocabulary).", examples: ["LiveKit, WebRTC"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.room.auto_subscribe, type: boolean, stability: development, brief: "The room was joined with auto-subscribe.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.room.e2ee, type: boolean, stability: development, brief: "End-to-end encryption is on.", examples: [false], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.room.remote_participant_count, type: int, stability: development, brief: "Remote participants when the agent connected.", examples: [1], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.room_io.participant_filter, type: string, stability: development, brief: "How room I/O picked its participant.", examples: ["identity", "kind"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.first_frame_delay, type: double, stability: development, brief: "Seconds from waiting for a track to its first frame.", examples: [0.25], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.pre_connect_audio.duration, type: double, stability: development, brief: "Seconds of pre-connect audio replayed.", examples: [1.5], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.connection_state, type: string, stability: development, brief: "Room connection state.", examples: ["connected"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.old_state, type: string, stability: development, brief: "State before a transition.", examples: ["listening"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.new_state, type: string, stability: development, brief: "State after a transition.", examples: ["speaking"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.rpc.request_id, type: string, stability: development, brief: "RPC request id.", examples: ["rpc_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.rpc.caller_identity, type: string, stability: development, brief: "Identity of the RPC caller.", note: "A participant identity outside `lk.pii.*`, as Agents 2.0 ships it; the registry records it rather than renaming it.", examples: ["identity-0"], annotations: {livekit: {owner: agents, sensitivity: none, pii_heuristic_exception: "Shipped unmarked in Agents 2.0; renaming to lk.pii.rpc.caller_identity is a producer change, not a registry one."}}} + - {id: lk.rpc.destination_identity, type: string, stability: development, brief: "Identity the RPC is sent to.", examples: ["identity-1"], annotations: {livekit: {owner: agents, sensitivity: none, pii_heuristic_exception: "Shipped unmarked in Agents 2.0; see lk.rpc.caller_identity."}}} + - {id: lk.pii.rpc.payload, type: string, stability: development, brief: "RPC request payload.", examples: ["{\"q\":1}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.rpc.payload_size, type: int, stability: development, brief: "RPC request payload size in bytes.", examples: [42], annotations: {livekit: {owner: agents, sensitivity: none, unit: By}}} + - {id: lk.pii.rpc.response, type: string, stability: development, brief: "RPC response payload.", examples: ["ok"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.rpc.response_size, type: int, stability: development, brief: "RPC response size in bytes.", examples: [2], annotations: {livekit: {owner: agents, sensitivity: none, unit: By}}} + - {id: lk.rpc.response_timeout, type: double, stability: development, brief: "RPC response timeout in seconds.", examples: [10.0], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.rpc.error_code, type: int, stability: development, brief: "RPC error code.", examples: [1400], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.rpc.handler_registered, type: boolean, stability: development, brief: "A handler was registered for the method.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.lk.agents.lifecycle + type: attribute_group + display_name: LiveKit Agents lifecycle + brief: Session close, job shutdown and callbacks. + attributes: + - {id: lk.close_reason, type: string, stability: development, brief: "Why the session closed.", examples: ["user_initiated"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.close.drain, type: boolean, stability: development, brief: "The close drained pending speech first.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.shutdown.reason, type: string, stability: development, brief: "Why the job shut down.", examples: ["room_disconnected"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.shutdown.user_initiated, type: boolean, stability: development, brief: "The shutdown was requested by user code.", examples: [false], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.callback.name, type: string, stability: development, brief: "Name of the shutdown callback.", examples: ["flush_transcript"], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.lk.agents.turn + type: attribute_group + display_name: LiveKit Agents turns and generation + brief: User and agent turns, the LLM node and tool calls. + attributes: + - {id: lk.generation_id, type: string, stability: development, brief: "Id of the agent turn (generation).", examples: ["gen_abc123"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.parent_generation_id, type: string, stability: development, brief: "Generation this one continues (a tool-call follow-up).", examples: ["gen_abc122"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.generation_count, type: int, stability: development, brief: "Generations in the turn.", examples: [1], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.user_input, type: string, stability: development, brief: "The user's transcript for the turn.", examples: ["book a table"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.instructions, type: string, stability: development, brief: "Agent instructions in force.", examples: ["You are a helpful assistant."], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.interrupted, type: boolean, stability: development, brief: "The speech was interrupted.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.chat_ctx, type: string, stability: development, brief: "Serialized chat context sent to the LLM.", examples: ["[]"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.function_tools, type: "string[]", stability: development, brief: "Function tool names offered.", examples: [["lookup_weather"]], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.provider_tools, type: "string[]", stability: development, brief: "Provider-side tool names offered.", examples: [["web_search"]], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.tool_sets, type: "string[]", stability: development, brief: "Tool set names offered.", examples: [["mcp:docs"]], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.response.text, type: string, stability: development, brief: "Text the LLM produced.", examples: ["Sure."], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.response.function_calls, type: string, stability: development, brief: "Function calls the LLM produced, serialized.", examples: ["[]"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.response.ttft, type: double, stability: development, brief: "Seconds to the first LLM token.", examples: [0.42], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.input.delta, type: boolean, stability: development, brief: "The span's input record is a delta over its base span.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.input.base_span_id, type: string, stability: development, brief: "Span whose input record this one continues.", examples: ["7f3c2a1b9e8d4c5f"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.input.dropped_from_base, type: int, stability: development, brief: "Items of the base record not carried into this one.", examples: [0], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.function_tool.id, type: string, stability: development, brief: "Tool call id.", examples: ["call_7"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.function_tool.name, type: string, stability: development, brief: "Tool name.", examples: ["lookup_weather"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.function_tool.arguments, type: string, stability: development, brief: "Tool call arguments.", examples: ["{\"city\":\"Paris\"}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.function_tool.is_error, type: boolean, stability: development, brief: "The tool returned an error.", examples: [false], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.function_tool.output, type: string, stability: development, brief: "Tool output.", examples: ["sunny"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.speech.queue_wait, type: double, stability: development, brief: "Seconds a speech handle waited in the queue before generation was authorized.", examples: [0.05], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.e2e_latency, type: double, stability: development, brief: "Seconds from the end of the user's speech to the agent's first audio.", examples: [1.1], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.llm_metrics, type: string, stability: development, brief: "LLM metrics of the turn, serialized.", examples: ["{}"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.tts_metrics, type: string, stability: development, brief: "TTS metrics of the turn, serialized.", examples: ["{}"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.realtime_model_metrics, type: string, stability: development, brief: "Realtime model metrics of the turn, serialized.", examples: ["{}"], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.lk.agents.tts + type: attribute_group + display_name: LiveKit Agents TTS + brief: The TTS node and the Node binding's TTS drivers. + attributes: + - {id: lk.pii.input_text, type: string, stability: development, brief: "Text sent to the TTS.", examples: ["Hello"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.tts.streaming, type: boolean, stability: development, brief: "The TTS instance streams (duplex path).", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.tts.label, type: string, stability: development, brief: "Label of the TTS instance.", examples: ["cartesia.TTS"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.response.ttfb, type: double, stability: development, brief: "Seconds to the first TTS audio byte.", examples: [0.3], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.tts.no_duplex_path, type: boolean, stability: development, brief: "The TTS reported no duplex path, so the stream adapter was selected (Node binding; not a failure).", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.tts.frames, type: int, stability: development, brief: "Audio frames the TTS driver collected (Node binding).", examples: [120], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.tts.bytes, type: int, stability: development, brief: "Audio bytes the TTS driver collected (Node binding).", examples: [230400], annotations: {livekit: {owner: agents, sensitivity: none, unit: By}}} + - {id: lk.error.retryable, type: boolean, stability: development, brief: "The provider error is retryable (Node binding).", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.metric, type: string, stability: development, brief: "Metric type noted from a plugin task (Node binding `metrics` span).", examples: ["tts_metrics"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - id: lk.frame.resolution + stability: development + brief: How the inbound frame of a plugin-task metric was resolved (Node binding). + type: + members: + - {id: root, value: root, brief: The root frame., stability: development} + - {id: live, value: live, brief: A live speech frame., stability: development} + - {id: stale, value: stale, brief: A frame whose speech already ended., stability: development} + - {id: unknown, value: unknown, brief: Not a frame the slab knows., stability: development} + annotations: {livekit: {owner: agents, sensitivity: none}} + + - id: registry.lk.agents.eou + type: attribute_group + display_name: LiveKit Agents end-of-utterance + brief: Turn detection, STT and the endpointing wait. + attributes: + - {id: lk.eou.probability, type: double, stability: development, brief: "End-of-utterance probability.", examples: [0.93], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.eou.unlikely_threshold, type: double, stability: development, brief: "Threshold under which the turn is unlikely to be over.", examples: [0.15], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.eou.endpointing_delay, type: double, stability: development, brief: "Seconds of endpointing delay applied.", examples: [0.5], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.eou.language, type: string, stability: development, brief: "Language the detector ran for.", examples: ["en"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.pii.user_transcript, type: string, stability: development, brief: "Transcript the detector scored.", examples: ["book a table"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.interim_transcript, type: string, stability: development, brief: "An interim (non-final) transcript, on a log record.", examples: ["book a"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.stt.events, type: string, stability: development, brief: "JSON list of transcript events on the user turn, in arrival order (received_at, type, transcript_length).", examples: ["[]"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.transcript_confidence, type: double, stability: development, brief: "Confidence of the final transcript.", examples: [0.98], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.transcription_delay, type: double, stability: development, brief: "Seconds from the end of speech to the final transcript.", examples: [0.4], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.end_of_turn_delay, type: double, stability: development, brief: "Seconds from the end of speech to the turn decision.", examples: [0.7], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.eou.source, type: string, stability: development, brief: "What decided the end of the turn.", examples: ["turn_detector", "vad"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.eou.detection_delay, type: double, stability: development, brief: "Seconds the detector inference took.", examples: [0.08], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.eou.from_cache, type: boolean, stability: development, brief: "The detector answer came from the cache.", examples: [false], annotations: {livekit: {owner: agents, sensitivity: none}}} + - id: lk.eou.outcome + stability: development + brief: How the endpointing wait ended. + type: + members: + - {id: committed, value: committed, brief: The turn was committed., stability: development} + - {id: user_resumed, value: user_resumed, brief: The user spoke again., stability: development} + - {id: dropped, value: dropped, brief: The turn was dropped., stability: development} + annotations: {livekit: {owner: agents, sensitivity: none}} + - {id: lk.eou.wait_duration, type: double, stability: development, brief: "Seconds from the end of the user's speech to the turn decision.", examples: [0.6], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.eou.rearm_count, type: int, stability: development, brief: "Times the endpointing wait restarted on a later trigger.", examples: [0], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.eou.not_committed_count, type: int, stability: development, brief: "Turn decisions the wait rejected before it ended.", examples: [0], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.eou.resume_count, type: int, stability: development, brief: "Endpointing waits the user cut short by speaking again.", examples: [1], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.on_user_turn_completed_delay, type: double, stability: development, brief: "Seconds the `on_user_turn_completed` hook took.", examples: [0.01], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + + - id: registry.lk.agents.amd + type: attribute_group + display_name: LiveKit Agents answering-machine detection and interruptions + brief: AMD results, interruption detection, fallbacks and playout. + attributes: + - {id: lk.amd.category, type: string, stability: development, brief: "AMD classification.", examples: ["human", "machine"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.amd.reason, type: string, stability: development, brief: "Why the AMD classifier decided.", examples: ["greeting_length"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.amd.speech_duration, type: double, stability: development, brief: "Seconds of speech the AMD classifier heard.", examples: [2.1], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.amd.delay, type: double, stability: development, brief: "Seconds from the call start to the AMD decision.", examples: [3.0], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.pii.amd.transcript, type: string, stability: development, brief: "Transcript the AMD classifier used.", examples: ["hello?"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.interruption.source, type: string, stability: development, brief: "What triggered the interruption.", examples: ["vad", "manual"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.playout.position, type: double, stability: development, brief: "Seconds of the agent's speech played out when it was interrupted.", examples: [1.2], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.fallback.label, type: string, stability: development, brief: "Label of the fallback instance that served the request.", examples: ["openai.LLM"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.fallback.index, type: int, stability: development, brief: "Index of the fallback instance that served the request.", examples: [1], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.fallback.recovery, type: boolean, stability: development, brief: "The request was a recovery probe of a failed instance.", examples: [false], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.is_interruption, type: boolean, stability: development, brief: "The user turn was classified as an interruption.", examples: [true], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.interruption.probability, type: double, stability: development, brief: "Interruption probability.", examples: [0.8], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.interruption.total_duration, type: double, stability: development, brief: "Seconds of the interruption from detection to resolution.", examples: [0.5], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.interruption.prediction_duration, type: double, stability: development, brief: "Seconds the interruption prediction took.", examples: [0.05], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.interruption.detection_delay, type: double, stability: development, brief: "Seconds from the user's speech to the interruption decision.", examples: [0.2], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + + - id: registry.lk.agents.blocking + type: attribute_group + display_name: LiveKit Agents event-loop blocking + brief: Synchronous blocks detected on the agent event loop. + attributes: + - {id: lk.blocking.duration, type: double, stability: development, brief: "Seconds the loop was blocked.", examples: [0.3], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.blocking.threshold, type: double, stability: development, brief: "Seconds above which a block is reported.", examples: [0.1], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.blocking.severity, type: string, stability: development, brief: "Severity bucket of the block.", examples: ["warning"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.task, type: string, stability: development, brief: "Name of the task that was running.", examples: ["Task-12"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.stack, type: string, stability: development, brief: "Loop-thread stack sampled while blocked (source locations only).", examples: ["agent.py:42"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.gc_time, type: double, stability: development, brief: "Seconds of garbage collection on the loop thread during the block.", examples: [0.02], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.blocking.cpu_time, type: double, stability: development, brief: "CPU seconds the loop thread consumed during the block.", examples: [0.25], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.blocking.import, type: string, stability: development, brief: "Module a lazy import was loading when sampled.", examples: ["numpy"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.suppressed, type: int, stability: development, brief: "Reports dropped by rate limiting since the previous emitted span.", examples: [0], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.count, type: int, stability: development, brief: "Blocks in the session.", examples: [2], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: lk.blocking.total_duration, type: double, stability: development, brief: "Seconds blocked in total over the session.", examples: [0.6], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + - {id: lk.blocking.max_duration, type: double, stability: development, brief: "Seconds of the longest block in the session.", examples: [0.4], annotations: {livekit: {owner: agents, sensitivity: none, unit: s}}} + + - id: registry.lk.agents.logs + type: attribute_group + display_name: LiveKit Agents log attributes + brief: PII keys the producers attach to log records (`extra={...}` / `log::warn!` key-values). + attributes: + - {id: lk.pii.raw_arguments, type: string, stability: development, brief: "Malformed tool-call arguments as received.", examples: ["{'x':1}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.repaired, type: string, stability: development, brief: "Tool-call arguments after repair.", examples: ["{\"x\":1}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.error, type: string, stability: development, brief: "Error text that may quote user content.", examples: ["expected ','"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.progress_message, type: string, stability: development, brief: "MCP tool progress message.", examples: ["fetching"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.arguments, type: string, stability: development, brief: "Tool-call arguments on a log record.", examples: ["{}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.output, type: string, stability: development, brief: "Tool-call output on a log record.", examples: ["ok"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.transcript, type: string, stability: development, brief: "Transcript text on a log record.", examples: ["hello"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.text, type: string, stability: development, brief: "Message text on a log record.", examples: ["hello"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.event, type: string, stability: development, brief: "Raw provider event on a debug log record.", examples: ["{}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: lk.pii.dtmf_inputs, type: string, stability: development, brief: "DTMF digits received.", examples: ["1234"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + + - id: registry.lk.agents.metric_dimensions + type: attribute_group + display_name: LiveKit Agents metric dimensions + brief: The dimensions on the `lk.agents.*` instruments, as `otel_metrics.py` names them. + note: Bare keys, outside `lk.*`, as shipped; registered so the metrics can declare them. + attributes: + - {id: model_name, type: string, stability: development, brief: "Model name the plugin reported.", examples: ["gpt-4o-mini"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: model_provider, type: string, stability: development, brief: "Provider the plugin reported.", examples: ["openai"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: connection_reused, type: string, stability: development, brief: "Whether a pooled connection was reused, as the string `true`/`false`.", examples: ["true"], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.gen_ai.agents_local + type: attribute_group + display_name: GenAI keys Agents sends that no upstream registry defines + brief: > + `gen_ai.token.type` is deprecated in OTel semconv 1.44.0 (excluded from dependency + resolution); the LangFuse spellings are not OTel conventions at all. Declared here as emitted + so the generated constants cover them; every other `gen_ai.*` key is referenced upstream. + attributes: + - {id: gen_ai.token.type, type: string, stability: development, brief: "Token type (`input` or `output`); deprecated upstream, a dimension of `gen_ai.client.token.usage`.", examples: ["input", "output"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.input_text_tokens, type: int, stability: development, brief: "LangFuse spelling of text input tokens.", examples: [120], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.input_audio_tokens, type: int, stability: development, brief: "LangFuse spelling of audio input tokens.", examples: [800], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.input_cached_tokens, type: int, stability: development, brief: "LangFuse spelling of cached input tokens.", examples: [64], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.output_text_tokens, type: int, stability: development, brief: "LangFuse spelling of text output tokens.", examples: [40], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.output_audio_tokens, type: int, stability: development, brief: "LangFuse spelling of audio output tokens.", examples: [300], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: gen_ai.usage.reasoning_tokens, type: int, stability: development, brief: "LangFuse spelling of reasoning tokens.", examples: [12], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: langfuse.observation.completion_start_time, type: string, stability: development, brief: "LangFuse's completion start time (ISO 8601), set next to `gen_ai.response.time_to_first_chunk`.", examples: ["2026-10-19T12:00:00.000Z"], annotations: {livekit: {owner: agents, sensitivity: none}}} + + - id: registry.agents.session_report + type: attribute_group + display_name: Agents session report + brief: Attributes of the session-report log records Agents sends on the `chat_history` and `evaluations` loggers. + note: > + Plain log records with no event name, one per report part, with the body `session report`, + `chat item`, `evaluation`, `tag` or `outcome`. The keys are registered as emitted: three sit + in the upstream `session.*` namespace (`annotations.livekit.upstream_namespace`), the others + have no namespace. A map-valued key carries an OTLP AnyValue map; its fields are payload, not + attribute keys. + attributes: + - {id: session.options, type: any, stability: development, brief: "The session's options, as a map of option name to value.", note: "In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it.", examples: ["{\"allow_interruptions\": true}"], annotations: {livekit: {owner: agents, sensitivity: none, upstream_namespace: session}}} + - {id: session.report_timestamp, type: double, stability: development, brief: "When the report was made, in Unix seconds.", note: "In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it.", examples: [1760870400.5], annotations: {livekit: {owner: agents, sensitivity: none, upstream_namespace: session}}} + - {id: session.tags, type: "string[]", stability: development, brief: "The session's tags, sorted. Absent when the session has none.", note: "In the upstream `session.*` namespace, which OTel owns; registered as Agents sends it.", examples: [["support", "escalated"]], annotations: {livekit: {owner: agents, sensitivity: none, upstream_namespace: session}}} + - {id: agent_name, type: string, stability: development, brief: "The agent's name.", examples: ["support-agent"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: sdk_version, type: string, stability: development, brief: "The Agents SDK version.", examples: ["2.0.0"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: usage, type: any, stability: development, brief: "Model usage, as a list of maps, one per model. Absent when there was none.", examples: ["[{\"model\": \"gpt-4o\", \"input_tokens\": 120}]"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: chat.item, type: any, stability: development, brief: "One chat history item, as a map. Carries conversation content.", note: "With redaction on, the collector drops the whole `chat.item` record.", examples: ["{\"type\": \"message\", \"role\": \"user\"}"], annotations: {livekit: {owner: agents, sensitivity: pii}}} + - {id: evaluation, type: any, stability: development, brief: "One evaluation result, as a map of string fields.", examples: ["{\"name\": \"task_completion\", \"verdict\": \"pass\"}"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: tag, type: any, stability: development, brief: "One tag with metadata, as a map with the fields `name` and `metadata`.", examples: ["{\"name\": \"escalated\", \"metadata\": {}}"], annotations: {livekit: {owner: agents, sensitivity: none}}} + - {id: outcome, type: any, stability: development, brief: "The session outcome, as a map with the field `outcome` (`success` or `fail`) and an optional `reason`.", note: "`reason` is a field of this map, not an attribute key.", examples: ["{\"outcome\": \"fail\", \"reason\": \"user hung up\"}"], annotations: {livekit: {owner: agents, sensitivity: none}}} diff --git a/otel/model/agents/signals.yaml b/otel/model/agents/signals.yaml new file mode 100644 index 000000000..4108f8b83 --- /dev/null +++ b/otel/model/agents/signals.yaml @@ -0,0 +1,958 @@ +# Agents spans and metrics, under the names LiveKit Agents 2.0 emits today (the span list is +# tests/trace_schema.py's SPAN_PARENTS plus the Rust Node binding's three spans; the metrics are +# `otel_metrics.py` / `metrics/otel.rs`). Span names carry no `lk.` prefix, as shipped. +# +# Attribute tables are a first pass: each span lists the keys its producer sets at the span start +# or that trace_types.py documents for it, with the upstream OTel keys (`gen_ai.*`, `error.type`, +# `exception.*`, `rpc.method`) referenced from the dependencies and never redefined. The OTel GenAI +# span events the LLM node attaches (`gen_ai.*.message`, `gen_ai.choice`, deprecated upstream and +# excluded from dependency resolution; `gen_ai.client.inference.operation.details`, an event, which +# weaver cannot `ref`) are referenced by name as checkpoints, the registry's span-event mechanism. +groups: + # --- Job and session --------------------------------------------------------------------- + - id: span.job_entrypoint + type: span + span_kind: internal + stability: development + brief: The job's entrypoint, root of a job's trace. + annotations: + livekit: + checkpoints: + - {name: job_received, requirement: best_effort, brief: "The worker received the job request."} + - {name: job_accepted, requirement: best_effort, brief: "The worker accepted the job."} + - {name: job_assigned, requirement: best_effort, brief: "The server assigned the job."} + - {name: process_assigned, requirement: best_effort, brief: "A job process was launched for it."} + - {name: entrypoint_started, requirement: best_effort, brief: "The entrypoint started running."} + - {name: job_started_on_server, requirement: best_effort, brief: "The server-side job start time, as the worker learned it."} + - {name: entrypoint_cancelled, requirement: best_effort, brief: "The entrypoint was cancelled."} + - {name: entrypoint_returned, requirement: best_effort, brief: "The entrypoint returned."} + attributes: + - ref: lk.job_id + requirement_level: required + - ref: lk.room_sid + requirement_level: recommended + - ref: lk.pii.room_name + requirement_level: recommended + - ref: lk.agent_name + requirement_level: recommended + - ref: lk.job.worker_id + requirement_level: recommended + - ref: lk.job.agent_id + requirement_level: recommended + - ref: lk.cloud_agent_id + requirement_level: opt_in + - ref: lk.deployment_id + requirement_level: opt_in + - ref: lk.dispatch_id + requirement_level: opt_in + - ref: lk.job.accept_latency + requirement_level: recommended + - ref: lk.job.assignment_latency + requirement_level: recommended + - ref: lk.job.launch_latency + requirement_level: recommended + - ref: lk.job.entrypoint_latency + requirement_level: recommended + - ref: lk.job.dispatch_latency + requirement_level: recommended + - ref: lk.simulation.enabled + requirement_level: opt_in + - ref: lk.simulation.run_id + requirement_level: opt_in + - ref: lk.simulation.job_id + requirement_level: opt_in + - ref: lk.redaction.enabled + requirement_level: opt_in + - id: span.job_shutdown + type: span + span_kind: internal + stability: development + brief: The job shutting down. + attributes: + - ref: lk.shutdown.reason + requirement_level: recommended + - ref: lk.shutdown.user_initiated + requirement_level: recommended + - id: span.shutdown_callback + type: span + span_kind: internal + stability: development + brief: One shutdown callback. + attributes: + - ref: lk.callback.name + requirement_level: required + - id: span.on_session_end + type: span + span_kind: internal + stability: development + brief: The session-end hook. + - id: span.session_end_upload + type: span + span_kind: internal + stability: development + brief: Uploading the session report. + - id: span.room_connect + type: span + span_kind: client + stability: development + brief: Connecting the job to its room. + attributes: + - ref: lk.room.auto_subscribe + requirement_level: recommended + - ref: lk.room.e2ee + requirement_level: recommended + - ref: lk.room.remote_participant_count + requirement_level: recommended + - ref: lk.connection_state + requirement_level: recommended + - id: span.room_disconnect + type: span + span_kind: client + stability: development + brief: Disconnecting from the room. + - id: span.agent_session + type: span + span_kind: internal + stability: development + brief: The agent session, parent of every turn. + annotations: + livekit: + checkpoints: + - {name: participant_linked, requirement: best_effort, brief: "The session linked to its participant."} + - {name: agent_state_changed, requirement: best_effort, brief: "The agent state changed (`lk.old_state` -> `lk.new_state`)."} + - {name: user_state_changed, requirement: best_effort, brief: "The user state changed (`lk.old_state` -> `lk.new_state`)."} + - {name: participant_disconnected, requirement: best_effort, brief: "The linked participant left the room, with `lk.disconnect_reason`."} + - {name: event_loop_blocked, requirement: best_effort, brief: "A synchronous block on the event loop, with `lk.blocking.duration`; mirrors the span of the same name."} + - {name: exception, requirement: best_effort, brief: "The OTel exception event (`exception.*`), recorded on whichever span the exception was caught on; declared here once."} + attributes: + - ref: lk.pii.participant_identity + requirement_level: recommended + - ref: lk.participant_id + requirement_level: recommended + note: Agents sends the SID of the participant it is linked to, not of the agent itself. + - ref: lk.participant_kind + requirement_level: recommended + - ref: lk.agent_name + requirement_level: recommended + - ref: lk.disconnect_reason + requirement_level: opt_in + note: Sent on the `participant_disconnected` checkpoint, not on the span. + - ref: lk.pii.sip.phoneNumber + requirement_level: opt_in + - ref: lk.blocking.count + requirement_level: opt_in + - ref: lk.blocking.total_duration + requirement_level: opt_in + - ref: lk.blocking.max_duration + requirement_level: opt_in + - ref: lk.success + requirement_level: opt_in + - ref: lk.fail + requirement_level: opt_in + - ref: exception.type + requirement_level: opt_in + note: On the span's exception events, as the OTel SDK records them. + - ref: exception.message + requirement_level: opt_in + note: Replaced by a fixed marker when redaction is on; `error.type` keeps the class. + - ref: exception.stacktrace + requirement_level: opt_in + note: Dropped when redaction is on. + - id: span.session_start + type: span + span_kind: internal + stability: development + brief: Starting the session (room I/O, first activity). + - id: span.session_close + type: span + span_kind: internal + stability: development + brief: Closing the session. + attributes: + - ref: lk.close_reason + requirement_level: recommended + - ref: lk.close.drain + requirement_level: recommended + - id: span.update_agent + type: span + span_kind: internal + stability: development + brief: Replacing the session's agent. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - ref: lk.previous_agent_label + requirement_level: recommended + - id: span.wait_for_participant + type: span + span_kind: internal + stability: development + brief: Waiting for the participant to link to. + attributes: + - ref: lk.room_io.participant_filter + requirement_level: recommended + - id: span.wait_for_audio_track + type: span + span_kind: internal + stability: development + brief: Waiting for the participant's audio track. + annotations: + livekit: + checkpoints: + - {name: track_subscribed, requirement: best_effort, brief: "The track was subscribed, with `lk.track_sid` and `lk.track_source`."} + - {name: first_frame, requirement: best_effort, brief: "The first frame arrived."} + - {name: pre_connect_audio, requirement: best_effort, brief: "Pre-connect audio was replayed, with `lk.pre_connect_audio.duration`."} + attributes: + - ref: lk.track_sid + requirement_level: recommended + - ref: lk.track_source + requirement_level: recommended + note: Sent on the `track_subscribed` checkpoint, not on the span. + - ref: lk.first_frame_delay + requirement_level: recommended + - id: span.wait_for_video_track + type: span + span_kind: internal + stability: development + brief: Waiting for the participant's video track. + annotations: + livekit: + checkpoints: + - {name: track_subscribed, requirement: best_effort, brief: "The track was subscribed, with `lk.track_sid` and `lk.track_source`."} + - {name: first_frame, requirement: best_effort, brief: "The first frame arrived."} + attributes: + - ref: lk.track_sid + requirement_level: recommended + - ref: lk.track_source + requirement_level: recommended + note: Sent on the `track_subscribed` checkpoint, not on the span. + - ref: lk.first_frame_delay + requirement_level: recommended + - id: span.publish_audio_output + type: span + span_kind: internal + stability: development + brief: Publishing the agent's audio track. + attributes: + - ref: lk.track_sid + requirement_level: recommended + - ref: lk.pre_connect_audio.duration + requirement_level: opt_in + - id: span.setup_toolsets + type: span + span_kind: internal + stability: development + brief: Initializing the agent's tool sets. + attributes: + - ref: lk.tool_sets + requirement_level: recommended + - id: span.rpc_call + type: span + span_kind: client + stability: development + brief: An outgoing RPC. + attributes: + - ref: rpc.method + requirement_level: required + note: The LiveKit RPC method name. + - ref: lk.rpc.request_id + requirement_level: recommended + - ref: lk.rpc.destination_identity + requirement_level: recommended + - ref: lk.pii.rpc.payload + requirement_level: opt_in + - ref: lk.rpc.payload_size + requirement_level: recommended + - ref: lk.pii.rpc.response + requirement_level: opt_in + - ref: lk.rpc.response_size + requirement_level: recommended + - ref: lk.rpc.response_timeout + requirement_level: recommended + - ref: lk.rpc.error_code + requirement_level: {conditionally_required: On failure.} + - id: span.rpc_handler + type: span + span_kind: server + stability: development + brief: An incoming RPC handled by the agent. + attributes: + - ref: rpc.method + requirement_level: required + note: The LiveKit RPC method name. + - ref: lk.rpc.request_id + requirement_level: recommended + - ref: lk.rpc.caller_identity + requirement_level: recommended + - ref: lk.pii.rpc.payload + requirement_level: opt_in + - ref: lk.rpc.payload_size + requirement_level: recommended + - ref: lk.pii.rpc.response + requirement_level: opt_in + - ref: lk.rpc.response_size + requirement_level: recommended + - ref: lk.rpc.response_timeout + requirement_level: recommended + - ref: lk.rpc.handler_registered + requirement_level: recommended + - ref: lk.rpc.error_code + requirement_level: {conditionally_required: On failure.} + + # --- Activity lifecycle ------------------------------------------------------------------ + - id: span.start_agent_activity + type: span + span_kind: internal + stability: development + brief: Starting an agent activity. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.drain_agent_activity + type: span + span_kind: internal + stability: development + brief: Draining an agent activity before it ends. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.pause_agent_activity + type: span + span_kind: internal + stability: development + brief: Pausing an agent activity. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.resume_agent_activity + type: span + span_kind: internal + stability: development + brief: Resuming an agent activity. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.on_enter + type: span + span_kind: internal + stability: development + brief: The agent's `on_enter` hook. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.on_exit + type: span + span_kind: internal + stability: development + brief: The agent's `on_exit` hook. + attributes: + - ref: lk.agent_label + requirement_level: recommended + - id: span.keyterm_detection + type: span + span_kind: internal + stability: development + brief: A keyterm update to the STT. + attributes: + - ref: lk.keyterms.count + requirement_level: recommended + - ref: lk.keyterms.added + requirement_level: recommended + - ref: lk.keyterms.removed + requirement_level: recommended + - ref: lk.pii.keyterms + requirement_level: opt_in + - id: span.event_loop_blocked + type: span + span_kind: internal + stability: development + brief: A synchronous block on the agent event loop. + attributes: + - ref: lk.blocking.duration + requirement_level: required + - ref: lk.blocking.threshold + requirement_level: recommended + - ref: lk.blocking.severity + requirement_level: recommended + - ref: lk.blocking.task + requirement_level: recommended + - ref: lk.blocking.stack + requirement_level: recommended + - ref: lk.blocking.gc_time + requirement_level: recommended + - ref: lk.blocking.cpu_time + requirement_level: recommended + - ref: lk.blocking.import + requirement_level: opt_in + - ref: lk.blocking.suppressed + requirement_level: recommended + + # --- Turns ------------------------------------------------------------------------------- + - id: span.user_speaking + type: span + span_kind: internal + stability: development + brief: The user speaking (VAD). + - id: span.agent_speaking + type: span + span_kind: internal + stability: development + brief: The agent's audio playing out. + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: lk.interrupted + requirement_level: recommended + - ref: lk.playout.position + requirement_level: {conditionally_required: When interrupted.} + - ref: lk.interruption.source + requirement_level: {conditionally_required: When interrupted.} + - id: span.user_turn + type: span + span_kind: internal + stability: development + brief: One user turn, from speech to the turn decision. + attributes: + - ref: lk.pii.user_transcript + requirement_level: opt_in + - ref: lk.pii.interim_transcript + requirement_level: opt_in + note: On the interim-transcript log records of the turn, not on the span. + - ref: lk.stt.events + requirement_level: recommended + - ref: lk.transcript_confidence + requirement_level: recommended + - ref: lk.transcription_delay + requirement_level: recommended + - ref: lk.end_of_turn_delay + requirement_level: recommended + - ref: lk.eou.source + requirement_level: recommended + - ref: lk.eou.resume_count + requirement_level: recommended + - ref: lk.is_interruption + requirement_level: recommended + - ref: lk.interruption.probability + requirement_level: opt_in + - ref: lk.interruption.total_duration + requirement_level: opt_in + - ref: lk.interruption.prediction_duration + requirement_level: opt_in + - ref: lk.interruption.detection_delay + requirement_level: opt_in + - id: span.eou_detection + type: span + span_kind: internal + stability: development + brief: One end-of-utterance inference. + annotations: + livekit: + checkpoints: + - {name: superseded, requirement: best_effort, brief: "A newer detection superseded this one."} + attributes: + - ref: lk.eou.probability + requirement_level: recommended + - ref: lk.eou.unlikely_threshold + requirement_level: recommended + - ref: lk.eou.language + requirement_level: recommended + - ref: lk.eou.detection_delay + requirement_level: recommended + - ref: lk.eou.from_cache + requirement_level: recommended + - ref: lk.pii.user_transcript + requirement_level: opt_in + - id: span.eou_wait + type: span + span_kind: internal + stability: development + brief: The endpointing wait, from the user's last speech to the turn decision. + annotations: + livekit: + checkpoints: + - {name: rearmed, requirement: best_effort, brief: "A later trigger re-armed the wait."} + - {name: user_resumed, requirement: best_effort, brief: "The user spoke again during the wait."} + attributes: + - ref: lk.eou.outcome + requirement_level: required + - ref: lk.eou.wait_duration + requirement_level: recommended + - ref: lk.eou.endpointing_delay + requirement_level: recommended + - ref: lk.eou.rearm_count + requirement_level: recommended + - ref: lk.eou.not_committed_count + requirement_level: recommended + - id: span.on_user_turn_completed + type: span + span_kind: internal + stability: development + brief: The `on_user_turn_completed` hook. + annotations: + livekit: + checkpoints: + - {name: stop_response, requirement: best_effort, brief: "The hook stopped the response."} + attributes: + - ref: lk.on_user_turn_completed_delay + requirement_level: recommended + - id: span.agent_turn + type: span + span_kind: internal + stability: development + brief: One agent turn (generation), parent of the LLM and TTS nodes. + annotations: + livekit: + checkpoints: + - {name: generation, requirement: best_effort, brief: "One generation started (the generation attributes)."} + - {name: preemptive_generation_discarded, requirement: best_effort, brief: "A preemptive generation was discarded in favour of this one (`lk.speech_id` of the discarded handle)."} + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: lk.generation_id + requirement_level: recommended + - ref: lk.parent_generation_id + requirement_level: opt_in + - ref: lk.generation_count + requirement_level: recommended + - ref: lk.pii.user_input + requirement_level: opt_in + - ref: lk.pii.instructions + requirement_level: opt_in + - ref: lk.interrupted + requirement_level: recommended + - ref: lk.speech.queue_wait + requirement_level: recommended + - ref: lk.e2e_latency + requirement_level: recommended + - ref: lk.transcription_delay + requirement_level: opt_in + - ref: lk.end_of_turn_delay + requirement_level: opt_in + - ref: lk.on_user_turn_completed_delay + requirement_level: opt_in + - ref: lk.llm_metrics + requirement_level: opt_in + - ref: lk.tts_metrics + requirement_level: opt_in + - ref: lk.realtime_model_metrics + requirement_level: opt_in + - ref: lk.provider_request_ids + requirement_level: opt_in + - id: span.llm_node + type: span + span_kind: internal + stability: development + brief: The agent's LLM node for one generation. + note: > + Carries the OTel GenAI request identity when a custom node does its own inference and no + `llm_request` span exists. Its span events are the OTel GenAI message events (see the + `event.gen_ai.*` groups below). + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: lk.agent_label + requirement_level: recommended + - ref: lk.pii.chat_ctx + requirement_level: opt_in + - ref: lk.function_tools + requirement_level: recommended + - ref: lk.provider_tools + requirement_level: recommended + - ref: lk.tool_sets + requirement_level: recommended + - ref: lk.pii.response.text + requirement_level: opt_in + - ref: lk.pii.response.function_calls + requirement_level: opt_in + - ref: lk.response.ttft + requirement_level: recommended + - ref: lk.input.delta + requirement_level: opt_in + - ref: lk.input.base_span_id + requirement_level: opt_in + - ref: lk.input.dropped_from_base + requirement_level: opt_in + - ref: gen_ai.request.model + requirement_level: opt_in + - ref: gen_ai.provider.name + requirement_level: opt_in + - ref: gen_ai.retrieval.documents + requirement_level: opt_in + - ref: gen_ai.retrieval.query.text + requirement_level: opt_in + - ref: gen_ai.memory.query.text + requirement_level: opt_in + - ref: gen_ai.memory.records + requirement_level: opt_in + - ref: gen_ai.workflow.name + requirement_level: opt_in + - ref: gen_ai.prompt.variable + requirement_level: opt_in + - id: span.llm_request + type: span + span_kind: client + stability: development + brief: One LLM inference request, the OTel GenAI inference span. + annotations: + livekit: + checkpoints: + - {name: gen_ai.client.inference.operation.details, requirement: best_effort, brief: OTel GenAI inference details event (the GenAI registry defines it; events cannot be referenced, so it is named here).} + attributes: + - ref: lk.retry_count + requirement_level: recommended + - ref: lk.provider_request_ids + requirement_level: opt_in + - ref: lk.input.delta + requirement_level: opt_in + - ref: lk.input.base_span_id + requirement_level: opt_in + - ref: lk.input.dropped_from_base + requirement_level: opt_in + - ref: lk.fallback.label + requirement_level: opt_in + - ref: lk.fallback.index + requirement_level: opt_in + - ref: lk.fallback.recovery + requirement_level: opt_in + - ref: gen_ai.operation.name + requirement_level: required + - ref: gen_ai.provider.name + requirement_level: recommended + - ref: gen_ai.request.model + requirement_level: recommended + - ref: gen_ai.request.stream + requirement_level: recommended + - ref: gen_ai.response.id + requirement_level: recommended + - ref: gen_ai.response.model + requirement_level: recommended + - ref: gen_ai.response.finish_reasons + requirement_level: recommended + - ref: gen_ai.response.time_to_first_chunk + requirement_level: recommended + - ref: gen_ai.usage.input_tokens + requirement_level: recommended + - ref: gen_ai.usage.output_tokens + requirement_level: recommended + - ref: gen_ai.usage.cache_read.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.cache_write.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.reasoning.output_tokens + requirement_level: opt_in + - ref: gen_ai.usage.text.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.text.output_tokens + requirement_level: opt_in + - ref: gen_ai.usage.text.cache_read.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.audio.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.audio.output_tokens + requirement_level: opt_in + - ref: gen_ai.usage.audio.cache_read.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.image.input_tokens + requirement_level: opt_in + - ref: gen_ai.usage.image.cache_read.input_tokens + requirement_level: opt_in + - ref: gen_ai.conversation.id + requirement_level: recommended + - ref: gen_ai.agent.name + requirement_level: recommended + - ref: gen_ai.system_instructions + requirement_level: opt_in + - ref: gen_ai.input.messages + requirement_level: opt_in + - ref: gen_ai.output.messages + requirement_level: opt_in + - ref: gen_ai.output.type + requirement_level: recommended + - ref: gen_ai.tool.definitions + requirement_level: opt_in + - ref: error.type + requirement_level: {conditionally_required: On failure.} + - id: span.llm_request_run + type: span + span_kind: internal + stability: development + brief: The LLM request's stream being consumed. + - id: span.llm_fallback_adapter + type: span + span_kind: internal + stability: development + brief: The LLM fallback adapter choosing an instance. + attributes: + - ref: lk.fallback.label + requirement_level: recommended + - ref: lk.fallback.index + requirement_level: recommended + - ref: lk.fallback.recovery + requirement_level: recommended + - id: span.realtime_inference + type: span + span_kind: client + stability: development + brief: One realtime-model generation. + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: gen_ai.request.model + requirement_level: recommended + - ref: gen_ai.provider.name + requirement_level: recommended + - id: span.realtime_metrics + type: span + span_kind: internal + stability: development + brief: Realtime-model metrics attached to a turn. + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: lk.realtime_model_metrics + requirement_level: recommended + - id: span.function_tool + type: span + span_kind: internal + stability: development + brief: One function tool execution, the OTel GenAI execute-tool span. + attributes: + - ref: lk.function_tool.id + requirement_level: recommended + - ref: lk.function_tool.name + requirement_level: required + - ref: lk.pii.function_tool.arguments + requirement_level: opt_in + - ref: lk.function_tool.is_error + requirement_level: recommended + - ref: lk.pii.function_tool.output + requirement_level: opt_in + - ref: gen_ai.operation.name + requirement_level: recommended + - ref: gen_ai.tool.name + requirement_level: recommended + - ref: gen_ai.tool.call.id + requirement_level: recommended + - ref: gen_ai.tool.description + requirement_level: opt_in + - ref: gen_ai.tool.type + requirement_level: recommended + - ref: gen_ai.tool.call.arguments + requirement_level: opt_in + - ref: gen_ai.tool.call.result + requirement_level: opt_in + - ref: error.type + requirement_level: {conditionally_required: On failure.} + - id: span.tts_node + type: span + span_kind: internal + stability: development + brief: The agent's TTS node for one generation. + attributes: + - ref: lk.speech_id + requirement_level: recommended + - ref: lk.agent_label + requirement_level: recommended + - ref: lk.pii.input_text + requirement_level: opt_in + - ref: lk.tts.streaming + requirement_level: recommended + - ref: lk.tts.label + requirement_level: recommended + - ref: lk.response.ttfb + requirement_level: recommended + - id: span.tts_request + type: span + span_kind: client + stability: development + brief: One TTS request. + attributes: + - ref: lk.retry_count + requirement_level: recommended + - ref: lk.provider_request_ids + requirement_level: opt_in + - ref: lk.tts.label + requirement_level: recommended + - ref: gen_ai.request.model + requirement_level: opt_in + - ref: gen_ai.provider.name + requirement_level: opt_in + - ref: error.type + requirement_level: {conditionally_required: On failure.} + - id: span.tts_request_run + type: span + span_kind: internal + stability: development + brief: The TTS request's stream being consumed. + - id: span.tts_fallback_adapter + type: span + span_kind: internal + stability: development + brief: The TTS fallback adapter choosing an instance. + attributes: + - ref: lk.fallback.label + requirement_level: recommended + - ref: lk.fallback.index + requirement_level: recommended + - ref: lk.fallback.recovery + requirement_level: recommended + - id: span.tts_stream_adapter + type: span + span_kind: internal + stability: development + brief: The sentence-splitting adapter over a non-streaming TTS. + attributes: + - ref: lk.tts.label + requirement_level: recommended + - id: span.amd + type: span + span_kind: internal + stability: development + brief: Answering-machine detection on an outbound call. + attributes: + - ref: lk.amd.category + requirement_level: required + - ref: lk.amd.reason + requirement_level: recommended + - ref: lk.amd.speech_duration + requirement_level: recommended + - ref: lk.amd.delay + requirement_level: recommended + - ref: lk.pii.amd.transcript + requirement_level: opt_in + - id: span.judge_evaluation + type: span + span_kind: internal + stability: development + brief: One LLM-judge evaluation of the session. + attributes: + - ref: gen_ai.evaluation.name + requirement_level: recommended + - ref: gen_ai.evaluation.score.label + requirement_level: recommended + - ref: gen_ai.evaluation.explanation + requirement_level: opt_in + + # --- Rust Node binding ------------------------------------------------------------------- + - id: span.metrics + type: span + span_kind: internal + stability: development + brief: A plugin-task metric noted through the Node binding, stamped with its resolved frame. + attributes: + - ref: lk.metric + requirement_level: required + - ref: lk.frame.resolution + requirement_level: required + - ref: lk.speech_id + requirement_level: {conditionally_required: When the frame resolves to a speech.} + - id: span.tts_synthesize + type: span + span_kind: internal + stability: development + brief: The Node binding driving `Tts::synthesize` (the simplex path). + attributes: + - ref: lk.tts.label + requirement_level: required + - ref: lk.tts.streaming + requirement_level: required + - ref: lk.tts.frames + requirement_level: {conditionally_required: On success.} + - ref: lk.tts.bytes + requirement_level: {conditionally_required: On success.} + - ref: lk.error.retryable + requirement_level: {conditionally_required: On failure.} + - id: span.tts_stream + type: span + span_kind: internal + stability: development + brief: The Node binding driving `Tts::stream` (the duplex path). + attributes: + - ref: lk.tts.label + requirement_level: required + - ref: lk.tts.no_duplex_path + requirement_level: {conditionally_required: When the instance has no duplex path.} + - ref: lk.tts.frames + requirement_level: {conditionally_required: On success.} + - ref: lk.tts.bytes + requirement_level: {conditionally_required: On success.} + - ref: lk.error.retryable + requirement_level: {conditionally_required: On failure.} + + # --- Metrics (`otel_metrics.py`, `metrics/otel.rs`) -------------------------------------- + # Units are exactly what the producer ships: the nine counters Python creates without a unit + # are declared with `unit: ""` (the OTel SDK default), not with the `{token}` / `{character}` + # / `{request}` annotations they arguably deserve; changing that is a wire change. + - {id: metric.lk.agents.turn.e2e_latency, type: metric, metric_name: lk.agents.turn.e2e_latency, instrument: histogram, unit: s, stability: development, brief: "End-to-end latency of a turn.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.turn.llm_ttft, type: metric, metric_name: lk.agents.turn.llm_ttft, instrument: histogram, unit: s, stability: development, brief: "Time to the first LLM token.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.turn.tts_ttfb, type: metric, metric_name: lk.agents.turn.tts_ttfb, instrument: histogram, unit: s, stability: development, brief: "Time to the first TTS byte.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.turn.transcription_delay, type: metric, metric_name: lk.agents.turn.transcription_delay, instrument: histogram, unit: s, stability: development, brief: "Delay from the end of speech to the final transcript.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.turn.end_of_turn_delay, type: metric, metric_name: lk.agents.turn.end_of_turn_delay, instrument: histogram, unit: s, stability: development, brief: "Delay from the end of speech to the turn decision.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.turn.on_user_turn_completed_delay, type: metric, metric_name: lk.agents.turn.on_user_turn_completed_delay, instrument: histogram, unit: s, stability: development, brief: "Duration of the `on_user_turn_completed` hook.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_input_tokens, type: metric, metric_name: lk.agents.usage.llm_input_tokens, instrument: counter, unit: "", stability: development, brief: "LLM input tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_input_cached_tokens, type: metric, metric_name: lk.agents.usage.llm_input_cached_tokens, instrument: counter, unit: "", stability: development, brief: "LLM cached input tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_output_tokens, type: metric, metric_name: lk.agents.usage.llm_output_tokens, instrument: counter, unit: "", stability: development, brief: "LLM output tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_input_audio_tokens, type: metric, metric_name: lk.agents.usage.llm_input_audio_tokens, instrument: counter, unit: "", stability: development, brief: "Realtime-model audio input tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_input_text_tokens, type: metric, metric_name: lk.agents.usage.llm_input_text_tokens, instrument: counter, unit: "", stability: development, brief: "Realtime-model text input tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_output_audio_tokens, type: metric, metric_name: lk.agents.usage.llm_output_audio_tokens, instrument: counter, unit: "", stability: development, brief: "Realtime-model audio output tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_output_text_tokens, type: metric, metric_name: lk.agents.usage.llm_output_text_tokens, instrument: counter, unit: "", stability: development, brief: "Realtime-model text output tokens.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.llm_session_duration, type: metric, metric_name: lk.agents.usage.llm_session_duration, instrument: counter, unit: s, stability: development, brief: "Realtime-model session seconds.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.tts_characters, type: metric, metric_name: lk.agents.usage.tts_characters, instrument: counter, unit: "", stability: development, brief: "Characters synthesized.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.tts_audio_duration, type: metric, metric_name: lk.agents.usage.tts_audio_duration, instrument: counter, unit: s, stability: development, brief: "Seconds of audio synthesized.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.stt_audio_duration, type: metric, metric_name: lk.agents.usage.stt_audio_duration, instrument: counter, unit: s, stability: development, brief: "Seconds of audio transcribed.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.usage.interruption_num_requests, type: metric, metric_name: lk.agents.usage.interruption_num_requests, instrument: counter, unit: "", stability: development, brief: "Interruption-detector requests.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}]} + - {id: metric.lk.agents.connection.acquire_time, type: metric, metric_name: lk.agents.connection.acquire_time, instrument: histogram, unit: s, stability: development, brief: "Seconds to acquire a provider connection.", attributes: [{ref: model_name, requirement_level: recommended}, {ref: model_provider, requirement_level: recommended}, {ref: connection_reused, requirement_level: required}]} + - {id: metric.lk.agents.event_loop.blocked_duration, type: metric, metric_name: lk.agents.event_loop.blocked_duration, instrument: histogram, unit: s, stability: development, brief: "Duration of synchronous blocks detected on an agent event loop."} + + # --- GenAI span events and metrics no upstream registry defines today ------------------- + # The five message events and `gen_ai.client.token.usage` / `...time_to_first_chunk` are + # deprecated in OTel semconv 1.44.0 and excluded from dependency resolution; Agents still + # names them, so they are declared here as emitted. They are span events on `llm_node` and + # the PII filter drops them whole; weaver's only event type is the log event. + - id: event.gen_ai.system.message + type: event + name: gen_ai.system.message + stability: development + brief: OTel GenAI system (or developer) message, a span event on `llm_node`. + note: Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + annotations: + livekit: {severity: [], body: "Not a log record: a span event whose `content` attribute carries the message."} + - id: event.gen_ai.user.message + type: event + name: gen_ai.user.message + stability: development + brief: OTel GenAI user message, a span event on `llm_node`. + note: Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + annotations: + livekit: {severity: [], body: "Not a log record: a span event whose `content` attribute carries the message."} + - id: event.gen_ai.assistant.message + type: event + name: gen_ai.assistant.message + stability: development + brief: OTel GenAI assistant message (also a function call), a span event on `llm_node`. + note: Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + annotations: + livekit: {severity: [], body: "Not a log record: a span event whose `content` attribute carries the message."} + - id: event.gen_ai.tool.message + type: event + name: gen_ai.tool.message + stability: development + brief: OTel GenAI tool message (a function call output), a span event on `llm_node`. + note: Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + annotations: + livekit: {severity: [], body: "Not a log record: a span event whose `content` attribute carries the message."} + - id: event.gen_ai.choice + type: event + name: gen_ai.choice + stability: development + brief: OTel GenAI choice, a span event on `llm_node`. + note: Deprecated upstream (semconv 1.44.0 `gen-ai/deprecated/events-deprecated.yaml`); declared as emitted. + annotations: + livekit: {severity: [], body: "Not a log record: a span event whose `content` attribute carries the message."} + # gen_ai.client.operation.duration is current in the GenAI registry, but weaver 0.28.0 refuses to + # import it: semconv v1.44.0 keeps a deprecated copy marked `dependency_resolution: exclude`, and + # an import that matches an excluded metric is an error. Declared here as emitted, with the GenAI + # registry's instrument and unit, until weaver can import it (groups.yaml imports the other two). + - {id: metric.gen_ai.client.operation.duration, type: metric, metric_name: gen_ai.client.operation.duration, instrument: histogram, unit: s, stability: development, brief: "GenAI operation duration, as the GenAI registry defines it (declared here because weaver cannot import it).", attributes: [{ref: gen_ai.operation.name, requirement_level: required}, {ref: gen_ai.provider.name, requirement_level: recommended}, {ref: gen_ai.request.model, requirement_level: recommended}]} + - {id: metric.gen_ai.client.token.usage, type: metric, metric_name: gen_ai.client.token.usage, instrument: histogram, unit: "{token}", stability: development, brief: "Input and output tokens used by the GenAI operation (deprecated upstream; declared as emitted).", attributes: [{ref: gen_ai.token.type, requirement_level: required}, {ref: gen_ai.operation.name, requirement_level: required}, {ref: gen_ai.provider.name, requirement_level: recommended}, {ref: gen_ai.request.model, requirement_level: recommended}]} + - {id: metric.gen_ai.client.operation.time_to_first_chunk, type: metric, metric_name: gen_ai.client.operation.time_to_first_chunk, instrument: histogram, unit: s, stability: development, brief: "Time to the first chunk of a streaming GenAI response (deprecated upstream; declared as emitted).", attributes: [{ref: gen_ai.operation.name, requirement_level: required}, {ref: gen_ai.provider.name, requirement_level: recommended}, {ref: gen_ai.request.model, requirement_level: recommended}]} diff --git a/otel/model/client/groups.yaml b/otel/model/client/groups.yaml new file mode 100644 index 000000000..2363bc89e --- /dev/null +++ b/otel/model/client/groups.yaml @@ -0,0 +1,110 @@ +# Attribute sets the client cores put on every resource / record. They exist so live-check +# matchers (otel/live-check/weaver.toml) can compare those samples with something, which needs +# public v2 attribute groups, hence this file's format. Reference attributes only here: define +# keys in the v1 files, where otel/policies sees them. +file_format: definition/2 +attribute_groups: + - id: livekit.client.resource + visibility: public + stability: development + brief: Resource attributes of the client pipeline, one Resource per process. + note: > + Set once per pipeline. Session-specific keys (`lk.room_sid`, `lk.participant_id`, + `lk.track_sid`) never go here; they stay on records and spans. + attributes: + - ref: service.name + requirement_level: required + note: "`livekit-client-`: swift, android, flutter, react-native, unity, rust, js; `livekit-client` when the platform names none. Set by the platform SDK." + examples: ["livekit-client-swift", "livekit-client-js"] + - ref: service.version + requirement_level: required + note: The LiveKit client SDK's version. Set by the platform SDK. + examples: ["2.17.0"] + - ref: os.name + requirement_level: recommended + note: Set by the platform SDK; the JS port only when a host SDK supplies it. + examples: ["iOS", "macOS", "Android"] + - ref: os.version + requirement_level: recommended + note: Set by the platform SDK; the JS port only when a host SDK supplies it. + examples: ["18.5"] + - ref: device.model.identifier + requirement_level: recommended + note: Set by the platform SDK where it knows the model; the JS port only when a host SDK supplies it. + examples: ["iPhone16,1", "Mac16,8"] + - ref: telemetry.sdk.name + requirement_level: required + note: The telemetry library, `livekit-telemetry`, in every client. + examples: ["livekit-telemetry"] + - ref: telemetry.sdk.language + requirement_level: required + note: "`rust` from the Rust core (every native SDK), `webjs` from the JS port." + examples: ["rust", "webjs"] + - ref: telemetry.sdk.version + requirement_level: required + note: The telemetry library's version (the JS port sends `0.1.0`). + examples: ["0.1.0"] + - ref: lk.sdk.name + requirement_level: recommended + - ref: lk.sdk.version + requirement_level: recommended + - ref: browser.name + requirement_level: {conditionally_required: The JS SDK running in a browser.} + - ref: browser.version + requirement_level: {conditionally_required: The JS SDK running in a browser.} + + - id: livekit.client.record + visibility: public + stability: development + brief: Attributes on every log record and span of a session. + note: > + One session is one Room connection lifetime, across reconnects, and one trace. A record's + attributes and timestamp are taken when it is captured, the session's included, so a record + captured before a disconnect keeps its room. Records outside a room (device state, pre-room + errors, self-telemetry) belong to the process's own session and carry only `session.id`. + Keys the SDK owns (`lk.*`, `otel.*`, `code.*`, `session.id`, `error.type`) must never be set + by an app; an app's correlation attributes (e.g. `app.call_id`, `enduser.id`) ride along, + within the `custom` event's limits. + attributes: + - ref: session.id + requirement_level: required + note: The session's trace id (32 hex characters), also on the process's own records. + examples: ["4bf92f3577b34da6a3ce929d0e0e4736"] + - ref: otel.event.name + requirement_level: {conditionally_required: On event records; duplicates the record's `event_name`.} + note: For backends that do not surface the OTLP `event_name` field yet. + examples: ["lk.rtc.stats.sample"] + - ref: lk.room_sid + requirement_level: {conditionally_required: Once the client has joined the room.} + - ref: lk.pii.room_name + requirement_level: {conditionally_required: Once the client has joined the room.} + - ref: lk.participant_id + requirement_level: {conditionally_required: Once the client has joined the room.} + - ref: lk.pii.participant_identity + requirement_level: {conditionally_required: Once the client has joined the room.} + + - id: livekit.client.log + visibility: public + stability: development + brief: Attributes of a plain log record (no event name). + note: > + The body is the log message. Only WARN and ERROR records are sent; WebRTC lines only at + ERROR, and the telemetry module's own lines never. In records copied from the Rust core + (`lk.log.source = ffi`), JWT-shaped substrings must be masked as ``. + attributes: + - ref: lk.log.source + requirement_level: required + - ref: lk.log.logger + requirement_level: required + - ref: code.function.name + requirement_level: recommended + note: The function that logged the line, as the platform names it. + examples: ["connect(url:token:)"] + - ref: code.file.path + requirement_level: recommended + note: The source file that logged the line. + examples: ["Sources/LiveKit/Core/Room.swift"] + - ref: code.line.number + requirement_level: recommended + note: The line in `code.file.path`. + examples: [42] diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml new file mode 100644 index 000000000..7f6c0af0f --- /dev/null +++ b/otel/model/client/registry.yaml @@ -0,0 +1,760 @@ +# Client-owned attributes, under the names the client cores emit today (rust-sdks +# livekit-telemetry SPEC.md, and the JS port in client-sdk-js). The Client team approves changes +# here alone, unless one touches a shared concept (otel/model/shared). The RTC stats durations, +# RTT and jitter are milliseconds, converted from the W3C stats' seconds, through an explicit +# `unit_exception`. +groups: + - id: registry.lk.device + type: attribute_group + display_name: LiveKit client device state + brief: Device conditions the platform observes, reported on change. + note: > + The platform SDK reads the OS; its changes become `lk.device.*` events. A platform + without a source for a reading sends no event for it rather than a guess. + attributes: + - id: lk.device.thermal.state + stability: development + brief: Thermal pressure as the OS reports it. + note: > + Apple `ProcessInfo.thermalState`, Android `PowerManager` thermal status, web Compute + Pressure API, all mapped to this vocabulary. The platform's `unknown` (no thermal source) + is no reading and is never emitted. iOS, macOS and Android; optional elsewhere. + type: + members: + - {id: nominal, value: nominal, brief: Within normal limits., stability: development} + - {id: fair, value: fair, brief: Slightly elevated., stability: development} + - {id: serious, value: serious, brief: High; the system reduces performance., stability: development} + - {id: critical, value: critical, brief: Critical; the device needs to cool down., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.low_power.enabled + type: boolean + stability: development + brief: Low Power Mode / Battery Saver is on. + note: Absent where the platform has no source. iOS, macOS and Android; optional elsewhere. + examples: [true] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.app_state + stability: development + brief: Whether the app is in front of the user. + note: All platforms; the web maps page visibility. + type: + members: + - {id: foreground, value: foreground, brief: In front of the user., stability: development} + - {id: background, value: background, brief: In the background., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.memory.pressure + stability: development + brief: Memory pressure as the OS reports it. + note: > + Apple `DispatchSource` memory-pressure levels; Android `onTrimMemory`: RUNNING_LOW and + BACKGROUND map to `warning`, RUNNING_CRITICAL and COMPLETE to `critical`. iOS, macOS and + Android; optional elsewhere. + type: + members: + - {id: normal, value: normal, brief: No pressure., stability: development} + - {id: warning, value: warning, brief: Low memory., stability: development} + - {id: critical, value: critical, brief: Critically low memory., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.network.expensive + type: boolean + stability: development + brief: The network is metered (cellular, personal hotspot). + note: Apple `NWPath.isExpensive`, Android metered networks; on the web only where the Network Information API exists. + examples: [false] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.network.constrained + type: boolean + stability: development + brief: The user asked for less traffic (Low Data Mode, Data Saver). + note: Apple `NWPath.isConstrained`, Android Data Saver; on the web `navigator.connection.saveData`, only where the Network Information API exists. + examples: [false] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.battery.level + type: int + stability: development + brief: Battery charge in percent, 0-100. + note: > + Only where the level is known (iOS, Android; not desktops or tvOS); never from the JS + port. An integer percentage, not a 0-1 ratio. + examples: [42] + annotations: + livekit: {owner: client, sensitivity: none, unit: "%"} + - id: lk.device.battery.charging + type: boolean + stability: development + brief: The device is charging. + note: Sent together with `lk.device.battery.level`. + examples: [false] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.audio_route.reason + stability: development + brief: Why the audio route changed. + note: AVAudioSession route change reasons on iOS; Android maps its audio device callbacks; `unknown` where the platform gives none. + type: + members: + - {id: new_device, value: new_device, brief: A new output became available., stability: development} + - {id: old_device_unavailable, value: old_device_unavailable, brief: The previous output went away., stability: development} + - {id: category_change, value: category_change, brief: The audio session category changed., stability: development} + - {id: override, value: override, brief: The route was overridden., stability: development} + - {id: wake_from_sleep, value: wake_from_sleep, brief: The device woke from sleep., stability: development} + - {id: no_suitable_route, value: no_suitable_route, brief: No route fits the category., stability: development} + - {id: route_configuration_change, value: route_configuration_change, brief: The route's configuration changed., stability: development} + - {id: unknown, value: unknown, brief: The platform gave no reason., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.audio_route.outputs + type: string + stability: development + brief: The active outputs, comma-separated. + note: > + Each item is one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, + hdmi, usb, other. A string, not an array: the vocabulary is closed but the list is joined. + examples: ["bluetooth", "speaker,usb"] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.audio.interruption + stability: development + brief: Phase of an audio session interruption. + note: iOS audio session interruptions; Android audio focus loss and gain. + type: + members: + - {id: began, value: began, brief: Interruption began (another app or a call took the audio session)., stability: development} + - {id: ended, value: ended, brief: Interruption ended., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.capture.device + stability: development + brief: Capture device that failed. + note: All platforms. + type: + members: + - {id: camera, value: camera, brief: Camera., stability: development} + - {id: microphone, value: microphone, brief: Microphone., stability: development} + - {id: screen_share, value: screen_share, brief: Screen capture., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.capture.reason + stability: development + brief: Why capture failed, in the getUserMedia failure taxonomy. + note: > + iOS: authorization status and capture interruptions; Android: permission checks and camera + callbacks; web: DOMException names. + type: + members: + - {id: permission_denied, value: permission_denied, brief: The user or OS denied access., stability: development} + - {id: not_found, value: not_found, brief: No such device., stability: development} + - {id: in_use, value: in_use, brief: Another app holds the device., stability: development} + - {id: disconnected, value: disconnected, brief: The device went away., stability: development} + - {id: other, value: other, brief: Anything else., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + + - id: registry.lk.track + type: attribute_group + display_name: LiveKit client track details + brief: Track details on publish/subscribe spans and RTC stats. The track SID and source are shared (`lk.track_sid`, `lk.track_source`). + attributes: + - id: lk.track.kind + stability: development + brief: Media kind of the track. + note: On publish and subscribe spans and RTC stats windows. + type: + members: + - {id: audio, value: audio, brief: Audio., stability: development} + - {id: video, value: video, brief: Video., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.track.direction + stability: development + brief: Whether the stats describe sent or received media. + note: On RTC stats windows only. + type: + members: + - {id: inbound, value: inbound, brief: Received media (inbound-rtp)., stability: development} + - {id: outbound, value: outbound, brief: Sent media (outbound-rtp)., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + + - id: registry.lk.rtc + type: attribute_group + display_name: LiveKit client RTC stats + brief: > + One `lk.rtc.stats.sample` window per track and direction. Counters are cumulative (the last + W3C webrtc-stats reading, monotonic); gauges are min/max/avg over the window's readings that + report them. + note: > + Simulcast layers of one track are folded into one monotonic series before windowing. The + W3C stats report durations, RTT and jitter in seconds; these keys carry them as integer + milliseconds (durations) and double milliseconds (RTT, jitter). A field the platform or direction lacks is omitted, never sent as 0. + attributes: + - id: lk.rtc.codec + type: string + stability: development + brief: Codec mimeType, when known. + note: The `codec` stats entry the RTP stream's `codecId` names. + examples: ["audio/opus", "video/VP8"] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.rtc.window_ms + type: int + stability: development + brief: Actual length of the stats window. + note: From the window's start to its close, on a monotonic clock. + examples: [60000] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.samples + type: int + stability: development + brief: Stats readings folded into the window. + note: A gauge's `.avg` divides by the readings that reported that gauge, which can be fewer. + examples: [2] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{reading}"} + - id: lk.rtc.bytes + type: int + stability: development + brief: Bytes sent or received (cumulative). + note: Outbound `bytesSent`, inbound `bytesReceived`. + examples: [1048576] + annotations: + livekit: {owner: client, sensitivity: none, unit: By} + - id: lk.rtc.packets + type: int + stability: development + brief: Packets sent or received (cumulative). + note: Outbound `packetsSent`, inbound `packetsReceived`. + examples: [3000] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{packet}"} + - id: lk.rtc.packets_lost + type: int + stability: development + brief: Packets lost, inbound (cumulative). + note: Inbound `packetsLost`. Loss ratio = packets_lost / (packets + packets_lost). + examples: [3] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{packet}"} + - id: lk.rtc.freeze_count + type: int + stability: development + brief: Video freezes, inbound video (cumulative). + note: Inbound `freezeCount`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{freeze}"} + - id: lk.rtc.freezes_duration_ms + type: int + stability: development + brief: Total freeze time, inbound video (cumulative). + note: Inbound `totalFreezesDuration`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.pause_count + type: int + stability: development + brief: Video pauses, inbound video (cumulative). + note: Inbound `pauseCount`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{pause}"} + - id: lk.rtc.pauses_duration_ms + type: int + stability: development + brief: Total pause time, inbound video (cumulative). + note: Inbound `totalPausesDuration`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.concealed_samples + type: int + stability: development + brief: Concealed audio samples, inbound audio (cumulative). + note: Inbound `concealedSamples`. + examples: [480] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{sample}"} + - id: lk.rtc.silent_concealed_samples + type: int + stability: development + brief: Concealed samples that were silent, inbound audio (cumulative). + note: Inbound `silentConcealedSamples`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{sample}"} + - id: lk.rtc.concealment_events + type: int + stability: development + brief: Concealment events, inbound audio (cumulative). + note: Inbound `concealmentEvents`. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{event}"} + - id: lk.rtc.interruption_count + type: int + stability: development + brief: Audio interruptions, inbound audio (cumulative). + note: Inbound `interruptionCount`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{interruption}"} + - id: lk.rtc.interruptions_duration_ms + type: int + stability: development + brief: Total interruption time, inbound audio (cumulative). + note: Inbound `totalInterruptionDuration`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.jitter_buffer_delay_ms + type: int + stability: development + brief: Jitter buffer delay, inbound (cumulative). + note: Inbound `jitterBufferDelay`. Average delay = jitter_buffer_delay_ms / jitter_buffer_emitted_count. + examples: [1200] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.jitter_buffer_emitted_count + type: int + stability: development + brief: Samples or frames emitted by the jitter buffer, inbound (cumulative). + note: Inbound `jitterBufferEmittedCount`. + examples: [48000] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{sample}"} + - id: lk.rtc.quality_limitation.bandwidth_ms + type: int + stability: development + brief: Time the encoder was limited by bandwidth, outbound video (cumulative). + note: Outbound `qualityLimitationDurations.bandwidth`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.quality_limitation.cpu_ms + type: int + stability: development + brief: Time the encoder was limited by CPU, outbound video (cumulative). + note: Outbound `qualityLimitationDurations.cpu`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.quality_limitation.other_ms + type: int + stability: development + brief: Time the encoder was limited for other reasons, outbound video (cumulative). + note: Outbound `qualityLimitationDurations.other`. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.jitter_ms.min + type: double + stability: development + brief: Minimum jitter in the window, inbound. + note: Inbound `jitter` (seconds × 1000). + examples: [0.5] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.jitter_ms.max + type: double + stability: development + brief: Maximum jitter in the window, inbound. + note: Inbound `jitter` (seconds × 1000). + examples: [4.2] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.jitter_ms.avg + type: double + stability: development + brief: Average jitter in the window, inbound. + note: Inbound `jitter` (seconds × 1000), averaged over the window's readings that report it. + examples: [1.8] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.rtt_ms.min + type: double + stability: development + brief: Minimum round-trip time in the window. + note: > + Outbound: the `remote-inbound-rtp` entry's `roundTripTime`; inbound: the nominated + candidate pair's `currentRoundTripTime` (seconds × 1000). + examples: [12.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.rtt_ms.max + type: double + stability: development + brief: Maximum round-trip time in the window. + note: Same sources as `lk.rtc.rtt_ms.min`. + examples: [80.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.rtt_ms.avg + type: double + stability: development + brief: Average round-trip time in the window. + note: Same sources as `lk.rtc.rtt_ms.min`, averaged over the window's readings that report it. + examples: [25.5] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} + - id: lk.rtc.fps.min + type: double + stability: development + brief: Minimum frame rate in the window, video. + note: "`framesPerSecond`, either direction." + examples: [24.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{frame}/s"} + - id: lk.rtc.fps.max + type: double + stability: development + brief: Maximum frame rate in the window, video. + note: "`framesPerSecond`, either direction." + examples: [30.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{frame}/s"} + - id: lk.rtc.fps.avg + type: double + stability: development + brief: Average frame rate in the window, video. + note: "`framesPerSecond`, either direction, averaged over the window's readings that report it." + examples: [29.7] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{frame}/s"} + - id: lk.rtc.audio_level.min + type: double + stability: development + brief: Minimum audio level in the window, 0-1, inbound audio. + note: Inbound `audioLevel`. + examples: [0.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "1"} + - id: lk.rtc.audio_level.max + type: double + stability: development + brief: Maximum audio level in the window, 0-1, inbound audio. + note: Inbound `audioLevel`. + examples: [0.3] + annotations: + livekit: {owner: client, sensitivity: none, unit: "1"} + - id: lk.rtc.audio_level.avg + type: double + stability: development + brief: Average audio level in the window, 0-1, inbound audio. + note: Inbound `audioLevel`, averaged over the window's readings that report it. + examples: [0.05] + annotations: + livekit: {owner: client, sensitivity: none, unit: "1"} + + - id: registry.lk.span + type: attribute_group + display_name: LiveKit client span details + brief: Attributes on the LiveKit client spans. + attributes: + - id: lk.outcome + stability: development + brief: How the attempt ended. Rollups read this, never the span status. + note: > + Exactly once on every LiveKit span, set by the SDK from how the span ended; a producer + must drop an attribute of this name set any other way. Status follows it: `Error` for + `error`, `Unset` for `ok` and `cancelled`. + type: + members: + - {id: ok, value: ok, brief: Succeeded; span status Unset., stability: development} + - {id: error, value: error, brief: "Failed; span status Error, with `error.type` when the failure has a type.", stability: development} + - {id: cancelled, value: cancelled, brief: Cancelled by the caller or superseded; span status Unset., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.connect.attempt + type: int + stability: development + brief: 1-based connect attempt. + note: 1 for the user-initiated connect; a platform that retries sets the next number on the next `lk.connect` span. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.reason + stability: development + brief: What triggered the reconnect cycle. + note: The protocol's `ReconnectReason` (`RR_*`), plus reasons only a client knows. + type: + members: + - {id: signal_disconnected, value: signal_disconnected, brief: The signaling socket closed (RR_SIGNAL_DISCONNECTED)., stability: development} + - {id: publisher_failed, value: publisher_failed, brief: The publisher peer connection failed (RR_PUBLISHER_FAILED)., stability: development} + - {id: subscriber_failed, value: subscriber_failed, brief: The subscriber peer connection failed (RR_SUBSCRIBER_FAILED)., stability: development} + - {id: transport_failed, value: transport_failed, brief: A peer connection failed and the platform did not say which (client only)., stability: development} + - {id: switch_candidate, value: switch_candidate, brief: The server asked for a candidate switch (RR_SWITCH_CANDIDATE)., stability: development} + - {id: network_changed, value: network_changed, brief: The device moved to another network (client only)., stability: development} + - {id: debug, value: debug, brief: A test or a debug menu asked for it (client only)., stability: development} + - {id: unknown, value: unknown, brief: RR_UNKNOWN or a number the client does not know., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.mode + stability: development + brief: Mode of the last reconnect attempt. + note: Updated with each `attempt ` checkpoint; matches the last one. + type: + members: + - {id: quick, value: quick, brief: Resume the session (ICE restart)., stability: development} + - {id: full, value: full, brief: Reconnect from scratch., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.attempts + type: int + stability: development + brief: Reconnect attempts made in the cycle. + note: Equals `` of the last `attempt ` checkpoint. + examples: [2] + annotations: + livekit: {owner: client, sensitivity: none} + + - id: registry.lk.log + type: attribute_group + display_name: LiveKit client log records + brief: Attributes on plain log records (no event name). + attributes: + - id: lk.log.source + stability: development + brief: Which layer wrote the log line. + note: Only WARN and ERROR records are sent; WebRTC lines only at ERROR. + type: + members: + - {id: sdk, value: sdk, brief: The platform SDK., stability: development} + - {id: ffi, value: ffi, brief: "The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``.", stability: development} + - {id: webrtc, value: webrtc, brief: WebRTC., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.log.logger + type: string + stability: development + brief: Logger, type, module or file that wrote the line. + note: Free text from the platform's logger. + examples: ["Room", "livekit::rtc_engine"] + annotations: + livekit: {owner: client, sensitivity: none} + + - id: registry.lk.telemetry + type: attribute_group + display_name: LiveKit client self-telemetry + brief: Counters on `lk.telemetry.report`, counted since the previous report. + note: > + Losses by policy (a project that receives nothing, the opt-out) are never reported; they + stay local. Sent by the Rust core and the JS port unless a key says otherwise. + attributes: + - id: lk.ping.seq + type: int + stability: development + brief: Monotonically increasing sequence number per pipeline. + note: Optional; on `lk.ping` only. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.telemetry.uploads.sent + type: int + stability: development + brief: Batches the collector accepted. + note: Always present on a report, even when 0. + examples: [4] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{batch}"} + - id: lk.telemetry.uploads.bytes + type: int + stability: development + brief: Compressed bytes the collector accepted. + note: What telemetry cost the uplink. Always present on a report, even when 0. + examples: [20480] + annotations: + livekit: {owner: client, sensitivity: none, unit: By} + - id: lk.telemetry.uploads.failed + type: int + stability: development + brief: Upload attempts that failed transiently (no answer, 429, 5xx). + note: Always present on a report, even when 0. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{attempt}"} + - id: lk.telemetry.uploads.timeouts + type: int + stability: development + brief: Upload attempts that hit the export timeout. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{attempt}"} + - id: lk.telemetry.uploads.unauthorized + type: int + stability: development + brief: 401/403 answers (tokens the collector refused). + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{attempt}"} + - id: lk.telemetry.holds.capped + type: int + stability: development + brief: Soft upload holds that reached their cap. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{hold}"} + - id: lk.telemetry.cache.batches + type: int + stability: development + brief: Batches waiting in the cache now. + note: A gauge, not a count since the previous report. Always present on a report. + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{batch}"} + - id: lk.telemetry.cache.write_errors + type: int + stability: development + brief: Batches the disk refused, kept in memory instead. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{batch}"} + - id: lk.telemetry.dropped.queue_full + type: int + stability: development + brief: Records evicted from the in-memory queue. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.cache_error + type: int + stability: development + brief: Records no cache, not even memory, could take. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.cache_full + type: int + stability: development + brief: Records evicted by the cache's size or file-count bound. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.expired + type: int + stability: development + brief: Records in batches past the cache's age limit. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.corrupt + type: int + stability: development + brief: Records in cached batches that failed their CRC. + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.invalid + type: int + stability: development + brief: Custom events, attributes or SDK strings over the limits, rejected. + note: Only when non-zero. Over-limit values are rejected, never truncated (a truncated id collides). + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.rejected + type: int + stability: development + brief: Records the collector rejected (final 4xx/5xx, OTLP partial success). + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.oversized + type: int + stability: development + brief: Single records larger than the collector accepts (413). + note: Only when non-zero. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.throttled + type: int + stability: development + brief: Records evicted from the cache during a server-directed pause. + note: Only when non-zero. Rust core only; the JS port never sends it. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.rate_limited + type: int + stability: development + brief: Discrete events dropped by the flood guard. + note: Only when non-zero. RTC stats windows and reports are exempt from the guard. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.cache.list_errors + type: int + stability: development + brief: Times the cache could not be listed, so nothing was reconciled or sent. + note: Only when non-zero. JS port only. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{attempt}"} + - id: lk.telemetry.cache.read_errors + type: int + stability: development + brief: Cached batches that could not be read right now, kept for the next pass. + note: Only when non-zero. JS port only. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{batch}"} + - id: lk.telemetry.cache.delete_errors + type: int + stability: development + brief: Sent batches the cache could not delete; never sent twice from the same launch. + note: Only when non-zero. JS port only. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{batch}"} + - id: lk.telemetry.dropped.unattributed + type: int + stability: development + brief: Log lines from a Room-shaped logging context with no telemetry scope behind it, dropped. + note: Only when non-zero. JS port only. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + + - id: registry.livekit.browser + type: attribute_group + display_name: LiveKit client browser + brief: The browser the JS SDK runs in. Resource attributes, JS port only. + note: > + Set only when the JS SDK runs in a browser, not when a host SDK (React Native, a wrapper) + supplies its own identity. LiveKit-defined keys: OTel 1.44.0 defines no `browser.name` or + `browser.version`. + attributes: + - id: browser.name + type: string + stability: development + brief: Browser name. + note: Resource attribute, JS port only. + examples: ["Chrome", "Safari", "Firefox"] + annotations: + livekit: {owner: client, sensitivity: none} + - id: browser.version + type: string + stability: development + brief: Browser version. + note: Resource attribute, JS port only. + examples: ["124.0"] + annotations: + livekit: {owner: client, sensitivity: none} diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml new file mode 100644 index 000000000..b4ea45da9 --- /dev/null +++ b/otel/model/client/signals.yaml @@ -0,0 +1,453 @@ +# Client signals, under the names the client cores emit today (rust-sdks livekit-telemetry +# SPEC.md, and the JS port in client-sdk-js). Events are OTLP log records with `event_name`; spans +# are one attempt each. +# +# Event annotations (`annotations.livekit`): +# severity: OTLP severity_text values the event is sent with +# body: what the log body carries (an event without a body of its own sends its name) +# Span annotations: +# checkpoints: span events, in order: {name, requirement: required|best_effort, brief} +# checkpoint_pattern: a dynamic checkpoint name, as {name, pattern (regex), brief} +# limits: caps per span; what goes past them is counted in the OTLP +# dropped_events_count / dropped_attributes_count fields +groups: + # --- Log events ------------------------------------------------------------------------- + - id: event.lk.ping + type: event + name: lk.ping + stability: development + brief: Pipeline smoke test. + note: Never emitted in production paths. Not sent by the JS port. + annotations: + livekit: {severity: [INFO], body: The event name.} + attributes: + - ref: lk.ping.seq + requirement_level: recommended + + - id: event.lk.telemetry.report + type: event + name: lk.telemetry.report + stability: development + brief: Self-telemetry; counts since the previous report. + note: > + Sent after a loss, failure, refusal, capped hold or disk error, and once at shutdown as the + session summary, so fleet-wide success rates have denominators. A record of the process, not + of a room session. Losses by policy (a project that receives nothing, the opt-out) are never + reported. + annotations: + livekit: + severity: [INFO] + body: > + Rust core: a one-line summary, e.g. `telemetry: 4 batches sent (20480 B), 0 failed, + 0 dropped, 0 cached`. JS port: the event name. + attributes: + - ref: lk.telemetry.uploads.sent + requirement_level: required + - ref: lk.telemetry.uploads.bytes + requirement_level: required + - ref: lk.telemetry.uploads.failed + requirement_level: required + - ref: lk.telemetry.cache.batches + requirement_level: required + - ref: lk.telemetry.uploads.timeouts + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.uploads.unauthorized + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.holds.capped + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.cache.write_errors + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.queue_full + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.cache_error + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.cache_full + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.expired + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.corrupt + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.invalid + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.rejected + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.oversized + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.dropped.throttled + requirement_level: {conditionally_required: When non-zero (Rust core only).} + - ref: lk.telemetry.dropped.rate_limited + requirement_level: {conditionally_required: When non-zero.} + - ref: lk.telemetry.cache.list_errors + requirement_level: {conditionally_required: When non-zero (JS port only).} + - ref: lk.telemetry.cache.read_errors + requirement_level: {conditionally_required: When non-zero (JS port only).} + - ref: lk.telemetry.cache.delete_errors + requirement_level: {conditionally_required: When non-zero (JS port only).} + - ref: lk.telemetry.dropped.unattributed + requirement_level: {conditionally_required: When non-zero (JS port only).} + + - id: event.lk.device.thermal.changed + type: event + name: lk.device.thermal.changed + stability: development + brief: Thermal state changed. + note: > + Sent once with the initial reading, then on each change. A platform without a thermal + source sends none. A device event: a record of the process, not of a room session. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `thermal: serious` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.device.thermal.state + requirement_level: required + + - id: event.lk.device.low_power.changed + type: event + name: lk.device.low_power.changed + stability: development + brief: Low Power Mode changed. + note: Sent once with the initial reading, then on each change; never where the platform has no source. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `low power mode: on` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.device.low_power.enabled + requirement_level: required + + - id: event.lk.device.app_state.changed + type: event + name: lk.device.app_state.changed + stability: development + brief: The app moved to the foreground or background. + note: Sent once with the initial state, then on each change. All platforms. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `app: background` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.device.app_state + requirement_level: required + + - id: event.lk.device.memory.changed + type: event + name: lk.device.memory.changed + stability: development + brief: Memory pressure changed. + note: Sent once with the initial reading, then on each change. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `memory pressure: warning` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.device.memory.pressure + requirement_level: required + + - id: event.lk.device.network.changed + type: event + name: lk.device.network.changed + stability: development + brief: The network path changed. + note: > + Sent once with the initial state, then when any of its attributes changes. All platforms. A + platform that cannot read the connection's details (a browser without the Network + Information API) sends only `network.connection.type`, as `unknown` or `unavailable`. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `network: cell, expensive` (Rust core); the event name (JS port)."} + attributes: + - ref: network.connection.type + requirement_level: required + note: > + Clients also send `vpn`, `bluetooth` and `other`, which are not OTel members; `unknown` + when the platform cannot tell, `unavailable` when offline. + examples: ["wifi", "cell"] + - ref: lk.device.network.expensive + requirement_level: {conditionally_required: When the platform reports it.} + - ref: lk.device.network.constrained + requirement_level: {conditionally_required: When the platform reports it.} + + - id: event.lk.device.battery.changed + type: event + name: lk.device.battery.changed + stability: development + brief: Charging changed, or the level crossed 20 % or 10 % unplugged. + note: Never per percent; silent where the level is unknown. iOS and Android; not sent by the JS port. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `battery: 18%` or `battery: 64%, charging`."} + attributes: + - ref: lk.device.battery.level + requirement_level: required + - ref: lk.device.battery.charging + requirement_level: required + + - id: event.lk.device.audio_route.changed + type: event + name: lk.device.audio_route.changed + stability: development + brief: The audio route changed. + note: iOS; Android from its audio device callbacks; optional elsewhere. Not sent by the JS port. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `audio route: speaker (old_device_unavailable)`."} + attributes: + - ref: lk.device.audio_route.outputs + requirement_level: required + - ref: lk.device.audio_route.reason + requirement_level: required + + - id: event.lk.device.audio.interruption + type: event + name: lk.device.audio.interruption + stability: development + brief: An audio session interruption began or ended. + note: iOS; Android from audio focus loss and gain. Not sent by the JS port. + annotations: + livekit: {severity: [INFO], body: "Display line, e.g. `audio interruption began`."} + attributes: + - ref: lk.device.audio.interruption + requirement_level: required + + - id: event.lk.device.capture.failed + type: event + name: lk.device.capture.failed + stability: development + brief: A capture device failed. + note: All platforms. + annotations: + livekit: {severity: [WARN], body: "Display line, e.g. `capture failed: camera (permission_denied)` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.device.capture.device + requirement_level: required + - ref: lk.device.capture.reason + requirement_level: required + + - id: event.lk.rtc.stats.sample + type: event + name: lk.rtc.stats.sample + stability: development + brief: One RTC stats window for one track and direction. + note: > + One per track and direction per window, timestamped at the window's end, also closed early + when the track ends, at disconnect and at shutdown. Keyed by session and track: two rooms + receiving the same track send two windows. Only the track, kind, direction, window and sample + count are always present; counters and gauges a platform or direction lacks are omitted. + annotations: + livekit: + severity: [INFO] + body: "Display summary, e.g. `video inbound: 33 kbps, loss 0.0%, rtt 0 ms, 10 fps` (Rust core); the event name (JS port)." + attributes: + - ref: lk.track_sid + requirement_level: required + - ref: lk.track.kind + requirement_level: required + - ref: lk.track.direction + requirement_level: required + - ref: lk.rtc.codec + requirement_level: {conditionally_required: When known.} + - ref: lk.rtc.window_ms + requirement_level: required + - ref: lk.rtc.samples + requirement_level: required + - ref: lk.rtc.bytes + requirement_level: {conditionally_required: When the stats report it.} + - ref: lk.rtc.packets + requirement_level: {conditionally_required: When the stats report it.} + - ref: lk.rtc.packets_lost + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.freeze_count + requirement_level: {conditionally_required: Inbound video.} + - ref: lk.rtc.freezes_duration_ms + requirement_level: {conditionally_required: Inbound video.} + - ref: lk.rtc.pause_count + requirement_level: {conditionally_required: Inbound video.} + - ref: lk.rtc.pauses_duration_ms + requirement_level: {conditionally_required: Inbound video.} + - ref: lk.rtc.concealed_samples + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.silent_concealed_samples + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.concealment_events + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.interruption_count + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.interruptions_duration_ms + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.jitter_buffer_delay_ms + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.jitter_buffer_emitted_count + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.quality_limitation.bandwidth_ms + requirement_level: {conditionally_required: Outbound video.} + - ref: lk.rtc.quality_limitation.cpu_ms + requirement_level: {conditionally_required: Outbound video.} + - ref: lk.rtc.quality_limitation.other_ms + requirement_level: {conditionally_required: Outbound video.} + - ref: lk.rtc.jitter_ms.min + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.jitter_ms.max + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.jitter_ms.avg + requirement_level: {conditionally_required: Inbound.} + - ref: lk.rtc.rtt_ms.min + requirement_level: {conditionally_required: When the window has readings.} + - ref: lk.rtc.rtt_ms.max + requirement_level: {conditionally_required: When the window has readings.} + - ref: lk.rtc.rtt_ms.avg + requirement_level: {conditionally_required: When the window has readings.} + - ref: lk.rtc.fps.min + requirement_level: {conditionally_required: Video.} + - ref: lk.rtc.fps.max + requirement_level: {conditionally_required: Video.} + - ref: lk.rtc.fps.avg + requirement_level: {conditionally_required: Video.} + - ref: lk.rtc.audio_level.min + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.audio_level.max + requirement_level: {conditionally_required: Inbound audio.} + - ref: lk.rtc.audio_level.avg + requirement_level: {conditionally_required: Inbound audio.} + + - id: event.lk.room.disconnected + type: event + name: lk.room.disconnected + stability: development + brief: The Room left connected for good. + note: Once per session, never on a reconnect. INFO when the client hung up (`CLIENT_INITIATED`), WARN otherwise. + annotations: + livekit: {severity: [INFO, WARN], body: "Display line, e.g. `disconnected: client_initiated` (Rust core); the event name (JS port)."} + attributes: + - ref: lk.disconnect_reason + requirement_level: required + + - id: event.custom + type: event + name: custom + stability: development + brief: Namespace of app-defined events, named `custom.` and carrying the app's own attributes. + note: > + The `custom.` prefix keeps app events from colliding with or spoofing `lk.*` events. An app's + attributes, on its events and on the session (correlation attributes such as `app.call_id`), + must not use the reserved keys and must stay within the limits; a producer must reject an + attribute that breaks either rule, never truncate it, and count it in + `lk.telemetry.dropped.invalid`. An event's own attributes override the session's. + annotations: + livekit: + severity: [INFO] + body: The event name (`custom.`). + reserved: {prefixes: ["lk.", "otel.", "code."], keys: [session.id, error.type]} + limits: {name_bytes: 128, key_bytes: 128, string_value_bytes: 1024, attributes_per_event: 64, attributes_per_session: 64} + + # --- Spans: one attempt at an operation; checkpoints are span events without attributes --- + - id: span.lk.connect + type: span + span_kind: client + stability: development + brief: One connect attempt. + note: > + Dashboards compute connect phases only from the required checkpoints, which every platform + sends in this order; best-effort ones refine a platform's own view and may be missing or + ordered differently. + annotations: + livekit: + checkpoints: + - {name: ws_open, requirement: required, brief: The signaling WebSocket opened.} + - {name: signal, requirement: required, brief: Signaling is up (stamped with ws_open on the web and with join_recv on Swift).} + - {name: join_recv, requirement: required, brief: The join response arrived.} + - {name: pc_created, requirement: required, brief: The peer connections exist (stamped once the join is adopted).} + - {name: early_pc_created, requirement: best_effort, brief: A peer connection was created before the join response (JS port only).} + - {name: offer_sent, requirement: best_effort, brief: The publisher's SDP offer went out.} + - {name: answer_sent, requirement: best_effort, brief: The subscriber's SDP answer went out.} + - {name: engine, requirement: best_effort, brief: The RTC engine is ready (Swift stamps it with pc_connected).} + - {name: pc_connected, requirement: best_effort, brief: The primary peer connection connected.} + - {name: room_connected, requirement: best_effort, brief: The Room reached the connected state.} + limits: {span_events: 128, attributes: 128} + attributes: + - ref: lk.connect.attempt + requirement_level: required + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: Only when `lk.outcome` is `error` and the failure has a type.} + note: > + A type name, never a message, at most 128 bytes, platform-defined: Swift sends + `LiveKitError.`, `CancellationError` or the Swift error type. Dashboards + group by it per `service.name`. Set only by the SDK, at most once. + examples: ["LiveKitError.12", "signal_close"] + + - id: span.lk.reconnect + type: span + span_kind: client + stability: development + brief: One reconnect cycle. + note: > + `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint + named `attempt `, mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. + annotations: + livekit: + checkpoints: [] + checkpoint_pattern: {name: "attempt ", pattern: "^attempt [1-9][0-9]* (quick|full)$", brief: One reconnect attempt; n counts from 1.} + limits: {span_events: 128, attributes: 128} + attributes: + - ref: lk.reconnect.reason + requirement_level: required + - ref: lk.reconnect.mode + requirement_level: {conditionally_required: After the first attempt.} + - ref: lk.reconnect.attempts + requirement_level: {conditionally_required: After the first attempt.} + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: Only when `lk.outcome` is `error` and the failure has a type.} + note: A type name, never a message, at most 128 bytes. Set only by the SDK, at most once. + examples: ["reconnect_failed"] + + - id: span.lk.publish + type: span + span_kind: internal + stability: development + brief: One track publish. + note: > + A child of the ambient span when there is one; a publish before the connect completes (a + microphone published early) is its own span in the session's trace. + annotations: + livekit: + checkpoints: [] + limits: {span_events: 128, attributes: 128} + attributes: + - ref: lk.track.kind + requirement_level: required + - ref: lk.track_source + requirement_level: required + - ref: lk.track_sid + requirement_level: {conditionally_required: On success; the server assigns it.} + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: Only when `lk.outcome` is `error` and the failure has a type.} + note: A type name, never a message, at most 128 bytes. Set only by the SDK, at most once. + examples: ["LiveKitError.12"] + + - id: span.lk.subscribe + type: span + span_kind: internal + stability: development + brief: One subscription, from the intent to subscribe to the first media. + note: > + Starts when the intent exists: a remote publish under autoSubscribe, a manual subscribe, or, + for tracks already in the room, the join. Ends `ok` at the first inbound stats reading with + bytes; `cancelled` on unsubscribe or unpublish before media; `error` on a subscription + failure, or with `error.type = timed_out` when no media came within 30 s, also through a + later disconnect. The core owns the span; an SDK only reports the remote track's lifecycle. + annotations: + livekit: + checkpoints: + - {name: subscribed, requirement: required, brief: The platform reported the remote track subscribed.} + - {name: first_media, requirement: required, brief: The first inbound stats reading with bytes; the span ends ok here.} + limits: {span_events: 128, attributes: 128} + attributes: + - ref: lk.track_sid + requirement_level: required + - ref: lk.track.kind + requirement_level: required + - ref: lk.track_source + requirement_level: required + - ref: lk.pii.remote_participant_identity + requirement_level: recommended + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: Only when `lk.outcome` is `error` and the failure has a type.} + note: A type name, never a message, at most 128 bytes. `timed_out` when no media came within 30 s. + examples: ["timed_out"] diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml new file mode 100644 index 000000000..19e0ab7a0 --- /dev/null +++ b/otel/model/manifest.yaml @@ -0,0 +1,14 @@ +file_format: definition_manifest/2.0 +# Points at this registry. Weaver requires a schema_url and reads its last path segment (`otel`) +# as the version. Producers do not emit it, and it resolves once this directory is on main. +schema_url: https://github.com/livekit/protocol/tree/main/otel +stability: development +description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). +dependencies: + - schema_url: https://opentelemetry.io/schemas/1.44.0 + registry_path: https://github.com/open-telemetry/semantic-conventions.git@v1.44.0[model] + # The OTel GenAI conventions moved out of the main semconv repo (which keeps only deprecated + # gen-ai groups); this is the registry Agents' `gen_ai.*` keys are referenced from. It has no + # tags, so it is pinned by commit. It depends on semconv v1.44.0 itself, the same as above. + - schema_url: https://opentelemetry.io/schemas/gen-ai-dev/1.42.0-dev + registry_path: https://github.com/open-telemetry/semantic-conventions-genai.git@547934afa90a34298441caf80c43b27255c3a292[model] diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml new file mode 100644 index 000000000..74247c208 --- /dev/null +++ b/otel/model/shared/registry.yaml @@ -0,0 +1,233 @@ +# Shared vocabulary: a change here needs Client + Agents + Observability review. +# Upstream OTel keys (service.*, os.*, device.*, network.*, error.type, session.id, code.*) +# are referenced with `ref:` where used, never redefined here. +groups: + - id: registry.lk.identity + type: attribute_group + display_name: LiveKit shared identity + brief: Cross-producer identity keys. Flat `lk._`, never `lk..sid`. + note: > + Record and span attributes, never Resource attributes: a Resource lives for the process, + these for one session. Omitted until the server has assigned them, never sent empty. + attributes: + - id: lk.room_sid + type: string + stability: development + brief: Server-assigned room session SID (`RM_...`). + note: > + On every record and span of a session once the client has joined; absent before join. + High cardinality, never a default metric dimension. + examples: ["RM_5Fq8xQ2ZbXwN"] + annotations: + livekit: + owner: shared + sensitivity: none + authority: cloud + renamed_from: {key: lk.room.sid} + # Physical name per producer/store. Weaver carries these; nothing validates them. + bindings: + agents_otlp: [lk.room_sid, room_id] # spans | resource + metric points + collector_column: room_id # materialized from attribute room_id|roomID only + roomobs: room_session.id + - id: lk.participant_id + type: string + stability: development + brief: Server-assigned SID of the local participant (`PA_...`). + note: > + On every record and span of a session once the client has joined. High cardinality, + never a default metric dimension. + examples: ["PA_9kLm2NpQ4rTs"] + annotations: + livekit: {owner: shared, sensitivity: none, authority: cloud, renamed_from: {key: lk.participant.sid}} + - id: lk.track_sid + type: string + stability: development + brief: Server-assigned track SID (`TR_...`). + note: > + On publish and subscribe spans and RTC stats windows. A publish learns it only on + success. High cardinality, never a default metric dimension. + examples: ["TR_AMkTyV3Zx8Qp"] + annotations: + livekit: {owner: shared, sensitivity: none, authority: cloud, renamed_from: {key: lk.track.sid}} + - id: lk.job_id + type: string + stability: development + brief: Agent job ID (`AJ_...`). + note: > + Sent by Agents, not by clients. High cardinality; Agents' job-scoped metrics are the one + sanctioned metric-dimension exception. + examples: ["AJ_kVbMBWtnVHvT"] + annotations: + livekit: + owner: shared + sensitivity: none + authority: cloud + bindings: + agents_otlp: [lk.job_id, job_id] # spans | resource + metric points + collector_column: none # only agents_rollup has job_id, fed outside OTLP + + - id: registry.lk.pii + type: attribute_group + display_name: LiveKit PII + brief: > + Customer-supplied values. The `pii` key segment is the collector's redaction contract + (agents-private pii-redaction/redact `(^|\.)pii(\.|$)`): a key without it is never redacted. + note: > + When redaction is on, the collector deletes every attribute whose key has a `pii` segment, at + resource, scope, record, event and link level; it never masks or hashes. Log bodies and + metrics are never redacted, so PII must not go there. + attributes: + - id: lk.pii.room_name + type: string + stability: development + brief: Customer-supplied room name. + note: > + On every record and span of a session once the client has joined. + examples: ["my-room"] + annotations: + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.room.name}} + - id: lk.pii.participant_identity + type: string + stability: development + brief: Customer-supplied identity of the local participant. + note: > + On every record and span of a session once the client has joined. + examples: ["identity-0"] + annotations: + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.participant.identity}} + - id: lk.pii.remote_participant_identity + type: string + stability: development + brief: Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). + note: > + On subscribe spans: the identity of the participant publishing the track. + examples: ["identity-1"] + annotations: + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.participant.remote_identity}} + + - id: registry.lk.sdk + type: attribute_group + display_name: LiveKit SDK identity + brief: Which LiveKit SDK produced the telemetry. Resource attributes, next to OTel `service.*`. + note: > + `service.name` names the producer (`livekit-client-swift`); `telemetry.sdk.*` stays reserved + for the OTel-level library (`livekit-telemetry`). + attributes: + - id: lk.sdk.name + type: string + stability: development + brief: LiveKit SDK name. + note: > + Resource attribute. A low-cardinality metric dimension. Matches the `` part of + `service.name = livekit-client-`. + examples: ["swift", "android", "flutter", "react-native", "unity", "rust", "js"] + annotations: + livekit: {owner: shared, sensitivity: none} + - id: lk.sdk.version + type: string + stability: development + brief: LiveKit SDK version. + note: Resource attribute. A low-cardinality metric dimension. + examples: ["2.17.0"] + annotations: + livekit: {owner: shared, sensitivity: none} + + - id: registry.lk.shared + type: attribute_group + display_name: LiveKit shared details + brief: Concepts clients and Agents both send, under the Agents spelling and value set. + note: > + Where a client and Agents named the same concept differently, the Agents key and values are + the shared ones: a flat `lk._` key, and the enum names Agents sends as values. + attributes: + - id: lk.track_source + stability: development + brief: Source of the track, as the Agents RTC FFI `TrackSource` enum name. + note: > + On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. + The value is the RTC FFI enum name exactly, e.g. `SOURCE_SCREENSHARE`. The signal + protocol's `TrackSource` names differ (`CAMERA`, `SCREEN_SHARE`), so clients map their + track source to these names explicitly. + type: + members: + - {id: source_unknown, value: SOURCE_UNKNOWN, brief: Unknown source., stability: development} + - {id: source_camera, value: SOURCE_CAMERA, brief: Camera., stability: development} + - {id: source_microphone, value: SOURCE_MICROPHONE, brief: Microphone., stability: development} + - {id: source_screenshare, value: SOURCE_SCREENSHARE, brief: Screen share video., stability: development} + - {id: source_screenshare_audio, value: SOURCE_SCREENSHARE_AUDIO, brief: Screen share audio., stability: development} + annotations: + livekit: + owner: shared + sensitivity: none + renamed_from: + key: lk.track.source + values: {camera: SOURCE_CAMERA, microphone: SOURCE_MICROPHONE, screen_share: SOURCE_SCREENSHARE, screen_share_audio: SOURCE_SCREENSHARE_AUDIO, unknown: SOURCE_UNKNOWN} + - id: lk.disconnect_reason + stability: development + brief: Why a participant left the room, as the protobuf `DisconnectReason` enum name. + note: > + On the client `lk.room.disconnected` event (the local participant) and on the Agents + `participant_disconnected` checkpoint (the linked participant). `RECONNECT_FAILED` is the + one value outside the protobuf enum: a client that ran out of reconnect attempts sends it. + type: + members: + - {id: unknown_reason, value: UNKNOWN_REASON, brief: "No reason given, or a number the producer does not know.", stability: development} + - {id: client_initiated, value: CLIENT_INITIATED, brief: The participant hung up., stability: development} + - {id: duplicate_identity, value: DUPLICATE_IDENTITY, brief: Another participant joined with the same identity., stability: development} + - {id: server_shutdown, value: SERVER_SHUTDOWN, brief: The server instance shut down., stability: development} + - {id: participant_removed, value: PARTICIPANT_REMOVED, brief: RoomService.RemoveParticipant was called., stability: development} + - {id: room_deleted, value: ROOM_DELETED, brief: RoomService.DeleteRoom was called., stability: development} + - {id: state_mismatch, value: STATE_MISMATCH, brief: The server did not know the session the client tried to resume., stability: development} + - {id: join_failure, value: JOIN_FAILURE, brief: The client could not connect fully., stability: development} + - {id: migration, value: MIGRATION, brief: "The server asked the participant to migrate (Cloud only).", stability: development} + - {id: signal_close, value: SIGNAL_CLOSE, brief: The signal WebSocket closed unexpectedly., stability: development} + - {id: room_closed, value: ROOM_CLOSED, brief: The room closed after its participants left., stability: development} + - {id: user_unavailable, value: USER_UNAVAILABLE, brief: The SIP callee did not answer in time., stability: development} + - {id: user_rejected, value: USER_REJECTED, brief: The SIP callee rejected the call., stability: development} + - {id: sip_trunk_failure, value: SIP_TRUNK_FAILURE, brief: SIP protocol failure., stability: development} + - {id: connection_timeout, value: CONNECTION_TIMEOUT, brief: The server timed out the session., stability: development} + - {id: media_failure, value: MEDIA_FAILURE, brief: Media failed or timed out., stability: development} + - {id: agent_error, value: AGENT_ERROR, brief: The agent failed., stability: development} + - {id: reconnect_failed, value: RECONNECT_FAILED, brief: "The client ran out of reconnect attempts (client only, not a protobuf value).", stability: development} + annotations: + livekit: + owner: shared + sensitivity: none + renamed_from: + key: lk.disconnect.reason + values: + client_initiated: CLIENT_INITIATED + duplicate_identity: DUPLICATE_IDENTITY + server_shutdown: SERVER_SHUTDOWN + participant_removed: PARTICIPANT_REMOVED + room_deleted: ROOM_DELETED + state_mismatch: STATE_MISMATCH + join_failure: JOIN_FAILURE + migration: MIGRATION + signal_close: SIGNAL_CLOSE + room_closed: ROOM_CLOSED + user_unavailable: USER_UNAVAILABLE + user_rejected: USER_REJECTED + sip_trunk_failure: SIP_TRUNK_FAILURE + connection_timeout: CONNECTION_TIMEOUT + media_failure: MEDIA_FAILURE + agent_error: AGENT_ERROR + unknown: UNKNOWN_REASON + reconnect_failed: RECONNECT_FAILED + - id: lk.agent_name + type: string + stability: development + brief: Name of the agent a job was dispatched to. + note: > + On the Agents Resource and the `job_entrypoint` and `agent_session` spans, set when the job + has an agent name (explicit dispatch). A client that reports the agent it talks to uses + this key. The participant attribute `lk.agent.name` carries the same name on the room; it + is not a telemetry key. + examples: ["my-agent"] + annotations: + livekit: + owner: shared + sensitivity: none + bindings: + agents_participant_attribute: lk.agent.name # room participant attribute, not an OTLP key + agents_session_report: agent_name # bare key on the session report log diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego new file mode 100644 index 000000000..8043ec7d0 --- /dev/null +++ b/otel/policies/lib.rego @@ -0,0 +1,185 @@ +package livekit.lib + +# The LiveKit rules, shared by livekit.rego (each definition file, default mode) and resolved.rego +# (the resolved registry, the only pass weaver runs with --v2). Call them by full path: +# data.livekit.lib.. Each finding id is documented in otel/README.md, and +# otel/tests/negatives has a fixture for each, run in both modes. + +import rego.v1 + +# Mirrors the collector's redaction matcher, agents-private +# pii-redaction/redact/oteltraces.go:16. Keep the two in sync (and otel/live-check/livekit.rego). +pii_segment := `(^|\.)pii(\.|$)` + +# ponytail: keyword heuristic, misses PII with innocent names; reviewers still own it. +pii_looking := `(identity|email|phone|ip_address|user_agent|(room|participant)[._]name)` + +canonical_identity := { + "lk.room.sid": "lk.room_sid", + "lk.participant.sid": "lk.participant_id", + "lk.participant_sid": "lk.participant_id", + "lk.track.sid": "lk.track_sid", + "lk.job.id": "lk.job_id", +} + +high_cardinality := {"lk.room_sid", "lk.participant_id", "lk.track_sid", "lk.job_id", "lk.client.connect_id"} + +owners := {"shared", "client", "agents", "cloud"} + +# Mirrors protocol protobufs/logger/options.proto Sensitivity (UNSPECIFIED/PII/SECRET). +sensitivities := {"none", "pii", "secret"} + +# A duration word ending the key, optionally followed by `_ms` and one aggregation segment: +# lk.client.rtt, lk.rtc.freezes_duration_ms, lk.client.rtt.avg, lk.rtc.rtt_ms.max. +duration_like := `(^|[._])(duration|latency|delay|rtt|jitter)(_ms)?([._](min|max|avg|mean|median|sum|total|last|p[0-9]+))?$` + +finding(id, msg) := {"id": id, "level": "violation", "message": msg} + +lk(obj, field) := object.get(obj, ["annotations", "livekit", field], null) + +# A documented reason to keep milliseconds (the RTC stats keys). +ms_exception(attr) if { + lk(attr, "unit") == "ms" + is_string(lk(attr, "unit_exception")) + lk(attr, "unit_exception") != "" +} + +# Every violation of one LiveKit attribute definition, its key in `id`. +attr_findings(attr) := ((((((owner(attr) | sensitivity(attr)) | secret(attr)) | pii_marker(attr)) | pii_heuristic(attr)) | flat_identity(attr)) | seconds(attr)) | unit(attr) + +owner(attr) := {finding("livekit_owner", sprintf("%s: annotations.livekit.owner must be one of %v", [attr.id, owners])) | + not owners[lk(attr, "owner")] +} + +sensitivity(attr) := {finding("livekit_sensitivity", sprintf("%s: annotations.livekit.sensitivity must be one of %v (protocol logger.Sensitivity)", [attr.id, sensitivities])) | + not sensitivities[lk(attr, "sensitivity")] +} + +secret(attr) := {finding("livekit_secret", sprintf("%s: SECRET values are never emitted as telemetry", [attr.id])) | + lk(attr, "sensitivity") == "secret" +} + +# PII keys the collector matches by exact name instead of a `pii` segment, so they keep their +# shipped names: pii-redaction/redact/otellogs.go:19 drops every record that carries chat.item. +pii_by_name := {"chat.item"} + +redacted(key) := true if regex.match(pii_segment, key) + +else := true if pii_by_name[key] + +else := false + +pii_marker(attr) := {finding("livekit_pii_marker", sprintf("%s: sensitivity=%v, but the collector redacts by key, so a `pii` key segment must be present iff sensitivity=pii", [attr.id, lk(attr, "sensitivity")])) | + sensitivities[lk(attr, "sensitivity")] + (lk(attr, "sensitivity") == "pii") != redacted(attr.id) +} + +pii_heuristic(attr) := {finding("livekit_pii_heuristic", sprintf("%s looks like PII; name it lk.pii.* so the collector redacts it", [attr.id])) | + regex.match(pii_looking, attr.id) + not regex.match(pii_segment, attr.id) + not heuristic_exception(attr) +} + +# The shipped keys the heuristic would flag, registered as emitted. Only a key listed HERE may +# carry `annotations.livekit.pii_heuristic_exception` (the annotation documents it at the key; +# this map is the gate), so a new identity/email/phone key cannot opt out of the rule by +# annotating itself. The collector still does not redact these: this documents, it does not +# excuse. Adding a key means editing this file, in review. +pii_heuristic_allowlist := { + "lk.rpc.caller_identity": "Agents 2.0 ships the RPC caller identity unmarked; renaming it to lk.pii.rpc.caller_identity is a producer change", + "lk.rpc.destination_identity": "Agents 2.0 ships the RPC destination identity unmarked; same as lk.rpc.caller_identity", +} + +heuristic_exception(attr) if { + pii_heuristic_allowlist[attr.id] + is_string(lk(attr, "pii_heuristic_exception")) + lk(attr, "pii_heuristic_exception") != "" +} + +flat_identity(attr) := {finding("livekit_flat_identity", sprintf("%s: shared identity keys are flat, use %s", [attr.id, canonical_identity[attr.id]])) | + canonical_identity[attr.id] +} + +seconds(attr) := {finding("livekit_seconds", sprintf("%s: LiveKit durations are seconds; a *_ms key needs annotations.livekit.unit: ms plus a unit_exception reason", [attr.id])) | + regex.match(`_ms($|\.)`, attr.id) + not ms_exception(attr) +} + +unit(attr) := {finding("livekit_unit", sprintf("%s: duration-like attribute must declare annotations.livekit.unit: s (or unit: ms with a unit_exception)", [attr.id])) | + regex.match(duration_like, attr.id) + lk(attr, "unit") != "s" + not ms_exception(attr) +} + +# A metric dimension list against the cardinality rule; `exception` is the metric's own flag. +cardinality(name, where, keys, exception) := {finding("livekit_cardinality", sprintf("%s (%s): %s is high-cardinality, not a default metric dimension", [name, where, key])) | + exception != true # Agents' job-scoped exception + some key in keys + high_cardinality[key] +} + +lk_all(obj) := object.get(obj, ["annotations", "livekit"], null) + +# `annotations.livekit.shipped_values` on a ref: values a producer sends for an upstream key that +# the upstream definition does not list. Every problem with one ref's list, as messages. +shipped_value_problems(key, attr) := ((((lk_key_shipped(key) | unshippable(key, attr)) | malformed_shipped(key, attr)) | upstream_shipped(key, attr)) | upstream_id_shipped(key, attr)) | repeated_id_shipped(key, attr) + +lk_key_shipped(key) := {sprintf("%s: shipped_values are for upstream keys; list a LiveKit key's values as enum members", [key]) | + startswith(key, "lk.") +} + +# A string, string[] or string enum key: the only types a list of string values fits. An enum +# with any non-string member (rpc.grpc.status_code is an int enum) does not. +shippable_type(t) if t == "string" + +shippable_type(t) if t == "string[]" + +shippable_type(t) if { + is_object(t) + count(t.members) > 0 + every m in t.members { + is_string(m.value) + } +} + +unshippable(key, attr) := {sprintf("%s: shipped_values need a string, string[] or string enum key, not %v", [key, attr.type]) | + not shippable_type(attr.type) +} + +# A lowercase snake_case id, so upper(id) is exactly the constant name the templates generate. +shipped_entry_ok(v) if { + is_string(v.id) + regex.match(`^[a-z][a-z0-9_]*$`, v.id) + is_string(v.value) + v.value != "" + is_string(v.brief) +} + +malformed_shipped(key, attr) := {sprintf("%s: every shipped value needs a lowercase snake_case id (^[a-z][a-z0-9_]*$), a non-empty value and a brief, not %v", [key, v]) | + some v in lk(attr, "shipped_values") + not shipped_entry_ok(v) +} + +upstream_shipped(key, attr) := {sprintf("%s: shipped value %v is already an upstream value; use the upstream constant", [key, v.value]) | + some v in lk(attr, "shipped_values") + some m in object.get(attr, ["type", "members"], []) + v.value == m.value +} + +# A shipped id equal to an upstream member id read as lowercase snake_case (`aws.bedrock` as +# `aws_bedrock`): both would generate the same constant. Upstream ids that need more case +# conversion than this are left to the CI compile of the generated code. +upstream_id_shipped(key, attr) := {sprintf("%s: shipped value id %v generates the same constant as upstream member %v", [key, v.id, m.id]) | + some v in lk(attr, "shipped_values") + some m in object.get(attr, ["type", "members"], []) + v.id == regex.replace(lower(m.id), `[^a-z0-9]`, "_") +} + +# Two shipped entries with the same id. +repeated_id_shipped(key, attr) := {sprintf("%s: two shipped values have the id %v", [key, a.id]) | + values := lk(attr, "shipped_values") + some i, a in values + some j, b in values + i < j + a.id == b.id +} diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego new file mode 100644 index 000000000..8c4536bb3 --- /dev/null +++ b/otel/policies/livekit.rego @@ -0,0 +1,34 @@ +package before_resolution + +# The LiveKit rules on each definition file, in weaver's default mode (with --v2 weaver skips +# before_resolution policies; resolved.rego covers that mode). The rules are in lib.rego. + +import rego.v1 + +# Attribute definitions in this file, lk.* or not (refs and deprecated keys are exempt). +lk_attrs contains attr if { + some group in input.groups + some attr in group.attributes + is_string(attr.id) # a definition, not a ref + not attr.deprecated +} + +# definition/2 files list attributes at the top level, keyed by `key`. +lk_attrs contains object.union(attr, {"id": attr.key}) if { + some attr in input.attributes + not attr.deprecated +} + +deny contains f if { + some attr in lk_attrs + some f in data.livekit.lib.attr_findings(attr) +} + +# A definition/2 metric refinement is absent from the default mode's resolved registry, so its +# dimensions are checked here (resolved.rego sees them with --v2). +deny contains f if { + some r in object.get(input, "metric_refinements", []) + keys := [a.ref | some a in object.get(r, "attributes", []); a.ref] + exception := data.livekit.lib.lk(r, "cardinality_exception") + some f in data.livekit.lib.cardinality(r.id, "metric refinement", keys, exception) +} diff --git a/otel/policies/resolved.rego b/otel/policies/resolved.rego new file mode 100644 index 000000000..eb9bcdf7a --- /dev/null +++ b/otel/policies/resolved.rego @@ -0,0 +1,155 @@ +package after_resolution + +# The LiveKit rules on the resolved registry, in both of weaver's representations: default mode +# hands this package `input.groups`, --v2 hands it `input.registry` and `input.refinements`. It +# sees what refs, `extends`, refinements and definition/2 files resolve to. The rules are in +# lib.rego. + +import rego.v1 + +# --v2 skips the per-file pass, so the attribute rules run here on the resolved definitions. +deny contains f if { + some attr in object.get(input, ["registry", "attributes"], []) + not attr.deprecated + some f in data.livekit.lib.attr_findings(object.union(attr, {"id": attr.key})) +} + +# Renames recorded in the model: each shared key's `renamed_from` names the old spelling and, for +# an enum, maps the old values to its members. Read from every attribute, either representation. +attributes contains {"key": a.name, "attr": a} if { + some g in object.get(input, "groups", []) + some a in g.attributes +} + +attributes contains {"key": a.key, "attr": a} if { + some a in object.get(input, ["registry", "attributes"], []) +} + +renamed_to[old] := a.key if { + some a in attributes + old := data.livekit.lib.lk(a.attr, "renamed_from").key +} + +# An old spelling stays unregistered, deprecated or not: the rename lives on the shared key. +deny contains data.livekit.lib.finding("livekit_shared_rename", msg) if { + some a in attributes + new := renamed_to[a.key] + msg := sprintf("%s: renamed to %s (renamed_from on %s); the old spelling may not be defined, deprecated or not", [a.key, new, new]) +} + +# A mapped value must be a member of the shared key's enum. +deny contains data.livekit.lib.finding("livekit_rename_value", msg) if { + some a in attributes + some old_value, new_value in object.get(a.attr, ["annotations", "livekit", "renamed_from", "values"], {}) + members := {m.value | some m in object.get(a.attr, ["type", "members"], [])} + not new_value in members + msg := sprintf("%s: renamed_from maps %s to %s, which is not a member", [a.key, old_value, new_value]) +} + +# Every resolved metric, either representation: name, unit, dimension keys, exception, origin. +metrics contains {"name": g.metric_name, "unit": unit, "keys": keys, "exception": exception, "where": where} if { + some g in object.get(input, "groups", []) + g.type == "metric" + unit := object.get(g, "unit", "") + keys := [a.name | some a in g.attributes] + exception := data.livekit.lib.lk(g, "cardinality_exception") + where := object.get(g, ["lineage", "provenance", "path"], g.id) +} + +metrics contains {"name": m.name, "unit": unit, "keys": keys, "exception": exception, "where": where} if { + some section in ["registry", "refinements"] + some m in object.get(input, [section, "metrics"], []) + unit := object.get(m, "unit", "") + keys := [a.key | some a in object.get(m, "attributes", [])] + exception := data.livekit.lib.lk(m, "cardinality_exception") + where := object.get(m, ["provenance", "path"], m.name) +} + +deny contains data.livekit.lib.finding("livekit_metric_unit", msg) if { + some m in metrics + m.unit == "ms" + msg := sprintf("%s (%s): metric unit must be s, not ms", [m.name, m.where]) +} + +deny contains f if { + some m in metrics + some f in data.livekit.lib.cardinality(m.name, m.where, m.keys, m.exception) +} + +# A ref inherits the referenced key's classification. Overriding it would let one signal re-label +# a key, e.g. as non-PII, while the collector keeps redacting by key name. +override_msg(key, signal, where) := sprintf("%s in %s (%s): a ref may not override the annotations of a LiveKit key", [key, signal, where]) + +# Default mode: keys defined in otel/model. A definition's copy has no lineage entry and a ref's +# copy has one, so membership comes from the definition, whatever the ref does to its annotations. +defined contains a.name if { + some g in object.get(input, "groups", []) + some a in g.attributes + not g.lineage.attributes[a.name] +} + +# Default mode: the lineage records which fields a ref overrode. +deny contains data.livekit.lib.finding("livekit_ref_override", msg) if { + some group in object.get(input, "groups", []) + some key, lineage in object.get(group, ["lineage", "attributes"], {}) + defined[key] + "annotations" in object.get(lineage, "locally_overridden_fields", []) + msg := override_msg(key, group.id, object.get(group, ["lineage", "provenance", "path"], group.id)) +} + +# --v2: no lineage, so compare each signal's copy of a key with the key's definition. +definitions := {a.key: data.livekit.lib.lk_all(a) | some a in object.get(input, ["registry", "attributes"], [])} + +v2_signals contains s if { + some kind in ["attribute_groups", "metrics", "spans", "events", "entities"] + some s in object.get(input, ["registry", kind], []) +} + +v2_signals contains s if { + some kind in ["metrics", "spans", "events", "entities"] + some s in object.get(input, ["refinements", kind], []) +} + +deny contains data.livekit.lib.finding("livekit_ref_override", msg) if { + some s in v2_signals + some a in object.get(s, "attributes", []) + definitions[a.key] + data.livekit.lib.lk_all(a) != object.get(definitions, a.key, null) + signal := object.get(s, "id", object.get(s, "name", object.get(s, "type", "?"))) + msg := override_msg(a.key, signal, object.get(s, ["provenance", "path"], signal)) +} + +# Shipped values, either representation: default mode keeps every group's resolved attributes, +# definitions included; --v2 keeps a ref's copy in the signal or attribute group that holds it, +# and the definitions in `registry.attributes`. +shipped contains {"key": a.name, "attr": a} if { + some g in object.get(input, "groups", []) + some a in g.attributes + data.livekit.lib.lk(a, "shipped_values") != null +} + +shipped contains {"key": a.key, "attr": a} if { + some s in v2_signals + some a in object.get(s, "attributes", []) + data.livekit.lib.lk(a, "shipped_values") != null +} + +shipped contains {"key": a.key, "attr": a} if { + some a in object.get(input, ["registry", "attributes"], []) + data.livekit.lib.lk(a, "shipped_values") != null +} + +deny contains data.livekit.lib.finding("livekit_shipped_value", msg) if { + some s in shipped + some msg in data.livekit.lib.shipped_value_problems(s.key, s.attr) +} + +# One shipped_values list per key across the whole registry: codegen merges every ref's list into +# one module, so two lists could generate one constant twice. Copies of the same list (a key's +# definition in each signal, a group used twice) count once. +deny contains data.livekit.lib.finding("livekit_shipped_value", msg) if { + some key in {s.key | some s in shipped} + lists := {data.livekit.lib.lk(s.attr, "shipped_values") | some s in shipped; s.key == key} + count(lists) > 1 + msg := sprintf("%s: %d refs carry different shipped_values lists; keep one list for the key", [key, count(lists)]) +} diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 new file mode 100644 index 000000000..306561153 --- /dev/null +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -0,0 +1,206 @@ +{#- One markdown table cell: no newline, no unescaped pipe. -#} +{%- macro cell(s) -%} +{{ s | trim | replace("\r", " ") | replace("\n", " ") | replace("|", "\\|") }} +{%- endmacro -%} +{#- Brief and note; OTel briefs may lack a full stop and link relative to their own docs. -#} +{%- macro desc(a) -%} +{%- set brief = cell(a.brief) | replace("](/docs/", "](https://github.com/open-telemetry/semantic-conventions/blob/v1.44.0/docs/") -%} +{{ brief }}{% if a.note %}{% if brief[-1:] not in ".!?" %}.{% endif %} {{ cell(a.note) }}{% endif %} +{%- endmacro -%} +{%- macro req(a) -%} +{%- if a.requirement_level is string -%}{{ {"required": "Required", "recommended": "Recommended", "opt_in": "Opt-in"}[a.requirement_level] or a.requirement_level }} +{%- else -%}{% for k, v in a.requirement_level | items %}Conditionally required: {{ cell(v) }}{% endfor %}{%- endif -%} +{%- endmacro -%} +{%- macro unit(a) -%} +{%- if a.annotations and a.annotations.livekit and a.annotations.livekit.unit %}, `{{ a.annotations.livekit.unit }}`{% endif -%} +{%- endmacro -%} +{%- macro ty(a) -%} +{% if a.type is mapping %}enum{% else %}{{ a.type }}{% endif %}{{ unit(a) }} +{%- endmacro -%} +{%- macro attr_table(attrs) -%} +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +{% for a in attrs | attribute_sort %}| `{{ a.key }}` | {{ ty(a) }} | {{ req(a) }} | {{ desc(a) }} | +{% endfor %} +{%- endmacro -%} +{%- set custom = ctx.events | selectattr("name", "eq", "custom") | first -%} + +# LiveKit telemetry semantic conventions + +The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. +Stability `development`. Built on +[OpenTelemetry semantic conventions v1.44.0](https://github.com/open-telemetry/semantic-conventions/tree/v1.44.0): +OTel keys are used as OTel defines them and are not redefined here. + +Generated from `otel/model` in livekit/protocol; change the model, not this file. It registers +the keys shared across producers and what the LiveKit client SDKs send today (the Rust core +behind the native SDKs, and the JS port). How and when telemetry is sent (transport, cadence, +batching, retries, storage) is not part of the conventions. + +Contents: [Conventions](#conventions) · [Resource](#resource) · [Every record](#every-record) · +[Plain log records](#plain-log-records) · [Events](#events) · [Spans](#spans) · +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) · +[Old spellings](#old-spellings) + +## Conventions + +**OTel first.** OTel defines the record model (timestamp, severity, trace and span ids, span +status, `service.name`) and the keys LiveKit reuses (`service.*`, `os.*`, `device.*`, +`network.connection.type`, `error.type`, `session.id`, `code.*`, `telemetry.sdk.*`). LiveKit keys +carry the `lk.` prefix. Requirement levels have their OTel meaning; a conditional requirement's +condition is prose. + +**Ownership.** `shared` keys are concepts several producers send; changing one needs Client, +Agents and Observability review. `client` keys belong to the Client team. Every LiveKit key's +owner is in the [attribute reference](#attribute-reference). + +**Flat shared identity.** Shared identity keys are flat, `lk._`, never +`lk..sid`: +{% for a in ctx.attributes | selectattr("annotations.livekit.authority", "eq", "cloud") %}`{{ a.key }}`{% if not loop.last %}, {% endif %}{% endfor %}. +They are record and span attributes, never Resource attributes, omitted until the server has +assigned them, and never default metric dimensions (high cardinality). + +**One name per concept.** Where a client and Agents named the same concept differently, the +Agents spelling and value set is the shared key: a flat key, and the enum names Agents sends as +values (`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The +other spelling is never registered, deprecated or not. The shared key records it in +`renamed_from`, listed under [Old spellings](#old-spellings). + +**Upstream metrics.** Agents creates three GenAI metrics that the GenAI registry defines. The +registry imports `gen_ai.invoke_agent.duration` and `gen_ai.execute_tool.duration`, and the +generated code lists them in its upstream section. Weaver cannot import +`gen_ai.client.operation.duration`, because semconv 1.44.0 keeps an excluded, deprecated copy, so +the registry declares it as emitted. + +**Values beyond upstream.** A ref to an upstream key can list `shipped_values`: values a LiveKit +producer sends that the upstream definition does not list. The generated code gives each one a +constant next to the upstream values, and a policy rejects a value that upstream already lists. + +| Key | Value | Meaning | +| --- | --- | --- | +{% for g in ctx.groups %}{% for a in g.attributes %}{% if a.annotations and a.annotations.livekit and a.annotations.livekit.shipped_values %}{% for v in a.annotations.livekit.shipped_values %}| `{{ a.key }}` | `{{ cell(v.value) }}` | {{ cell(v.brief) }} | +{% endfor %}{% endif %}{% endfor %}{% endfor %} +**Session report.** Agents' session report sends plain log records whose keys predate this +registry, registered as emitted: three in the upstream `session.*` namespace, and seven without a +namespace. A map-valued key carries an OTLP map, and its fields are payload, not attribute keys. +`chat.item` carries conversation content: with redaction on, the collector drops the whole record. + +**PII.** Customer-supplied values go under `lk.pii.*`. The `pii` key segment is the collector's +redaction contract: with redaction on, it deletes every attribute whose key has a `pii` segment and +keeps everything else. Log bodies and metrics are never redacted, so PII must not go there. +PII keys: {% for a in ctx.attributes | selectattr("annotations.livekit.sensitivity", "eq", "pii") %}`{{ a.key }}`{% if not loop.last %}, {% endif %}{% endfor %}. + +**Units.** Durations are seconds (OTel), on a monotonic clock; timestamps are the record's own +OTLP time. One documented exception: the RTC stats keys ending in `_ms` carry milliseconds. The +window length (`lk.rtc.window_ms`) is measured by the producer; the others are converted from the +seconds the W3C stats report: +{% for a in ctx.attributes | selectattr("annotations.livekit.unit_exception") %}`{{ a.key }}`{% if not loop.last %}, {% endif %}{% endfor %}. + +**Keys the SDK owns.** Apps must not set these, on their own events or as session attributes, +and a producer must reject such an attribute: +prefixes {% for p in custom.annotations.livekit.reserved.prefixes %}`{{ p }}`{% if not loop.last %}, {% endif %}{% endfor %} and keys +{% for k in custom.annotations.livekit.reserved.keys %}`{{ k }}`{% if not loop.last %}, {% endif %}{% endfor %}. + +**App events.** An app's events are named `custom.` (see [`custom`](#custom)), so they never +collide with or spoof LiveKit events, and must stay within these limits: +{% for k, v in custom.annotations.livekit.limits | items %}{{ k | replace("_", " ") }} {{ v }}{% if not loop.last %}, {% endif %}{% endfor %}. +A producer must reject anything over a limit, never truncate it, and count it in +`lk.telemetry.dropped.invalid`. + +**Records.** An event is an OTLP log record with `event_name`; its severity and body are listed +with it. The JS port sends every event's name as its body. A record's timestamp and attributes are +taken when it is captured. Every record and span of a room session carries the +[session attributes](#every-record). + +**Spans.** A span is one attempt at an operation; the session is the trace. LiveKit span names are +verbs, never ids. Span status is `Unset` on success and on cancellation, `Error` on failure. +`lk.outcome` (`ok`, `error`, `cancelled`) is on every span exactly once, and rollups read it, never +the status. `error.type` is a type name, never a message, at most 128 bytes, present only when the +span failed with a type. Only the SDK sets either; a producer must drop a span attribute of either +name set any other way. Checkpoints are span events without attributes, named from a fixed list +per span. A producer must hold each span to its limits: later checkpoints are dropped and counted +in OTLP `dropped_events_count`; past the attribute limit, app correlation attributes go first, then +the span's own tail, counted in `dropped_attributes_count`; the session's attributes, `lk.outcome` +and `error.type` always ship. + +## Resource + +{% for g in ctx.groups | selectattr("id", "eq", "livekit.client.resource") %}{{ g.brief | trim }} {{ g.note | trim }} + +{{ attr_table(g.attributes) }}{% endfor %} +## Every record + +{% for g in ctx.groups | selectattr("id", "eq", "livekit.client.record") %}{{ g.brief | trim }} {{ g.note | trim }} + +{{ attr_table(g.attributes) }}{% endfor %} +## Plain log records + +{% for g in ctx.groups | selectattr("id", "eq", "livekit.client.log") %}{{ g.brief | trim }} {{ g.note | trim }} + +{{ attr_table(g.attributes) }}{% endfor %} +## Events +{% for e in ctx.events %} +### `{{ e.name }}` + +{{ e.brief | trim }}{% if e.note %} {{ e.note | trim }}{% endif %} + +- Severity: {% for s in e.annotations.livekit.severity %}`{{ s }}`{% if not loop.last %} or {% endif %}{% endfor %} +- Body: {{ cell(e.annotations.livekit.body) }} +{% if e.attributes %} +{{ attr_table(e.attributes) }}{% endif %}{% endfor %} +## Spans +{% for s in ctx.spans %}{% set lk = (s.annotations.livekit if s.annotations and s.annotations.livekit else {}) %} +### `{{ s.type }}` + +Kind `{{ s.kind }}`. {{ s.brief | trim }}{% if s.note %} {{ s.note | trim }}{% endif %} + +{{ attr_table(s.attributes) }} +{% if lk.checkpoints or lk.checkpoint_pattern %}| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +{% for c in lk.checkpoints %}| `{{ c.name }}` | {{ {"required": "Required", "best_effort": "Best effort"}[c.requirement] or c.requirement }} | {{ cell(c.brief) }} | +{% endfor %}{% if lk.checkpoint_pattern %}| `{{ lk.checkpoint_pattern.name }}` | One per attempt | {{ cell(lk.checkpoint_pattern.brief) }} Matches `{{ cell(lk.checkpoint_pattern.pattern) }}`. | +{% endif %}{% else %}No checkpoints. +{% endif %} +{% if lk.limits %}Limits: {% for k, v in lk.limits | items %}{{ v }} {{ k | replace("_", " ") }}{% if not loop.last %}, {% endif %}{% endfor %} per span.{% else %}No limits declared.{% endif %} +{% endfor %} +## Attribute reference + +Every key this registry defines, with its meaning; the OTel keys used above are defined by OTel. +"Used on" names the attribute sets, events and spans that reference the key. + +| Key | Type | Owner | Sensitivity | Description | Examples | Used on | +| --- | --- | --- | --- | --- | --- | --- | +{% for a in ctx.attributes %}| `{{ a.key }}` | {{ ty(a) }} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {{ desc(a) }} | {% for x in a.examples or [] %}`{{ cell(x | string) }}`{% if not loop.last %}, {% endif %}{% endfor %} | {% for g in ctx.groups %}{% if a.key in (g.attributes | map(attribute="key") | list) %}{{ g.id | replace("livekit.client.", "") }} {% endif %}{% endfor %}{% for e in ctx.events %}{% if a.key in ((e.attributes or []) | map(attribute="key") | list) %}`{{ e.name }}` {% endif %}{% endfor %}{% for s in ctx.spans %}{% if a.key in (s.attributes | map(attribute="key") | list) %}`{{ s.type }}` {% endif %}{% endfor %}| +{% endfor %} +## Enum values +{% for a in ctx.attributes %}{% if a.type is mapping %} +### `{{ a.key }}` + +| Value | Meaning | +| --- | --- | +{% for m in a.type.members %}| `{{ cell(m.value) }}` | {{ cell(m.brief) }}{% if m.note %} {{ cell(m.note) }}{% endif %} | +{% endfor %}{% endif %}{% endfor %} +## Bindings + +The physical name of a concept in other producers and stores. Carried for reference; nothing +validates it. + +| Key | Producer or store | Name there | +| --- | --- | --- | +{% for a in ctx.attributes %}{% if a.annotations.livekit.bindings %}{% for k, v in a.annotations.livekit.bindings | items %}| `{{ a.key }}` | {{ k }} | {% if v is string %}`{{ v }}`{% else %}{% for x in v %}`{{ x }}`{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} | +{% endfor %}{% endif %}{% endfor %} +`agents_otlp` lists Agents' span key first, then its Resource and metric-point key. +`agents_participant_attribute` is the room participant attribute, which is not an OTLP key. +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID`. + +## Old spellings + +Keys and values that producers sent before this registry, and the key and value to send instead. +Each row comes from `renamed_from` on the shared key. Live-check reports an old spelling as +`livekit_shared_rename` and names the replacement. + +| Old key | Old value | Key to send | Value to send | +| --- | --- | --- | --- | +{% for a in ctx.attributes %}{% set from = a.annotations.livekit.renamed_from %}{% if from %}{% if from["values"] %}{% for old, new in from["values"] | items %}| `{{ from.key }}` | `{{ old }}` | `{{ a.key }}` | `{{ new }}` | +{% endfor %}{% else %}| `{{ from.key }}` | any | `{{ a.key }}` | unchanged | +{% endif %}{% endif %}{% endfor %} diff --git a/otel/templates/registry/markdown/weaver.yaml b/otel/templates/registry/markdown/weaver.yaml new file mode 100644 index 000000000..3cde97bbc --- /dev/null +++ b/otel/templates/registry/markdown/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.md.j2 + filter: '{attributes: .registry.attributes | sort_by(.key), events: .registry.events | sort_by(.name), spans: .registry.spans | sort_by(.type), metrics: .registry.metrics | sort_by(.name), groups: .registry.attribute_groups | sort_by(.id)}' + application_mode: single + file_name: semconv.md diff --git a/otel/templates/registry/python/semconv.py.j2 b/otel/templates/registry/python/semconv.py.j2 new file mode 100644 index 000000000..eb19a3ffc --- /dev/null +++ b/otel/templates/registry/python/semconv.py.j2 @@ -0,0 +1,56 @@ +{#- A Python string literal, double-quoted: the same escapes as the Rust template, which Python + reads identically (\\ \" \r \n \t). -#} +{%- macro lit(s) -%} +"{{ s | replace("\\", "\\\\") | replace("\"", "\\\"") | replace("\r", "\\r") | replace("\n", "\\n") | replace("\t", "\\t") }}" +{%- endmacro -%} +{#- One comment line. -#} +{%- macro line(s) -%} +{{ s | trim | replace("\r", " ") | replace("\n", " ") }} +{%- endmacro -%} +{%- macro doc(a) -%} +{{ line(a.brief) }}{% if a.annotations and a.annotations.livekit and a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations and a.annotations.livekit and a.annotations.livekit.unit %} Unit: `{{ line(a.annotations.livekit.unit) }}`.{% endif %} +{%- endmacro -%} +{#- A flat module: one constant per key, enum values as KEY_VALUE constants. -#} +{%- macro attr(a) -%} +#: {{ doc(a) }} +{{ a.key | screaming_snake_case }} = {{ lit(a.key) }} +{% if a.type is mapping %}{% for m in a.type.members %}#: {{ line(m.brief) or "`" ~ m.value ~ "`." }} +{{ a.key | screaming_snake_case }}_{{ m.id | screaming_snake_case }} = {{ lit(m.value) }} +{% endfor %}{% endif %}{% for v in a.shipped_values or [] %}#: {{ line(v.brief) }} Sent by LiveKit, not an upstream value. +{{ a.key | screaming_snake_case }}_{{ v.id | screaming_snake_case }} = {{ lit(v.value) }} +{% endfor %} +{%- endmacro -%} +# Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +"""LiveKit telemetry semantic conventions.""" + +{% for a in ctx.attributes %}{{ attr(a) }} +{% endfor %} +# Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, +# never here), so a producer can name them from one place. +{% for a in ctx.upstream %}{{ attr(a) }} +{% endfor %}{% if ctx.upstream_metrics %} +# Upstream metrics that LiveKit producers create, imported from the registry's dependencies. +{% for m in ctx.upstream_metrics %}#: {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit or "") }}`. +METRIC_{{ m.name | screaming_snake_case }} = {{ lit(m.name) }} +#: Unit of the instrument above, as the upstream definition gives it (empty = none). +METRIC_{{ m.name | screaming_snake_case }}_UNIT = {{ lit(m.unit or "") }} +{% endfor %}{% endif %} +# Event names (OTLP `LogRecord.event_name`). +{% for e in ctx.events %}#: {{ line(e.brief) }} +EVENT_{{ e.name | screaming_snake_case }} = {{ lit(e.name) }} +{% endfor %} +# Span names. +{% for s in ctx.spans %}#: {{ line(s.brief) }} Kind: {{ s.kind }}. +SPAN_{{ s.type | screaming_snake_case }} = {{ lit(s.type) }} +{% endfor %} +# Checkpoint names: span events of the spans above. `lk.reconnect` names its attempts +# `attempt ` instead. +{% for c in ctx.checkpoints %}#: {{ line(c.brief) }} +CHECKPOINT_{{ c.name | screaming_snake_case }} = {{ lit(c.name) }} +{% endfor %}{% if ctx.metrics %} +# Metric names. +{% for m in ctx.metrics %}#: {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit or "") }}`. +METRIC_{{ m.name | screaming_snake_case }} = {{ lit(m.name) }} +#: Unit of the instrument above, exactly as the producer creates it (empty = none). +METRIC_{{ m.name | screaming_snake_case }}_UNIT = {{ lit(m.unit or "") }} +{% endfor %}{% endif %} diff --git a/otel/templates/registry/python/weaver.yaml b/otel/templates/registry/python/weaver.yaml new file mode 100644 index 000000000..eb93a2900 --- /dev/null +++ b/otel/templates/registry/python/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.py.j2 + filter: '(.registry.attributes | map(.key)) as $local | {attributes: .registry.attributes | sort_by(.key), upstream: ([.registry.spans[], .registry.events[], .registry.metrics[], .registry.attribute_groups[] | (.attributes // [])[] | select(.key as $k | $local | index($k) | not)] | group_by(.key) | map(.[0] + {shipped_values: ([.[] | (.annotations.livekit.shipped_values // [])[]] | unique_by(.value))})), events: .registry.events | sort_by(.name), spans: .registry.spans | sort_by(.type), checkpoints: [.registry.spans[] | (.annotations.livekit.checkpoints // [])[]] | unique_by(.name), metrics: [.registry.metrics[] | select(.provenance.source == null)] | sort_by(.name), upstream_metrics: [.registry.metrics[] | select(.provenance.source != null)] | sort_by(.name)}' + application_mode: single + file_name: semconv.py diff --git a/otel/templates/registry/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 new file mode 100644 index 000000000..14e0cf373 --- /dev/null +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -0,0 +1,73 @@ +{#- A Rust string literal: anything but `"`, `\` and a bare CR may appear raw; LF and tab are + escaped too so every constant stays on one line. -#} +{%- macro lit(s) -%} +"{{ s | replace("\\", "\\\\") | replace("\"", "\\\"") | replace("\r", "\\r") | replace("\n", "\\n") | replace("\t", "\\t") }}" +{%- endmacro -%} +{#- One `///` line: a newline would end the comment. -#} +{%- macro line(s) -%} +{{ s | trim | replace("\r", " ") | replace("\n", " ") }} +{%- endmacro -%} +{%- macro doc(a) -%} +{{ line(a.brief) }}{% if a.annotations and a.annotations.livekit and a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations and a.annotations.livekit and a.annotations.livekit.unit %} Unit: `{{ line(a.annotations.livekit.unit) }}`.{% endif %} +{%- endmacro -%} +{#- An attribute constant with, for an enum, a module of its values. -#} +{%- macro attr(a) -%} +/// {{ doc(a) }} +pub const {{ a.key | screaming_snake_case }}: &str = {{ lit(a.key) }}; +{% if a.type is mapping or a.shipped_values %} +/// Values of [`{{ a.key | screaming_snake_case }}`]. +pub mod {{ a.key | snake_case }} { +{% for m in (a.type.members if a.type is mapping else []) %} /// {{ line(m.brief) or "`" ~ m.value ~ "`." }} + pub const {{ m.id | screaming_snake_case }}: &str = {{ lit(m.value) }}; +{% endfor %}{% for v in a.shipped_values or [] %} /// {{ line(v.brief) }} Sent by LiveKit, not an upstream value. + pub const {{ v.id | screaming_snake_case }}: &str = {{ lit(v.value) }}; +{% endfor %}} +{% endif %} +{%- endmacro -%} +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +//! LiveKit telemetry semantic conventions. +//! +//! In a `log` key-value list, write a constant in parentheses, `(LK_PII_ERROR) = value`: `log` +//! stringifies a bare identifier, so `LK_PII_ERROR = value` sends the key `LK_PII_ERROR`. +{% for a in ctx.attributes %} +{{ attr(a) }}{% endfor %} +/// Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, +/// never here), so a producer can name them from one place. +pub mod upstream { +{% for a in ctx.upstream %} {{ attr(a) | indent(4) }} +{% endfor %}{% if ctx.upstream_metrics %} + /// Upstream metrics that LiveKit producers create, imported from the registry's dependencies. + pub mod metric { +{% for m in ctx.upstream_metrics %} /// {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit or "") }}`. + pub const {{ m.name | screaming_snake_case }}: &str = {{ lit(m.name) }}; + /// Unit of [`{{ m.name | screaming_snake_case }}`], as the upstream definition gives it (empty = none). + pub const {{ m.name | screaming_snake_case }}_UNIT: &str = {{ lit(m.unit or "") }}; +{% endfor %} } +{% endif %}} +/// Event names (OTLP `LogRecord.event_name`). +pub mod event { +{% for e in ctx.events %} /// {{ line(e.brief) }} + pub const {{ e.name | screaming_snake_case }}: &str = {{ lit(e.name) }}; +{% endfor %}} + +/// Span names. +pub mod span { +{% for s in ctx.spans %} /// {{ line(s.brief) }} Kind: {{ s.kind }}. + pub const {{ s.type | screaming_snake_case }}: &str = {{ lit(s.type) }}; +{% endfor %}} + +/// Checkpoint names: span events of the spans above. `lk.reconnect` names its attempts +/// `attempt ` instead. +pub mod checkpoint { +{% for c in ctx.checkpoints %} /// {{ line(c.brief) }} + pub const {{ c.name | screaming_snake_case }}: &str = {{ lit(c.name) }}; +{% endfor %}} +{% if ctx.metrics %} +/// Metric names. +pub mod metric { +{% for m in ctx.metrics %} /// {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit or "") }}`. + pub const {{ m.name | screaming_snake_case }}: &str = {{ lit(m.name) }}; + /// Unit of [`{{ m.name | screaming_snake_case }}`], exactly as the producer creates the instrument (empty = none). + pub const {{ m.name | screaming_snake_case }}_UNIT: &str = {{ lit(m.unit or "") }}; +{% endfor %}} +{% endif %} diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml new file mode 100644 index 000000000..3e6f0e5a3 --- /dev/null +++ b/otel/templates/registry/rust/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.rs.j2 + filter: '(.registry.attributes | map(.key)) as $local | {attributes: .registry.attributes | sort_by(.key), upstream: ([.registry.spans[], .registry.events[], .registry.metrics[], .registry.attribute_groups[] | (.attributes // [])[] | select(.key as $k | $local | index($k) | not)] | group_by(.key) | map(.[0] + {shipped_values: ([.[] | (.annotations.livekit.shipped_values // [])[]] | unique_by(.value))})), events: .registry.events | sort_by(.name), spans: .registry.spans | sort_by(.type), checkpoints: [.registry.spans[] | (.annotations.livekit.checkpoints // [])[]] | unique_by(.name), metrics: [.registry.metrics[] | select(.provenance.source == null)] | sort_by(.name), upstream_metrics: [.registry.metrics[] | select(.provenance.source != null)] | sort_by(.name)}' + application_mode: single + file_name: semconv.rs diff --git a/otel/templates/registry/typescript/semconv.ts.j2 b/otel/templates/registry/typescript/semconv.ts.j2 new file mode 100644 index 000000000..6c8618ba5 --- /dev/null +++ b/otel/templates/registry/typescript/semconv.ts.j2 @@ -0,0 +1,27 @@ +{#- Literals use tojson: a JSON string (with <>&' escaped too), which is also a valid TS string literal. -#} +{#- One-line JSDoc text: `*/` would end the comment. -#} +{%- macro line(s) -%} +{{ s | trim | replace("\r", " ") | replace("\n", " ") | replace("*/", "*\\/") }} +{%- endmacro -%} +{%- macro doc(a) -%} +{{ line(a.brief) }}{% if a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations.livekit.unit %} Unit: `{{ line(a.annotations.livekit.unit) }}`.{% endif %} +{%- endmacro -%} +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +{% for a in ctx.attributes %} +/** {{ doc(a) }} */ +export const ATTR_{{ a.key | screaming_snake_case }} = {{ a.key | tojson }} as const; +{% if a.type is mapping %}{% for m in a.type.members %}/** {{ line(m.brief) }} */ +export const {{ a.key | screaming_snake_case }}_VALUE_{{ m.id | screaming_snake_case }} = {{ m.value | tojson }} as const; +{% endfor %}{% endif %}{% endfor %} +{% for e in ctx.events %}/** {{ line(e.brief) }} */ +export const EVENT_{{ e.name | screaming_snake_case }} = {{ e.name | tojson }} as const; +{% endfor %} +{% for s in ctx.spans %}/** {{ line(s.brief) }} Kind: {{ s.kind }}. */ +export const SPAN_{{ s.type | screaming_snake_case }} = {{ s.type | tojson }} as const; +{% endfor %} +{% for c in ctx.checkpoints %}/** Checkpoint (span event): {{ line(c.brief) }} */ +export const CHECKPOINT_{{ c.name | screaming_snake_case }} = {{ c.name | tojson }} as const; +{% endfor %}{% for m in ctx.metrics %} +/** {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit) }}`. */ +export const METRIC_{{ m.name | screaming_snake_case }} = {{ m.name | tojson }} as const; +{% endfor %} diff --git a/otel/templates/registry/typescript/weaver.yaml b/otel/templates/registry/typescript/weaver.yaml new file mode 100644 index 000000000..7b17b873f --- /dev/null +++ b/otel/templates/registry/typescript/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.ts.j2 + filter: '{attributes: .registry.attributes | sort_by(.key), events: .registry.events | sort_by(.name), spans: .registry.spans | sort_by(.type), checkpoints: [.registry.spans[] | (.annotations.livekit.checkpoints // [])[]] | unique_by(.name), metrics: .registry.metrics | sort_by(.name)}' + application_mode: single + file_name: semconv.ts diff --git a/otel/tests/codegen.sh b/otel/tests/codegen.sh new file mode 100755 index 000000000..163d6cb46 --- /dev/null +++ b/otel/tests/codegen.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Generates Rust, TypeScript and Python from otel/model plus tests/codegen/escape.yaml, compiles +# them, and checks the tricky enum value comes back byte for byte from rustc, tsc + node and python3. +set -euo pipefail +cd "$(dirname "$0")/.." +W=${WEAVER:-weaver} +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT +want=$'a "quoted" back\\slash, \'single\', & */ tab\tnew\nline' + +cp -R model "$tmp/model" && cp tests/codegen/escape.yaml "$tmp/model/" +for target in rust typescript python; do + "$W" registry generate --v2 -r "$tmp/model" -t templates "$target" "$tmp/$target" > "$tmp/gen.log" 2>&1 || + { cat "$tmp/gen.log"; echo "FAIL weaver could not generate $target" >&2; exit 1; } +done + +rustc --edition 2021 --crate-type lib -D warnings -D missing_docs "$tmp/rust/semconv.rs" --out-dir "$tmp" +printf '#[allow(dead_code)]\n#[path = "%s"]\nmod semconv;\nfn main() { print!("{}", semconv::lk_test_escape::TRICKY); }\n' \ + "$tmp/rust/semconv.rs" > "$tmp/main.rs" +rustc --edition 2021 -D warnings "$tmp/main.rs" -o "$tmp/main" +[[ $("$tmp/main"; printf .) == "$want." ]] || { echo "FAIL Rust literal does not round-trip" >&2; exit 1; } + +npx --yes -p typescript@5.9.3 tsc --strict --target es2020 --module commonjs --outDir "$tmp/js" "$tmp/typescript/semconv.ts" +got=$(node -e 'process.stdout.write(require(process.argv[1]).LK_TEST_ESCAPE_VALUE_TRICKY)' "$tmp/js/semconv.js"; printf .) +[[ $got == "$want." ]] || { echo "FAIL TypeScript literal does not round-trip" >&2; exit 1; } +got=$(python3 -I -c 'import sys, importlib.util; s = importlib.util.spec_from_file_location("semconv", sys.argv[1]); m = importlib.util.module_from_spec(s); s.loader.exec_module(m); sys.stdout.write(m.LK_TEST_ESCAPE_TRICKY)' "$tmp/python/semconv.py"; printf .) +[[ $got == "$want." ]] || { echo "FAIL Python literal does not round-trip" >&2; exit 1; } +echo "PASS codegen: escaped literals round-trip through rustc, tsc and python3" diff --git a/otel/tests/codegen/escape.yaml b/otel/tests/codegen/escape.yaml new file mode 100644 index 000000000..433223f7b --- /dev/null +++ b/otel/tests/codegen/escape.yaml @@ -0,0 +1,18 @@ +# A value and docs no generated literal or comment may break. tests/codegen.sh round-trips the +# value through rustc and tsc+node; keep it in sync with `want` there. +groups: + - id: registry.lk.codegen_test + type: attribute_group + brief: Codegen fixture. + attributes: + - id: lk.test.escape + stability: development + brief: Docs that end a block comment */ and say "quoted" \ things. + type: + members: + - id: tricky + value: "a \"quoted\" back\\slash, 'single', & */ tab\tnew\nline" + brief: "Member docs with */ and a\nsecond line." + stability: development + annotations: + livekit: {owner: client, sensitivity: none} diff --git a/otel/tests/generate.sh b/otel/tests/generate.sh new file mode 100755 index 000000000..dbe9f3c1c --- /dev/null +++ b/otel/tests/generate.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# otel/generate.sh against a stub weaver: a failing target leaves gen/ byte-identical, and a +# successful run replaces gen/ whole, so outputs that no target writes any more disappear. +set -euo pipefail +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT +cp "$(dirname "$0")/../generate.sh" "$tmp/generate.sh" +cat > "$tmp/weaver" <<'STUB' +#!/usr/bin/env bash +# weaver registry generate ... +target=${@: -2:1} out=${@: -1} +[[ $target == "${FAIL_TARGET:-}" ]] && exit 3 +mkdir -p "$out" && echo "new $target" > "$out/semconv.$target" +STUB +chmod +x "$tmp/weaver" +mkdir -p "$tmp/gen/rust" "$tmp/gen/obsolete-dir" +echo old > "$tmp/gen/rust/semconv.rust" +echo old > "$tmp/gen/obsolete.txt" +echo old > "$tmp/gen/obsolete-dir/file" +cp -Rp "$tmp/gen" "$tmp/before" + +if WEAVER=$tmp/weaver FAIL_TARGET=python "$tmp/generate.sh" 2>/dev/null; then + echo "FAIL generate.sh succeeded although the python target failed" >&2; exit 1 +fi +diff -r "$tmp/before" "$tmp/gen" || { echo "FAIL a failing target changed gen/" >&2; exit 1; } + +WEAVER=$tmp/weaver "$tmp/generate.sh" +got=$(cd "$tmp/gen" && find . -mindepth 1 | sort | tr '\n' ' ') +want="./docs ./docs/semconv.markdown ./python ./python/semconv.python ./rust ./rust/semconv.rust ./ts ./ts/semconv.typescript " +[[ $got == "$want" ]] || { echo "FAIL gen/ after a successful run: $got" >&2; exit 1; } +[[ $(cat "$tmp/gen/rust/semconv.rust") == "new rust" ]] || { echo "FAIL gen/ kept an old output" >&2; exit 1; } +echo "PASS generate: a failing target leaves gen/ unchanged, a successful run removes obsolete outputs" diff --git a/otel/tests/live-agents.json b/otel/tests/live-agents.json new file mode 100644 index 000000000..0e4ef42fe --- /dev/null +++ b/otel/tests/live-agents.json @@ -0,0 +1,13 @@ +[ + {"attribute": {"name": "lk.job_id", "value": "AJ_kVbMBWtnVHvT"}}, + {"attribute": {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}}, + {"attribute": {"name": "lk.pii.room_name", "value": "my-room"}}, + {"attribute": {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}}, + {"attribute": {"name": "lk.pii.participant_identity", "value": "identity-1"}}, + {"attribute": {"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}}, + {"attribute": {"name": "lk.agent_name", "value": "my-agent"}}, + {"attribute": {"name": "lk.track_source", "value": "SOURCE_MICROPHONE"}}, + {"attribute": {"name": "lk.track_source", "value": "SOURCE_CAMERA"}}, + {"attribute": {"name": "lk.disconnect_reason", "value": "CLIENT_INITIATED"}}, + {"attribute": {"name": "lk.disconnect_reason", "value": "UNKNOWN_REASON"}} +] diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh new file mode 100755 index 000000000..37bea9821 --- /dev/null +++ b/otel/tests/live-check.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# The smoke sample (a real client capture, anonymized, canonical names) must pass with no +# findings, and so must a sparse RTC window, a JS network event without its flags, the shared keys +# as Agents sends them, and a record without severity text. The same smoke sample with the +# client's current identity/PII keys and spellings of shared keys, an unregistered lk.* span, +# unlisted checkpoints (one empty), outcomes that disagree with the span status (Error for ok, Ok +# for ok) and events at the wrong severity (by text, and by number) must be flagged. Last, a stub +# weaver checks the wrapper: it keeps weaver's status, shows weaver's log when there is no report, +# and never summarizes a report left over from an earlier run. +set -euo pipefail +cd "$(dirname "$0")/.." +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT + +good=$(./live-check.sh tests/live-sample.json "$tmp/good") +echo "$good" +[[ $good == *'"advice_level_counts": {}'* ]] || { echo "FAIL smoke sample has findings" >&2; exit 1; } + +# The core omits every counter and gauge a reading lacks (RtcStatsSample's Option fields). +sparse=$(./live-check.sh tests/live-sparse.json "$tmp/sparse") +echo "$sparse" +[[ $sparse == *'"advice_level_counts": {}'* ]] || { echo "FAIL sparse RTC sample has findings" >&2; exit 1; } + +# A browser without the Network Information API: the network event without its two flags. +js=$(./live-check.sh tests/live-js-network.json "$tmp/js") +echo "$js" +[[ $js == *'"advice_level_counts": {}'* ]] || { echo "FAIL JS network sample has findings" >&2; exit 1; } + +# What Agents sends for the shared keys, as bare attributes (Agents spans have no matchers). +agents=$(./live-check.sh tests/live-agents.json "$tmp/agents" --search-all-attributes) +echo "$agents" +[[ $agents == *'"advice_level_counts": {}'* ]] || { echo "FAIL Agents shared-key sample has findings" >&2; exit 1; } + +# severity_text is optional in OTLP: a record with only a valid severity number passes, whether +# it comes as weaver samples or as OTLP/JSON lines. +for sample in tests/live-no-severity-text.json tests/live-no-severity-text.jsonl; do + out=$(./live-check.sh "$sample" "$tmp/$(basename "$sample")") + echo "$out" + [[ $out == *'"advice_level_counts": {}'* ]] || { echo "FAIL $sample has findings" >&2; exit 1; } +done + +sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.name"/g' \ + -e 's/"name": "lk\.publish"/"name": "lk.bogus"/' \ + -e '/"name": "lk\.connect"/s/"name": "ws_open"/"name": "ws_opened"/' \ + -e '/"name": "lk\.connect"/s/"name": "lk\.outcome", "value": "ok"/"name": "lk.outcome", "value": "error"/' \ + -e '/"lk\.device\.network\.changed"/s/"severity_text": "INFO"/"severity_text": "WARN"/' \ + -e '/"name": "lk\.subscribe"/s/"span_events": \[/"span_events": [{"name": "", "attributes": []}, /' \ + -e '/"name": "lk\.subscribe"/s/"status": {"code": "unset"/"status": {"code": "ok"/' \ + -e '/"event_name": "lk\.rtc\.stats\.sample"/s/"severity_number": 9/"severity_number": 17/' \ + -e 's/"name": "lk\.track_source", "value": "SOURCE_CAMERA"/"name": "lk.track.source", "value": "camera"/g' \ + -e 's/"name": "lk\.disconnect_reason", "value": "CLIENT_INITIATED"/"name": "lk.disconnect.reason", "value": "client_initiated"/' \ + -e '/"name": "lk\.connect"/s/"name": "lk\.pii\.participant_identity"/"name": "lk.caller_identity"/' \ + tests/live-sample.json > "$tmp/legacy.json" +legacy=$(./live-check.sh "$tmp/legacy.json" "$tmp/legacy" --fail-on none) +echo "$legacy" +for want in "livekit_shared_rename lk.room.sid" "livekit_shared_rename lk.room.name" "livekit_unknown_span lk.bogus" \ + "livekit_checkpoint lk.connect" "livekit_span_outcome lk.connect" "livekit_severity lk.device.network.changed" \ + "livekit_checkpoint lk.subscribe" "livekit_span_outcome lk.subscribe" "livekit_severity lk.rtc.stats.sample" \ + "livekit_shared_rename lk.track.source" "livekit_shared_rename lk.disconnect.reason" \ + "livekit_pii_unmarked lk.caller_identity"; do + id=${want% *} key=${want#* } + grep -q -E -- "${id}: .*${key//./\\.}" <<<"$legacy" || { echo "FAIL not flagged: $id on $key" >&2; exit 1; } +done +# The key and value to send come from renamed_from in the model. +for want in "send 'lk.room_sid'" "send 'lk.track_source' = 'SOURCE_CAMERA'" "send 'lk.disconnect_reason' = 'CLIENT_INITIATED'"; do + grep -q -F -- "$want" "$tmp/legacy/live_check.json" || { echo "FAIL no finding says: $want" >&2; exit 1; } +done + +cat > "$tmp/weaver" <<'STUB' +#!/usr/bin/env bash +# weaver registry live-check ... -o : copies $STUB_REPORT there when set, exits $STUB_STATUS. +while [[ $# -gt 0 ]]; do [[ $1 == -o ]] && dir=$2; shift; done +echo "stub weaver: diagnostic" +[[ -n ${STUB_REPORT:-} ]] && cp "$STUB_REPORT" "$dir/live_check.json" +exit "${STUB_STATUS:-0}" +STUB +chmod +x "$tmp/weaver" +# stub : sets $res (output) and $st (exit). +stub() { st=0; res=$(STUB_STATUS=$1 STUB_REPORT=$2 WEAVER=$tmp/weaver ./live-check.sh tests/live-sample.json "$3" 2>&1) || st=$?; } +fail() { echo "FAIL $1 (exit $st): $res" >&2; exit 1; } +report=$tmp/legacy/live_check.json # a valid report with violations, from the run above + +stub 7 "" "$tmp/stub7" +[[ $st == 7 && $res == *"stub weaver: diagnostic"* && $res != *Traceback* && $res != *"report:"* ]] || + fail "no report, weaver exit 7: must show the weaver log and exit 7" +stub 0 "" "$tmp/stub0" +[[ $st == 1 && $res == *"stub weaver: diagnostic"* && $res != *"report:"* ]] || + fail "no report, weaver exit 0: must show the weaver log and exit 1" +stub 1 "$report" "$tmp/stub1" +[[ $st == 1 && $res == *"totals:"* && $res == *"report: $tmp/stub1/live_check.json"* ]] || + fail "valid report, weaver exit 1: must summarize the report and exit 1" +mkdir -p "$tmp/reused" && cp "$report" "$tmp/reused/live_check.json" && echo keep > "$tmp/reused/unrelated.txt" +stub 7 "" "$tmp/reused" +[[ $st == 7 && $res != *"totals:"* && $res != *"report:"* && -f $tmp/reused/unrelated.txt && ! -e $tmp/reused/live_check.json ]] || + fail "stale report in a reused directory: must not be summarized, and an unrelated file must stay" +echo "PASS live-check: smoke, sparse, JS, Agents and no-severity-text samples clean, every planted violation flagged, wrapper status and stale-report cases" diff --git a/otel/tests/live-js-network.json b/otel/tests/live-js-network.json new file mode 100644 index 000000000..655c85758 --- /dev/null +++ b/otel/tests/live-js-network.json @@ -0,0 +1,5 @@ +[ + {"resource": {"attributes": [{"name": "service.name", "value": "livekit-client-js"}, {"name": "service.version", "value": "2.15.0"}, {"name": "telemetry.sdk.name", "value": "livekit-telemetry"}, {"name": "telemetry.sdk.language", "value": "webjs"}, {"name": "telemetry.sdk.version", "value": "0.1.0"}, {"name": "browser.name", "value": "Firefox"}, {"name": "browser.version", "value": "131.0"}]}}, + {"log": {"event_name": "lk.device.network.changed", "severity_number": 9, "severity_text": "INFO", "body": "lk.device.network.changed", "attributes": [{"name": "network.connection.type", "value": "unknown"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.device.network.changed"}]}}, + {"log": {"event_name": "lk.device.network.changed", "severity_number": 9, "severity_text": "INFO", "body": "lk.device.network.changed", "attributes": [{"name": "network.connection.type", "value": "unavailable"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.device.network.changed"}]}} +] diff --git a/otel/tests/live-no-severity-text.json b/otel/tests/live-no-severity-text.json new file mode 100644 index 000000000..af2a64925 --- /dev/null +++ b/otel/tests/live-no-severity-text.json @@ -0,0 +1,3 @@ +[ + {"log": {"event_name": "lk.device.app_state.changed", "severity_number": 9, "body": "app: foreground", "attributes": [{"name": "lk.device.app_state", "value": "foreground"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.device.app_state.changed"}]}} +] diff --git a/otel/tests/live-no-severity-text.jsonl b/otel/tests/live-no-severity-text.jsonl new file mode 100644 index 000000000..67c50f686 --- /dev/null +++ b/otel/tests/live-no-severity-text.jsonl @@ -0,0 +1 @@ +{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"livekit-client-swift"}},{"key":"service.version","value":{"stringValue":"2.17.0"}},{"key":"telemetry.sdk.name","value":{"stringValue":"livekit-telemetry"}},{"key":"telemetry.sdk.language","value":{"stringValue":"rust"}},{"key":"telemetry.sdk.version","value":{"stringValue":"0.1.0"}}]},"scopeLogs":[{"scope":{"name":"livekit-telemetry","version":"0.1.0"},"logRecords":[{"timeUnixNano":"1760000000000000000","severityNumber":9,"body":{"stringValue":"app: foreground"},"eventName":"lk.device.app_state.changed","attributes":[{"key":"lk.device.app_state","value":{"stringValue":"foreground"}},{"key":"session.id","value":{"stringValue":"4bf92f3577b34da6a3ce929d0e0e4736"}},{"key":"otel.event.name","value":{"stringValue":"lk.device.app_state.changed"}}]}]}]}]} diff --git a/otel/tests/live-sample.json b/otel/tests/live-sample.json new file mode 100644 index 000000000..60457def8 --- /dev/null +++ b/otel/tests/live-sample.json @@ -0,0 +1,11 @@ +[ + {"resource": {"attributes": [{"name": "service.name", "value": "livekit-client-swift"}, {"name": "service.version", "value": "2.17.0"}, {"name": "os.name", "value": "iOS"}, {"name": "os.version", "value": "18.5"}, {"name": "device.model.identifier", "value": "iPhone16,1"}, {"name": "telemetry.sdk.name", "value": "livekit-telemetry"}, {"name": "telemetry.sdk.language", "value": "rust"}, {"name": "telemetry.sdk.version", "value": "0.1.0"}]}}, + {"span": {"name": "lk.connect", "kind": "client", "status": {"code": "unset", "message": ""}, "attributes": [{"name": "lk.connect.attempt", "value": 1}, {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}, {"name": "lk.pii.room_name", "value": "my-room"}, {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}, {"name": "lk.pii.participant_identity", "value": "identity-0"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "lk.outcome", "value": "ok"}], "span_events": [{"name": "ws_open", "attributes": []}, {"name": "signal", "attributes": []}, {"name": "join_recv", "attributes": []}, {"name": "pc_created", "attributes": []}, {"name": "offer_sent", "attributes": []}, {"name": "engine", "attributes": []}, {"name": "pc_connected", "attributes": []}, {"name": "room_connected", "attributes": []}], "span_links": []}}, + {"span": {"name": "lk.publish", "kind": "internal", "status": {"code": "unset", "message": ""}, "attributes": [{"name": "lk.track.kind", "value": "video"}, {"name": "lk.track_source", "value": "SOURCE_CAMERA"}, {"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}, {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}, {"name": "lk.pii.room_name", "value": "my-room"}, {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}, {"name": "lk.pii.participant_identity", "value": "identity-0"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "lk.outcome", "value": "ok"}], "span_events": [], "span_links": []}}, + {"span": {"name": "lk.subscribe", "kind": "internal", "status": {"code": "unset", "message": ""}, "attributes": [{"name": "lk.track.kind", "value": "video"}, {"name": "lk.track_source", "value": "SOURCE_CAMERA"}, {"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}, {"name": "lk.pii.remote_participant_identity", "value": "identity-1"}, {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}, {"name": "lk.pii.room_name", "value": "my-room"}, {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}, {"name": "lk.pii.participant_identity", "value": "identity-0"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "lk.outcome", "value": "ok"}], "span_events": [{"name": "subscribed", "attributes": []}, {"name": "first_media", "attributes": []}], "span_links": []}}, + {"log": {"event_name": "lk.device.network.changed", "severity_number": 9, "severity_text": "INFO", "body": "network: wifi", "attributes": [{"name": "network.connection.type", "value": "wifi"}, {"name": "lk.device.network.expensive", "value": false}, {"name": "lk.device.network.constrained", "value": false}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.device.network.changed"}]}}, + {"log": {"event_name": "", "severity_number": 17, "severity_text": "ERROR", "body": "Participant active", "attributes": [{"name": "lk.log.source", "value": "sdk"}, {"name": "lk.log.logger", "value": "Room"}, {"name": "code.function.name", "value": "connect(url:token:)"}, {"name": "code.file.path", "value": "Sources/LiveKit/Core/Room.swift"}, {"name": "code.line.number", "value": 161}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}]}}, + {"log": {"event_name": "lk.rtc.stats.sample", "severity_number": 9, "severity_text": "INFO", "body": "video inbound: 33 kbps, loss 0.0%, rtt 0 ms, 10 fps", "attributes": [{"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}, {"name": "lk.track.kind", "value": "video"}, {"name": "lk.track.direction", "value": "inbound"}, {"name": "lk.rtc.window_ms", "value": 55780}, {"name": "lk.rtc.samples", "value": 2}, {"name": "lk.rtc.codec", "value": "video/VP8"}, {"name": "lk.rtc.bytes", "value": 251478}, {"name": "lk.rtc.packets", "value": 373}, {"name": "lk.rtc.packets_lost", "value": 0}, {"name": "lk.rtc.freeze_count", "value": 0}, {"name": "lk.rtc.freezes_duration_ms", "value": 0}, {"name": "lk.rtc.jitter_buffer_delay_ms", "value": 3999}, {"name": "lk.rtc.jitter_buffer_emitted_count", "value": 287}, {"name": "lk.rtc.pause_count", "value": 0}, {"name": "lk.rtc.pauses_duration_ms", "value": 0}, {"name": "lk.rtc.jitter_ms.min", "value": 0.0}, {"name": "lk.rtc.jitter_ms.max", "value": 0.0}, {"name": "lk.rtc.jitter_ms.avg", "value": 0.0}, {"name": "lk.rtc.rtt_ms.min", "value": 0.0}, {"name": "lk.rtc.rtt_ms.max", "value": 1.0}, {"name": "lk.rtc.rtt_ms.avg", "value": 0.5}, {"name": "lk.rtc.fps.min", "value": 10.0}, {"name": "lk.rtc.fps.max", "value": 10.0}, {"name": "lk.rtc.fps.avg", "value": 10.0}, {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}, {"name": "lk.pii.room_name", "value": "my-room"}, {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}, {"name": "lk.pii.participant_identity", "value": "identity-0"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.rtc.stats.sample"}]}}, + {"log": {"event_name": "lk.rtc.stats.sample", "severity_number": 9, "severity_text": "INFO", "body": "video outbound: 33 kbps, rtt 1 ms, 7 fps", "attributes": [{"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}, {"name": "lk.track.kind", "value": "video"}, {"name": "lk.track.direction", "value": "outbound"}, {"name": "lk.rtc.window_ms", "value": 55884}, {"name": "lk.rtc.samples", "value": 2}, {"name": "lk.rtc.codec", "value": "video/VP8"}, {"name": "lk.rtc.bytes", "value": 262918}, {"name": "lk.rtc.packets", "value": 399}, {"name": "lk.rtc.quality_limitation.bandwidth_ms", "value": 0}, {"name": "lk.rtc.quality_limitation.cpu_ms", "value": 0}, {"name": "lk.rtc.quality_limitation.other_ms", "value": 0}, {"name": "lk.rtc.rtt_ms.min", "value": 1.0}, {"name": "lk.rtc.rtt_ms.max", "value": 1.0}, {"name": "lk.rtc.rtt_ms.avg", "value": 1.0}, {"name": "lk.rtc.fps.min", "value": 4.0}, {"name": "lk.rtc.fps.max", "value": 10.0}, {"name": "lk.rtc.fps.avg", "value": 7.0}, {"name": "lk.room_sid", "value": "RM_5Fq8xQ2ZbXwN"}, {"name": "lk.pii.room_name", "value": "my-room"}, {"name": "lk.participant_id", "value": "PA_9kLm2NpQ4rTs"}, {"name": "lk.pii.participant_identity", "value": "identity-0"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.rtc.stats.sample"}]}}, + {"log": {"event_name": "lk.room.disconnected", "severity_number": 9, "severity_text": "INFO", "body": "disconnected: client_initiated", "attributes": [{"name": "lk.disconnect_reason", "value": "CLIENT_INITIATED"}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.room.disconnected"}]}} +] diff --git a/otel/tests/live-sparse.json b/otel/tests/live-sparse.json new file mode 100644 index 000000000..16382b415 --- /dev/null +++ b/otel/tests/live-sparse.json @@ -0,0 +1,3 @@ +[ + {"log": {"event_name": "lk.rtc.stats.sample", "severity_number": 9, "severity_text": "INFO", "body": "video inbound", "attributes": [{"name": "lk.track_sid", "value": "TR_AMkTyV3Zx8Qp"}, {"name": "lk.track.kind", "value": "video"}, {"name": "lk.track.direction", "value": "inbound"}, {"name": "lk.rtc.window_ms", "value": 60000}, {"name": "lk.rtc.samples", "value": 1}, {"name": "session.id", "value": "4bf92f3577b34da6a3ce929d0e0e4736"}, {"name": "otel.event.name", "value": "lk.rtc.stats.sample"}]}} +] diff --git a/otel/tests/negatives.sh b/otel/tests/negatives.sh new file mode 100755 index 000000000..e993ecfc5 --- /dev/null +++ b/otel/tests/negatives.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Each otel/tests/negatives/.yaml is added to a copy of otel/model; `weaver registry +# check` must then fail and name the finding on the fixture's `# expect:` line, in the default +# mode and with --v2 (the mode generate.sh runs the policies in). +set -uo pipefail +cd "$(dirname "$0")/.." || exit +W=${WEAVER:-weaver} +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT +failed=0 +for fixture in tests/negatives/*.yaml; do + expect=$(sed -n '1s/^# expect: //p' "$fixture") + model=$tmp/$(basename "$fixture" .yaml) + cp -R model "$model" && cp "$fixture" "$model/negative.yaml" + for mode in default --v2; do + flags=() && [[ $mode == --v2 ]] && flags=(--v2) + out=$("$W" registry check ${flags[@]+"${flags[@]}"} -r "$model" -p policies 2>&1 | sed $'s/\x1b\\[[0-9;]*m//g') + status=${PIPESTATUS[0]} + if [[ $status != 0 && -n $expect && $out == *"$expect"* ]]; then + echo "PASS $fixture ($mode) fails with: $expect" + else + echo "FAIL $fixture ($mode, exit $status), expected: $expect"; echo "$out" | tail -20; failed=1 + fi + done +done +exit "$failed" diff --git a/otel/tests/negatives/cardinality-extends.yaml b/otel/tests/negatives/cardinality-extends.yaml new file mode 100644 index 000000000..16d5f5e16 --- /dev/null +++ b/otel/tests/negatives/cardinality-extends.yaml @@ -0,0 +1,16 @@ +# expect: livekit_cardinality +groups: + - id: attributes.lk.negative + type: attribute_group + brief: Attributes a metric inherits. + attributes: + - ref: lk.room_sid + requirement_level: required + - id: metric.client.negative.count + type: metric + metric_name: client.negative.count + instrument: counter + unit: "{event}" + stability: development + brief: A metric inheriting a high-cardinality dimension through extends. + extends: attributes.lk.negative diff --git a/otel/tests/negatives/cardinality-refinement.yaml b/otel/tests/negatives/cardinality-refinement.yaml new file mode 100644 index 000000000..d72df98ec --- /dev/null +++ b/otel/tests/negatives/cardinality-refinement.yaml @@ -0,0 +1,15 @@ +# expect: livekit_cardinality +file_format: definition/2 +metrics: + - name: client.negative.count + instrument: counter + unit: "{event}" + stability: development + brief: A base metric without high-cardinality dimensions. +metric_refinements: + - id: client.negative.count.by_room + ref: client.negative.count + brief: A refinement that adds a high-cardinality dimension. + attributes: + - ref: lk.room_sid + requirement_level: required diff --git a/otel/tests/negatives/cardinality-v2.yaml b/otel/tests/negatives/cardinality-v2.yaml new file mode 100644 index 000000000..76f48d2b2 --- /dev/null +++ b/otel/tests/negatives/cardinality-v2.yaml @@ -0,0 +1,11 @@ +# expect: livekit_cardinality +file_format: definition/2 +metrics: + - name: client.negative.count + instrument: counter + unit: "{event}" + stability: development + brief: A definition/2 metric with a high-cardinality dimension. + attributes: + - ref: lk.room_sid + requirement_level: required diff --git a/otel/tests/negatives/cardinality.yaml b/otel/tests/negatives/cardinality.yaml new file mode 100644 index 000000000..8fcc2ad22 --- /dev/null +++ b/otel/tests/negatives/cardinality.yaml @@ -0,0 +1,12 @@ +# expect: livekit_cardinality +groups: + - id: metric.client.reconnect.count + type: metric + metric_name: client.reconnect.count + instrument: counter + unit: "{reconnect}" + stability: development + brief: A high-cardinality metric dimension. + attributes: + - ref: lk.room_sid + requirement_level: required diff --git a/otel/tests/negatives/duplicate.yaml b/otel/tests/negatives/duplicate.yaml new file mode 100644 index 000000000..ec335dfb8 --- /dev/null +++ b/otel/tests/negatives/duplicate.yaml @@ -0,0 +1,11 @@ +# expect: is declared multiple times +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.room_sid + type: int + stability: development + brief: Defined twice. + annotations: {livekit: {owner: shared, sensitivity: none}} diff --git a/otel/tests/negatives/flat-identity.yaml b/otel/tests/negatives/flat-identity.yaml new file mode 100644 index 000000000..5b68a859c --- /dev/null +++ b/otel/tests/negatives/flat-identity.yaml @@ -0,0 +1,11 @@ +# expect: livekit_flat_identity +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.room.sid + type: string + stability: development + brief: Dotted shared identity. + annotations: {livekit: {owner: shared, sensitivity: none}} diff --git a/otel/tests/negatives/metric-unit-v2.yaml b/otel/tests/negatives/metric-unit-v2.yaml new file mode 100644 index 000000000..200a493f1 --- /dev/null +++ b/otel/tests/negatives/metric-unit-v2.yaml @@ -0,0 +1,8 @@ +# expect: livekit_metric_unit +file_format: definition/2 +metrics: + - name: client.negative.duration + instrument: histogram + unit: ms + stability: development + brief: A definition/2 metric in milliseconds. diff --git a/otel/tests/negatives/metric-unit.yaml b/otel/tests/negatives/metric-unit.yaml new file mode 100644 index 000000000..3fae0b2f2 --- /dev/null +++ b/otel/tests/negatives/metric-unit.yaml @@ -0,0 +1,9 @@ +# expect: livekit_metric_unit +groups: + - id: metric.client.connection.duration + type: metric + metric_name: client.connection.duration + instrument: histogram + unit: ms + stability: development + brief: A metric in milliseconds. diff --git a/otel/tests/negatives/owner.yaml b/otel/tests/negatives/owner.yaml new file mode 100644 index 000000000..5b54538b7 --- /dev/null +++ b/otel/tests/negatives/owner.yaml @@ -0,0 +1,11 @@ +# expect: livekit_owner +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.retry_reason + type: string + stability: development + brief: No owner. + annotations: {livekit: {sensitivity: none}} diff --git a/otel/tests/negatives/pii-heuristic-exception.yaml b/otel/tests/negatives/pii-heuristic-exception.yaml new file mode 100644 index 000000000..de9397496 --- /dev/null +++ b/otel/tests/negatives/pii-heuristic-exception.yaml @@ -0,0 +1,14 @@ +# expect: livekit_pii_heuristic +# A key outside the policy's pii_heuristic_allowlist cannot opt out by annotating itself. +groups: + - id: registry.negative.pii_heuristic_exception + type: attribute_group + brief: negative + attributes: + - id: lk.rpc.other_identity + type: string + stability: development + brief: Looks like PII, outside lk.pii.*, with the annotation but not on the allowlist. + examples: ["identity-0"] + annotations: + livekit: {owner: agents, sensitivity: none, pii_heuristic_exception: "not good enough on its own"} diff --git a/otel/tests/negatives/pii-heuristic.yaml b/otel/tests/negatives/pii-heuristic.yaml new file mode 100644 index 000000000..f76689ae0 --- /dev/null +++ b/otel/tests/negatives/pii-heuristic.yaml @@ -0,0 +1,11 @@ +# expect: livekit_pii_heuristic +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.participant.identity + type: string + stability: development + brief: PII without the pii segment. + annotations: {livekit: {owner: shared, sensitivity: none}} diff --git a/otel/tests/negatives/pii-marker.yaml b/otel/tests/negatives/pii-marker.yaml new file mode 100644 index 000000000..4e225415f --- /dev/null +++ b/otel/tests/negatives/pii-marker.yaml @@ -0,0 +1,11 @@ +# expect: livekit_pii_marker +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.pii.client_note + type: string + stability: development + brief: A pii segment on a non-PII key. + annotations: {livekit: {owner: client, sensitivity: none}} diff --git a/otel/tests/negatives/ref-override-drop-owner.yaml b/otel/tests/negatives/ref-override-drop-owner.yaml new file mode 100644 index 000000000..95704c0de --- /dev/null +++ b/otel/tests/negatives/ref-override-drop-owner.yaml @@ -0,0 +1,12 @@ +# expect: livekit_ref_override +# A ref that drops chat.item's owner while it re-labels the key as non-PII. +groups: + - id: span.lk.review.ref-drop-owner + type: span + span_kind: internal + stability: development + brief: Review ref override probe. + attributes: + - ref: chat.item + requirement_level: recommended + annotations: {livekit: {sensitivity: none}} diff --git a/otel/tests/negatives/ref-override-empty-annotations.yaml b/otel/tests/negatives/ref-override-empty-annotations.yaml new file mode 100644 index 000000000..a7b7ce73e --- /dev/null +++ b/otel/tests/negatives/ref-override-empty-annotations.yaml @@ -0,0 +1,12 @@ +# expect: livekit_ref_override +# A ref that empties chat.item's annotations. +groups: + - id: span.lk.review.ref-empty-annotations + type: span + span_kind: internal + stability: development + brief: Review ref override probe. + attributes: + - ref: chat.item + requirement_level: recommended + annotations: {} diff --git a/otel/tests/negatives/ref-override-null-owner.yaml b/otel/tests/negatives/ref-override-null-owner.yaml new file mode 100644 index 000000000..2b71fcabe --- /dev/null +++ b/otel/tests/negatives/ref-override-null-owner.yaml @@ -0,0 +1,12 @@ +# expect: livekit_ref_override +# A ref that sets a prefixed key's owner to null while it re-labels the key as non-PII. +groups: + - id: span.lk.review.ref-null-owner + type: span + span_kind: internal + stability: development + brief: Review ref override probe. + attributes: + - ref: lk.pii.room_name + requirement_level: recommended + annotations: {livekit: {owner: null, sensitivity: none}} diff --git a/otel/tests/negatives/ref-override-unprefixed.yaml b/otel/tests/negatives/ref-override-unprefixed.yaml new file mode 100644 index 000000000..093c168f8 --- /dev/null +++ b/otel/tests/negatives/ref-override-unprefixed.yaml @@ -0,0 +1,11 @@ +# expect: livekit_ref_override +file_format: definition/2 +attribute_groups: + - id: livekit.negative + visibility: public + stability: development + brief: A ref that re-labels an unprefixed LiveKit key as non-PII. + attributes: + - ref: chat.item + requirement_level: recommended + annotations: {livekit: {owner: agents, sensitivity: none}} diff --git a/otel/tests/negatives/ref-override-v2.yaml b/otel/tests/negatives/ref-override-v2.yaml new file mode 100644 index 000000000..f62794bb5 --- /dev/null +++ b/otel/tests/negatives/ref-override-v2.yaml @@ -0,0 +1,11 @@ +# expect: livekit_ref_override +file_format: definition/2 +attribute_groups: + - id: livekit.negative + visibility: public + stability: development + brief: A definition/2 ref that overrides the referenced key's classification. + attributes: + - ref: lk.pii.room_name + requirement_level: recommended + annotations: {livekit: {owner: shared, sensitivity: none}} diff --git a/otel/tests/negatives/ref-override.yaml b/otel/tests/negatives/ref-override.yaml new file mode 100644 index 000000000..8d60d1b26 --- /dev/null +++ b/otel/tests/negatives/ref-override.yaml @@ -0,0 +1,11 @@ +# expect: livekit_ref_override +groups: + - id: span.lk.negative + type: span + span_kind: internal + stability: development + brief: A ref that overrides the referenced key's classification. + attributes: + - ref: lk.room_sid + requirement_level: required + annotations: {livekit: {owner: bogus, sensitivity: pii}} diff --git a/otel/tests/negatives/rename-value.yaml b/otel/tests/negatives/rename-value.yaml new file mode 100644 index 000000000..5e2fb7ec9 --- /dev/null +++ b/otel/tests/negatives/rename-value.yaml @@ -0,0 +1,17 @@ +# expect: livekit_rename_value +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.negative_mode + stability: development + brief: A rename whose value map points outside the enum. + type: + members: + - {id: source_on, value: SOURCE_ON, brief: On., stability: development} + annotations: + livekit: + owner: shared + sensitivity: none + renamed_from: {key: lk.negative.mode, values: {on: SOURCE_ON, off: SOURCE_OFF}} diff --git a/otel/tests/negatives/rtt-aggregate.yaml b/otel/tests/negatives/rtt-aggregate.yaml new file mode 100644 index 000000000..3392916bb --- /dev/null +++ b/otel/tests/negatives/rtt-aggregate.yaml @@ -0,0 +1,11 @@ +# expect: livekit_unit +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.rtt.avg + type: double + stability: development + brief: An aggregated duration in ms without a unit_exception. + annotations: {livekit: {owner: client, sensitivity: none, unit: ms}} diff --git a/otel/tests/negatives/seconds.yaml b/otel/tests/negatives/seconds.yaml new file mode 100644 index 000000000..4e68d81a5 --- /dev/null +++ b/otel/tests/negatives/seconds.yaml @@ -0,0 +1,11 @@ +# expect: livekit_seconds +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.setup_ms + type: int + stability: development + brief: Milliseconds without a unit_exception. + annotations: {livekit: {owner: client, sensitivity: none, unit: ms}} diff --git a/otel/tests/negatives/secret.yaml b/otel/tests/negatives/secret.yaml new file mode 100644 index 000000000..b42d6a379 --- /dev/null +++ b/otel/tests/negatives/secret.yaml @@ -0,0 +1,11 @@ +# expect: livekit_secret +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.api_secret + type: string + stability: development + brief: A secret. + annotations: {livekit: {owner: client, sensitivity: secret}} diff --git a/otel/tests/negatives/sensitivity.yaml b/otel/tests/negatives/sensitivity.yaml new file mode 100644 index 000000000..322d1f13b --- /dev/null +++ b/otel/tests/negatives/sensitivity.yaml @@ -0,0 +1,11 @@ +# expect: livekit_sensitivity +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.retry_reason + type: string + stability: development + brief: Unknown sensitivity. + annotations: {livekit: {owner: client, sensitivity: public}} diff --git a/otel/tests/negatives/shared-rename-deprecated.yaml b/otel/tests/negatives/shared-rename-deprecated.yaml new file mode 100644 index 000000000..cb55842f1 --- /dev/null +++ b/otel/tests/negatives/shared-rename-deprecated.yaml @@ -0,0 +1,22 @@ +# expect: livekit_shared_rename +# Codex round 5: a deprecated old spelling, referenced by a span, slipped past the rule. +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.track.source + type: string + stability: development + brief: The client spelling of the shared key lk.track_source, defined again as deprecated. + examples: ["camera"] + deprecated: {reason: uncategorized, note: Legacy key still accepted.} + annotations: {livekit: {owner: client, sensitivity: none}} + - id: span.lk.probe + type: span + span_kind: internal + stability: development + brief: A span that requires the deprecated old spelling. + attributes: + - ref: lk.track.source + requirement_level: required diff --git a/otel/tests/negatives/shared-rename.yaml b/otel/tests/negatives/shared-rename.yaml new file mode 100644 index 000000000..955666179 --- /dev/null +++ b/otel/tests/negatives/shared-rename.yaml @@ -0,0 +1,12 @@ +# expect: livekit_shared_rename +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.track.source + type: string + stability: development + brief: The client spelling of the shared key lk.track_source, defined again. + examples: ["camera"] + annotations: {livekit: {owner: client, sensitivity: none}} diff --git a/otel/tests/negatives/shipped-value-camelcase-duplicate.yaml b/otel/tests/negatives/shipped-value-camelcase-duplicate.yaml new file mode 100644 index 000000000..a7a021405 --- /dev/null +++ b/otel/tests/negatives/shipped-value-camelcase-duplicate.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# Codex r2 duplicate-camelcase: otherName and other_name would generate one constant; otherName is not lowercase snake_case. +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: otherName, value: custom1, brief: fake}, {id: other_name, value: custom2, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-camelcase-id.yaml b/otel/tests/negatives/shipped-value-camelcase-id.yaml new file mode 100644 index 000000000..1fa64b4b1 --- /dev/null +++ b/otel/tests/negatives/shipped-value-camelcase-id.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# Codex r2 normalized-camelcase: id invokeAgent is not lowercase snake_case (it would generate INVOKE_AGENT, the upstream invoke_agent constant). +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: invokeAgent, value: custom, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-definition.yaml b/otel/tests/negatives/shipped-value-definition.yaml new file mode 100644 index 000000000..7709e7de0 --- /dev/null +++ b/otel/tests/negatives/shipped-value-definition.yaml @@ -0,0 +1,17 @@ +# expect: livekit_shipped_value +# A shipped_values list on a standalone definition: --v2 must check definitions, not only refs. +groups: + - id: registry.review + type: attribute_group + brief: review + attributes: + - id: lk.review + type: string + stability: development + brief: review + examples: ["fake"] + annotations: + livekit: + owner: agents + sensitivity: none + shipped_values: [{id: fake, value: fake, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-duplicate-id.yaml b/otel/tests/negatives/shipped-value-duplicate-id.yaml new file mode 100644 index 000000000..a82de2f0a --- /dev/null +++ b/otel/tests/negatives/shipped-value-duplicate-id.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# Two shipped entries with the same id generate the same constant twice. +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: other, value: custom1, brief: fake}, {id: other, value: custom2, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-normalized-id.yaml b/otel/tests/negatives/shipped-value-normalized-id.yaml new file mode 100644 index 000000000..08739f7ce --- /dev/null +++ b/otel/tests/negatives/shipped-value-normalized-id.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# Id actian_ingres generates the same constant as the upstream member actian.ingres. +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: db.system.name + annotations: + livekit: + shipped_values: [{id: actian_ingres, value: custom, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-numeric-enum.yaml b/otel/tests/negatives/shipped-value-numeric-enum.yaml new file mode 100644 index 000000000..fc4253da9 --- /dev/null +++ b/otel/tests/negatives/shipped-value-numeric-enum.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# An upstream enum with integer values (rpc.grpc.status_code) cannot take string shipped values. +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: rpc.grpc.status_code + annotations: + livekit: + shipped_values: [{id: other, value: mistral, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-split-groups.yaml b/otel/tests/negatives/shipped-value-split-groups.yaml new file mode 100644 index 000000000..5e02c2dd0 --- /dev/null +++ b/otel/tests/negatives/shipped-value-split-groups.yaml @@ -0,0 +1,22 @@ +# expect: livekit_shipped_value +# Codex r2 duplicate-split-groups: two refs to one upstream key with different lists; codegen merges them into one module. +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: other, value: custom1, brief: fake}] + - id: livekit.review.second + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: other, value: custom2, brief: fake}] diff --git a/otel/tests/negatives/shipped-value-upstream.yaml b/otel/tests/negatives/shipped-value-upstream.yaml new file mode 100644 index 000000000..923371574 --- /dev/null +++ b/otel/tests/negatives/shipped-value-upstream.yaml @@ -0,0 +1,13 @@ +# expect: livekit_shipped_value +# A shipped value that the upstream definition already lists (chat). +file_format: definition/2 +attribute_groups: + - id: livekit.review + visibility: public + stability: development + brief: review + attributes: + - ref: gen_ai.operation.name + annotations: + livekit: + shipped_values: [{id: chat_again, value: chat, brief: fake}] diff --git a/otel/tests/negatives/unit.yaml b/otel/tests/negatives/unit.yaml new file mode 100644 index 000000000..39583ba7e --- /dev/null +++ b/otel/tests/negatives/unit.yaml @@ -0,0 +1,11 @@ +# expect: livekit_unit +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.connection.duration + type: double + stability: development + brief: A duration without unit s. + annotations: {livekit: {owner: client, sensitivity: none}} diff --git a/otel/tests/negatives/unknown-field.yaml b/otel/tests/negatives/unknown-field.yaml new file mode 100644 index 000000000..e96b43a80 --- /dev/null +++ b/otel/tests/negatives/unknown-field.yaml @@ -0,0 +1,12 @@ +# expect: unexpected properties: bogus_field +groups: + - id: registry.lk.negative + type: attribute_group + brief: Negative fixture. + attributes: + - id: lk.client.retry_reason + type: string + stability: development + brief: Typo in a field name. + bogus_field: 1 + annotations: {livekit: {owner: client, sensitivity: none}} diff --git a/otel/tests/negatives/unprefixed-owner.yaml b/otel/tests/negatives/unprefixed-owner.yaml new file mode 100644 index 000000000..ad872f486 --- /dev/null +++ b/otel/tests/negatives/unprefixed-owner.yaml @@ -0,0 +1,12 @@ +# expect: livekit_owner +# The attribute rules cover every definition, not only lk.* keys. +groups: + - id: registry.negative.unprefixed + type: attribute_group + brief: Negative fixture. + attributes: + - id: retry_reason + type: string + stability: development + brief: An unprefixed key with no owner. + annotations: {livekit: {sensitivity: none}} diff --git a/otel/tests/negatives/unprefixed-pii.yaml b/otel/tests/negatives/unprefixed-pii.yaml new file mode 100644 index 000000000..ef9813347 --- /dev/null +++ b/otel/tests/negatives/unprefixed-pii.yaml @@ -0,0 +1,13 @@ +# expect: livekit_pii_marker +# A bare PII key: the collector redacts by a `pii` key segment, so an unprefixed key with +# sensitivity pii and no such segment is never redacted (chat.item is the one named exception). +groups: + - id: registry.negative.unprefixed + type: attribute_group + brief: Negative fixture. + attributes: + - id: caller_number + type: string + stability: development + brief: A customer-supplied phone number under a bare key. + annotations: {livekit: {owner: client, sensitivity: pii}} diff --git a/otel/tests/negatives/v2-definition.yaml b/otel/tests/negatives/v2-definition.yaml new file mode 100644 index 000000000..d7cb2d7ac --- /dev/null +++ b/otel/tests/negatives/v2-definition.yaml @@ -0,0 +1,8 @@ +# expect: livekit_flat_identity +file_format: definition/2 +attributes: + - key: lk.room.sid + type: string + stability: development + brief: A definition/2 file gets the same rules. + annotations: {livekit: {owner: shared, sensitivity: none}}