From e8a15fcbe78bb46cdaf2bbec2698c0c68d538fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:52:25 +0200 Subject: [PATCH 01/36] feat(otel): add the telemetry semantic-conventions registry Add a weaver registry under otel/ pinned to OTel semantic conventions v1.44.0. It registers the shared identity, PII and SDK keys and what the client core emits today under its current names, with Rego policies for ownership, sensitivity, PII naming, flat identity, units and metric cardinality. The lk.client.* namespace rule is advice that never fails. Every violation has a negative fixture that must fail with its finding. --- otel/advice/livekit.rego | 28 + otel/check.sh | 27 + otel/model/client/groups.yaml | 69 +++ otel/model/client/registry.yaml | 654 ++++++++++++++++++++++++ otel/model/client/signals.yaml | 312 +++++++++++ otel/model/manifest.yaml | 10 + otel/model/shared/registry.yaml | 108 ++++ otel/policies/livekit.rego | 113 ++++ otel/tests/negatives.sh | 21 + otel/tests/negatives/cardinality.yaml | 12 + otel/tests/negatives/duplicate.yaml | 11 + otel/tests/negatives/flat-identity.yaml | 11 + otel/tests/negatives/metric-unit.yaml | 9 + otel/tests/negatives/owner.yaml | 11 + otel/tests/negatives/pii-heuristic.yaml | 11 + otel/tests/negatives/pii-marker.yaml | 11 + otel/tests/negatives/seconds.yaml | 11 + otel/tests/negatives/secret.yaml | 11 + otel/tests/negatives/sensitivity.yaml | 11 + otel/tests/negatives/unit.yaml | 11 + otel/tests/negatives/unknown-field.yaml | 12 + otel/tests/negatives/v2-definition.yaml | 8 + 22 files changed, 1482 insertions(+) create mode 100644 otel/advice/livekit.rego create mode 100755 otel/check.sh create mode 100644 otel/model/client/groups.yaml create mode 100644 otel/model/client/registry.yaml create mode 100644 otel/model/client/signals.yaml create mode 100644 otel/model/manifest.yaml create mode 100644 otel/model/shared/registry.yaml create mode 100644 otel/policies/livekit.rego create mode 100755 otel/tests/negatives.sh create mode 100644 otel/tests/negatives/cardinality.yaml create mode 100644 otel/tests/negatives/duplicate.yaml create mode 100644 otel/tests/negatives/flat-identity.yaml create mode 100644 otel/tests/negatives/metric-unit.yaml create mode 100644 otel/tests/negatives/owner.yaml create mode 100644 otel/tests/negatives/pii-heuristic.yaml create mode 100644 otel/tests/negatives/pii-marker.yaml create mode 100644 otel/tests/negatives/seconds.yaml create mode 100644 otel/tests/negatives/secret.yaml create mode 100644 otel/tests/negatives/sensitivity.yaml create mode 100644 otel/tests/negatives/unit.yaml create mode 100644 otel/tests/negatives/unknown-field.yaml create mode 100644 otel/tests/negatives/v2-definition.yaml diff --git a/otel/advice/livekit.rego b/otel/advice/livekit.rego new file mode 100644 index 000000000..886ed997b --- /dev/null +++ b/otel/advice/livekit.rego @@ -0,0 +1,28 @@ +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 + +# LiveKit attribute definitions, from v1 `groups:` files and definition/2 files alike. +lk_attrs contains attr if { + some group in input.groups + some attr in group.attributes + startswith(attr.id, "lk.") + not attr.deprecated +} + +lk_attrs contains object.union(attr, {"id": attr.key}) if { + some attr in input.attributes + startswith(attr.key, "lk.") + not attr.deprecated +} + +# The Notion conventions put client-owned attributes under lk.client.*. The client core ships +# its current names until that decision is made; this lists what the rule would move. +deny contains {"id": "livekit_client_namespace", "level": "improvement", "message": sprintf("%s: client-owned; the Notion convention would move it under 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..0c6e34add --- /dev/null +++ b/otel/check.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Checks otel/model against OTel semconv v1.44.0 and the LiveKit policies (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] +set -euo pipefail +cd "$(dirname "$0")" +W=${WEAVER:-weaver} + +"$W" registry check -r model -p policies "$@" + +# 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/model/client/groups.yaml b/otel/model/client/groups.yaml new file mode 100644 index 000000000..a3cf19a33 --- /dev/null +++ b/otel/model/client/groups.yaml @@ -0,0 +1,69 @@ +# Attribute sets the client core puts 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 per process). + attributes: + - ref: service.name + requirement_level: required + examples: ["livekit-client-swift"] + - ref: service.version + requirement_level: required + - ref: os.name + requirement_level: recommended + - ref: os.version + requirement_level: recommended + - ref: device.model.identifier + requirement_level: recommended + - ref: telemetry.sdk.name + requirement_level: required + examples: ["livekit-telemetry"] + - ref: telemetry.sdk.language + requirement_level: required + - ref: telemetry.sdk.version + requirement_level: required + - ref: lk.sdk.name + requirement_level: recommended + - ref: lk.sdk.version + requirement_level: recommended + + - id: livekit.client.record + visibility: public + stability: development + brief: Attributes the core stamps on every record and span of a session. + attributes: + - ref: session.id + requirement_level: required + note: The session's trace id, also on records outside a room. + - ref: otel.event.name + requirement_level: recommended + note: Duplicates the log record's `event_name` for backends that do not surface it. + - ref: lk.room_sid + requirement_level: recommended + - ref: lk.pii.room_name + requirement_level: recommended + - ref: lk.participant_id + requirement_level: recommended + - ref: lk.pii.participant_identity + requirement_level: recommended + + - id: livekit.client.log + visibility: public + stability: development + brief: Attributes of a plain log record (no event name). + attributes: + - ref: lk.log.source + requirement_level: required + - ref: lk.log.logger + requirement_level: required + - ref: code.function.name + requirement_level: recommended + - ref: code.file.path + requirement_level: recommended + - ref: code.line.number + requirement_level: recommended diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml new file mode 100644 index 000000000..381ab3998 --- /dev/null +++ b/otel/model/client/registry.yaml @@ -0,0 +1,654 @@ +# Client-owned attributes, under the names the client core emits today (rust-sdks +# livekit-telemetry SPEC.md). The Client team approves changes here alone, unless one touches +# a shared concept (otel/model/shared). Durations and RTT/jitter in RTC stats stay in ms, the +# W3C webrtc-stats unit, through an explicit `unit_exception`. +groups: + - id: registry.lk.device + type: attribute_group + display_name: LiveKit client device state + brief: Device conditions the host SDK observes and the core reports on change. + attributes: + - id: lk.device.thermal.state + stability: development + brief: Thermal pressure as the OS reports it. + note: The platform's `unknown` is no reading and is never emitted. + type: + members: + - {id: nominal, value: nominal, brief: No thermal pressure., stability: development} + - {id: fair, value: fair, brief: Slightly elevated., stability: development} + - {id: serious, value: serious, brief: High; the core doubles its cadence., stability: development} + - {id: critical, value: critical, brief: Critical; the core quadruples its cadence., 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. + 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. + type: + members: + - {id: foreground, value: foreground, brief: In front of the user., stability: development} + - {id: background, value: background, brief: In the background; forces a flush., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.memory.pressure + stability: development + brief: Memory pressure as the OS reports it. + note: "Android `onTrimMemory`: RUNNING_LOW/BACKGROUND map to `warning`, RUNNING_CRITICAL/COMPLETE to `critical`." + 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). + 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). + 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: SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. + examples: [42] + annotations: + livekit: {owner: client, sensitivity: none, unit: "%"} + - id: lk.device.battery.charging + type: boolean + stability: development + brief: The device is charging. + note: SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. + examples: [false] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.audio_route.reason + stability: development + brief: Why the audio route changed (AVAudioSession route change reasons). + 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: > + Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, + car_audio, air_play, hdmi, usb, other. + examples: ["bluetooth", "speaker,usb"] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.device.audio.interruption + stability: development + brief: Phase of an audio session interruption. + type: + members: + - {id: began, value: began, brief: Interruption began., 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. + 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 (the getUserMedia failure taxonomy). + 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 is shared (`lk.track_sid`). + attributes: + - id: lk.track.kind + stability: development + brief: Media kind of the track. + 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.source + stability: development + brief: Source of the track (the protocol's TrackSource, snake_case). + 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 share video., stability: development} + - {id: screen_share_audio, value: screen_share_audio, brief: Screen share audio., stability: development} + - {id: unknown, value: unknown, brief: Unknown source., stability: development} + annotations: + livekit: + owner: client + sensitivity: none + bindings: + agents_otlp: lk.track_source # values are the FFI enum names, e.g. SOURCE_MICROPHONE + - id: lk.track.direction + stability: development + brief: Whether the stats describe sent or received media. + type: + members: + - {id: inbound, value: inbound, brief: Received media., stability: development} + - {id: outbound, value: outbound, brief: Sent media., 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. + attributes: + - id: lk.rtc.codec + type: string + stability: development + brief: Codec mimeType, when known. + 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. + examples: [60000] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.samples + type: int + stability: development + brief: Stats readings in the window. + examples: [2] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{reading}"} + - id: lk.rtc.bytes + type: int + stability: development + brief: Bytes sent or received (cumulative). + examples: [1048576] + annotations: + livekit: {owner: client, sensitivity: none, unit: By} + - id: lk.rtc.packets + type: int + stability: development + brief: Packets sent or received (cumulative). + examples: [3000] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{packet}"} + - id: lk.rtc.packets_lost + type: int + stability: development + brief: Packets lost, inbound (cumulative). + 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). + 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). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.pause_count + type: int + stability: development + brief: Video pauses, inbound video (cumulative). + 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). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.concealed_samples + type: int + stability: development + brief: Concealed audio samples, inbound audio (cumulative). + 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). + 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). + 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). + 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). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.jitter_buffer_delay_ms + type: int + stability: development + brief: Jitter buffer delay, inbound (cumulative). + examples: [1200] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.jitter_buffer_emitted_count + type: int + stability: development + brief: Samples or frames emitted by the jitter buffer, inbound (cumulative). + 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). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.quality_limitation.cpu_ms + type: int + stability: development + brief: Time the encoder was limited by CPU, outbound video (cumulative). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.quality_limitation.other_ms + type: int + stability: development + brief: Time the encoder was limited for other reasons, outbound video (cumulative). + examples: [0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.jitter_ms.min + type: double + stability: development + brief: Minimum jitter in the window. + examples: [0.5] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.jitter_ms.max + type: double + stability: development + brief: Maximum jitter in the window. + examples: [4.2] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.jitter_ms.avg + type: double + stability: development + brief: Average jitter in the window. + examples: [1.8] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.rtt_ms.min + type: double + stability: development + brief: Minimum round-trip time in the window (remote-inbound for outbound, candidate pair for inbound). + examples: [12.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.rtt_ms.max + type: double + stability: development + brief: Maximum round-trip time in the window. + examples: [80.0] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.rtt_ms.avg + type: double + stability: development + brief: Average round-trip time in the window. + examples: [25.5] + annotations: + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + - id: lk.rtc.fps.min + type: double + stability: development + brief: Minimum frame rate in the window, video. + 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. + 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. + 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, audio. + 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, audio. + 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, audio. + 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 core's spans. + attributes: + - id: lk.outcome + stability: development + brief: How the attempt ended. Always present on a span; rollups read this, never the span status. + note: The Notion conventions name `lk.client.connection.result` (success|failure|cancelled) for connects only. Open decision. + type: + members: + - {id: ok, value: ok, brief: Succeeded., stability: development} + - {id: error, value: error, brief: Failed; `error.type` names why., stability: development} + - {id: cancelled, value: cancelled, brief: Cancelled by the caller or a newer attempt., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.connect.attempt + type: int + stability: development + brief: 1-based connect attempt; 1 is the user-initiated connect. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.reason + stability: development + brief: What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). + type: + members: + - {id: signal_disconnected, value: signal_disconnected, brief: The signaling socket closed., stability: development} + - {id: publisher_failed, value: publisher_failed, brief: The publisher peer connection failed., stability: development} + - {id: subscriber_failed, value: subscriber_failed, brief: The subscriber peer connection failed., stability: development} + - {id: transport_failed, value: transport_failed, brief: A peer connection failed; the platform did not say which., stability: development} + - {id: switch_candidate, value: switch_candidate, brief: The server asked for a candidate switch., stability: development} + - {id: network_changed, value: network_changed, brief: The device moved to another network., stability: development} + - {id: debug, value: debug, brief: A test or debug menu asked for it., stability: development} + - {id: unknown, value: unknown, brief: Unknown reason., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.mode + stability: development + brief: Mode of the last reconnect attempt. + type: + members: + - {id: quick, value: quick, brief: ICE restart / resume., stability: development} + - {id: full, value: full, brief: Full reconnect., stability: development} + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.reconnect.attempts + type: int + stability: development + brief: Reconnect attempts made in the cycle. + examples: [2] + annotations: + livekit: {owner: client, sensitivity: none} + + - id: registry.lk.session + type: attribute_group + display_name: LiveKit client session + brief: Session lifecycle attributes. + attributes: + - id: lk.disconnect.reason + stability: development + brief: Why the session ended (the protocol's DisconnectReason, plus the client giving up). + type: + members: + - {id: client_initiated, value: client_initiated, brief: The client 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 shut down., stability: development} + - {id: participant_removed, value: participant_removed, brief: The participant was removed., stability: development} + - {id: room_deleted, value: room_deleted, brief: The room was deleted., stability: development} + - {id: state_mismatch, value: state_mismatch, brief: Client and server state diverged., stability: development} + - {id: join_failure, value: join_failure, brief: The join failed., stability: development} + - {id: migration, value: migration, brief: The session migrated., stability: development} + - {id: signal_close, value: signal_close, brief: The signaling connection closed., stability: development} + - {id: room_closed, value: room_closed, brief: The room closed., stability: development} + - {id: user_unavailable, value: user_unavailable, brief: SIP callee unavailable., stability: development} + - {id: user_rejected, value: user_rejected, brief: SIP callee rejected the call., stability: development} + - {id: sip_trunk_failure, value: sip_trunk_failure, brief: SIP trunk failure., stability: development} + - {id: connection_timeout, value: connection_timeout, brief: The connection timed out., stability: development} + - {id: media_failure, value: media_failure, brief: Media failed., stability: development} + - {id: agent_error, value: agent_error, brief: The agent failed., stability: development} + - {id: reconnect_failed, value: reconnect_failed, brief: The reconnect policy ran out of attempts., stability: development} + - {id: unknown, value: unknown, brief: Unknown reason., stability: development} + 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. + type: + members: + - {id: sdk, value: sdk, brief: The platform SDK., stability: development} + - {id: ffi, value: ffi, brief: The Rust core., 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. + 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. + attributes: + - id: lk.ping.seq + type: int + stability: development + brief: Monotonically increasing sequence number per pipeline. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none} + - id: lk.telemetry.uploads.sent + type: int + stability: development + brief: Batches the collector accepted. + 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. + 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). + 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. + 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). + 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 the 60 s cap. + 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 (a gauge). + 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. + 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. + 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. + 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. + 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 24 h age limit. + 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. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} + - id: lk.telemetry.dropped.invalid + type: int + stability: development + brief: Custom events or attributes over the limits (rejected). + 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, partial success). + 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). + 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. + 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. + examples: [1] + annotations: + livekit: {owner: client, sensitivity: none, unit: "{record}"} diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml new file mode 100644 index 000000000..2a9b3fc90 --- /dev/null +++ b/otel/model/client/signals.yaml @@ -0,0 +1,312 @@ +# Client signals, under the names the client core emits today (rust-sdks livekit-telemetry +# SPEC.md). Events are OTLP log records with `event_name`; spans are one attempt each. +groups: + # --- Log events ------------------------------------------------------------------------- + - id: event.lk.ping + type: event + name: lk.ping + stability: development + brief: Pipeline smoke test. Never emitted in production paths. + attributes: + - ref: lk.ping.seq + requirement_level: recommended + + - id: event.lk.telemetry.report + type: event + name: lk.telemetry.report + stability: development + brief: Self-telemetry, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. + 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.} + - ref: lk.telemetry.dropped.rate_limited + requirement_level: {conditionally_required: When non-zero.} + + - id: event.lk.device.thermal.changed + type: event + name: lk.device.thermal.changed + stability: development + brief: Thermal state changed (and its initial value). + 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 (and its initial value). + 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: App moved to the foreground or background (and its initial value). + 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 (and its initial value). + 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: Network path changed (and its initial value). + attributes: + - ref: network.connection.type + requirement_level: required + note: The client also sends `vpn`, `bluetooth` and `other`, which are not upstream members. + - ref: lk.device.network.expensive + requirement_level: required + - ref: lk.device.network.constrained + requirement_level: required + + - 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. Never per percent. + 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: Audio route changed. + 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: Audio session interruption began or ended. + 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. Severity WARN. + 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. + 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: required + - ref: lk.rtc.packets + requirement_level: required + - 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: recommended + - ref: lk.rtc.rtt_ms.max + requirement_level: recommended + - ref: lk.rtc.rtt_ms.avg + requirement_level: recommended + - 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: Audio.} + - ref: lk.rtc.audio_level.max + requirement_level: {conditionally_required: Audio.} + - ref: lk.rtc.audio_level.avg + requirement_level: {conditionally_required: Audio.} + + - id: event.lk.room.disconnected + type: event + name: lk.room.disconnected + stability: development + brief: The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. + attributes: + - ref: lk.disconnect.reason + requirement_level: required + + - id: event.custom + type: event + name: custom + stability: development + brief: Namespace of app-defined events. The core emits `custom.` with the app's own attributes. + note: App attribute keys may not use `lk.*`, `otel.*` or `session.id`. + + # --- Spans: one attempt at an operation; checkpoints are span events --------------------- + - id: span.lk.connect + type: span + span_kind: client + stability: development + brief: One connect attempt. + note: > + Checkpoints, in order: ws_open, signal, join_recv, pc_created (required on every platform), + then engine, pc_connected, offer_sent, answer_sent, room_connected (best effort). + attributes: + - ref: lk.connect.attempt + requirement_level: required + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: When `lk.outcome` is `error`.} + + - id: span.lk.reconnect + type: span + span_kind: client + stability: development + brief: One reconnect cycle. + note: Checkpoints are named `attempt `, one per attempt (dynamic; open decision). + 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: When `lk.outcome` is `error`.} + + - id: span.lk.publish + type: span + span_kind: internal + stability: development + brief: One track publish. + 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.} + - ref: lk.outcome + requirement_level: required + - ref: error.type + requirement_level: {conditionally_required: When `lk.outcome` is `error`.} + + - id: span.lk.subscribe + type: span + span_kind: internal + stability: development + brief: One subscription, from the intent to subscribe to the first media (or timed_out after 30 s). + note: "Checkpoints: subscribed, first_media." + 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: When `lk.outcome` is `error`.} diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml new file mode 100644 index 000000000..7d7b09ee4 --- /dev/null +++ b/otel/model/manifest.yaml @@ -0,0 +1,10 @@ +file_format: definition_manifest/2.0 +# The registry version IS the telemetry schema version: producers stamp this URL on +# OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). +# Bump it in the same PR as any change under otel/model (see otel/README.md). +schema_url: https://livekit.io/schemas/telemetry/0.1.0 +stability: development +description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-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] diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml new file mode 100644 index 000000000..3c996ea86 --- /dev/null +++ b/otel/model/shared/registry.yaml @@ -0,0 +1,108 @@ +# 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`. + attributes: + - id: lk.room_sid + type: string + stability: development + brief: Server-assigned room session SID (`RM_...`). + note: Omit before join. High cardinality, never a default metric dimension. + examples: ["RM_5Fq8xQ2ZbXwN"] + annotations: + livekit: + owner: shared + sensitivity: none + authority: cloud + # 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 # TBC by Observability + - id: lk.participant_id + type: string + stability: development + brief: Server-assigned participant SID (`PA_...`). + note: High cardinality, never a default metric dimension. + examples: ["PA_9kLm2NpQ4rTs"] + annotations: + livekit: {owner: shared, sensitivity: none, authority: cloud} + - id: lk.track_sid + type: string + stability: development + brief: Server-assigned track SID (`TR_...`). + note: High cardinality, never a default metric dimension. + examples: ["TR_AMkTyV3Zx8Qp"] + annotations: + livekit: {owner: shared, sensitivity: none, authority: cloud} + - id: lk.job_id + type: string + stability: development + brief: Agent job ID (`AJ_...`). + note: High cardinality; Agents' job-scoped metrics are the one 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. + attributes: + - id: lk.pii.room_name + type: string + stability: development + brief: Customer-supplied room name. + examples: ["my-room"] + annotations: + livekit: {owner: shared, sensitivity: pii} + - id: lk.pii.participant_identity + type: string + stability: development + brief: Customer-supplied identity of the local participant. + examples: ["identity-0"] + annotations: + livekit: {owner: shared, sensitivity: pii} + - 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: Name TBD, needs sign-off; the Notion conventions name no key for it. + examples: ["identity-1"] + annotations: + livekit: {owner: shared, sensitivity: pii} + + - 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. + 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. + examples: ["2.17.0"] + annotations: + livekit: {owner: shared, sensitivity: none} diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego new file mode 100644 index 000000000..402576822 --- /dev/null +++ b/otel/policies/livekit.rego @@ -0,0 +1,113 @@ +package before_resolution + +# LiveKit registry rules (violations), run by otel/check.sh. Each finding id is +# documented in otel/README.md; otel/tests/negatives has one failing fixture per violation. + +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"} + +# LiveKit attribute definitions in this file (refs and deprecated keys are exempt). +lk_attrs contains attr if { + some group in input.groups + some attr in group.attributes + startswith(attr.id, "lk.") + 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 + startswith(attr.key, "lk.") + not attr.deprecated +} + +lk(obj, field) := object.get(obj, ["annotations", "livekit", field], null) + +# A documented reason to keep milliseconds (e.g. RTC stats in webrtc-stats units). +ms_exception(attr) if { + lk(attr, "unit") == "ms" + is_string(lk(attr, "unit_exception")) + lk(attr, "unit_exception") != "" +} + +finding(id, msg) := {"id": id, "level": "violation", "message": msg} + +deny contains finding("livekit_owner", sprintf("%s: annotations.livekit.owner must be one of %v", [attr.id, owners])) if { + some attr in lk_attrs + not owners[lk(attr, "owner")] +} + +deny contains finding("livekit_sensitivity", sprintf("%s: annotations.livekit.sensitivity must be one of %v (protocol logger.Sensitivity)", [attr.id, sensitivities])) if { + some attr in lk_attrs + not sensitivities[lk(attr, "sensitivity")] +} + +deny contains finding("livekit_secret", sprintf("%s: SECRET values are never emitted as telemetry", [attr.id])) if { + some attr in lk_attrs + lk(attr, "sensitivity") == "secret" +} + +deny contains 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")])) if { + some attr in lk_attrs + sensitivities[lk(attr, "sensitivity")] + (lk(attr, "sensitivity") == "pii") != regex.match(pii_segment, attr.id) +} + +deny contains finding("livekit_pii_heuristic", sprintf("%s looks like PII; name it lk.pii.* so the collector redacts it", [attr.id])) if { + some attr in lk_attrs + regex.match(pii_looking, attr.id) + not regex.match(pii_segment, attr.id) +} + +deny contains finding("livekit_flat_identity", sprintf("%s: shared identity keys are flat, use %s", [attr.id, canonical_identity[attr.id]])) if { + some attr in lk_attrs + canonical_identity[attr.id] +} + +deny contains finding("livekit_seconds", sprintf("%s: LiveKit durations are seconds; a *_ms key needs annotations.livekit.unit: ms plus a unit_exception reason", [attr.id])) if { + some attr in lk_attrs + regex.match(`_ms($|\.)`, attr.id) + not ms_exception(attr) +} + +deny contains finding("livekit_unit", sprintf("%s: duration-like attribute must declare annotations.livekit.unit: s", [attr.id])) if { + some attr in lk_attrs + regex.match(`(duration|latency|delay|rtt|jitter)$`, attr.id) + lk(attr, "unit") != "s" + not ms_exception(attr) +} + +deny contains finding("livekit_metric_unit", sprintf("%s: metric unit must be s, not ms", [group.metric_name])) if { + some group in input.groups + group.type == "metric" + group.unit == "ms" +} + +deny contains finding("livekit_cardinality", sprintf("%s: %s is high-cardinality, not a default metric dimension", [group.metric_name, a.ref])) if { + some group in input.groups + group.type == "metric" + some a in group.attributes + high_cardinality[a.ref] + lk(group, "cardinality_exception") != true # Agents' job-scoped exception sets true +} diff --git a/otel/tests/negatives.sh b/otel/tests/negatives.sh new file mode 100755 index 000000000..11ccdaa60 --- /dev/null +++ b/otel/tests/negatives.sh @@ -0,0 +1,21 @@ +#!/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. +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" + out=$("$W" registry check -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 fails with: $expect" + else + echo "FAIL $fixture (exit $status), expected: $expect"; echo "$out" | tail -20; failed=1 + fi +done +exit "$failed" 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.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.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/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/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/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}} From 7f494b9686ad6611f6d2d30b81631b23c4206397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:52:25 +0200 Subject: [PATCH 02/36] feat(otel): generate Rust, TypeScript and docs from the registry Templates render SCHEMA_URL, attribute, enum value, event and span constants for Rust and TypeScript, and a markdown reference with owner, sensitivity, units and bindings. The outputs are committed; generate.sh reproduces them. --- otel/gen/docs/semconv.md | 365 +++++++++++++ otel/gen/rust/semconv.rs | 501 ++++++++++++++++++ otel/gen/ts/semconv.ts | 434 +++++++++++++++ otel/generate.sh | 9 + .../templates/registry/markdown/semconv.md.j2 | 49 ++ otel/templates/registry/markdown/weaver.yaml | 5 + otel/templates/registry/rust/semconv.rs.j2 | 36 ++ otel/templates/registry/rust/weaver.yaml | 5 + .../registry/typescript/semconv.ts.j2 | 22 + .../templates/registry/typescript/weaver.yaml | 5 + 10 files changed, 1431 insertions(+) create mode 100644 otel/gen/docs/semconv.md create mode 100644 otel/gen/rust/semconv.rs create mode 100644 otel/gen/ts/semconv.ts create mode 100755 otel/generate.sh create mode 100644 otel/templates/registry/markdown/semconv.md.j2 create mode 100644 otel/templates/registry/markdown/weaver.yaml create mode 100644 otel/templates/registry/rust/semconv.rs.j2 create mode 100644 otel/templates/registry/rust/weaver.yaml create mode 100644 otel/templates/registry/typescript/semconv.ts.j2 create mode 100644 otel/templates/registry/typescript/weaver.yaml diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md new file mode 100644 index 000000000..86e4af9eb --- /dev/null +++ b/otel/gen/docs/semconv.md @@ -0,0 +1,365 @@ + +# LiveKit telemetry conventions + +Schema: `https://livekit.io/schemas/telemetry/0.1.0`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. + +## Attributes + +| Key | Type | Owner | Sensitivity | Unit | Description | +| --- | --- | --- | --- | --- | --- | +| `lk.connect.attempt` | int | client | none | | 1-based connect attempt; 1 is the user-initiated connect. | +| `lk.device.app_state` | enum: `foreground`, `background` | client | none | | Whether the app is in front of the user. | +| `lk.device.audio.interruption` | enum: `began`, `ended` | client | none | | Phase of an audio session interruption. | +| `lk.device.audio_route.outputs` | string | client | none | | Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. | +| `lk.device.audio_route.reason` | enum: `new_device`, `old_device_unavailable`, `category_change`, `override`, `wake_from_sleep`, `no_suitable_route`, `route_configuration_change`, `unknown` | client | none | | Why the audio route changed (AVAudioSession route change reasons). | +| `lk.device.battery.charging` | boolean | client | none | | The device is charging. SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. | +| `lk.device.battery.level` | int | client | none | `%` | Battery charge in percent, 0-100. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | +| `lk.device.capture.device` | enum: `camera`, `microphone`, `screen_share` | client | none | | Capture device that failed. | +| `lk.device.capture.reason` | enum: `permission_denied`, `not_found`, `in_use`, `disconnected`, `other` | client | none | | Why capture failed (the getUserMedia failure taxonomy). | +| `lk.device.low_power.enabled` | boolean | client | none | | Low Power Mode / Battery Saver is on. | +| `lk.device.memory.pressure` | enum: `normal`, `warning`, `critical` | client | none | | Memory pressure as the OS reports it. Android `onTrimMemory`: RUNNING_LOW/BACKGROUND map to `warning`, RUNNING_CRITICAL/COMPLETE to `critical`. | +| `lk.device.network.constrained` | boolean | client | none | | The user asked for less traffic (Low Data Mode, Data Saver). | +| `lk.device.network.expensive` | boolean | client | none | | The network is metered (cellular, personal hotspot). | +| `lk.device.thermal.state` | enum: `nominal`, `fair`, `serious`, `critical` | client | none | | Thermal pressure as the OS reports it. The platform's `unknown` is no reading and is never emitted. | +| `lk.disconnect.reason` | enum: `client_initiated`, `duplicate_identity`, `server_shutdown`, `participant_removed`, `room_deleted`, `state_mismatch`, `join_failure`, `migration`, `signal_close`, `room_closed`, `user_unavailable`, `user_rejected`, `sip_trunk_failure`, `connection_timeout`, `media_failure`, `agent_error`, `reconnect_failed`, `unknown` | client | none | | Why the session ended (the protocol's DisconnectReason, plus the client giving up). | +| `lk.job_id` | string | shared | none | | Agent job ID (`AJ_...`). High cardinality; Agents' job-scoped metrics are the one exception. | +| `lk.log.logger` | string | client | none | | Logger, type, module or file that wrote the line. | +| `lk.log.source` | enum: `sdk`, `ffi`, `webrtc` | client | none | | Which layer wrote the log line. | +| `lk.outcome` | enum: `ok`, `error`, `cancelled` | client | none | | How the attempt ended. Always present on a span; rollups read this, never the span status. The Notion conventions name `lk.client.connection.result` (success\|failure\|cancelled) for connects only. Open decision. | +| `lk.participant_id` | string | shared | none | | Server-assigned participant SID (`PA_...`). High cardinality, never a default metric dimension. | +| `lk.pii.participant_identity` | string | shared | pii | | Customer-supplied identity of the local participant. | +| `lk.pii.remote_participant_identity` | string | shared | pii | | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). Name TBD, needs sign-off; the Notion conventions name no key for it. | +| `lk.pii.room_name` | string | shared | pii | | Customer-supplied room name. | +| `lk.ping.seq` | int | client | none | | Monotonically increasing sequence number per pipeline. | +| `lk.reconnect.attempts` | int | client | none | | Reconnect attempts made in the cycle. | +| `lk.reconnect.mode` | enum: `quick`, `full` | client | none | | Mode of the last reconnect attempt. | +| `lk.reconnect.reason` | enum: `signal_disconnected`, `publisher_failed`, `subscriber_failed`, `transport_failed`, `switch_candidate`, `network_changed`, `debug`, `unknown` | client | none | | What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). | +| `lk.room_sid` | string | shared | none | | Server-assigned room session SID (`RM_...`). Omit before join. High cardinality, never a default metric dimension. | +| `lk.rtc.audio_level.avg` | double | client | none | `1` | Average audio level in the window, 0-1, audio. | +| `lk.rtc.audio_level.max` | double | client | none | `1` | Maximum audio level in the window, 0-1, audio. | +| `lk.rtc.audio_level.min` | double | client | none | `1` | Minimum audio level in the window, 0-1, audio. | +| `lk.rtc.bytes` | int | client | none | `By` | Bytes sent or received (cumulative). | +| `lk.rtc.codec` | string | client | none | | Codec mimeType, when known. | +| `lk.rtc.concealed_samples` | int | client | none | `{sample}` | Concealed audio samples, inbound audio (cumulative). | +| `lk.rtc.concealment_events` | int | client | none | `{event}` | Concealment events, inbound audio (cumulative). | +| `lk.rtc.fps.avg` | double | client | none | `{frame}/s` | Average frame rate in the window, video. | +| `lk.rtc.fps.max` | double | client | none | `{frame}/s` | Maximum frame rate in the window, video. | +| `lk.rtc.fps.min` | double | client | none | `{frame}/s` | Minimum frame rate in the window, video. | +| `lk.rtc.freeze_count` | int | client | none | `{freeze}` | Video freezes, inbound video (cumulative). | +| `lk.rtc.freezes_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total freeze time, inbound video (cumulative). | +| `lk.rtc.interruption_count` | int | client | none | `{interruption}` | Audio interruptions, inbound audio (cumulative). | +| `lk.rtc.interruptions_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total interruption time, inbound audio (cumulative). | +| `lk.rtc.jitter_buffer_delay_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Jitter buffer delay, inbound (cumulative). | +| `lk.rtc.jitter_buffer_emitted_count` | int | client | none | `{sample}` | Samples or frames emitted by the jitter buffer, inbound (cumulative). | +| `lk.rtc.jitter_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average jitter in the window. | +| `lk.rtc.jitter_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum jitter in the window. | +| `lk.rtc.jitter_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum jitter in the window. | +| `lk.rtc.packets` | int | client | none | `{packet}` | Packets sent or received (cumulative). | +| `lk.rtc.packets_lost` | int | client | none | `{packet}` | Packets lost, inbound (cumulative). | +| `lk.rtc.pause_count` | int | client | none | `{pause}` | Video pauses, inbound video (cumulative). | +| `lk.rtc.pauses_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total pause time, inbound video (cumulative). | +| `lk.rtc.quality_limitation.bandwidth_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by bandwidth, outbound video (cumulative). | +| `lk.rtc.quality_limitation.cpu_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by CPU, outbound video (cumulative). | +| `lk.rtc.quality_limitation.other_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited for other reasons, outbound video (cumulative). | +| `lk.rtc.rtt_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average round-trip time in the window. | +| `lk.rtc.rtt_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum round-trip time in the window. | +| `lk.rtc.rtt_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum round-trip time in the window (remote-inbound for outbound, candidate pair for inbound). | +| `lk.rtc.samples` | int | client | none | `{reading}` | Stats readings in the window. | +| `lk.rtc.silent_concealed_samples` | int | client | none | `{sample}` | Concealed samples that were silent, inbound audio (cumulative). | +| `lk.rtc.window_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Actual length of the stats window. | +| `lk.sdk.name` | string | shared | none | | LiveKit SDK name. | +| `lk.sdk.version` | string | shared | none | | LiveKit SDK version. | +| `lk.telemetry.cache.batches` | int | client | none | `{batch}` | Batches waiting in the cache now (a gauge). | +| `lk.telemetry.cache.write_errors` | int | client | none | `{batch}` | Batches the disk refused, kept in memory. | +| `lk.telemetry.dropped.cache_error` | int | client | none | `{record}` | Records no cache, not even memory, could take. | +| `lk.telemetry.dropped.cache_full` | int | client | none | `{record}` | Records evicted by the cache's size or file-count bound. | +| `lk.telemetry.dropped.corrupt` | int | client | none | `{record}` | Records in cached batches that failed their CRC. | +| `lk.telemetry.dropped.expired` | int | client | none | `{record}` | Records in batches past the 24 h age limit. | +| `lk.telemetry.dropped.invalid` | int | client | none | `{record}` | Custom events or attributes over the limits (rejected). | +| `lk.telemetry.dropped.oversized` | int | client | none | `{record}` | Single records larger than the collector accepts (413). | +| `lk.telemetry.dropped.queue_full` | int | client | none | `{record}` | Records evicted from the in-memory queue. | +| `lk.telemetry.dropped.rate_limited` | int | client | none | `{record}` | Discrete events dropped by the flood guard. | +| `lk.telemetry.dropped.rejected` | int | client | none | `{record}` | Records the collector rejected (final 4xx/5xx, partial success). | +| `lk.telemetry.dropped.throttled` | int | client | none | `{record}` | Records evicted from the cache during a server-directed pause. | +| `lk.telemetry.holds.capped` | int | client | none | `{hold}` | Soft upload holds that reached the 60 s cap. | +| `lk.telemetry.uploads.bytes` | int | client | none | `By` | Compressed bytes the collector accepted. | +| `lk.telemetry.uploads.failed` | int | client | none | `{attempt}` | Upload attempts that failed transiently (no answer, 429, 5xx). | +| `lk.telemetry.uploads.sent` | int | client | none | `{batch}` | Batches the collector accepted. | +| `lk.telemetry.uploads.timeouts` | int | client | none | `{attempt}` | Upload attempts that hit the export timeout. | +| `lk.telemetry.uploads.unauthorized` | int | client | none | `{attempt}` | 401/403 answers (tokens the collector refused). | +| `lk.track.direction` | enum: `inbound`, `outbound` | client | none | | Whether the stats describe sent or received media. | +| `lk.track.kind` | enum: `audio`, `video` | client | none | | Media kind of the track. | +| `lk.track.source` | enum: `camera`, `microphone`, `screen_share`, `screen_share_audio`, `unknown` | client | none | | Source of the track (the protocol's TrackSource, snake_case). | +| `lk.track_sid` | string | shared | none | | Server-assigned track SID (`TR_...`). High cardinality, never a default metric dimension. | + +## Bindings + +Physical names of a concept in other producers and stores. Carried for reference; nothing validates them. + +| Key | Binding | 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` | +| `lk.track.source` | agents_otlp | `lk.track_source` | + +## Events + +### `custom` + +Namespace of app-defined events. The core emits `custom.` with the app's own attributes. App attribute keys may not use `lk.*`, `otel.*` or `session.id`. + +### `lk.device.app_state.changed` + +App moved to the foreground or background (and its initial value). + +| Attribute | Requirement | +| --- | --- | +| `lk.device.app_state` | required | + +### `lk.device.audio.interruption` + +Audio session interruption began or ended. + +| Attribute | Requirement | +| --- | --- | +| `lk.device.audio.interruption` | required | + +### `lk.device.audio_route.changed` + +Audio route changed. + +| Attribute | Requirement | +| --- | --- | +| `lk.device.audio_route.outputs` | required | +| `lk.device.audio_route.reason` | required | + +### `lk.device.battery.changed` + +Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent. + +| Attribute | Requirement | +| --- | --- | +| `lk.device.battery.charging` | required | +| `lk.device.battery.level` | required | + +### `lk.device.capture.failed` + +A capture device failed. Severity WARN. + +| Attribute | Requirement | +| --- | --- | +| `lk.device.capture.device` | required | +| `lk.device.capture.reason` | required | + +### `lk.device.low_power.changed` + +Low Power Mode changed (and its initial value). + +| Attribute | Requirement | +| --- | --- | +| `lk.device.low_power.enabled` | required | + +### `lk.device.memory.changed` + +Memory pressure changed (and its initial value). + +| Attribute | Requirement | +| --- | --- | +| `lk.device.memory.pressure` | required | + +### `lk.device.network.changed` + +Network path changed (and its initial value). + +| Attribute | Requirement | +| --- | --- | +| `lk.device.network.constrained` | required | +| `lk.device.network.expensive` | required | +| `network.connection.type` | required | + +### `lk.device.thermal.changed` + +Thermal state changed (and its initial value). + +| Attribute | Requirement | +| --- | --- | +| `lk.device.thermal.state` | required | + +### `lk.ping` + +Pipeline smoke test. Never emitted in production paths. + +| Attribute | Requirement | +| --- | --- | +| `lk.ping.seq` | recommended | + +### `lk.room.disconnected` + +The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. + +| Attribute | Requirement | +| --- | --- | +| `lk.disconnect.reason` | required | + +### `lk.rtc.stats.sample` + +One RTC stats window for one track and direction. + +| Attribute | Requirement | +| --- | --- | +| `lk.rtc.audio_level.avg` | conditionally_required: Audio. | +| `lk.rtc.audio_level.max` | conditionally_required: Audio. | +| `lk.rtc.audio_level.min` | conditionally_required: Audio. | +| `lk.rtc.bytes` | required | +| `lk.rtc.codec` | conditionally_required: When known. | +| `lk.rtc.concealed_samples` | conditionally_required: Inbound audio. | +| `lk.rtc.concealment_events` | conditionally_required: Inbound audio. | +| `lk.rtc.fps.avg` | conditionally_required: Video. | +| `lk.rtc.fps.max` | conditionally_required: Video. | +| `lk.rtc.fps.min` | conditionally_required: Video. | +| `lk.rtc.freeze_count` | conditionally_required: Inbound video. | +| `lk.rtc.freezes_duration_ms` | conditionally_required: Inbound video. | +| `lk.rtc.interruption_count` | conditionally_required: Inbound audio. | +| `lk.rtc.interruptions_duration_ms` | conditionally_required: Inbound audio. | +| `lk.rtc.jitter_buffer_delay_ms` | conditionally_required: Inbound. | +| `lk.rtc.jitter_buffer_emitted_count` | conditionally_required: Inbound. | +| `lk.rtc.jitter_ms.avg` | conditionally_required: Inbound. | +| `lk.rtc.jitter_ms.max` | conditionally_required: Inbound. | +| `lk.rtc.jitter_ms.min` | conditionally_required: Inbound. | +| `lk.rtc.packets` | required | +| `lk.rtc.packets_lost` | conditionally_required: Inbound. | +| `lk.rtc.pause_count` | conditionally_required: Inbound video. | +| `lk.rtc.pauses_duration_ms` | conditionally_required: Inbound video. | +| `lk.rtc.quality_limitation.bandwidth_ms` | conditionally_required: Outbound video. | +| `lk.rtc.quality_limitation.cpu_ms` | conditionally_required: Outbound video. | +| `lk.rtc.quality_limitation.other_ms` | conditionally_required: Outbound video. | +| `lk.rtc.rtt_ms.avg` | recommended | +| `lk.rtc.rtt_ms.max` | recommended | +| `lk.rtc.rtt_ms.min` | recommended | +| `lk.rtc.samples` | required | +| `lk.rtc.silent_concealed_samples` | conditionally_required: Inbound audio. | +| `lk.rtc.window_ms` | required | +| `lk.track.direction` | required | +| `lk.track.kind` | required | +| `lk.track_sid` | required | + +### `lk.telemetry.report` + +Self-telemetry, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. + +| Attribute | Requirement | +| --- | --- | +| `lk.telemetry.cache.batches` | required | +| `lk.telemetry.cache.write_errors` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.cache_error` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.cache_full` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.corrupt` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.expired` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.invalid` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.oversized` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.queue_full` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.rate_limited` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.rejected` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.throttled` | conditionally_required: When non-zero. | +| `lk.telemetry.holds.capped` | conditionally_required: When non-zero. | +| `lk.telemetry.uploads.bytes` | required | +| `lk.telemetry.uploads.failed` | required | +| `lk.telemetry.uploads.sent` | required | +| `lk.telemetry.uploads.timeouts` | conditionally_required: When non-zero. | +| `lk.telemetry.uploads.unauthorized` | conditionally_required: When non-zero. | + +## Spans + +### `lk.connect` (client) + +One connect attempt. Checkpoints, in order: ws_open, signal, join_recv, pc_created (required on every platform), then engine, pc_connected, offer_sent, answer_sent, room_connected (best effort). + +| Attribute | Requirement | +| --- | --- | +| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `lk.connect.attempt` | required | +| `lk.outcome` | required | + +### `lk.publish` (internal) + +One track publish. + +| Attribute | Requirement | +| --- | --- | +| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `lk.outcome` | required | +| `lk.track.kind` | required | +| `lk.track.source` | required | +| `lk.track_sid` | conditionally_required: On success. | + +### `lk.reconnect` (client) + +One reconnect cycle. Checkpoints are named `attempt `, one per attempt (dynamic; open decision). + +| Attribute | Requirement | +| --- | --- | +| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `lk.outcome` | required | +| `lk.reconnect.attempts` | conditionally_required: After the first attempt. | +| `lk.reconnect.mode` | conditionally_required: After the first attempt. | +| `lk.reconnect.reason` | required | + +### `lk.subscribe` (internal) + +One subscription, from the intent to subscribe to the first media (or timed_out after 30 s). Checkpoints: subscribed, first_media. + +| Attribute | Requirement | +| --- | --- | +| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `lk.outcome` | required | +| `lk.pii.remote_participant_identity` | recommended | +| `lk.track.kind` | required | +| `lk.track.source` | required | +| `lk.track_sid` | required | + +## Attribute sets + +### `livekit.client.log` + +Attributes of a plain log record (no event name). + +| Attribute | Requirement | +| --- | --- | +| `code.file.path` | recommended | +| `code.function.name` | recommended | +| `code.line.number` | recommended | +| `lk.log.logger` | required | +| `lk.log.source` | required | + +### `livekit.client.record` + +Attributes the core stamps on every record and span of a session. + +| Attribute | Requirement | +| --- | --- | +| `lk.participant_id` | recommended | +| `lk.pii.participant_identity` | recommended | +| `lk.pii.room_name` | recommended | +| `lk.room_sid` | recommended | +| `otel.event.name` | recommended | +| `session.id` | required | + +### `livekit.client.resource` + +Resource attributes of the client pipeline (one per process). + +| Attribute | Requirement | +| --- | --- | +| `device.model.identifier` | recommended | +| `lk.sdk.name` | recommended | +| `lk.sdk.version` | recommended | +| `os.name` | recommended | +| `os.version` | recommended | +| `service.name` | required | +| `service.version` | required | +| `telemetry.sdk.language` | required | +| `telemetry.sdk.name` | required | +| `telemetry.sdk.version` | required | diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs new file mode 100644 index 000000000..b090d866c --- /dev/null +++ b/otel/gen/rust/semconv.rs @@ -0,0 +1,501 @@ +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +//! LiveKit telemetry semantic conventions. + +/// Telemetry schema version; set as `schema_url` on every OTLP scope. +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.1.0"; + +/// 1-based connect attempt; 1 is the user-initiated connect. +pub const LK_CONNECT_ATTEMPT: &str = "lk.connect.attempt"; + +/// 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; forces a flush. + 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. + pub const BEGAN: &str = "began"; + /// Interruption ended. + pub const ENDED: &str = "ended"; +} + +/// Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. +pub const LK_DEVICE_AUDIO_ROUTE_OUTPUTS: &str = "lk.device.audio_route.outputs"; + +/// Why the audio route changed (AVAudioSession route change reasons). +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 (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 { + /// No thermal pressure. + pub const NOMINAL: &str = "nominal"; + /// Slightly elevated. + pub const FAIR: &str = "fair"; + /// High; the core doubles its cadence. + pub const SERIOUS: &str = "serious"; + /// Critical; the core quadruples its cadence. + pub const CRITICAL: &str = "critical"; +} + +/// Why the session ended (the protocol's DisconnectReason, plus the client giving up). +pub const LK_DISCONNECT_REASON: &str = "lk.disconnect.reason"; + +/// Values of [`LK_DISCONNECT_REASON`]. +pub mod lk_disconnect_reason { + /// The client 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 shut down. + pub const SERVER_SHUTDOWN: &str = "server_shutdown"; + /// The participant was removed. + pub const PARTICIPANT_REMOVED: &str = "participant_removed"; + /// The room was deleted. + pub const ROOM_DELETED: &str = "room_deleted"; + /// Client and server state diverged. + pub const STATE_MISMATCH: &str = "state_mismatch"; + /// The join failed. + pub const JOIN_FAILURE: &str = "join_failure"; + /// The session migrated. + pub const MIGRATION: &str = "migration"; + /// The signaling connection closed. + pub const SIGNAL_CLOSE: &str = "signal_close"; + /// The room closed. + pub const ROOM_CLOSED: &str = "room_closed"; + /// SIP callee unavailable. + pub const USER_UNAVAILABLE: &str = "user_unavailable"; + /// SIP callee rejected the call. + pub const USER_REJECTED: &str = "user_rejected"; + /// SIP trunk failure. + pub const SIP_TRUNK_FAILURE: &str = "sip_trunk_failure"; + /// The connection timed out. + pub const CONNECTION_TIMEOUT: &str = "connection_timeout"; + /// Media failed. + pub const MEDIA_FAILURE: &str = "media_failure"; + /// The agent failed. + pub const AGENT_ERROR: &str = "agent_error"; + /// The reconnect policy ran out of attempts. + pub const RECONNECT_FAILED: &str = "reconnect_failed"; + /// Unknown reason. + pub const UNKNOWN: &str = "unknown"; +} + +/// Agent job ID (`AJ_...`). +pub const LK_JOB_ID: &str = "lk.job_id"; + +/// 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. + pub const FFI: &str = "ffi"; + /// WebRTC. + pub const WEBRTC: &str = "webrtc"; +} + +/// How the attempt ended. Always present on a span; rollups read this, never the span status. +pub const LK_OUTCOME: &str = "lk.outcome"; + +/// Values of [`LK_OUTCOME`]. +pub mod lk_outcome { + /// Succeeded. + pub const OK: &str = "ok"; + /// Failed; `error.type` names why. + pub const ERROR: &str = "error"; + /// Cancelled by the caller or a newer attempt. + pub const CANCELLED: &str = "cancelled"; +} + +/// Server-assigned participant SID (`PA_...`). +pub const LK_PARTICIPANT_ID: &str = "lk.participant_id"; + +/// Customer-supplied identity of the local participant. PII: redacted by the collector. +pub const LK_PII_PARTICIPANT_IDENTITY: &str = "lk.pii.participant_identity"; + +/// 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"; + +/// Customer-supplied room name. PII: redacted by the collector. +pub const LK_PII_ROOM_NAME: &str = "lk.pii.room_name"; + +/// Monotonically increasing sequence number per pipeline. +pub const LK_PING_SEQ: &str = "lk.ping.seq"; + +/// 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 { + /// ICE restart / resume. + pub const QUICK: &str = "quick"; + /// Full reconnect. + pub const FULL: &str = "full"; +} + +/// What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). +pub const LK_RECONNECT_REASON: &str = "lk.reconnect.reason"; + +/// Values of [`LK_RECONNECT_REASON`]. +pub mod lk_reconnect_reason { + /// The signaling socket closed. + pub const SIGNAL_DISCONNECTED: &str = "signal_disconnected"; + /// The publisher peer connection failed. + pub const PUBLISHER_FAILED: &str = "publisher_failed"; + /// The subscriber peer connection failed. + pub const SUBSCRIBER_FAILED: &str = "subscriber_failed"; + /// A peer connection failed; the platform did not say which. + pub const TRANSPORT_FAILED: &str = "transport_failed"; + /// The server asked for a candidate switch. + pub const SWITCH_CANDIDATE: &str = "switch_candidate"; + /// The device moved to another network. + pub const NETWORK_CHANGED: &str = "network_changed"; + /// A test or debug menu asked for it. + pub const DEBUG: &str = "debug"; + /// Unknown reason. + pub const UNKNOWN: &str = "unknown"; +} + +/// Server-assigned room session SID (`RM_...`). +pub const LK_ROOM_SID: &str = "lk.room_sid"; + +/// Average audio level in the window, 0-1, audio. Unit: `1`. +pub const LK_RTC_AUDIO_LEVEL_AVG: &str = "lk.rtc.audio_level.avg"; + +/// Maximum audio level in the window, 0-1, audio. Unit: `1`. +pub const LK_RTC_AUDIO_LEVEL_MAX: &str = "lk.rtc.audio_level.max"; + +/// Minimum audio level in the window, 0-1, 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. Unit: `ms`. +pub const LK_RTC_JITTER_MS_AVG: &str = "lk.rtc.jitter_ms.avg"; + +/// Maximum jitter in the window. Unit: `ms`. +pub const LK_RTC_JITTER_MS_MAX: &str = "lk.rtc.jitter_ms.max"; + +/// Minimum jitter in the window. 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 (remote-inbound for outbound, candidate pair for inbound). Unit: `ms`. +pub const LK_RTC_RTT_MS_MIN: &str = "lk.rtc.rtt_ms.min"; + +/// Stats readings in 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"; + +/// Batches waiting in the cache now (a gauge). Unit: `{batch}`. +pub const LK_TELEMETRY_CACHE_BATCHES: &str = "lk.telemetry.cache.batches"; + +/// Batches the disk refused, kept in memory. 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 24 h age limit. Unit: `{record}`. +pub const LK_TELEMETRY_DROPPED_EXPIRED: &str = "lk.telemetry.dropped.expired"; + +/// Custom events or attributes 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, 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"; + +/// Soft upload holds that reached the 60 s 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"; + +/// 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. + pub const INBOUND: &str = "inbound"; + /// Sent media. + 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"; +} + +/// Source of the track (the protocol's TrackSource, snake_case). +pub const LK_TRACK_SOURCE: &str = "lk.track.source"; + +/// Values of [`LK_TRACK_SOURCE`]. +pub mod lk_track_source { + /// Camera. + pub const CAMERA: &str = "camera"; + /// Microphone. + pub const MICROPHONE: &str = "microphone"; + /// Screen share video. + pub const SCREEN_SHARE: &str = "screen_share"; + /// Screen share audio. + pub const SCREEN_SHARE_AUDIO: &str = "screen_share_audio"; + /// Unknown source. + pub const UNKNOWN: &str = "unknown"; +} + +/// Server-assigned track SID (`TR_...`). +pub const LK_TRACK_SID: &str = "lk.track_sid"; + +/// Event names (OTLP `LogRecord.event_name`). +pub mod event { + /// Namespace of app-defined events. The core emits `custom.` with the app's own attributes. + pub const CUSTOM: &str = "custom"; + /// App moved to the foreground or background (and its initial value). + pub const LK_DEVICE_APP_STATE_CHANGED: &str = "lk.device.app_state.changed"; + /// Audio session interruption began or ended. + pub const LK_DEVICE_AUDIO_INTERRUPTION: &str = "lk.device.audio.interruption"; + /// 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. Never per percent. + pub const LK_DEVICE_BATTERY_CHANGED: &str = "lk.device.battery.changed"; + /// A capture device failed. Severity WARN. + pub const LK_DEVICE_CAPTURE_FAILED: &str = "lk.device.capture.failed"; + /// Low Power Mode changed (and its initial value). + pub const LK_DEVICE_LOW_POWER_CHANGED: &str = "lk.device.low_power.changed"; + /// Memory pressure changed (and its initial value). + pub const LK_DEVICE_MEMORY_CHANGED: &str = "lk.device.memory.changed"; + /// Network path changed (and its initial value). + pub const LK_DEVICE_NETWORK_CHANGED: &str = "lk.device.network.changed"; + /// Thermal state changed (and its initial value). + pub const LK_DEVICE_THERMAL_CHANGED: &str = "lk.device.thermal.changed"; + /// Pipeline smoke test. Never emitted in production paths. + pub const LK_PING: &str = "lk.ping"; + /// The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. + 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, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. + pub const LK_TELEMETRY_REPORT: &str = "lk.telemetry.report"; +} + +/// Span names. +pub mod span { + /// 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 (or timed_out after 30 s). Kind: internal. + pub const LK_SUBSCRIBE: &str = "lk.subscribe"; +} diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts new file mode 100644 index 000000000..797870fc1 --- /dev/null +++ b/otel/gen/ts/semconv.ts @@ -0,0 +1,434 @@ +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. + +/** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ +export const SCHEMA_URL = 'https://livekit.io/schemas/telemetry/0.1.0' as const; + +/** 1-based connect attempt; 1 is the user-initiated connect. */ +export const ATTR_LK_CONNECT_ATTEMPT = 'lk.connect.attempt' 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; forces a flush. */ +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. */ +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_BEGAN = 'began' as const; +/** Interruption ended. */ +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_ENDED = 'ended' as const; + +/** Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. */ +export const ATTR_LK_DEVICE_AUDIO_ROUTE_OUTPUTS = 'lk.device.audio_route.outputs' as const; + +/** Why the audio route changed (AVAudioSession route change reasons). */ +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 (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; +/** No thermal pressure. */ +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 core doubles its cadence. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_SERIOUS = 'serious' as const; +/** Critical; the core quadruples its cadence. */ +export const LK_DEVICE_THERMAL_STATE_VALUE_CRITICAL = 'critical' as const; + +/** Why the session ended (the protocol's DisconnectReason, plus the client giving up). */ +export const ATTR_LK_DISCONNECT_REASON = 'lk.disconnect.reason' as const; +/** The client 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 shut down. */ +export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = 'server_shutdown' as const; +/** The participant was removed. */ +export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = 'participant_removed' as const; +/** The room was deleted. */ +export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = 'room_deleted' as const; +/** Client and server state diverged. */ +export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = 'state_mismatch' as const; +/** The join failed. */ +export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = 'join_failure' as const; +/** The session migrated. */ +export const LK_DISCONNECT_REASON_VALUE_MIGRATION = 'migration' as const; +/** The signaling connection closed. */ +export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = 'signal_close' as const; +/** The room closed. */ +export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = 'room_closed' as const; +/** SIP callee unavailable. */ +export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = 'user_unavailable' as const; +/** SIP callee rejected the call. */ +export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = 'user_rejected' as const; +/** SIP trunk failure. */ +export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = 'sip_trunk_failure' as const; +/** The connection timed out. */ +export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = 'connection_timeout' as const; +/** Media failed. */ +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 reconnect policy ran out of attempts. */ +export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = 'reconnect_failed' as const; +/** Unknown reason. */ +export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = 'unknown' as const; + +/** Agent job ID (`AJ_...`). */ +export const ATTR_LK_JOB_ID = 'lk.job_id' 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. */ +export const LK_LOG_SOURCE_VALUE_FFI = 'ffi' as const; +/** WebRTC. */ +export const LK_LOG_SOURCE_VALUE_WEBRTC = 'webrtc' as const; + +/** How the attempt ended. Always present on a span; rollups read this, never the span status. */ +export const ATTR_LK_OUTCOME = 'lk.outcome' as const; +/** Succeeded. */ +export const LK_OUTCOME_VALUE_OK = 'ok' as const; +/** Failed; `error.type` names why. */ +export const LK_OUTCOME_VALUE_ERROR = 'error' as const; +/** Cancelled by the caller or a newer attempt. */ +export const LK_OUTCOME_VALUE_CANCELLED = 'cancelled' as const; + +/** Server-assigned participant SID (`PA_...`). */ +export const ATTR_LK_PARTICIPANT_ID = 'lk.participant_id' 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; + +/** 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; + +/** Customer-supplied room name. PII: redacted by the collector. */ +export const ATTR_LK_PII_ROOM_NAME = 'lk.pii.room_name' as const; + +/** Monotonically increasing sequence number per pipeline. */ +export const ATTR_LK_PING_SEQ = 'lk.ping.seq' 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; +/** ICE restart / resume. */ +export const LK_RECONNECT_MODE_VALUE_QUICK = 'quick' as const; +/** Full reconnect. */ +export const LK_RECONNECT_MODE_VALUE_FULL = 'full' as const; + +/** What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). */ +export const ATTR_LK_RECONNECT_REASON = 'lk.reconnect.reason' as const; +/** The signaling socket closed. */ +export const LK_RECONNECT_REASON_VALUE_SIGNAL_DISCONNECTED = 'signal_disconnected' as const; +/** The publisher peer connection failed. */ +export const LK_RECONNECT_REASON_VALUE_PUBLISHER_FAILED = 'publisher_failed' as const; +/** The subscriber peer connection failed. */ +export const LK_RECONNECT_REASON_VALUE_SUBSCRIBER_FAILED = 'subscriber_failed' as const; +/** A peer connection failed; the platform did not say which. */ +export const LK_RECONNECT_REASON_VALUE_TRANSPORT_FAILED = 'transport_failed' as const; +/** The server asked for a candidate switch. */ +export const LK_RECONNECT_REASON_VALUE_SWITCH_CANDIDATE = 'switch_candidate' as const; +/** The device moved to another network. */ +export const LK_RECONNECT_REASON_VALUE_NETWORK_CHANGED = 'network_changed' as const; +/** A test or debug menu asked for it. */ +export const LK_RECONNECT_REASON_VALUE_DEBUG = 'debug' as const; +/** Unknown reason. */ +export const LK_RECONNECT_REASON_VALUE_UNKNOWN = 'unknown' as const; + +/** Server-assigned room session SID (`RM_...`). */ +export const ATTR_LK_ROOM_SID = 'lk.room_sid' as const; + +/** Average audio level in the window, 0-1, 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, 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, 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. Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_MS_AVG = 'lk.rtc.jitter_ms.avg' as const; + +/** Maximum jitter in the window. Unit: `ms`. */ +export const ATTR_LK_RTC_JITTER_MS_MAX = 'lk.rtc.jitter_ms.max' as const; + +/** Minimum jitter in the window. 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 (remote-inbound for outbound, candidate pair for inbound). Unit: `ms`. */ +export const ATTR_LK_RTC_RTT_MS_MIN = 'lk.rtc.rtt_ms.min' as const; + +/** Stats readings in 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; + +/** Batches waiting in the cache now (a gauge). Unit: `{batch}`. */ +export const ATTR_LK_TELEMETRY_CACHE_BATCHES = 'lk.telemetry.cache.batches' as const; + +/** Batches the disk refused, kept in memory. 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 24 h age limit. Unit: `{record}`. */ +export const ATTR_LK_TELEMETRY_DROPPED_EXPIRED = 'lk.telemetry.dropped.expired' as const; + +/** Custom events or attributes 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, 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; + +/** Soft upload holds that reached the 60 s 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; + +/** Whether the stats describe sent or received media. */ +export const ATTR_LK_TRACK_DIRECTION = 'lk.track.direction' as const; +/** Received media. */ +export const LK_TRACK_DIRECTION_VALUE_INBOUND = 'inbound' as const; +/** Sent media. */ +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; + +/** Source of the track (the protocol's TrackSource, snake_case). */ +export const ATTR_LK_TRACK_SOURCE = 'lk.track.source' as const; +/** Camera. */ +export const LK_TRACK_SOURCE_VALUE_CAMERA = 'camera' as const; +/** Microphone. */ +export const LK_TRACK_SOURCE_VALUE_MICROPHONE = 'microphone' as const; +/** Screen share video. */ +export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE = 'screen_share' as const; +/** Screen share audio. */ +export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE_AUDIO = 'screen_share_audio' as const; +/** Unknown source. */ +export const LK_TRACK_SOURCE_VALUE_UNKNOWN = 'unknown' as const; + +/** Server-assigned track SID (`TR_...`). */ +export const ATTR_LK_TRACK_SID = 'lk.track_sid' as const; + +/** Namespace of app-defined events. The core emits `custom.` with the app's own attributes. */ +export const EVENT_CUSTOM = 'custom' as const; +/** App moved to the foreground or background (and its initial value). */ +export const EVENT_LK_DEVICE_APP_STATE_CHANGED = 'lk.device.app_state.changed' as const; +/** Audio session interruption began or ended. */ +export const EVENT_LK_DEVICE_AUDIO_INTERRUPTION = 'lk.device.audio.interruption' as const; +/** 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. Never per percent. */ +export const EVENT_LK_DEVICE_BATTERY_CHANGED = 'lk.device.battery.changed' as const; +/** A capture device failed. Severity WARN. */ +export const EVENT_LK_DEVICE_CAPTURE_FAILED = 'lk.device.capture.failed' as const; +/** Low Power Mode changed (and its initial value). */ +export const EVENT_LK_DEVICE_LOW_POWER_CHANGED = 'lk.device.low_power.changed' as const; +/** Memory pressure changed (and its initial value). */ +export const EVENT_LK_DEVICE_MEMORY_CHANGED = 'lk.device.memory.changed' as const; +/** Network path changed (and its initial value). */ +export const EVENT_LK_DEVICE_NETWORK_CHANGED = 'lk.device.network.changed' as const; +/** Thermal state changed (and its initial value). */ +export const EVENT_LK_DEVICE_THERMAL_CHANGED = 'lk.device.thermal.changed' as const; +/** Pipeline smoke test. Never emitted in production paths. */ +export const EVENT_LK_PING = 'lk.ping' as const; +/** The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. */ +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, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. */ +export const EVENT_LK_TELEMETRY_REPORT = 'lk.telemetry.report' 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 (or timed_out after 30 s). Kind: internal. */ +export const SPAN_LK_SUBSCRIBE = 'lk.subscribe' as const; diff --git a/otel/generate.sh b/otel/generate.sh new file mode 100755 index 000000000..bfa360f6b --- /dev/null +++ b/otel/generate.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Regenerates otel/gen from otel/model (Rust, TypeScript, markdown docs). CI fails when the +# committed outputs differ from a fresh run. +set -euo pipefail +cd "$(dirname "$0")" +W=${WEAVER:-weaver} +for target in rust:rust typescript:ts markdown:docs; do + "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "gen/${target#*:}" +done diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 new file mode 100644 index 000000000..7a299dfb8 --- /dev/null +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -0,0 +1,49 @@ +{%- macro req(a) -%} +{%- if a.requirement_level is string -%}{{ a.requirement_level }}{%- else -%}{% for k, v in a.requirement_level | items %}{{ k }}: {{ v | trim }}{% endfor %}{%- endif -%} +{%- endmacro -%} +{%- macro table(attrs) -%} +| Attribute | Requirement | +| --- | --- | +{% for a in attrs %}| `{{ a.key }}` | {{ req(a) }} | +{% endfor %} +{%- endmacro -%} + +# LiveKit telemetry conventions + +Schema: `{{ ctx.schema_url }}`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. + +## Attributes + +| Key | Type | Owner | Sensitivity | Unit | Description | +| --- | --- | --- | --- | --- | --- | +{% for a in ctx.attributes %}| `{{ a.key }}` | {% if a.type is mapping %}enum: {% for m in a.type.members %}`{{ m.value }}`{% if not loop.last %}, {% endif %}{% endfor %}{% else %}{{ a.type }}{% endif %} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% if a.annotations.livekit.unit %}`{{ a.annotations.livekit.unit }}`{% if a.annotations.livekit.unit_exception %} (exception: {{ a.annotations.livekit.unit_exception }}){% endif %}{% endif %} | {{ a.brief | trim | replace("\n", " ") | replace("|", "\\|") }}{% if a.note %} {{ a.note | trim | replace("\n", " ") | replace("|", "\\|") }}{% endif %} | +{% endfor %} +## Bindings + +Physical names of a concept in other producers and stores. Carried for reference; nothing validates them. + +| Key | Binding | Name | +| --- | --- | --- | +{% 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 %} +## Events +{% for e in ctx.events %} +### `{{ e.name }}` + +{{ e.brief | trim }}{% if e.note %} {{ e.note | trim }}{% endif %} +{% if e.attributes %} +{{ table(e.attributes) }}{% endif %}{% endfor %} +## Spans +{% for s in ctx.spans %} +### `{{ s.type }}` ({{ s.kind }}) + +{{ s.brief | trim }}{% if s.note %} {{ s.note | trim | replace("\n", " ") }}{% endif %} + +{{ table(s.attributes) }}{% endfor %} +## Attribute sets +{% for g in ctx.groups %} +### `{{ g.id }}` + +{{ g.brief | trim }} + +{{ table(g.attributes) }}{% endfor %} diff --git a/otel/templates/registry/markdown/weaver.yaml b/otel/templates/registry/markdown/weaver.yaml new file mode 100644 index 000000000..3a461601b --- /dev/null +++ b/otel/templates/registry/markdown/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.md.j2 + filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 new file mode 100644 index 000000000..da698405c --- /dev/null +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -0,0 +1,36 @@ +{%- macro doc(a) -%} +{{ a.brief | trim | replace("\n", " ") }}{% if a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations.livekit.unit %} Unit: `{{ a.annotations.livekit.unit }}`.{% endif %} +{%- endmacro -%} +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +//! LiveKit telemetry semantic conventions. + +/// Telemetry schema version; set as `schema_url` on every OTLP scope. +pub const SCHEMA_URL: &str = "{{ ctx.schema_url }}"; +{% for a in ctx.attributes %} +/// {{ doc(a) }} +pub const {{ a.key | screaming_snake_case }}: &str = "{{ a.key }}"; +{% if a.type is mapping %} +/// Values of [`{{ a.key | screaming_snake_case }}`]. +pub mod {{ a.key | snake_case }} { +{% for m in a.type.members %} /// {{ m.brief | trim }} + pub const {{ m.id | screaming_snake_case }}: &str = "{{ m.value }}"; +{% endfor %}} +{% endif %}{% endfor %} +/// Event names (OTLP `LogRecord.event_name`). +pub mod event { +{% for e in ctx.events %} /// {{ e.brief | trim | replace("\n", " ") }} + pub const {{ e.name | screaming_snake_case }}: &str = "{{ e.name }}"; +{% endfor %}} + +/// Span names. +pub mod span { +{% for s in ctx.spans %} /// {{ s.brief | trim | replace("\n", " ") }} Kind: {{ s.kind }}. + pub const {{ s.type | screaming_snake_case }}: &str = "{{ s.type }}"; +{% endfor %}} +{% if ctx.metrics %} +/// Metric names. +pub mod metric { +{% for m in ctx.metrics %} /// {{ m.brief | trim | replace("\n", " ") }} Instrument: {{ m.instrument }}, unit: `{{ m.unit }}`. + pub const {{ m.name | screaming_snake_case }}: &str = "{{ m.name }}"; +{% endfor %}} +{% endif %} diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml new file mode 100644 index 000000000..43b217188 --- /dev/null +++ b/otel/templates/registry/rust/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.rs.j2 + filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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.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..0ad82619c --- /dev/null +++ b/otel/templates/registry/typescript/semconv.ts.j2 @@ -0,0 +1,22 @@ +{%- macro doc(a) -%} +{{ a.brief | trim | replace("\n", " ") }}{% if a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations.livekit.unit %} Unit: `{{ a.annotations.livekit.unit }}`.{% endif %} +{%- endmacro -%} +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. + +/** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ +export const SCHEMA_URL = '{{ ctx.schema_url }}' as const; +{% for a in ctx.attributes %} +/** {{ doc(a) }} */ +export const ATTR_{{ a.key | screaming_snake_case }} = '{{ a.key }}' as const; +{% if a.type is mapping %}{% for m in a.type.members %}/** {{ m.brief | trim }} */ +export const {{ a.key | screaming_snake_case }}_VALUE_{{ m.id | screaming_snake_case }} = '{{ m.value }}' as const; +{% endfor %}{% endif %}{% endfor %} +{% for e in ctx.events %}/** {{ e.brief | trim | replace("\n", " ") }} */ +export const EVENT_{{ e.name | screaming_snake_case }} = '{{ e.name }}' as const; +{% endfor %} +{% for s in ctx.spans %}/** {{ s.brief | trim | replace("\n", " ") }} Kind: {{ s.kind }}. */ +export const SPAN_{{ s.type | screaming_snake_case }} = '{{ s.type }}' as const; +{% endfor %}{% for m in ctx.metrics %} +/** {{ m.brief | trim | replace("\n", " ") }} Instrument: {{ m.instrument }}, unit: `{{ m.unit }}`. */ +export const METRIC_{{ m.name | screaming_snake_case }} = '{{ m.name }}' 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..85cfa40eb --- /dev/null +++ b/otel/templates/registry/typescript/weaver.yaml @@ -0,0 +1,5 @@ +templates: + - template: semconv.ts.j2 + filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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.ts From 5b052cb14ee9b7f59b7f31589ef24331a3e190c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:52:25 +0200 Subject: [PATCH 03/36] ci(otel): live-check captures and check the registry in CI live-check.sh checks an OTLP/JSON capture against the registry, with matchers for spans, plain logs and the resource. On the reference client session every finding is one of the expected identity/PII renames. The workflow pins weaver 0.28.0 and runs the check, the negatives, the generated-output diff and compile, the live-check smoke test and the schema-version bump rule. --- .github/workflows/otel.yaml | 66 ++++++++++++++ otel/README.md | 153 ++++++++++++++++++++++++++++++++ otel/live-check.sh | 25 ++++++ otel/live-check/livekit.rego | 45 ++++++++++ otel/live-check/otlp2samples.py | 54 +++++++++++ otel/live-check/summarize.py | 29 ++++++ otel/live-check/weaver.toml | 65 ++++++++++++++ otel/tests/live-check.sh | 19 ++++ otel/tests/live-sample.json | 11 +++ otel/tests/schema-bump.sh | 20 +++++ 10 files changed, 487 insertions(+) create mode 100644 .github/workflows/otel.yaml create mode 100644 otel/README.md create mode 100755 otel/live-check.sh create mode 100644 otel/live-check/livekit.rego create mode 100644 otel/live-check/otlp2samples.py create mode 100644 otel/live-check/summarize.py create mode 100644 otel/live-check/weaver.toml create mode 100755 otel/tests/live-check.sh create mode 100644 otel/tests/live-sample.json create mode 100755 otel/tests/schema-bump.sh diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml new file mode 100644 index 000000000..4c3403024 --- /dev/null +++ b/.github/workflows/otel.yaml @@ -0,0 +1,66 @@ +# 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 + with: + fetch-depth: 0 # the schema-version rule diffs against the base branch + + - 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 + + - name: Generated outputs are current + run: | + otel/generate.sh + git add --intent-to-add otel/gen + git diff --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 + + - name: Live-check the smoke sample + run: otel/tests/live-check.sh + + - name: Schema version bumped when the model changed + if: github.event_name == 'pull_request' + env: + BASE_REF: ${{ github.base_ref }} + run: otel/tests/schema-bump.sh "origin/$BASE_REF" diff --git a/otel/README.md b/otel/README.md new file mode 100644 index 000000000..cd2f21713 --- /dev/null +++ b/otel/README.md @@ -0,0 +1,153 @@ +# 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, +and what the client core (rust-sdks `livekit-telemetry`) emits today, under its current names. +Agents-owned keys are not registered yet. + +| Path | What | +|---|---| +| `model/manifest.yaml` | Registry version (`schema_url`) and the pinned OTel dependency | +| `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.*`) | +| `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) | +| `policies/` | LiveKit rules; any finding fails `check.sh` | +| `advice/` | Non-failing advice, printed by `check.sh` | +| `templates/registry/{rust,typescript,markdown}/` | Codegen templates | +| `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `docs/semconv.md` | +| `live-check/` | Live-check config, advice rules and the OTLP/JSON converter | +| `tests/` | Policy negatives, live-check smoke sample, schema-version rule | + +The generated docs, [`gen/docs/semconv.md`](gen/docs/semconv.md), list every key with its type, +owner, sensitivity and unit. + +## 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` and `cloud` are valid owners for when those teams register their keys, in their own + directories. + +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, 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. + +## 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; `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` (RTC stats keep webrtc-stats milliseconds) | + | `bindings` | map of producer/store → name | informational | + | `cardinality_exception` | `true`, on a metric | allows a high-cardinality ID as a metric dimension | + + 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 `otel/generate.sh`, and commit `gen/` with the change. +6. Bump the schema version (below). + +## Bumping the schema version + +`schema_url` in `model/manifest.yaml` is the telemetry schema version. Producers set it as the +OTLP scope `schema_url` (generated as `SCHEMA_URL`), which the collector already stores as a +column. It is versioned independently of the protocol package. + +Every PR that changes `model/` bumps it; CI fails otherwise (`tests/schema-bump.sh`). While the +registry is `0.x`, bump the minor version for added, renamed or removed keys, events or spans, and +the patch version for documentation-only changes. To rename a key that has shipped, keep the old +key with `deprecated: {reason: renamed, renamed_to: }` instead of deleting it; +`weaver registry diff` and live-check both report the rename. + +## Running the checks locally + +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 +prints 34 warnings: 33 about upstream's own files, and one because `client/groups.yaml` uses the +not-yet-stable `definition/2` format. + +```sh +otel/check.sh # registry + LiveKit policies, then advice +otel/tests/negatives.sh # each negative fixture fails with its named finding +otel/generate.sh && git status otel/gen # regenerate; no diff expected +otel/tests/live-check.sh # smoke sample clean, legacy keys flagged +otel/tests/schema-bump.sh origin/main # schema_url bumped if model/ changed + +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 +``` + +| 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 | +| `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*` | +| `livekit_flat_identity` | yes | no `lk.room.sid`, `lk.participant.sid`, `lk.track.sid`, `lk.job.id` | +| `livekit_seconds`, `livekit_unit`, `livekit_metric_unit` | yes | seconds, unless `unit_exception` | +| `livekit_cardinality` | yes | high-cardinality IDs are not metric dimensions | +| `livekit_client_namespace` | no (advice) | client-owned keys under `lk.client.*` | + +Weaver itself rejects duplicate keys, unknown fields and unresolved references. + +## 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_flat_identity` and `livekit_pii_unmarked`, each naming +the key to send instead. + +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 expected shared identity / PII renames: + +| Emitted today | Registered key | Findings | +|---|---|---| +| `lk.room.sid` | `lk.room_sid` | `missing_attribute`, `unexpected_attribute`, `livekit_flat_identity` ×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_pii_unmarked` ×10 each | +| `lk.participant.identity` | `lk.pii.participant_identity` | same, ×10 each | +| `lk.participant.remote_identity` | `lk.pii.remote_participant_identity` (name TBD) | same, ×2 each, plus `recommended_attribute_not_present` ×2 | + +With those six keys renamed in the same capture, live-check reports nothing and exits 0. +`tests/live-sample.json` is part of that capture, anonymized. + +Known limits: + +- Span event names (connect checkpoints, `attempt `) are not checked; weaver compares + span events only by attributes. +- 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. diff --git a/otel/live-check.sh b/otel/live-check.sh new file mode 100755 index 000000000..6a4aa8e98 --- /dev/null +++ b/otel/live-check.sh @@ -0,0 +1,25 @@ +#!/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). +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +W=${WEAVER:-weaver} +capture=$1 out=${2:-$(mktemp -d)} +mkdir -p "$out" + +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" --input-source "$samples" --input-format json \ + --format json -o "$out" "${@:3}" > "$out/weaver.log" 2>&1 || status=$? +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..a449e5164 --- /dev/null +++ b/otel/live-check/livekit.rego @@ -0,0 +1,45 @@ +package live_check_advice + +# Same contract as otel/policies/livekit.rego, applied to emitted telemetry. Each finding names +# the registered key a producer should send instead. + +import rego.v1 + +pii_segment := `(^|\.)pii(\.|$)` + +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", +} + +canonical_pii := { + "lk.room.name": "lk.pii.room_name", + "lk.participant.identity": "lk.pii.participant_identity", + "lk.participant.remote_identity": "lk.pii.remote_participant_identity", # name TBD +} + +lk_advice(type, ctx, msg) := { + "type": "advice", + "advice_type": type, + "advice_level": "violation", + "advice_context": ctx, + "message": msg, +} + +deny contains lk_advice("livekit_pii_unmarked", {"attribute_key": key, "expected": object.get(canonical_pii, key, null)}, sprintf("'%s' looks like PII but has no `pii` key segment, so the collector will not redact it%s", [key, hint])) if { + key := input.sample.attribute.name + startswith(key, "lk.") + regex.match(pii_looking, key) + not regex.match(pii_segment, key) + hint := object.get({k: sprintf("; send '%s'", [v]) | some k, v in canonical_pii}, key, "") +} + +deny contains lk_advice("livekit_flat_identity", {"attribute_key": key, "expected": want}, sprintf("'%s' must be emitted as '%s'", [key, want])) if { + key := input.sample.attribute.name + want := canonical_identity[key] +} 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/summarize.py b/otel/live-check/summarize.py new file mode 100644 index 000000000..33a4ffc8b --- /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 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/tests/live-check.sh b/otel/tests/live-check.sh new file mode 100755 index 000000000..3e4ff1431 --- /dev/null +++ b/otel/tests/live-check.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# The smoke sample (a real client capture, anonymized, canonical names) must pass with no +# findings; the same sample with the client's current identity/PII keys must be flagged. +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; } + +sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.name"/g' \ + 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_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name"; do + [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } +done +echo "PASS live-check: smoke sample clean, legacy keys flagged" diff --git a/otel/tests/live-sample.json b/otel/tests/live-sample.json new file mode 100644 index 000000000..43a673e8d --- /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": "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": "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/schema-bump.sh b/otel/tests/schema-bump.sh new file mode 100755 index 000000000..5793113c8 --- /dev/null +++ b/otel/tests/schema-bump.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Fails when otel/model differs from but its schema_url was not bumped. +# usage: otel/tests/schema-bump.sh (CI passes origin/) +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +base=$(git merge-base "$1" HEAD) +manifest=otel/model/manifest.yaml + +if git diff --quiet "$base" -- otel/model; then + echo "otel/model unchanged since $base"; exit 0 +fi +old=$(git show "$base:$manifest" 2>/dev/null | grep '^schema_url:' || true) +new=$(grep '^schema_url:' "$manifest") +if [[ -z $old ]]; then + echo "new registry: $new" +elif [[ $old == "$new" ]]; then + echo "otel/model changed since $base but $manifest still has $new: bump the version" >&2; exit 1 +else + echo "schema version bumped: ${old#schema_url: } -> ${new#schema_url: }" +fi From e3d3cf53c35784b3d0b1bd3e56af9f341476c981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:13:31 +0200 Subject: [PATCH 04/36] fix(otel): check metrics and ref overrides on the resolved registry The per-file policy saw only v1 metrics and their direct refs, so a definition/2 metric in ms, or a metric inheriting lk.room_sid through extends, passed. It also skipped refs, so a ref could re-label lk.room_sid as owner bogus / sensitivity pii and keep that after resolution. Metric units and dimensions are now checked in an after_resolution policy on the effective groups, and a ref that overrides the annotations of an lk.* key is rejected from its lineage, in either file format. New negatives cover a definition/2 metric unit, definition/2 and extends cardinality, and v1 and definition/2 ref overrides. --- otel/README.md | 8 +++-- otel/policies/livekit.rego | 20 ++--------- otel/policies/resolved.rego | 36 +++++++++++++++++++ otel/tests/negatives/cardinality-extends.yaml | 16 +++++++++ otel/tests/negatives/cardinality-v2.yaml | 11 ++++++ otel/tests/negatives/metric-unit-v2.yaml | 8 +++++ otel/tests/negatives/ref-override-v2.yaml | 11 ++++++ otel/tests/negatives/ref-override.yaml | 11 ++++++ 8 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 otel/policies/resolved.rego create mode 100644 otel/tests/negatives/cardinality-extends.yaml create mode 100644 otel/tests/negatives/cardinality-v2.yaml create mode 100644 otel/tests/negatives/metric-unit-v2.yaml create mode 100644 otel/tests/negatives/ref-override-v2.yaml create mode 100644 otel/tests/negatives/ref-override.yaml diff --git a/otel/README.md b/otel/README.md index cd2f21713..5c2cecb08 100644 --- a/otel/README.md +++ b/otel/README.md @@ -17,7 +17,7 @@ Agents-owned keys are not registered yet. | `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) | -| `policies/` | LiveKit rules; any finding fails `check.sh` | +| `policies/` | LiveKit rules, per file (`livekit.rego`) and on the resolved registry (`resolved.rego`); any finding fails `check.sh` | | `advice/` | Non-failing advice, printed by `check.sh` | | `templates/registry/{rust,typescript,markdown}/` | Codegen templates | | `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `docs/semconv.md` | @@ -108,8 +108,10 @@ npx -p typescript@5.9.3 tsc --strict --noEmit --target es2020 otel/gen/ts/semcon | `livekit_pii_marker` | yes | `sensitivity: pii` iff a `pii` key segment | | `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*` | | `livekit_flat_identity` | yes | no `lk.room.sid`, `lk.participant.sid`, `lk.track.sid`, `lk.job.id` | -| `livekit_seconds`, `livekit_unit`, `livekit_metric_unit` | yes | seconds, unless `unit_exception` | -| `livekit_cardinality` | yes | high-cardinality IDs are not metric dimensions | +| `livekit_seconds`, `livekit_unit` | yes | seconds, unless `unit_exception` | +| `livekit_metric_unit` | yes | metric units are not `ms` (resolved: v1, `definition/2` and `extends`) | +| `livekit_cardinality` | yes | high-cardinality IDs are not metric dimensions (resolved, so inherited ones count) | +| `livekit_ref_override` | yes | a `ref` to an `lk.*` key may not override its `annotations` (owner, sensitivity, unit) | | `livekit_client_namespace` | no (advice) | client-owned keys under `lk.client.*` | Weaver itself rejects duplicate keys, unknown fields and unresolved references. diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego index 402576822..5b9e36c3b 100644 --- a/otel/policies/livekit.rego +++ b/otel/policies/livekit.rego @@ -1,7 +1,7 @@ package before_resolution -# LiveKit registry rules (violations), run by otel/check.sh. Each finding id is -# documented in otel/README.md; otel/tests/negatives has one failing fixture per violation. +# LiveKit rules per definition file (violations), run by otel/check.sh. Each finding id is +# documented in otel/README.md; otel/tests/negatives has a failing fixture for each. import rego.v1 @@ -20,8 +20,6 @@ canonical_identity := { "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). @@ -98,16 +96,4 @@ deny contains finding("livekit_unit", sprintf("%s: duration-like attribute must not ms_exception(attr) } -deny contains finding("livekit_metric_unit", sprintf("%s: metric unit must be s, not ms", [group.metric_name])) if { - some group in input.groups - group.type == "metric" - group.unit == "ms" -} - -deny contains finding("livekit_cardinality", sprintf("%s: %s is high-cardinality, not a default metric dimension", [group.metric_name, a.ref])) if { - some group in input.groups - group.type == "metric" - some a in group.attributes - high_cardinality[a.ref] - lk(group, "cardinality_exception") != true # Agents' job-scoped exception sets true -} +# Metric rules (units, cardinality) and ref overrides live in otel/policies/resolved.rego. diff --git a/otel/policies/resolved.rego b/otel/policies/resolved.rego new file mode 100644 index 000000000..aa3c7a9e7 --- /dev/null +++ b/otel/policies/resolved.rego @@ -0,0 +1,36 @@ +package after_resolution + +# LiveKit rules on the resolved registry, run by otel/check.sh with otel/policies/livekit.rego. +# They see what each group ends up with after refs, `extends` and definition/2 files are +# resolved, which the per-file rules cannot. + +import rego.v1 + +high_cardinality := {"lk.room_sid", "lk.participant_id", "lk.track_sid", "lk.job_id", "lk.client.connect_id"} + +finding(id, msg) := {"id": id, "level": "violation", "message": msg} + +where(group) := object.get(group, ["lineage", "provenance", "path"], group.id) + +deny contains finding("livekit_metric_unit", sprintf("%s (%s): metric unit must be s, not ms", [group.metric_name, where(group)])) if { + some group in input.groups + group.type == "metric" + group.unit == "ms" +} + +deny contains finding("livekit_cardinality", sprintf("%s (%s): %s is high-cardinality, not a default metric dimension", [group.metric_name, where(group), attr.name])) if { + some group in input.groups + group.type == "metric" + some attr in group.attributes + high_cardinality[attr.name] + object.get(group, ["annotations", "livekit", "cardinality_exception"], false) != true # Agents' job-scoped 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. +deny contains finding("livekit_ref_override", sprintf("%s in %s (%s): a ref may not override the annotations of a LiveKit key", [key, group.id, where(group)])) if { + some group in input.groups + some key, lineage in object.get(group, ["lineage", "attributes"], {}) + startswith(key, "lk.") + "annotations" in object.get(lineage, "locally_overridden_fields", []) +} 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-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/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/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}} From 82c2065bc8038aa3ce239bcf315fb9bf41dd8951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:14:45 +0200 Subject: [PATCH 05/36] fix(otel): recognise durations before aggregation suffixes livekit_unit matched duration words only at the end of a key, so lk.client.rtt.avg in ms with no unit_exception passed. A duration word followed by an optional _ms and one aggregation segment (.avg, .min, .max, _p99, ...) now counts. The RTC keys' unit_exception still applies; the new rtt-aggregate negative fails with livekit_unit. --- otel/policies/livekit.rego | 8 ++++++-- otel/tests/negatives/rtt-aggregate.yaml | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 otel/tests/negatives/rtt-aggregate.yaml diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego index 5b9e36c3b..14a5b38ab 100644 --- a/otel/policies/livekit.rego +++ b/otel/policies/livekit.rego @@ -89,9 +89,13 @@ deny contains finding("livekit_seconds", sprintf("%s: LiveKit durations are seco not ms_exception(attr) } -deny contains finding("livekit_unit", sprintf("%s: duration-like attribute must declare annotations.livekit.unit: s", [attr.id])) if { +# 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]+))?$` + +deny contains finding("livekit_unit", sprintf("%s: duration-like attribute must declare annotations.livekit.unit: s (or unit: ms with a unit_exception)", [attr.id])) if { some attr in lk_attrs - regex.match(`(duration|latency|delay|rtt|jitter)$`, attr.id) + regex.match(duration_like, attr.id) lk(attr, "unit") != "s" not ms_exception(attr) } 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}} From 1b68e73a4067502d65a0496b034a06d77c8c884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:15:23 +0200 Subject: [PATCH 06/36] fix(otel): RTC counters and RTT are not required on a stats window RtcStatsSample's counters and gauges are Option and the core omits None, so a window can carry only the track, kind, direction, window and sample count. lk.rtc.bytes and lk.rtc.packets were required, which made such a window a live-check violation, and the RTT gauges were recommended. They are now conditionally required, and a sparse-window fixture must pass live-check with no findings. --- otel/gen/docs/semconv.md | 12 ++++++------ otel/model/client/signals.yaml | 11 ++++++----- otel/tests/live-check.sh | 10 ++++++++-- otel/tests/live-sparse.json | 3 +++ 4 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 otel/tests/live-sparse.json diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 86e4af9eb..c66241267 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -206,14 +206,14 @@ The Room left connected for good (never on a reconnect). INFO when the client hu ### `lk.rtc.stats.sample` -One RTC stats window for one track and direction. +One RTC stats window for one track and direction. Only the track, kind, direction, window and sample count are always present; counters and gauges a platform or direction lacks are omitted. | Attribute | Requirement | | --- | --- | | `lk.rtc.audio_level.avg` | conditionally_required: Audio. | | `lk.rtc.audio_level.max` | conditionally_required: Audio. | | `lk.rtc.audio_level.min` | conditionally_required: Audio. | -| `lk.rtc.bytes` | required | +| `lk.rtc.bytes` | conditionally_required: When the stats report it. | | `lk.rtc.codec` | conditionally_required: When known. | | `lk.rtc.concealed_samples` | conditionally_required: Inbound audio. | | `lk.rtc.concealment_events` | conditionally_required: Inbound audio. | @@ -229,16 +229,16 @@ One RTC stats window for one track and direction. | `lk.rtc.jitter_ms.avg` | conditionally_required: Inbound. | | `lk.rtc.jitter_ms.max` | conditionally_required: Inbound. | | `lk.rtc.jitter_ms.min` | conditionally_required: Inbound. | -| `lk.rtc.packets` | required | +| `lk.rtc.packets` | conditionally_required: When the stats report it. | | `lk.rtc.packets_lost` | conditionally_required: Inbound. | | `lk.rtc.pause_count` | conditionally_required: Inbound video. | | `lk.rtc.pauses_duration_ms` | conditionally_required: Inbound video. | | `lk.rtc.quality_limitation.bandwidth_ms` | conditionally_required: Outbound video. | | `lk.rtc.quality_limitation.cpu_ms` | conditionally_required: Outbound video. | | `lk.rtc.quality_limitation.other_ms` | conditionally_required: Outbound video. | -| `lk.rtc.rtt_ms.avg` | recommended | -| `lk.rtc.rtt_ms.max` | recommended | -| `lk.rtc.rtt_ms.min` | recommended | +| `lk.rtc.rtt_ms.avg` | conditionally_required: When the window has readings. | +| `lk.rtc.rtt_ms.max` | conditionally_required: When the window has readings. | +| `lk.rtc.rtt_ms.min` | conditionally_required: When the window has readings. | | `lk.rtc.samples` | required | | `lk.rtc.silent_concealed_samples` | conditionally_required: Inbound audio. | | `lk.rtc.window_ms` | required | diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml index 2a9b3fc90..ba439d1c6 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -151,6 +151,7 @@ groups: name: lk.rtc.stats.sample stability: development brief: One RTC stats window for one track and direction. + note: Only the track, kind, direction, window and sample count are always present; counters and gauges a platform or direction lacks are omitted. attributes: - ref: lk.track_sid requirement_level: required @@ -165,9 +166,9 @@ groups: - ref: lk.rtc.samples requirement_level: required - ref: lk.rtc.bytes - requirement_level: required + requirement_level: {conditionally_required: When the stats report it.} - ref: lk.rtc.packets - requirement_level: required + requirement_level: {conditionally_required: When the stats report it.} - ref: lk.rtc.packets_lost requirement_level: {conditionally_required: Inbound.} - ref: lk.rtc.freeze_count @@ -205,11 +206,11 @@ groups: - ref: lk.rtc.jitter_ms.avg requirement_level: {conditionally_required: Inbound.} - ref: lk.rtc.rtt_ms.min - requirement_level: recommended + requirement_level: {conditionally_required: When the window has readings.} - ref: lk.rtc.rtt_ms.max - requirement_level: recommended + requirement_level: {conditionally_required: When the window has readings.} - ref: lk.rtc.rtt_ms.avg - requirement_level: recommended + requirement_level: {conditionally_required: When the window has readings.} - ref: lk.rtc.fps.min requirement_level: {conditionally_required: Video.} - ref: lk.rtc.fps.max diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh index 3e4ff1431..f57293882 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # The smoke sample (a real client capture, anonymized, canonical names) must pass with no -# findings; the same sample with the client's current identity/PII keys must be flagged. +# findings, and so must a sparse RTC window; the same smoke sample with the client's current +# identity/PII keys must be flagged. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -9,6 +10,11 @@ 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; } + sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.name"/g' \ tests/live-sample.json > "$tmp/legacy.json" legacy=$(./live-check.sh "$tmp/legacy.json" "$tmp/legacy" --fail-on none) @@ -16,4 +22,4 @@ echo "$legacy" for want in "livekit_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name"; do [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } done -echo "PASS live-check: smoke sample clean, legacy keys flagged" +echo "PASS live-check: smoke and sparse samples clean, legacy keys flagged" 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"}]}} +] From 60b057bc7470ec5abc7e458dfd752d31f98e0efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:16:13 +0200 Subject: [PATCH 07/36] fix(otel): require a strictly higher schema_url version schema-bump.sh compared raw manifest lines, so a comment or quotes on an unchanged URL, or a downgrade from 0.1.0 to 0.0.1, counted as a bump. It now reads schema_url as YAML does, parses its MAJOR.MINOR.PATCH and requires a strict increase whenever otel/model changed. schema-bump-test.sh exercises the rule in a throwaway repo (comment-only, downgrade, quoted, non-semver, patch/minor/major bumps) and runs in CI next to the PR check, which calls the same script. --- .github/workflows/otel.yaml | 4 ++++ otel/README.md | 7 +++++-- otel/tests/schema-bump-test.sh | 29 +++++++++++++++++++++++++++ otel/tests/schema-bump.sh | 36 +++++++++++++++++++++++----------- 4 files changed, 63 insertions(+), 13 deletions(-) create mode 100755 otel/tests/schema-bump-test.sh diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml index 4c3403024..9031d853e 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -59,6 +59,10 @@ jobs: - name: Live-check the smoke sample run: otel/tests/live-check.sh + - name: Schema-version rule rejects comment-only changes and downgrades + run: otel/tests/schema-bump-test.sh + + # The same script as locally: a changed otel/model needs a strictly higher schema_url version. - name: Schema version bumped when the model changed if: github.event_name == 'pull_request' env: diff --git a/otel/README.md b/otel/README.md index 5c2cecb08..4fa522fea 100644 --- a/otel/README.md +++ b/otel/README.md @@ -76,7 +76,9 @@ nothing validates it. OTLP scope `schema_url` (generated as `SCHEMA_URL`), which the collector already stores as a column. It is versioned independently of the protocol package. -Every PR that changes `model/` bumps it; CI fails otherwise (`tests/schema-bump.sh`). While the +Every PR that changes `model/` bumps it: CI fails unless the version at the end of `schema_url` +(MAJOR.MINOR.PATCH) is strictly higher than the base branch's (`tests/schema-bump.sh`; a comment +or quotes on an unchanged URL, or a downgrade, do not count). While the registry is `0.x`, bump the minor version for added, renamed or removed keys, events or spans, and the patch version for documentation-only changes. To rename a key that has shipped, keep the old key with `deprecated: {reason: renamed, renamed_to: }` instead of deleting it; @@ -95,7 +97,8 @@ otel/check.sh # registry + LiveKit policies, then advic otel/tests/negatives.sh # each negative fixture fails with its named finding otel/generate.sh && git status otel/gen # regenerate; no diff expected otel/tests/live-check.sh # smoke sample clean, legacy keys flagged -otel/tests/schema-bump.sh origin/main # schema_url bumped if model/ changed +otel/tests/schema-bump.sh origin/main # schema_url version strictly higher if model/ changed +otel/tests/schema-bump-test.sh # that rule, against comment-only changes and downgrades 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 diff --git a/otel/tests/schema-bump-test.sh b/otel/tests/schema-bump-test.sh new file mode 100755 index 000000000..a78178c47 --- /dev/null +++ b/otel/tests/schema-bump-test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Exercises otel/tests/schema-bump.sh in a throwaway repo: a changed model passes only with a +# strictly higher schema_url version. +set -uo pipefail +bump=$(cd "$(dirname "$0")" && pwd)/schema-bump.sh +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT +cd "$tmp" && git init -q && mkdir -p otel/model +url() { printf 'file_format: definition_manifest/2.0\nschema_url: %s\n' "$1" > otel/model/manifest.yaml; } +url https://livekit.io/schemas/telemetry/0.1.0 && echo 'groups: []' > otel/model/a.yaml +git add -A && git -c user.name=t -c user.email=t@t commit -qm base + +failed=0 +case_() { # case_ + git checkout -q -- . && url "$3" + [[ $4 == 1 ]] && echo '# changed' >> otel/model/a.yaml + "$bump" HEAD > out.txt 2>&1 + local got=$? + if [[ $got == "$2" ]]; then echo "PASS $1 (exit $got)"; else echo "FAIL $1 (exit $got, want $2)"; cat out.txt; failed=1; fi +} +case_ "unchanged" 0 https://livekit.io/schemas/telemetry/0.1.0 0 +case_ "changed, not bumped" 1 https://livekit.io/schemas/telemetry/0.1.0 1 +case_ "changed, only a comment on the URL" 1 "https://livekit.io/schemas/telemetry/0.1.0 # bumped?" 1 +case_ "changed, downgraded" 1 https://livekit.io/schemas/telemetry/0.0.1 1 +case_ "changed, same version quoted" 1 '"https://livekit.io/schemas/telemetry/0.1.0"' 1 +case_ "changed, version not semver" 1 https://livekit.io/schemas/telemetry/next 1 +case_ "changed, patch bump" 0 https://livekit.io/schemas/telemetry/0.1.1 1 +case_ "changed, minor bump with a comment" 0 "https://livekit.io/schemas/telemetry/0.2.0 # renames" 1 +case_ "changed, major bump" 0 https://livekit.io/schemas/telemetry/1.0.0 1 +exit "$failed" diff --git a/otel/tests/schema-bump.sh b/otel/tests/schema-bump.sh index 5793113c8..ab02852d3 100755 --- a/otel/tests/schema-bump.sh +++ b/otel/tests/schema-bump.sh @@ -1,20 +1,34 @@ #!/usr/bin/env bash -# Fails when otel/model differs from but its schema_url was not bumped. +# Fails when otel/model differs from but the schema_url version did not strictly increase. # usage: otel/tests/schema-bump.sh (CI passes origin/) +# otel/tests/schema-bump-test.sh exercises it. set -euo pipefail cd "$(git rev-parse --show-toplevel)" base=$(git merge-base "$1" HEAD) -manifest=otel/model/manifest.yaml if git diff --quiet "$base" -- otel/model; then echo "otel/model unchanged since $base"; exit 0 fi -old=$(git show "$base:$manifest" 2>/dev/null | grep '^schema_url:' || true) -new=$(grep '^schema_url:' "$manifest") -if [[ -z $old ]]; then - echo "new registry: $new" -elif [[ $old == "$new" ]]; then - echo "otel/model changed since $base but $manifest still has $new: bump the version" >&2; exit 1 -else - echo "schema version bumped: ${old#schema_url: } -> ${new#schema_url: }" -fi +python3 -I - "$base" otel/model/manifest.yaml <<'PY' +import re, subprocess, sys + +base, path = sys.argv[1:] + +def version(text, where): + """schema_url as YAML reads it (quotes, trailing comment dropped) and its MAJOR.MINOR.PATCH.""" + m = re.search(r"""^schema_url:[ \t]*(["']?)([^"'#\s]+)\1[ \t]*(#.*)?$""", text, re.M) + v = m and re.fullmatch(r".*/(\d+)\.(\d+)\.(\d+)", m.group(2)) + if not v: + sys.exit(f"{where}: schema_url must end in a MAJOR.MINOR.PATCH version") + return m.group(2), tuple(int(n) for n in v.groups()) + +new_url, new = version(open(path).read(), path) +old = subprocess.run(["git", "show", f"{base}:{path}"], capture_output=True, text=True) +if old.returncode: + print(f"new registry: {new_url}") + sys.exit() +old_url, old_v = version(old.stdout, f"{base}:{path}") +if new <= old_v: + sys.exit(f"otel/model changed since {base[:12]}, but {new_url} is not a higher version than {old_url}: bump it") +print(f"schema version bumped: {old_url} -> {new_url}") +PY From 1cdb8cc022e55f650288f20f1bb21f20138c260a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:18:36 +0200 Subject: [PATCH 08/36] fix(otel): escape generated literals and doc comments The Rust and TypeScript templates wrote values verbatim, so an enum value with a quote, backslash or newline, or a brief with a newline (Rust ///) or */ (TS JSDoc), produced code that did not compile. Rust literals now escape backslash, quote, CR, LF and tab; TypeScript literals use tojson; doc comments are kept to one line and */ is broken up. codegen.sh generates a fixture with such a value and briefs, compiles both outputs and checks the value comes back byte for byte from rustc and tsc + node; it runs in CI. Committed TS output only switches to double quotes. --- .github/workflows/otel.yaml | 3 + otel/README.md | 5 +- otel/gen/ts/semconv.ts | 346 +++++++++--------- .../templates/registry/markdown/semconv.md.j2 | 2 +- otel/templates/registry/rust/semconv.rs.j2 | 31 +- .../registry/typescript/semconv.ts.j2 | 27 +- otel/tests/codegen.sh | 25 ++ otel/tests/codegen/escape.yaml | 18 + 8 files changed, 259 insertions(+), 198 deletions(-) create mode 100755 otel/tests/codegen.sh create mode 100644 otel/tests/codegen/escape.yaml diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml index 9031d853e..57af7c094 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -56,6 +56,9 @@ jobs: 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 + - name: Codegen escapes literals (round-trip through rustc and tsc) + run: otel/tests/codegen.sh + - name: Live-check the smoke sample run: otel/tests/live-check.sh diff --git a/otel/README.md b/otel/README.md index 4fa522fea..0fe2f42fc 100644 --- a/otel/README.md +++ b/otel/README.md @@ -22,7 +22,7 @@ Agents-owned keys are not registered yet. | `templates/registry/{rust,typescript,markdown}/` | Codegen templates | | `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `docs/semconv.md` | | `live-check/` | Live-check config, advice rules and the OTLP/JSON converter | -| `tests/` | Policy negatives, live-check smoke sample, schema-version rule | +| `tests/` | Policy negatives, live-check samples, codegen escaping, schema-version rule | The generated docs, [`gen/docs/semconv.md`](gen/docs/semconv.md), list every key with its type, owner, sensitivity and unit. @@ -96,7 +96,8 @@ not-yet-stable `definition/2` format. otel/check.sh # registry + LiveKit policies, then advice otel/tests/negatives.sh # each negative fixture fails with its named finding otel/generate.sh && git status otel/gen # regenerate; no diff expected -otel/tests/live-check.sh # smoke sample clean, legacy keys flagged +otel/tests/live-check.sh # smoke and sparse samples clean, legacy keys flagged +otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc and tsc otel/tests/schema-bump.sh origin/main # schema_url version strictly higher if model/ changed otel/tests/schema-bump-test.sh # that rule, against comment-only changes and downgrades diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 797870fc1..0a41a2f50 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,434 +1,434 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = 'https://livekit.io/schemas/telemetry/0.1.0' as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.1.0" as const; /** 1-based connect attempt; 1 is the user-initiated connect. */ -export const ATTR_LK_CONNECT_ATTEMPT = 'lk.connect.attempt' as const; +export const ATTR_LK_CONNECT_ATTEMPT = "lk.connect.attempt" as const; /** Whether the app is in front of the user. */ -export const ATTR_LK_DEVICE_APP_STATE = 'lk.device.app_state' as const; +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; +export const LK_DEVICE_APP_STATE_VALUE_FOREGROUND = "foreground" as const; /** In the background; forces a flush. */ -export const LK_DEVICE_APP_STATE_VALUE_BACKGROUND = 'background' as const; +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; +export const ATTR_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" as const; /** Interruption began. */ -export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_BEGAN = 'began' as const; +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_BEGAN = "began" as const; /** Interruption ended. */ -export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_ENDED = 'ended' as const; +export const LK_DEVICE_AUDIO_INTERRUPTION_VALUE_ENDED = "ended" as const; /** Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. */ -export const ATTR_LK_DEVICE_AUDIO_ROUTE_OUTPUTS = 'lk.device.audio_route.outputs' as const; +export const ATTR_LK_DEVICE_AUDIO_ROUTE_OUTPUTS = "lk.device.audio_route.outputs" as const; /** Why the audio route changed (AVAudioSession route change reasons). */ -export const ATTR_LK_DEVICE_AUDIO_ROUTE_REASON = 'lk.device.audio_route.reason' as const; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +export const LK_DEVICE_CAPTURE_DEVICE_VALUE_CAMERA = "camera" as const; /** Microphone. */ -export const LK_DEVICE_CAPTURE_DEVICE_VALUE_MICROPHONE = 'microphone' as const; +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; +export const LK_DEVICE_CAPTURE_DEVICE_VALUE_SCREEN_SHARE = "screen_share" as const; /** Why capture failed (the getUserMedia failure taxonomy). */ -export const ATTR_LK_DEVICE_CAPTURE_REASON = 'lk.device.capture.reason' as const; +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; +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; +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; +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; +export const LK_DEVICE_CAPTURE_REASON_VALUE_DISCONNECTED = "disconnected" as const; /** Anything else. */ -export const LK_DEVICE_CAPTURE_REASON_VALUE_OTHER = 'other' as const; +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; +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; +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; +export const LK_DEVICE_MEMORY_PRESSURE_VALUE_NORMAL = "normal" as const; /** Low memory. */ -export const LK_DEVICE_MEMORY_PRESSURE_VALUE_WARNING = 'warning' as const; +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; +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; +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; +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; +export const ATTR_LK_DEVICE_THERMAL_STATE = "lk.device.thermal.state" as const; /** No thermal pressure. */ -export const LK_DEVICE_THERMAL_STATE_VALUE_NOMINAL = 'nominal' as const; +export const LK_DEVICE_THERMAL_STATE_VALUE_NOMINAL = "nominal" as const; /** Slightly elevated. */ -export const LK_DEVICE_THERMAL_STATE_VALUE_FAIR = 'fair' as const; +export const LK_DEVICE_THERMAL_STATE_VALUE_FAIR = "fair" as const; /** High; the core doubles its cadence. */ -export const LK_DEVICE_THERMAL_STATE_VALUE_SERIOUS = 'serious' as const; +export const LK_DEVICE_THERMAL_STATE_VALUE_SERIOUS = "serious" as const; /** Critical; the core quadruples its cadence. */ -export const LK_DEVICE_THERMAL_STATE_VALUE_CRITICAL = 'critical' as const; +export const LK_DEVICE_THERMAL_STATE_VALUE_CRITICAL = "critical" as const; /** Why the session ended (the protocol's DisconnectReason, plus the client giving up). */ -export const ATTR_LK_DISCONNECT_REASON = 'lk.disconnect.reason' as const; +export const ATTR_LK_DISCONNECT_REASON = "lk.disconnect.reason" as const; /** The client hung up. */ -export const LK_DISCONNECT_REASON_VALUE_CLIENT_INITIATED = 'client_initiated' as const; +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; +export const LK_DISCONNECT_REASON_VALUE_DUPLICATE_IDENTITY = "duplicate_identity" as const; /** The server shut down. */ -export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = 'server_shutdown' as const; +export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = "server_shutdown" as const; /** The participant was removed. */ -export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = 'participant_removed' as const; +export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = "participant_removed" as const; /** The room was deleted. */ -export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = 'room_deleted' as const; +export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = "room_deleted" as const; /** Client and server state diverged. */ -export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = 'state_mismatch' as const; +export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = "state_mismatch" as const; /** The join failed. */ -export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = 'join_failure' as const; +export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = "join_failure" as const; /** The session migrated. */ -export const LK_DISCONNECT_REASON_VALUE_MIGRATION = 'migration' as const; +export const LK_DISCONNECT_REASON_VALUE_MIGRATION = "migration" as const; /** The signaling connection closed. */ -export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = 'signal_close' as const; +export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = "signal_close" as const; /** The room closed. */ -export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = 'room_closed' as const; +export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = "room_closed" as const; /** SIP callee unavailable. */ -export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = 'user_unavailable' as const; +export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = "user_unavailable" as const; /** SIP callee rejected the call. */ -export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = 'user_rejected' as const; +export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = "user_rejected" as const; /** SIP trunk failure. */ -export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = 'sip_trunk_failure' as const; +export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = "sip_trunk_failure" as const; /** The connection timed out. */ -export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = 'connection_timeout' as const; +export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = "connection_timeout" as const; /** Media failed. */ -export const LK_DISCONNECT_REASON_VALUE_MEDIA_FAILURE = 'media_failure' as const; +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; +export const LK_DISCONNECT_REASON_VALUE_AGENT_ERROR = "agent_error" as const; /** The reconnect policy ran out of attempts. */ -export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = 'reconnect_failed' as const; +export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = "reconnect_failed" as const; /** Unknown reason. */ -export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = 'unknown' as const; +export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = "unknown" as const; /** Agent job ID (`AJ_...`). */ -export const ATTR_LK_JOB_ID = 'lk.job_id' as const; +export const ATTR_LK_JOB_ID = "lk.job_id" as const; /** Logger, type, module or file that wrote the line. */ -export const ATTR_LK_LOG_LOGGER = 'lk.log.logger' as const; +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; +export const ATTR_LK_LOG_SOURCE = "lk.log.source" as const; /** The platform SDK. */ -export const LK_LOG_SOURCE_VALUE_SDK = 'sdk' as const; +export const LK_LOG_SOURCE_VALUE_SDK = "sdk" as const; /** The Rust core. */ -export const LK_LOG_SOURCE_VALUE_FFI = 'ffi' as const; +export const LK_LOG_SOURCE_VALUE_FFI = "ffi" as const; /** WebRTC. */ -export const LK_LOG_SOURCE_VALUE_WEBRTC = 'webrtc' as const; +export const LK_LOG_SOURCE_VALUE_WEBRTC = "webrtc" as const; /** How the attempt ended. Always present on a span; rollups read this, never the span status. */ -export const ATTR_LK_OUTCOME = 'lk.outcome' as const; +export const ATTR_LK_OUTCOME = "lk.outcome" as const; /** Succeeded. */ -export const LK_OUTCOME_VALUE_OK = 'ok' as const; +export const LK_OUTCOME_VALUE_OK = "ok" as const; /** Failed; `error.type` names why. */ -export const LK_OUTCOME_VALUE_ERROR = 'error' as const; +export const LK_OUTCOME_VALUE_ERROR = "error" as const; /** Cancelled by the caller or a newer attempt. */ -export const LK_OUTCOME_VALUE_CANCELLED = 'cancelled' as const; +export const LK_OUTCOME_VALUE_CANCELLED = "cancelled" as const; /** Server-assigned participant SID (`PA_...`). */ -export const ATTR_LK_PARTICIPANT_ID = 'lk.participant_id' as const; +export const ATTR_LK_PARTICIPANT_ID = "lk.participant_id" 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; +export const ATTR_LK_PII_PARTICIPANT_IDENTITY = "lk.pii.participant_identity" 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; +export const ATTR_LK_PII_REMOTE_PARTICIPANT_IDENTITY = "lk.pii.remote_participant_identity" as const; /** Customer-supplied room name. PII: redacted by the collector. */ -export const ATTR_LK_PII_ROOM_NAME = 'lk.pii.room_name' as const; +export const ATTR_LK_PII_ROOM_NAME = "lk.pii.room_name" as const; /** Monotonically increasing sequence number per pipeline. */ -export const ATTR_LK_PING_SEQ = 'lk.ping.seq' as const; +export const ATTR_LK_PING_SEQ = "lk.ping.seq" as const; /** Reconnect attempts made in the cycle. */ -export const ATTR_LK_RECONNECT_ATTEMPTS = 'lk.reconnect.attempts' as const; +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; +export const ATTR_LK_RECONNECT_MODE = "lk.reconnect.mode" as const; /** ICE restart / resume. */ -export const LK_RECONNECT_MODE_VALUE_QUICK = 'quick' as const; +export const LK_RECONNECT_MODE_VALUE_QUICK = "quick" as const; /** Full reconnect. */ -export const LK_RECONNECT_MODE_VALUE_FULL = 'full' as const; +export const LK_RECONNECT_MODE_VALUE_FULL = "full" as const; /** What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). */ -export const ATTR_LK_RECONNECT_REASON = 'lk.reconnect.reason' as const; +export const ATTR_LK_RECONNECT_REASON = "lk.reconnect.reason" as const; /** The signaling socket closed. */ -export const LK_RECONNECT_REASON_VALUE_SIGNAL_DISCONNECTED = 'signal_disconnected' as const; +export const LK_RECONNECT_REASON_VALUE_SIGNAL_DISCONNECTED = "signal_disconnected" as const; /** The publisher peer connection failed. */ -export const LK_RECONNECT_REASON_VALUE_PUBLISHER_FAILED = 'publisher_failed' as const; +export const LK_RECONNECT_REASON_VALUE_PUBLISHER_FAILED = "publisher_failed" as const; /** The subscriber peer connection failed. */ -export const LK_RECONNECT_REASON_VALUE_SUBSCRIBER_FAILED = 'subscriber_failed' as const; +export const LK_RECONNECT_REASON_VALUE_SUBSCRIBER_FAILED = "subscriber_failed" as const; /** A peer connection failed; the platform did not say which. */ -export const LK_RECONNECT_REASON_VALUE_TRANSPORT_FAILED = 'transport_failed' as const; +export const LK_RECONNECT_REASON_VALUE_TRANSPORT_FAILED = "transport_failed" as const; /** The server asked for a candidate switch. */ -export const LK_RECONNECT_REASON_VALUE_SWITCH_CANDIDATE = 'switch_candidate' as const; +export const LK_RECONNECT_REASON_VALUE_SWITCH_CANDIDATE = "switch_candidate" as const; /** The device moved to another network. */ -export const LK_RECONNECT_REASON_VALUE_NETWORK_CHANGED = 'network_changed' as const; +export const LK_RECONNECT_REASON_VALUE_NETWORK_CHANGED = "network_changed" as const; /** A test or debug menu asked for it. */ -export const LK_RECONNECT_REASON_VALUE_DEBUG = 'debug' as const; +export const LK_RECONNECT_REASON_VALUE_DEBUG = "debug" as const; /** Unknown reason. */ -export const LK_RECONNECT_REASON_VALUE_UNKNOWN = 'unknown' as const; +export const LK_RECONNECT_REASON_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned room session SID (`RM_...`). */ -export const ATTR_LK_ROOM_SID = 'lk.room_sid' as const; +export const ATTR_LK_ROOM_SID = "lk.room_sid" as const; /** Average audio level in the window, 0-1, audio. Unit: `1`. */ -export const ATTR_LK_RTC_AUDIO_LEVEL_AVG = 'lk.rtc.audio_level.avg' as const; +export const ATTR_LK_RTC_AUDIO_LEVEL_AVG = "lk.rtc.audio_level.avg" as const; /** Maximum audio level in the window, 0-1, audio. Unit: `1`. */ -export const ATTR_LK_RTC_AUDIO_LEVEL_MAX = 'lk.rtc.audio_level.max' as const; +export const ATTR_LK_RTC_AUDIO_LEVEL_MAX = "lk.rtc.audio_level.max" as const; /** Minimum audio level in the window, 0-1, audio. Unit: `1`. */ -export const ATTR_LK_RTC_AUDIO_LEVEL_MIN = 'lk.rtc.audio_level.min' as const; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +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; +export const ATTR_LK_RTC_JITTER_BUFFER_EMITTED_COUNT = "lk.rtc.jitter_buffer_emitted_count" as const; /** Average jitter in the window. Unit: `ms`. */ -export const ATTR_LK_RTC_JITTER_MS_AVG = 'lk.rtc.jitter_ms.avg' as const; +export const ATTR_LK_RTC_JITTER_MS_AVG = "lk.rtc.jitter_ms.avg" as const; /** Maximum jitter in the window. Unit: `ms`. */ -export const ATTR_LK_RTC_JITTER_MS_MAX = 'lk.rtc.jitter_ms.max' as const; +export const ATTR_LK_RTC_JITTER_MS_MAX = "lk.rtc.jitter_ms.max" as const; /** Minimum jitter in the window. Unit: `ms`. */ -export const ATTR_LK_RTC_JITTER_MS_MIN = 'lk.rtc.jitter_ms.min' as const; +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; +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; +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; +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; +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; +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; +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; +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; +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; +export const ATTR_LK_RTC_RTT_MS_MAX = "lk.rtc.rtt_ms.max" as const; /** Minimum round-trip time in the window (remote-inbound for outbound, candidate pair for inbound). Unit: `ms`. */ -export const ATTR_LK_RTC_RTT_MS_MIN = 'lk.rtc.rtt_ms.min' as const; +export const ATTR_LK_RTC_RTT_MS_MIN = "lk.rtc.rtt_ms.min" as const; /** Stats readings in the window. Unit: `{reading}`. */ -export const ATTR_LK_RTC_SAMPLES = 'lk.rtc.samples' as const; +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; +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; +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; +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; +export const ATTR_LK_SDK_VERSION = "lk.sdk.version" as const; /** Batches waiting in the cache now (a gauge). Unit: `{batch}`. */ -export const ATTR_LK_TELEMETRY_CACHE_BATCHES = 'lk.telemetry.cache.batches' as const; +export const ATTR_LK_TELEMETRY_CACHE_BATCHES = "lk.telemetry.cache.batches" as const; /** Batches the disk refused, kept in memory. Unit: `{batch}`. */ -export const ATTR_LK_TELEMETRY_CACHE_WRITE_ERRORS = 'lk.telemetry.cache.write_errors' as const; +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; +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; +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; +export const ATTR_LK_TELEMETRY_DROPPED_CORRUPT = "lk.telemetry.dropped.corrupt" as const; /** Records in batches past the 24 h age limit. Unit: `{record}`. */ -export const ATTR_LK_TELEMETRY_DROPPED_EXPIRED = 'lk.telemetry.dropped.expired' as const; +export const ATTR_LK_TELEMETRY_DROPPED_EXPIRED = "lk.telemetry.dropped.expired" as const; /** Custom events or attributes over the limits (rejected). Unit: `{record}`. */ -export const ATTR_LK_TELEMETRY_DROPPED_INVALID = 'lk.telemetry.dropped.invalid' as const; +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; +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; +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; +export const ATTR_LK_TELEMETRY_DROPPED_RATE_LIMITED = "lk.telemetry.dropped.rate_limited" as const; /** Records the collector rejected (final 4xx/5xx, partial success). Unit: `{record}`. */ -export const ATTR_LK_TELEMETRY_DROPPED_REJECTED = 'lk.telemetry.dropped.rejected' as const; +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; +export const ATTR_LK_TELEMETRY_DROPPED_THROTTLED = "lk.telemetry.dropped.throttled" as const; /** Soft upload holds that reached the 60 s cap. Unit: `{hold}`. */ -export const ATTR_LK_TELEMETRY_HOLDS_CAPPED = 'lk.telemetry.holds.capped' as const; +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; +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; +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; +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; +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; +export const ATTR_LK_TELEMETRY_UPLOADS_UNAUTHORIZED = "lk.telemetry.uploads.unauthorized" as const; /** Whether the stats describe sent or received media. */ -export const ATTR_LK_TRACK_DIRECTION = 'lk.track.direction' as const; +export const ATTR_LK_TRACK_DIRECTION = "lk.track.direction" as const; /** Received media. */ -export const LK_TRACK_DIRECTION_VALUE_INBOUND = 'inbound' as const; +export const LK_TRACK_DIRECTION_VALUE_INBOUND = "inbound" as const; /** Sent media. */ -export const LK_TRACK_DIRECTION_VALUE_OUTBOUND = 'outbound' as const; +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; +export const ATTR_LK_TRACK_KIND = "lk.track.kind" as const; /** Audio. */ -export const LK_TRACK_KIND_VALUE_AUDIO = 'audio' as const; +export const LK_TRACK_KIND_VALUE_AUDIO = "audio" as const; /** Video. */ -export const LK_TRACK_KIND_VALUE_VIDEO = 'video' as const; +export const LK_TRACK_KIND_VALUE_VIDEO = "video" as const; /** Source of the track (the protocol's TrackSource, snake_case). */ -export const ATTR_LK_TRACK_SOURCE = 'lk.track.source' as const; +export const ATTR_LK_TRACK_SOURCE = "lk.track.source" as const; /** Camera. */ -export const LK_TRACK_SOURCE_VALUE_CAMERA = 'camera' as const; +export const LK_TRACK_SOURCE_VALUE_CAMERA = "camera" as const; /** Microphone. */ -export const LK_TRACK_SOURCE_VALUE_MICROPHONE = 'microphone' as const; +export const LK_TRACK_SOURCE_VALUE_MICROPHONE = "microphone" as const; /** Screen share video. */ -export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE = 'screen_share' as const; +export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE = "screen_share" as const; /** Screen share audio. */ -export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE_AUDIO = 'screen_share_audio' as const; +export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE_AUDIO = "screen_share_audio" as const; /** Unknown source. */ -export const LK_TRACK_SOURCE_VALUE_UNKNOWN = 'unknown' as const; +export const LK_TRACK_SOURCE_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned track SID (`TR_...`). */ -export const ATTR_LK_TRACK_SID = 'lk.track_sid' as const; +export const ATTR_LK_TRACK_SID = "lk.track_sid" as const; /** Namespace of app-defined events. The core emits `custom.` with the app's own attributes. */ -export const EVENT_CUSTOM = 'custom' as const; +export const EVENT_CUSTOM = "custom" as const; /** App moved to the foreground or background (and its initial value). */ -export const EVENT_LK_DEVICE_APP_STATE_CHANGED = 'lk.device.app_state.changed' as const; +export const EVENT_LK_DEVICE_APP_STATE_CHANGED = "lk.device.app_state.changed" as const; /** Audio session interruption began or ended. */ -export const EVENT_LK_DEVICE_AUDIO_INTERRUPTION = 'lk.device.audio.interruption' as const; +export const EVENT_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" as const; /** Audio route changed. */ -export const EVENT_LK_DEVICE_AUDIO_ROUTE_CHANGED = 'lk.device.audio_route.changed' as const; +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. Never per percent. */ -export const EVENT_LK_DEVICE_BATTERY_CHANGED = 'lk.device.battery.changed' as const; +export const EVENT_LK_DEVICE_BATTERY_CHANGED = "lk.device.battery.changed" as const; /** A capture device failed. Severity WARN. */ -export const EVENT_LK_DEVICE_CAPTURE_FAILED = 'lk.device.capture.failed' as const; +export const EVENT_LK_DEVICE_CAPTURE_FAILED = "lk.device.capture.failed" as const; /** Low Power Mode changed (and its initial value). */ -export const EVENT_LK_DEVICE_LOW_POWER_CHANGED = 'lk.device.low_power.changed' as const; +export const EVENT_LK_DEVICE_LOW_POWER_CHANGED = "lk.device.low_power.changed" as const; /** Memory pressure changed (and its initial value). */ -export const EVENT_LK_DEVICE_MEMORY_CHANGED = 'lk.device.memory.changed' as const; +export const EVENT_LK_DEVICE_MEMORY_CHANGED = "lk.device.memory.changed" as const; /** Network path changed (and its initial value). */ -export const EVENT_LK_DEVICE_NETWORK_CHANGED = 'lk.device.network.changed' as const; +export const EVENT_LK_DEVICE_NETWORK_CHANGED = "lk.device.network.changed" as const; /** Thermal state changed (and its initial value). */ -export const EVENT_LK_DEVICE_THERMAL_CHANGED = 'lk.device.thermal.changed' as const; +export const EVENT_LK_DEVICE_THERMAL_CHANGED = "lk.device.thermal.changed" as const; /** Pipeline smoke test. Never emitted in production paths. */ -export const EVENT_LK_PING = 'lk.ping' as const; +export const EVENT_LK_PING = "lk.ping" as const; /** The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. */ -export const EVENT_LK_ROOM_DISCONNECTED = 'lk.room.disconnected' as const; +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; +export const EVENT_LK_RTC_STATS_SAMPLE = "lk.rtc.stats.sample" as const; /** Self-telemetry, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. */ -export const EVENT_LK_TELEMETRY_REPORT = 'lk.telemetry.report' as const; +export const EVENT_LK_TELEMETRY_REPORT = "lk.telemetry.report" as const; /** One connect attempt. Kind: client. */ -export const SPAN_LK_CONNECT = 'lk.connect' as const; +export const SPAN_LK_CONNECT = "lk.connect" as const; /** One track publish. Kind: internal. */ -export const SPAN_LK_PUBLISH = 'lk.publish' as const; +export const SPAN_LK_PUBLISH = "lk.publish" as const; /** One reconnect cycle. Kind: client. */ -export const SPAN_LK_RECONNECT = 'lk.reconnect' as const; +export const SPAN_LK_RECONNECT = "lk.reconnect" as const; /** One subscription, from the intent to subscribe to the first media (or timed_out after 30 s). Kind: internal. */ -export const SPAN_LK_SUBSCRIBE = 'lk.subscribe' as const; +export const SPAN_LK_SUBSCRIBE = "lk.subscribe" as const; diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index 7a299dfb8..b605d2e63 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -16,7 +16,7 @@ Schema: `{{ ctx.schema_url }}`. Upstream OTel keys (OTel semconv v1.44.0) are re | Key | Type | Owner | Sensitivity | Unit | Description | | --- | --- | --- | --- | --- | --- | -{% for a in ctx.attributes %}| `{{ a.key }}` | {% if a.type is mapping %}enum: {% for m in a.type.members %}`{{ m.value }}`{% if not loop.last %}, {% endif %}{% endfor %}{% else %}{{ a.type }}{% endif %} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% if a.annotations.livekit.unit %}`{{ a.annotations.livekit.unit }}`{% if a.annotations.livekit.unit_exception %} (exception: {{ a.annotations.livekit.unit_exception }}){% endif %}{% endif %} | {{ a.brief | trim | replace("\n", " ") | replace("|", "\\|") }}{% if a.note %} {{ a.note | trim | replace("\n", " ") | replace("|", "\\|") }}{% endif %} | +{% for a in ctx.attributes %}| `{{ a.key }}` | {% if a.type is mapping %}enum: {% for m in a.type.members %}`{{ m.value | replace("\r", " ") | replace("\n", " ") | replace("|", "\\|") }}`{% if not loop.last %}, {% endif %}{% endfor %}{% else %}{{ a.type }}{% endif %} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% if a.annotations.livekit.unit %}`{{ a.annotations.livekit.unit }}`{% if a.annotations.livekit.unit_exception %} (exception: {{ a.annotations.livekit.unit_exception }}){% endif %}{% endif %} | {{ a.brief | trim | replace("\n", " ") | replace("|", "\\|") }}{% if a.note %} {{ a.note | trim | replace("\n", " ") | replace("|", "\\|") }}{% endif %} | {% endfor %} ## Bindings diff --git a/otel/templates/registry/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 index da698405c..f96231ec2 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -1,36 +1,45 @@ +{#- 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) -%} -{{ a.brief | trim | replace("\n", " ") }}{% if a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations.livekit.unit %} Unit: `{{ a.annotations.livekit.unit }}`.{% endif %} +{{ 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. //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "{{ ctx.schema_url }}"; +pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; {% for a in ctx.attributes %} /// {{ doc(a) }} -pub const {{ a.key | screaming_snake_case }}: &str = "{{ a.key }}"; +pub const {{ a.key | screaming_snake_case }}: &str = {{ lit(a.key) }}; {% if a.type is mapping %} /// Values of [`{{ a.key | screaming_snake_case }}`]. pub mod {{ a.key | snake_case }} { -{% for m in a.type.members %} /// {{ m.brief | trim }} - pub const {{ m.id | screaming_snake_case }}: &str = "{{ m.value }}"; +{% for m in a.type.members %} /// {{ line(m.brief) }} + pub const {{ m.id | screaming_snake_case }}: &str = {{ lit(m.value) }}; {% endfor %}} {% endif %}{% endfor %} /// Event names (OTLP `LogRecord.event_name`). pub mod event { -{% for e in ctx.events %} /// {{ e.brief | trim | replace("\n", " ") }} - pub const {{ e.name | screaming_snake_case }}: &str = "{{ e.name }}"; +{% 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 %} /// {{ s.brief | trim | replace("\n", " ") }} Kind: {{ s.kind }}. - pub const {{ s.type | screaming_snake_case }}: &str = "{{ s.type }}"; +{% for s in ctx.spans %} /// {{ line(s.brief) }} Kind: {{ s.kind }}. + pub const {{ s.type | screaming_snake_case }}: &str = {{ lit(s.type) }}; {% endfor %}} {% if ctx.metrics %} /// Metric names. pub mod metric { -{% for m in ctx.metrics %} /// {{ m.brief | trim | replace("\n", " ") }} Instrument: {{ m.instrument }}, unit: `{{ m.unit }}`. - pub const {{ m.name | screaming_snake_case }}: &str = "{{ m.name }}"; +{% for m in ctx.metrics %} /// {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit) }}`. + pub const {{ m.name | screaming_snake_case }}: &str = {{ lit(m.name) }}; {% endfor %}} {% endif %} diff --git a/otel/templates/registry/typescript/semconv.ts.j2 b/otel/templates/registry/typescript/semconv.ts.j2 index 0ad82619c..59b2102a8 100644 --- a/otel/templates/registry/typescript/semconv.ts.j2 +++ b/otel/templates/registry/typescript/semconv.ts.j2 @@ -1,22 +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) -%} -{{ a.brief | trim | replace("\n", " ") }}{% if a.annotations.livekit.sensitivity == "pii" %} PII: redacted by the collector.{% endif %}{% if a.annotations.livekit.unit %} Unit: `{{ a.annotations.livekit.unit }}`.{% endif %} +{{ 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. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = '{{ ctx.schema_url }}' as const; +export const SCHEMA_URL = {{ ctx.schema_url | tojson }} as const; {% for a in ctx.attributes %} /** {{ doc(a) }} */ -export const ATTR_{{ a.key | screaming_snake_case }} = '{{ a.key }}' as const; -{% if a.type is mapping %}{% for m in a.type.members %}/** {{ m.brief | trim }} */ -export const {{ a.key | screaming_snake_case }}_VALUE_{{ m.id | screaming_snake_case }} = '{{ m.value }}' as const; +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 %}/** {{ e.brief | trim | replace("\n", " ") }} */ -export const EVENT_{{ e.name | screaming_snake_case }} = '{{ e.name }}' as const; +{% 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 %}/** {{ s.brief | trim | replace("\n", " ") }} Kind: {{ s.kind }}. */ -export const SPAN_{{ s.type | screaming_snake_case }} = '{{ s.type }}' as const; +{% 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 m in ctx.metrics %} -/** {{ m.brief | trim | replace("\n", " ") }} Instrument: {{ m.instrument }}, unit: `{{ m.unit }}`. */ -export const METRIC_{{ m.name | screaming_snake_case }} = '{{ m.name }}' as const; +/** {{ 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/tests/codegen.sh b/otel/tests/codegen.sh new file mode 100755 index 000000000..9ee370f7d --- /dev/null +++ b/otel/tests/codegen.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Generates Rust and TypeScript from otel/model plus tests/codegen/escape.yaml, compiles both, +# and checks the tricky enum value comes back byte for byte from rustc and from tsc + node. +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; 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; } +echo "PASS codegen: escaped literals round-trip through rustc and tsc" 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} From 8d6c5ecc1548ab31cc8fdd8e0b54b734698c2fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:20:04 +0200 Subject: [PATCH 09/36] feat(otel): flag unregistered lk.* spans in live-check A span no matcher places gets no finding from weaver when it carries no attributes, so an unknown lk.* span passed silently. livekit_unknown_span now reports any lk.* span without a registry match; the live-check test asserts it. The README documents what remains unchecked (spans outside lk.*, span event names) and when CI runs. --- otel/README.md | 10 +++++++++- otel/live-check/livekit.rego | 8 ++++++++ otel/live-check/summarize.py | 2 +- otel/tests/live-check.sh | 8 ++++---- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/otel/README.md b/otel/README.md index 0fe2f42fc..e7e12beb7 100644 --- a/otel/README.md +++ b/otel/README.md @@ -86,6 +86,9 @@ key with `deprecated: {reason: renamed, renamed_to: }` instead of delet ## 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. + 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 @@ -133,7 +136,8 @@ JSON file. `live-check/weaver.toml` matches spans by name and checks the resourc 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_flat_identity` and `livekit_pii_unmarked`, each naming -the key to send instead. +the key to send instead, and `livekit_unknown_span` for an `lk.*` span no matcher places (weaver +alone reports nothing for an unmatched span that has no attributes). 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 expected shared identity / PII renames: @@ -152,6 +156,10 @@ With those six keys renamed in the same capture, live-check reports nothing and 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`. - Span event names (connect checkpoints, `attempt `) are not checked; weaver compares span events only by attributes. - App-defined attributes (custom events, `Scope::set_attribute`) are reported as unknown keys. diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index a449e5164..0807e5466 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -43,3 +43,11 @@ deny contains lk_advice("livekit_flat_identity", {"attribute_key": key, "expecte key := input.sample.attribute.name want := canonical_identity[key] } + +# 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 +} diff --git a/otel/live-check/summarize.py b/otel/live-check/summarize.py index 33a4ffc8b..957c2c7ca 100644 --- a/otel/live-check/summarize.py +++ b/otel/live-check/summarize.py @@ -15,7 +15,7 @@ def walk(x): 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 adv.get("signal_name") or "-"] += 1 + 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) diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh index f57293882..f38632caf 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,7 +1,7 @@ #!/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; the same smoke sample with the client's current -# identity/PII keys must be flagged. +# identity/PII keys and an unregistered lk.* span must be flagged. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -16,10 +16,10 @@ echo "$sparse" [[ $sparse == *'"advice_level_counts": {}'* ]] || { echo "FAIL sparse RTC sample has findings" >&2; exit 1; } sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.name"/g' \ - tests/live-sample.json > "$tmp/legacy.json" + -e 's/"name": "lk\.publish"/"name": "lk.bogus"/' 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_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name"; do +for want in "livekit_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name" "livekit_unknown_span: lk.bogus"; do [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } done -echo "PASS live-check: smoke and sparse samples clean, legacy keys flagged" +echo "PASS live-check: smoke and sparse samples clean, legacy keys and unknown span flagged" From 0e8494e911cf0f581dd0fb9d8009f8b14e0afb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:27:48 +0200 Subject: [PATCH 10/36] feat(otel): register the client conventions completely Bring the registry to everything SPEC.md (a9ec8d52, with the stage-1 rules) and the JS port say about names and meaning. Every key, event and span now has a brief and a note, with W3C stats sources for RTC keys, platform sources for device keys and protocol enum names on members. Events carry their severity and body; spans their checkpoints (with the dynamic attempt pattern), outcome rules and caps; the custom namespace its reserved keys and limits. The resource, every-record and log sets say which keys go where. JS-only keys are registered as client-owned: browser.name/version and four lk.telemetry counters. Schema 0.2.0. --- otel/gen/docs/semconv.md | 246 +++++++++++++------------ otel/gen/rust/semconv.rs | 174 +++++++++--------- otel/gen/ts/semconv.ts | 174 +++++++++--------- otel/model/client/groups.yaml | 60 +++++-- otel/model/client/registry.yaml | 309 ++++++++++++++++++++++++-------- otel/model/client/signals.yaml | 204 +++++++++++++++++---- otel/model/manifest.yaml | 2 +- otel/model/shared/registry.yaml | 42 ++++- 8 files changed, 805 insertions(+), 406 deletions(-) diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index c66241267..3b9f19217 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -1,96 +1,100 @@ # LiveKit telemetry conventions -Schema: `https://livekit.io/schemas/telemetry/0.1.0`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. +Schema: `https://livekit.io/schemas/telemetry/0.2.0`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. ## Attributes | Key | Type | Owner | Sensitivity | Unit | Description | | --- | --- | --- | --- | --- | --- | -| `lk.connect.attempt` | int | client | none | | 1-based connect attempt; 1 is the user-initiated connect. | -| `lk.device.app_state` | enum: `foreground`, `background` | client | none | | Whether the app is in front of the user. | -| `lk.device.audio.interruption` | enum: `began`, `ended` | client | none | | Phase of an audio session interruption. | -| `lk.device.audio_route.outputs` | string | client | none | | Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. | -| `lk.device.audio_route.reason` | enum: `new_device`, `old_device_unavailable`, `category_change`, `override`, `wake_from_sleep`, `no_suitable_route`, `route_configuration_change`, `unknown` | client | none | | Why the audio route changed (AVAudioSession route change reasons). | +| `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. | +| `lk.device.app_state` | enum: `foreground`, `background` | client | none | | Whether the app is in front of the user. All platforms; the web maps page visibility. | +| `lk.device.audio.interruption` | enum: `began`, `ended` | client | none | | Phase of an audio session interruption. iOS audio session interruptions; Android audio focus loss and gain. | +| `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. | +| `lk.device.audio_route.reason` | enum: `new_device`, `old_device_unavailable`, `category_change`, `override`, `wake_from_sleep`, `no_suitable_route`, `route_configuration_change`, `unknown` | 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.battery.charging` | boolean | client | none | | The device is charging. SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. | -| `lk.device.battery.level` | int | client | none | `%` | Battery charge in percent, 0-100. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | -| `lk.device.capture.device` | enum: `camera`, `microphone`, `screen_share` | client | none | | Capture device that failed. | -| `lk.device.capture.reason` | enum: `permission_denied`, `not_found`, `in_use`, `disconnected`, `other` | client | none | | Why capture failed (the getUserMedia failure taxonomy). | -| `lk.device.low_power.enabled` | boolean | client | none | | Low Power Mode / Battery Saver is on. | -| `lk.device.memory.pressure` | enum: `normal`, `warning`, `critical` | client | none | | Memory pressure as the OS reports it. Android `onTrimMemory`: RUNNING_LOW/BACKGROUND map to `warning`, RUNNING_CRITICAL/COMPLETE to `critical`. | -| `lk.device.network.constrained` | boolean | client | none | | The user asked for less traffic (Low Data Mode, Data Saver). | -| `lk.device.network.expensive` | boolean | client | none | | The network is metered (cellular, personal hotspot). | -| `lk.device.thermal.state` | enum: `nominal`, `fair`, `serious`, `critical` | client | none | | Thermal pressure as the OS reports it. The platform's `unknown` is no reading and is never emitted. | -| `lk.disconnect.reason` | enum: `client_initiated`, `duplicate_identity`, `server_shutdown`, `participant_removed`, `room_deleted`, `state_mismatch`, `join_failure`, `migration`, `signal_close`, `room_closed`, `user_unavailable`, `user_rejected`, `sip_trunk_failure`, `connection_timeout`, `media_failure`, `agent_error`, `reconnect_failed`, `unknown` | client | none | | Why the session ended (the protocol's DisconnectReason, plus the client giving up). | -| `lk.job_id` | string | shared | none | | Agent job ID (`AJ_...`). High cardinality; Agents' job-scoped metrics are the one exception. | -| `lk.log.logger` | string | client | none | | Logger, type, module or file that wrote the line. | -| `lk.log.source` | enum: `sdk`, `ffi`, `webrtc` | client | none | | Which layer wrote the log line. | -| `lk.outcome` | enum: `ok`, `error`, `cancelled` | client | none | | How the attempt ended. Always present on a span; rollups read this, never the span status. The Notion conventions name `lk.client.connection.result` (success\|failure\|cancelled) for connects only. Open decision. | -| `lk.participant_id` | string | shared | none | | Server-assigned participant SID (`PA_...`). High cardinality, never a default metric dimension. | -| `lk.pii.participant_identity` | string | shared | pii | | Customer-supplied identity of the local participant. | -| `lk.pii.remote_participant_identity` | string | shared | pii | | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). Name TBD, needs sign-off; the Notion conventions name no key for it. | -| `lk.pii.room_name` | string | shared | pii | | Customer-supplied room name. | -| `lk.ping.seq` | int | client | none | | Monotonically increasing sequence number per pipeline. | -| `lk.reconnect.attempts` | int | client | none | | Reconnect attempts made in the cycle. | -| `lk.reconnect.mode` | enum: `quick`, `full` | client | none | | Mode of the last reconnect attempt. | -| `lk.reconnect.reason` | enum: `signal_disconnected`, `publisher_failed`, `subscriber_failed`, `transport_failed`, `switch_candidate`, `network_changed`, `debug`, `unknown` | client | none | | What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). | -| `lk.room_sid` | string | shared | none | | Server-assigned room session SID (`RM_...`). Omit before join. High cardinality, never a default metric dimension. | -| `lk.rtc.audio_level.avg` | double | client | none | `1` | Average audio level in the window, 0-1, audio. | -| `lk.rtc.audio_level.max` | double | client | none | `1` | Maximum audio level in the window, 0-1, audio. | -| `lk.rtc.audio_level.min` | double | client | none | `1` | Minimum audio level in the window, 0-1, audio. | -| `lk.rtc.bytes` | int | client | none | `By` | Bytes sent or received (cumulative). | -| `lk.rtc.codec` | string | client | none | | Codec mimeType, when known. | -| `lk.rtc.concealed_samples` | int | client | none | `{sample}` | Concealed audio samples, inbound audio (cumulative). | -| `lk.rtc.concealment_events` | int | client | none | `{event}` | Concealment events, inbound audio (cumulative). | -| `lk.rtc.fps.avg` | double | client | none | `{frame}/s` | Average frame rate in the window, video. | -| `lk.rtc.fps.max` | double | client | none | `{frame}/s` | Maximum frame rate in the window, video. | -| `lk.rtc.fps.min` | double | client | none | `{frame}/s` | Minimum frame rate in the window, video. | -| `lk.rtc.freeze_count` | int | client | none | `{freeze}` | Video freezes, inbound video (cumulative). | -| `lk.rtc.freezes_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total freeze time, inbound video (cumulative). | -| `lk.rtc.interruption_count` | int | client | none | `{interruption}` | Audio interruptions, inbound audio (cumulative). | -| `lk.rtc.interruptions_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total interruption time, inbound audio (cumulative). | -| `lk.rtc.jitter_buffer_delay_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Jitter buffer delay, inbound (cumulative). | -| `lk.rtc.jitter_buffer_emitted_count` | int | client | none | `{sample}` | Samples or frames emitted by the jitter buffer, inbound (cumulative). | -| `lk.rtc.jitter_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average jitter in the window. | -| `lk.rtc.jitter_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum jitter in the window. | -| `lk.rtc.jitter_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum jitter in the window. | -| `lk.rtc.packets` | int | client | none | `{packet}` | Packets sent or received (cumulative). | -| `lk.rtc.packets_lost` | int | client | none | `{packet}` | Packets lost, inbound (cumulative). | -| `lk.rtc.pause_count` | int | client | none | `{pause}` | Video pauses, inbound video (cumulative). | -| `lk.rtc.pauses_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total pause time, inbound video (cumulative). | -| `lk.rtc.quality_limitation.bandwidth_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by bandwidth, outbound video (cumulative). | -| `lk.rtc.quality_limitation.cpu_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by CPU, outbound video (cumulative). | -| `lk.rtc.quality_limitation.other_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited for other reasons, outbound video (cumulative). | -| `lk.rtc.rtt_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average round-trip time in the window. | -| `lk.rtc.rtt_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum round-trip time in the window. | -| `lk.rtc.rtt_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum round-trip time in the window (remote-inbound for outbound, candidate pair for inbound). | -| `lk.rtc.samples` | int | client | none | `{reading}` | Stats readings in the window. | -| `lk.rtc.silent_concealed_samples` | int | client | none | `{sample}` | Concealed samples that were silent, inbound audio (cumulative). | -| `lk.rtc.window_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Actual length of the stats window. | -| `lk.sdk.name` | string | shared | none | | LiveKit SDK name. | -| `lk.sdk.version` | string | shared | none | | LiveKit SDK version. | -| `lk.telemetry.cache.batches` | int | client | none | `{batch}` | Batches waiting in the cache now (a gauge). | -| `lk.telemetry.cache.write_errors` | int | client | none | `{batch}` | Batches the disk refused, kept in memory. | -| `lk.telemetry.dropped.cache_error` | int | client | none | `{record}` | Records no cache, not even memory, could take. | -| `lk.telemetry.dropped.cache_full` | int | client | none | `{record}` | Records evicted by the cache's size or file-count bound. | -| `lk.telemetry.dropped.corrupt` | int | client | none | `{record}` | Records in cached batches that failed their CRC. | -| `lk.telemetry.dropped.expired` | int | client | none | `{record}` | Records in batches past the 24 h age limit. | -| `lk.telemetry.dropped.invalid` | int | client | none | `{record}` | Custom events or attributes over the limits (rejected). | -| `lk.telemetry.dropped.oversized` | int | client | none | `{record}` | Single records larger than the collector accepts (413). | -| `lk.telemetry.dropped.queue_full` | int | client | none | `{record}` | Records evicted from the in-memory queue. | -| `lk.telemetry.dropped.rate_limited` | int | client | none | `{record}` | Discrete events dropped by the flood guard. | -| `lk.telemetry.dropped.rejected` | int | client | none | `{record}` | Records the collector rejected (final 4xx/5xx, partial success). | -| `lk.telemetry.dropped.throttled` | int | client | none | `{record}` | Records evicted from the cache during a server-directed pause. | -| `lk.telemetry.holds.capped` | int | client | none | `{hold}` | Soft upload holds that reached the 60 s cap. | -| `lk.telemetry.uploads.bytes` | int | client | none | `By` | Compressed bytes the collector accepted. | -| `lk.telemetry.uploads.failed` | int | client | none | `{attempt}` | Upload attempts that failed transiently (no answer, 429, 5xx). | -| `lk.telemetry.uploads.sent` | int | client | none | `{batch}` | Batches the collector accepted. | -| `lk.telemetry.uploads.timeouts` | int | client | none | `{attempt}` | Upload attempts that hit the export timeout. | -| `lk.telemetry.uploads.unauthorized` | int | client | none | `{attempt}` | 401/403 answers (tokens the collector refused). | -| `lk.track.direction` | enum: `inbound`, `outbound` | client | none | | Whether the stats describe sent or received media. | -| `lk.track.kind` | enum: `audio`, `video` | client | none | | Media kind of the track. | -| `lk.track.source` | enum: `camera`, `microphone`, `screen_share`, `screen_share_audio`, `unknown` | client | none | | Source of the track (the protocol's TrackSource, snake_case). | -| `lk.track_sid` | string | shared | none | | Server-assigned track SID (`TR_...`). High cardinality, never a default metric dimension. | +| `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. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | +| `lk.device.capture.device` | enum: `camera`, `microphone`, `screen_share` | client | none | | Capture device that failed. All platforms. | +| `lk.device.capture.reason` | enum: `permission_denied`, `not_found`, `in_use`, `disconnected`, `other` | 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.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. | +| `lk.device.memory.pressure` | enum: `normal`, `warning`, `critical` | 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.network.constrained` | boolean | client | none | | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. | +| `lk.device.network.expensive` | boolean | client | none | | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. | +| `lk.device.thermal.state` | enum: `nominal`, `fair`, `serious`, `critical` | 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.disconnect.reason` | enum: `client_initiated`, `duplicate_identity`, `server_shutdown`, `participant_removed`, `room_deleted`, `state_mismatch`, `join_failure`, `migration`, `signal_close`, `room_closed`, `user_unavailable`, `user_rejected`, `sip_trunk_failure`, `connection_timeout`, `media_failure`, `agent_error`, `reconnect_failed`, `unknown` | client | none | | Why the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | +| `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. | +| `lk.log.logger` | string | client | none | | Logger, type, module or file that wrote the line. Free text from the platform's logger. | +| `lk.log.source` | enum: `sdk`, `ffi`, `webrtc` | client | none | | Which layer wrote the log line. Only WARN and ERROR records are sent; WebRTC lines only at ERROR. | +| `lk.outcome` | enum: `ok`, `error`, `cancelled` | client | none | | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `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. The client core sends `lk.participant.sid` until the shared rename lands. | +| `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. The client core sends `lk.participant.identity` until the shared rename lands. | +| `lk.pii.remote_participant_identity` | string | shared | pii | | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends `lk.participant.remote_identity` until then. | +| `lk.pii.room_name` | string | shared | pii | | Customer-supplied room name. On every record and span of a session once the client has joined. The client core sends `lk.room.name` until the shared rename lands, which the collector cannot redact. | +| `lk.ping.seq` | int | client | none | | Monotonically increasing sequence number per pipeline. Optional; on `lk.ping` only. | +| `lk.reconnect.attempts` | int | client | none | | Reconnect attempts made in the cycle. Equals `` of the last `attempt ` checkpoint. | +| `lk.reconnect.mode` | enum: `quick`, `full` | client | none | | Mode of the last reconnect attempt. Updated with each `attempt ` checkpoint; matches the last one. | +| `lk.reconnect.reason` | enum: `signal_disconnected`, `publisher_failed`, `subscriber_failed`, `transport_failed`, `switch_candidate`, `network_changed`, `debug`, `unknown` | client | none | | What triggered the reconnect cycle. The protocol's `ReconnectReason` (`RR_*`), plus reasons only a client knows. | +| `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. The client core sends `lk.room.sid` until the shared rename lands. | +| `lk.rtc.audio_level.avg` | double | client | none | `1` | Average audio level in the window, 0-1, inbound audio. Inbound `audioLevel`, averaged over `lk.rtc.samples` readings. | +| `lk.rtc.audio_level.max` | double | client | none | `1` | Maximum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | +| `lk.rtc.audio_level.min` | double | client | none | `1` | Minimum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | +| `lk.rtc.bytes` | int | client | none | `By` | Bytes sent or received (cumulative). Outbound `bytesSent`, inbound `bytesReceived`. | +| `lk.rtc.codec` | string | client | none | | Codec mimeType, when known. The `codec` stats entry the RTP stream's `codecId` names. | +| `lk.rtc.concealed_samples` | int | client | none | `{sample}` | Concealed audio samples, inbound audio (cumulative). Inbound `concealedSamples`. | +| `lk.rtc.concealment_events` | int | client | none | `{event}` | Concealment events, inbound audio (cumulative). Inbound `concealmentEvents`. | +| `lk.rtc.fps.avg` | double | client | none | `{frame}/s` | Average frame rate in the window, video. `framesPerSecond`, either direction, averaged over `lk.rtc.samples` readings. | +| `lk.rtc.fps.max` | double | client | none | `{frame}/s` | Maximum frame rate in the window, video. `framesPerSecond`, either direction. | +| `lk.rtc.fps.min` | double | client | none | `{frame}/s` | Minimum frame rate in the window, video. `framesPerSecond`, either direction. | +| `lk.rtc.freeze_count` | int | client | none | `{freeze}` | Video freezes, inbound video (cumulative). Inbound `freezeCount`. | +| `lk.rtc.freezes_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total freeze time, inbound video (cumulative). Inbound `totalFreezesDuration`. | +| `lk.rtc.interruption_count` | int | client | none | `{interruption}` | Audio interruptions, inbound audio (cumulative). Inbound `interruptionCount`. | +| `lk.rtc.interruptions_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total interruption time, inbound audio (cumulative). Inbound `totalInterruptionDuration`. | +| `lk.rtc.jitter_buffer_delay_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Jitter buffer delay, inbound (cumulative). Inbound `jitterBufferDelay`. Average delay = jitter_buffer_delay_ms / jitter_buffer_emitted_count. | +| `lk.rtc.jitter_buffer_emitted_count` | int | client | none | `{sample}` | Samples or frames emitted by the jitter buffer, inbound (cumulative). Inbound `jitterBufferEmittedCount`. | +| `lk.rtc.jitter_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average jitter in the window, inbound. Inbound `jitter` (seconds × 1000), averaged over `lk.rtc.samples` readings. | +| `lk.rtc.jitter_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | +| `lk.rtc.jitter_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | +| `lk.rtc.packets` | int | client | none | `{packet}` | Packets sent or received (cumulative). Outbound `packetsSent`, inbound `packetsReceived`. | +| `lk.rtc.packets_lost` | int | client | none | `{packet}` | Packets lost, inbound (cumulative). Inbound `packetsLost`. Loss ratio = packets_lost / (packets + packets_lost). | +| `lk.rtc.pause_count` | int | client | none | `{pause}` | Video pauses, inbound video (cumulative). Inbound `pauseCount`. | +| `lk.rtc.pauses_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total pause time, inbound video (cumulative). Inbound `totalPausesDuration`. | +| `lk.rtc.quality_limitation.bandwidth_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by bandwidth, outbound video (cumulative). Outbound `qualityLimitationDurations.bandwidth`. | +| `lk.rtc.quality_limitation.cpu_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by CPU, outbound video (cumulative). Outbound `qualityLimitationDurations.cpu`. | +| `lk.rtc.quality_limitation.other_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited for other reasons, outbound video (cumulative). Outbound `qualityLimitationDurations.other`. | +| `lk.rtc.rtt_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`, averaged over `lk.rtc.samples` readings. | +| `lk.rtc.rtt_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`. | +| `lk.rtc.rtt_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | 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.samples` | int | client | none | `{reading}` | Stats readings folded into the window. Denominator for the gauges' averages. | +| `lk.rtc.silent_concealed_samples` | int | client | none | `{sample}` | Concealed samples that were silent, inbound audio (cumulative). Inbound `silentConcealedSamples`. | +| `lk.rtc.window_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Actual length of the stats window. From the window's start to its close, on the core's clock (the JS port uses wall-clock time). | +| `lk.sdk.name` | string | shared | none | | LiveKit SDK name. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet (it sends `service.name = livekit-client-`); the vocabulary is open. | +| `lk.sdk.version` | string | shared | none | | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. | +| `lk.telemetry.cache.batches` | int | client | none | `{batch}` | Batches waiting in the cache now. A gauge, not a count since the previous report. Always present on a report. | +| `lk.telemetry.cache.delete_errors` | int | client | none | `{batch}` | 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 | client | none | `{attempt}` | 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 | client | none | `{batch}` | 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 | client | none | `{batch}` | Batches the disk refused, kept in memory instead. Only when non-zero. | +| `lk.telemetry.dropped.cache_error` | int | client | none | `{record}` | Records no cache, not even memory, could take. Only when non-zero. | +| `lk.telemetry.dropped.cache_full` | int | client | none | `{record}` | Records evicted by the cache's size or file-count bound. Only when non-zero. | +| `lk.telemetry.dropped.corrupt` | int | client | none | `{record}` | Records in cached batches that failed their CRC. Only when non-zero. | +| `lk.telemetry.dropped.expired` | int | client | none | `{record}` | Records in batches past the cache's age limit. Only when non-zero. | +| `lk.telemetry.dropped.invalid` | int | client | none | `{record}` | 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 | client | none | `{record}` | Single records larger than the collector accepts (413). Only when non-zero. | +| `lk.telemetry.dropped.queue_full` | int | client | none | `{record}` | Records evicted from the in-memory queue. Only when non-zero. | +| `lk.telemetry.dropped.rate_limited` | int | client | none | `{record}` | 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 | client | none | `{record}` | Records the collector rejected (final 4xx/5xx, OTLP partial success). Only when non-zero. | +| `lk.telemetry.dropped.throttled` | int | client | none | `{record}` | 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 | client | none | `{record}` | 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 | client | none | `{hold}` | Soft upload holds that reached their cap. Only when non-zero. | +| `lk.telemetry.uploads.bytes` | int | client | none | `By` | Compressed bytes the collector accepted. What telemetry cost the uplink. Always present on a report, even when 0. | +| `lk.telemetry.uploads.failed` | int | client | none | `{attempt}` | Upload attempts that failed transiently (no answer, 429, 5xx). Always present on a report, even when 0. | +| `lk.telemetry.uploads.sent` | int | client | none | `{batch}` | Batches the collector accepted. Always present on a report, even when 0. | +| `lk.telemetry.uploads.timeouts` | int | client | none | `{attempt}` | Upload attempts that hit the export timeout. Only when non-zero. | +| `lk.telemetry.uploads.unauthorized` | int | client | none | `{attempt}` | 401/403 answers (tokens the collector refused). Only when non-zero. | +| `lk.track.direction` | enum: `inbound`, `outbound` | client | none | | Whether the stats describe sent or received media. On RTC stats windows only. | +| `lk.track.kind` | enum: `audio`, `video` | client | none | | Media kind of the track. On publish and subscribe spans and RTC stats windows. | +| `lk.track.source` | enum: `camera`, `microphone`, `screen_share`, `screen_share_audio`, `unknown` | client | none | | Source of the track. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | +| `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. The client core sends `lk.track.sid` until the shared rename lands. | ## Bindings @@ -109,11 +113,11 @@ Physical names of a concept in other producers and stores. Carried for reference ### `custom` -Namespace of app-defined events. The core emits `custom.` with the app's own attributes. App attribute keys may not use `lk.*`, `otel.*` or `session.id`. +Namespace of app-defined events. The core emits `custom.` with 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`), may not use the reserved keys, and stay within the limits; anything else is rejected, never truncated, and counted in `lk.telemetry.dropped.invalid`. An event's own attributes override the session's. ### `lk.device.app_state.changed` -App moved to the foreground or background (and its initial value). +The app moved to the foreground or background. Sent once with the initial state, then on each change. All platforms. | Attribute | Requirement | | --- | --- | @@ -121,7 +125,7 @@ App moved to the foreground or background (and its initial value). ### `lk.device.audio.interruption` -Audio session interruption began or ended. +An audio session interruption began or ended. iOS; Android from audio focus loss and gain. Not sent by the JS port. | Attribute | Requirement | | --- | --- | @@ -129,7 +133,7 @@ Audio session interruption began or ended. ### `lk.device.audio_route.changed` -Audio route changed. +The audio route changed. iOS; Android from its audio device callbacks; optional elsewhere. Not sent by the JS port. | Attribute | Requirement | | --- | --- | @@ -138,7 +142,7 @@ Audio route changed. ### `lk.device.battery.changed` -Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent. +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. | Attribute | Requirement | | --- | --- | @@ -147,7 +151,7 @@ Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent ### `lk.device.capture.failed` -A capture device failed. Severity WARN. +A capture device failed. All platforms. | Attribute | Requirement | | --- | --- | @@ -156,7 +160,7 @@ A capture device failed. Severity WARN. ### `lk.device.low_power.changed` -Low Power Mode changed (and its initial value). +Low Power Mode changed. Sent once with the initial reading, then on each change; never where the platform has no source. | Attribute | Requirement | | --- | --- | @@ -164,7 +168,7 @@ Low Power Mode changed (and its initial value). ### `lk.device.memory.changed` -Memory pressure changed (and its initial value). +Memory pressure changed. Sent once with the initial reading, then on each change. | Attribute | Requirement | | --- | --- | @@ -172,7 +176,7 @@ Memory pressure changed (and its initial value). ### `lk.device.network.changed` -Network path changed (and its initial value). +The network path changed. Sent once with the initial state, then when any of its attributes changes. iOS, macOS and Android; on the web, Chromium only. | Attribute | Requirement | | --- | --- | @@ -182,7 +186,7 @@ Network path changed (and its initial value). ### `lk.device.thermal.changed` -Thermal state changed (and its initial value). +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. | Attribute | Requirement | | --- | --- | @@ -190,7 +194,7 @@ Thermal state changed (and its initial value). ### `lk.ping` -Pipeline smoke test. Never emitted in production paths. +Pipeline smoke test. Never emitted in production paths. Not sent by the JS port. | Attribute | Requirement | | --- | --- | @@ -198,7 +202,7 @@ Pipeline smoke test. Never emitted in production paths. ### `lk.room.disconnected` -The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. +The Room left connected for good. Once per session, never on a reconnect. INFO when the client hung up (`client_initiated`), WARN otherwise. | Attribute | Requirement | | --- | --- | @@ -206,13 +210,13 @@ The Room left connected for good (never on a reconnect). INFO when the client hu ### `lk.rtc.stats.sample` -One RTC stats window for one track and direction. Only the track, kind, direction, window and sample count are always present; counters and gauges a platform or direction lacks are omitted. +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. | Attribute | Requirement | | --- | --- | -| `lk.rtc.audio_level.avg` | conditionally_required: Audio. | -| `lk.rtc.audio_level.max` | conditionally_required: Audio. | -| `lk.rtc.audio_level.min` | conditionally_required: Audio. | +| `lk.rtc.audio_level.avg` | conditionally_required: Inbound audio. | +| `lk.rtc.audio_level.max` | conditionally_required: Inbound audio. | +| `lk.rtc.audio_level.min` | conditionally_required: Inbound audio. | | `lk.rtc.bytes` | conditionally_required: When the stats report it. | | `lk.rtc.codec` | conditionally_required: When known. | | `lk.rtc.concealed_samples` | conditionally_required: Inbound audio. | @@ -248,11 +252,14 @@ One RTC stats window for one track and direction. Only the track, kind, directio ### `lk.telemetry.report` -Self-telemetry, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. +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. | Attribute | Requirement | | --- | --- | | `lk.telemetry.cache.batches` | required | +| `lk.telemetry.cache.delete_errors` | conditionally_required: When non-zero (JS port only). | +| `lk.telemetry.cache.list_errors` | conditionally_required: When non-zero (JS port only). | +| `lk.telemetry.cache.read_errors` | conditionally_required: When non-zero (JS port only). | | `lk.telemetry.cache.write_errors` | conditionally_required: When non-zero. | | `lk.telemetry.dropped.cache_error` | conditionally_required: When non-zero. | | `lk.telemetry.dropped.cache_full` | conditionally_required: When non-zero. | @@ -263,7 +270,8 @@ Self-telemetry, appended to the next upload after a loss, failure, refusal, capp | `lk.telemetry.dropped.queue_full` | conditionally_required: When non-zero. | | `lk.telemetry.dropped.rate_limited` | conditionally_required: When non-zero. | | `lk.telemetry.dropped.rejected` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.throttled` | conditionally_required: When non-zero. | +| `lk.telemetry.dropped.throttled` | conditionally_required: When non-zero (Rust core only). | +| `lk.telemetry.dropped.unattributed` | conditionally_required: When non-zero (JS port only). | | `lk.telemetry.holds.capped` | conditionally_required: When non-zero. | | `lk.telemetry.uploads.bytes` | required | | `lk.telemetry.uploads.failed` | required | @@ -275,33 +283,33 @@ Self-telemetry, appended to the next upload after a loss, failure, refusal, capp ### `lk.connect` (client) -One connect attempt. Checkpoints, in order: ws_open, signal, join_recv, pc_created (required on every platform), then engine, pc_connected, offer_sent, answer_sent, room_connected (best effort). +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 | Requirement | | --- | --- | -| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | | `lk.connect.attempt` | required | | `lk.outcome` | required | ### `lk.publish` (internal) -One track publish. +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 | Requirement | | --- | --- | -| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | | `lk.outcome` | required | | `lk.track.kind` | required | | `lk.track.source` | required | -| `lk.track_sid` | conditionally_required: On success. | +| `lk.track_sid` | conditionally_required: On success; the server assigns it. | ### `lk.reconnect` (client) -One reconnect cycle. Checkpoints are named `attempt `, one per attempt (dynamic; open decision). +One reconnect cycle. `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint named `attempt `: a dynamic name (the Notion conventions keep dynamic data out of names; open decision), mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. | Attribute | Requirement | | --- | --- | -| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | | `lk.outcome` | required | | `lk.reconnect.attempts` | conditionally_required: After the first attempt. | | `lk.reconnect.mode` | conditionally_required: After the first attempt. | @@ -309,11 +317,11 @@ One reconnect cycle. Checkpoints are named `attempt `, one per attempt ### `lk.subscribe` (internal) -One subscription, from the intent to subscribe to the first media (or timed_out after 30 s). Checkpoints: subscribed, first_media. +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 | Requirement | | --- | --- | -| `error.type` | conditionally_required: When `lk.outcome` is `error`. | +| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | | `lk.outcome` | required | | `lk.pii.remote_participant_identity` | recommended | | `lk.track.kind` | required | @@ -336,23 +344,25 @@ Attributes of a plain log record (no event name). ### `livekit.client.record` -Attributes the core stamps on every record and span of a session. +Attributes the core stamps on every log record and span of a session. | Attribute | Requirement | | --- | --- | -| `lk.participant_id` | recommended | -| `lk.pii.participant_identity` | recommended | -| `lk.pii.room_name` | recommended | -| `lk.room_sid` | recommended | -| `otel.event.name` | recommended | +| `lk.participant_id` | conditionally_required: Once the client has joined the room. | +| `lk.pii.participant_identity` | conditionally_required: Once the client has joined the room. | +| `lk.pii.room_name` | conditionally_required: Once the client has joined the room. | +| `lk.room_sid` | conditionally_required: Once the client has joined the room. | +| `otel.event.name` | conditionally_required: On event records; duplicates the record's `event_name`. | | `session.id` | required | ### `livekit.client.resource` -Resource attributes of the client pipeline (one per process). +Resource attributes of the client pipeline, one Resource per process. | Attribute | Requirement | | --- | --- | +| `browser.name` | conditionally_required: The JS SDK running in a browser. | +| `browser.version` | conditionally_required: The JS SDK running in a browser. | | `device.model.identifier` | recommended | | `lk.sdk.name` | recommended | | `lk.sdk.version` | recommended | diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index b090d866c..6c8fd6429 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,9 +2,9 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.1.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.2.0"; -/// 1-based connect attempt; 1 is the user-initiated connect. +/// 1-based connect attempt. pub const LK_CONNECT_ATTEMPT: &str = "lk.connect.attempt"; /// Whether the app is in front of the user. @@ -14,7 +14,7 @@ pub const LK_DEVICE_APP_STATE: &str = "lk.device.app_state"; pub mod lk_device_app_state { /// In front of the user. pub const FOREGROUND: &str = "foreground"; - /// In the background; forces a flush. + /// In the background. pub const BACKGROUND: &str = "background"; } @@ -23,16 +23,16 @@ 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. + /// Interruption began (another app or a call took the audio session). pub const BEGAN: &str = "began"; /// Interruption ended. pub const ENDED: &str = "ended"; } -/// Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. +/// The active outputs, comma-separated. pub const LK_DEVICE_AUDIO_ROUTE_OUTPUTS: &str = "lk.device.audio_route.outputs"; -/// Why the audio route changed (AVAudioSession route change reasons). +/// 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`]. @@ -74,7 +74,7 @@ pub mod lk_device_capture_device { pub const SCREEN_SHARE: &str = "screen_share"; } -/// Why capture failed (the getUserMedia failure taxonomy). +/// 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`]. @@ -118,56 +118,56 @@ pub const LK_DEVICE_THERMAL_STATE: &str = "lk.device.thermal.state"; /// Values of [`LK_DEVICE_THERMAL_STATE`]. pub mod lk_device_thermal_state { - /// No thermal pressure. + /// Within normal limits. pub const NOMINAL: &str = "nominal"; /// Slightly elevated. pub const FAIR: &str = "fair"; - /// High; the core doubles its cadence. + /// High; the system reduces performance. pub const SERIOUS: &str = "serious"; - /// Critical; the core quadruples its cadence. + /// Critical; the device needs to cool down. pub const CRITICAL: &str = "critical"; } -/// Why the session ended (the protocol's DisconnectReason, plus the client giving up). +/// Why the session ended. pub const LK_DISCONNECT_REASON: &str = "lk.disconnect.reason"; /// Values of [`LK_DISCONNECT_REASON`]. pub mod lk_disconnect_reason { - /// The client hung up. + /// The client hung up (CLIENT_INITIATED). pub const CLIENT_INITIATED: &str = "client_initiated"; - /// Another participant joined with the same identity. + /// Another participant joined with the same identity (DUPLICATE_IDENTITY). pub const DUPLICATE_IDENTITY: &str = "duplicate_identity"; - /// The server shut down. + /// The server instance shut down (SERVER_SHUTDOWN). pub const SERVER_SHUTDOWN: &str = "server_shutdown"; - /// The participant was removed. + /// RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). pub const PARTICIPANT_REMOVED: &str = "participant_removed"; - /// The room was deleted. + /// RoomService.DeleteRoom was called (ROOM_DELETED). pub const ROOM_DELETED: &str = "room_deleted"; - /// Client and server state diverged. + /// The server did not know the session the client tried to resume (STATE_MISMATCH). pub const STATE_MISMATCH: &str = "state_mismatch"; - /// The join failed. + /// The client could not connect fully (JOIN_FAILURE). pub const JOIN_FAILURE: &str = "join_failure"; - /// The session migrated. + /// The server asked the participant to migrate (MIGRATION, Cloud only). pub const MIGRATION: &str = "migration"; - /// The signaling connection closed. + /// The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). pub const SIGNAL_CLOSE: &str = "signal_close"; - /// The room closed. + /// The room closed after its participants left (ROOM_CLOSED). pub const ROOM_CLOSED: &str = "room_closed"; - /// SIP callee unavailable. + /// The SIP callee did not answer in time (USER_UNAVAILABLE). pub const USER_UNAVAILABLE: &str = "user_unavailable"; - /// SIP callee rejected the call. + /// The SIP callee rejected the call (USER_REJECTED). pub const USER_REJECTED: &str = "user_rejected"; - /// SIP trunk failure. + /// SIP protocol failure (SIP_TRUNK_FAILURE). pub const SIP_TRUNK_FAILURE: &str = "sip_trunk_failure"; - /// The connection timed out. + /// The server timed out the session (CONNECTION_TIMEOUT). pub const CONNECTION_TIMEOUT: &str = "connection_timeout"; - /// Media failed. + /// Media failed or timed out (MEDIA_FAILURE). pub const MEDIA_FAILURE: &str = "media_failure"; - /// The agent failed. + /// The agent failed (AGENT_ERROR). pub const AGENT_ERROR: &str = "agent_error"; - /// The reconnect policy ran out of attempts. + /// The client ran out of reconnect attempts (client only). pub const RECONNECT_FAILED: &str = "reconnect_failed"; - /// Unknown reason. + /// UNKNOWN_REASON or a number the client does not know. pub const UNKNOWN: &str = "unknown"; } @@ -184,26 +184,26 @@ pub const LK_LOG_SOURCE: &str = "lk.log.source"; pub mod lk_log_source { /// The platform SDK. pub const SDK: &str = "sdk"; - /// The Rust core. + /// 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"; } -/// How the attempt ended. Always present on a span; rollups read this, never the span status. +/// 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. + /// Succeeded; span status Unset. pub const OK: &str = "ok"; - /// Failed; `error.type` names why. + /// Failed; span status Error, with `error.type` when the failure has a type. pub const ERROR: &str = "error"; - /// Cancelled by the caller or a newer attempt. + /// Cancelled by the caller or superseded; span status Unset. pub const CANCELLED: &str = "cancelled"; } -/// Server-assigned participant SID (`PA_...`). +/// Server-assigned SID of the local participant (`PA_...`). pub const LK_PARTICIPANT_ID: &str = "lk.participant_id"; /// Customer-supplied identity of the local participant. PII: redacted by the collector. @@ -226,45 +226,45 @@ pub const LK_RECONNECT_MODE: &str = "lk.reconnect.mode"; /// Values of [`LK_RECONNECT_MODE`]. pub mod lk_reconnect_mode { - /// ICE restart / resume. + /// Resume the session (ICE restart). pub const QUICK: &str = "quick"; - /// Full reconnect. + /// Reconnect from scratch. pub const FULL: &str = "full"; } -/// What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). +/// 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. + /// The signaling socket closed (RR_SIGNAL_DISCONNECTED). pub const SIGNAL_DISCONNECTED: &str = "signal_disconnected"; - /// The publisher peer connection failed. + /// The publisher peer connection failed (RR_PUBLISHER_FAILED). pub const PUBLISHER_FAILED: &str = "publisher_failed"; - /// The subscriber peer connection failed. + /// The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). pub const SUBSCRIBER_FAILED: &str = "subscriber_failed"; - /// A peer connection failed; the platform did not say which. + /// 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. + /// The server asked for a candidate switch (RR_SWITCH_CANDIDATE). pub const SWITCH_CANDIDATE: &str = "switch_candidate"; - /// The device moved to another network. + /// The device moved to another network (client only). pub const NETWORK_CHANGED: &str = "network_changed"; - /// A test or debug menu asked for it. + /// A test or a debug menu asked for it (client only). pub const DEBUG: &str = "debug"; - /// Unknown reason. + /// RR_UNKNOWN or a number the client does not know. pub const UNKNOWN: &str = "unknown"; } /// Server-assigned room session SID (`RM_...`). pub const LK_ROOM_SID: &str = "lk.room_sid"; -/// Average audio level in the window, 0-1, audio. Unit: `1`. +/// 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, audio. Unit: `1`. +/// 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, audio. Unit: `1`. +/// 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`. @@ -306,13 +306,13 @@ 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. Unit: `ms`. +/// 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. Unit: `ms`. +/// 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. Unit: `ms`. +/// 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}`. @@ -342,10 +342,10 @@ 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 (remote-inbound for outbound, candidate pair for inbound). Unit: `ms`. +/// Minimum round-trip time in the window. Unit: `ms`. pub const LK_RTC_RTT_MS_MIN: &str = "lk.rtc.rtt_ms.min"; -/// Stats readings in the window. Unit: `{reading}`. +/// 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}`. @@ -360,10 +360,19 @@ pub const LK_SDK_NAME: &str = "lk.sdk.name"; /// LiveKit SDK version. pub const LK_SDK_VERSION: &str = "lk.sdk.version"; -/// Batches waiting in the cache now (a gauge). Unit: `{batch}`. +/// Batches waiting in the cache now. Unit: `{batch}`. pub const LK_TELEMETRY_CACHE_BATCHES: &str = "lk.telemetry.cache.batches"; -/// Batches the disk refused, kept in memory. Unit: `{batch}`. +/// 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}`. @@ -375,10 +384,10 @@ pub const LK_TELEMETRY_DROPPED_CACHE_FULL: &str = "lk.telemetry.dropped.cache_fu /// 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 24 h age limit. Unit: `{record}`. +/// Records in batches past the cache's age limit. Unit: `{record}`. pub const LK_TELEMETRY_DROPPED_EXPIRED: &str = "lk.telemetry.dropped.expired"; -/// Custom events or attributes over the limits (rejected). Unit: `{record}`. +/// 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}`. @@ -390,13 +399,16 @@ pub const LK_TELEMETRY_DROPPED_QUEUE_FULL: &str = "lk.telemetry.dropped.queue_fu /// 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, partial success). Unit: `{record}`. +/// 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"; -/// Soft upload holds that reached the 60 s cap. Unit: `{hold}`. +/// 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`. @@ -419,9 +431,9 @@ pub const LK_TRACK_DIRECTION: &str = "lk.track.direction"; /// Values of [`LK_TRACK_DIRECTION`]. pub mod lk_track_direction { - /// Received media. + /// Received media (inbound-rtp). pub const INBOUND: &str = "inbound"; - /// Sent media. + /// Sent media (outbound-rtp). pub const OUTBOUND: &str = "outbound"; } @@ -436,20 +448,20 @@ pub mod lk_track_kind { pub const VIDEO: &str = "video"; } -/// Source of the track (the protocol's TrackSource, snake_case). +/// Source of the track. pub const LK_TRACK_SOURCE: &str = "lk.track.source"; /// Values of [`LK_TRACK_SOURCE`]. pub mod lk_track_source { - /// Camera. + /// Camera (TrackSource CAMERA). pub const CAMERA: &str = "camera"; - /// Microphone. + /// Microphone (TrackSource MICROPHONE). pub const MICROPHONE: &str = "microphone"; - /// Screen share video. + /// Screen share video (TrackSource SCREEN_SHARE). pub const SCREEN_SHARE: &str = "screen_share"; - /// Screen share audio. + /// Screen share audio (TrackSource SCREEN_SHARE_AUDIO). pub const SCREEN_SHARE_AUDIO: &str = "screen_share_audio"; - /// Unknown source. + /// Unknown source (TrackSource UNKNOWN). pub const UNKNOWN: &str = "unknown"; } @@ -460,31 +472,31 @@ pub const LK_TRACK_SID: &str = "lk.track_sid"; pub mod event { /// Namespace of app-defined events. The core emits `custom.` with the app's own attributes. pub const CUSTOM: &str = "custom"; - /// App moved to the foreground or background (and its initial value). + /// The app moved to the foreground or background. pub const LK_DEVICE_APP_STATE_CHANGED: &str = "lk.device.app_state.changed"; - /// Audio session interruption began or ended. + /// An audio session interruption began or ended. pub const LK_DEVICE_AUDIO_INTERRUPTION: &str = "lk.device.audio.interruption"; - /// Audio route changed. + /// 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. Never per percent. + /// 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. Severity WARN. + /// A capture device failed. pub const LK_DEVICE_CAPTURE_FAILED: &str = "lk.device.capture.failed"; - /// Low Power Mode changed (and its initial value). + /// Low Power Mode changed. pub const LK_DEVICE_LOW_POWER_CHANGED: &str = "lk.device.low_power.changed"; - /// Memory pressure changed (and its initial value). + /// Memory pressure changed. pub const LK_DEVICE_MEMORY_CHANGED: &str = "lk.device.memory.changed"; - /// Network path changed (and its initial value). + /// The network path changed. pub const LK_DEVICE_NETWORK_CHANGED: &str = "lk.device.network.changed"; - /// Thermal state changed (and its initial value). + /// Thermal state changed. pub const LK_DEVICE_THERMAL_CHANGED: &str = "lk.device.thermal.changed"; - /// Pipeline smoke test. Never emitted in production paths. + /// Pipeline smoke test. pub const LK_PING: &str = "lk.ping"; - /// The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. + /// 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, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. + /// Self-telemetry; counts since the previous report. pub const LK_TELEMETRY_REPORT: &str = "lk.telemetry.report"; } @@ -496,6 +508,6 @@ pub mod span { 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 (or timed_out after 30 s). Kind: internal. + /// One subscription, from the intent to subscribe to the first media. Kind: internal. pub const LK_SUBSCRIBE: &str = "lk.subscribe"; } diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 0a41a2f50..2eeed4ea3 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,29 +1,29 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.1.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.2.0" as const; -/** 1-based connect attempt; 1 is the user-initiated connect. */ +/** 1-based connect attempt. */ export const ATTR_LK_CONNECT_ATTEMPT = "lk.connect.attempt" 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; forces a flush. */ +/** 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. */ +/** 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; -/** Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, car_audio, air_play, hdmi, usb, other. */ +/** 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 (AVAudioSession route change reasons). */ +/** 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; @@ -57,7 +57,7 @@ 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 (the getUserMedia failure taxonomy). */ +/** 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; @@ -90,52 +90,52 @@ export const ATTR_LK_DEVICE_NETWORK_EXPENSIVE = "lk.device.network.expensive" as /** Thermal pressure as the OS reports it. */ export const ATTR_LK_DEVICE_THERMAL_STATE = "lk.device.thermal.state" as const; -/** No thermal pressure. */ +/** 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 core doubles its cadence. */ +/** High; the system reduces performance. */ export const LK_DEVICE_THERMAL_STATE_VALUE_SERIOUS = "serious" as const; -/** Critical; the core quadruples its cadence. */ +/** Critical; the device needs to cool down. */ export const LK_DEVICE_THERMAL_STATE_VALUE_CRITICAL = "critical" as const; -/** Why the session ended (the protocol's DisconnectReason, plus the client giving up). */ +/** Why the session ended. */ export const ATTR_LK_DISCONNECT_REASON = "lk.disconnect.reason" as const; -/** The client hung up. */ +/** The client hung up (CLIENT_INITIATED). */ export const LK_DISCONNECT_REASON_VALUE_CLIENT_INITIATED = "client_initiated" as const; -/** Another participant joined with the same identity. */ +/** Another participant joined with the same identity (DUPLICATE_IDENTITY). */ export const LK_DISCONNECT_REASON_VALUE_DUPLICATE_IDENTITY = "duplicate_identity" as const; -/** The server shut down. */ +/** The server instance shut down (SERVER_SHUTDOWN). */ export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = "server_shutdown" as const; -/** The participant was removed. */ +/** RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). */ export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = "participant_removed" as const; -/** The room was deleted. */ +/** RoomService.DeleteRoom was called (ROOM_DELETED). */ export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = "room_deleted" as const; -/** Client and server state diverged. */ +/** The server did not know the session the client tried to resume (STATE_MISMATCH). */ export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = "state_mismatch" as const; -/** The join failed. */ +/** The client could not connect fully (JOIN_FAILURE). */ export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = "join_failure" as const; -/** The session migrated. */ +/** The server asked the participant to migrate (MIGRATION, Cloud only). */ export const LK_DISCONNECT_REASON_VALUE_MIGRATION = "migration" as const; -/** The signaling connection closed. */ +/** The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). */ export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = "signal_close" as const; -/** The room closed. */ +/** The room closed after its participants left (ROOM_CLOSED). */ export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = "room_closed" as const; -/** SIP callee unavailable. */ +/** The SIP callee did not answer in time (USER_UNAVAILABLE). */ export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = "user_unavailable" as const; -/** SIP callee rejected the call. */ +/** The SIP callee rejected the call (USER_REJECTED). */ export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = "user_rejected" as const; -/** SIP trunk failure. */ +/** SIP protocol failure (SIP_TRUNK_FAILURE). */ export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = "sip_trunk_failure" as const; -/** The connection timed out. */ +/** The server timed out the session (CONNECTION_TIMEOUT). */ export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = "connection_timeout" as const; -/** Media failed. */ +/** Media failed or timed out (MEDIA_FAILURE). */ export const LK_DISCONNECT_REASON_VALUE_MEDIA_FAILURE = "media_failure" as const; -/** The agent failed. */ +/** The agent failed (AGENT_ERROR). */ export const LK_DISCONNECT_REASON_VALUE_AGENT_ERROR = "agent_error" as const; -/** The reconnect policy ran out of attempts. */ +/** The client ran out of reconnect attempts (client only). */ export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = "reconnect_failed" as const; -/** Unknown reason. */ +/** UNKNOWN_REASON or a number the client does not know. */ export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = "unknown" as const; /** Agent job ID (`AJ_...`). */ @@ -148,21 +148,21 @@ export const ATTR_LK_LOG_LOGGER = "lk.log.logger" as const; 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. */ +/** 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; -/** How the attempt ended. Always present on a span; rollups read this, never the span status. */ +/** How the attempt ended. Rollups read this, never the span status. */ export const ATTR_LK_OUTCOME = "lk.outcome" as const; -/** Succeeded. */ +/** Succeeded; span status Unset. */ export const LK_OUTCOME_VALUE_OK = "ok" as const; -/** Failed; `error.type` names why. */ +/** 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 a newer attempt. */ +/** Cancelled by the caller or superseded; span status Unset. */ export const LK_OUTCOME_VALUE_CANCELLED = "cancelled" as const; -/** Server-assigned participant SID (`PA_...`). */ +/** Server-assigned SID of the local participant (`PA_...`). */ export const ATTR_LK_PARTICIPANT_ID = "lk.participant_id" as const; /** Customer-supplied identity of the local participant. PII: redacted by the collector. */ @@ -182,40 +182,40 @@ 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; -/** ICE restart / resume. */ +/** Resume the session (ICE restart). */ export const LK_RECONNECT_MODE_VALUE_QUICK = "quick" as const; -/** Full reconnect. */ +/** Reconnect from scratch. */ export const LK_RECONNECT_MODE_VALUE_FULL = "full" as const; -/** What triggered the reconnect cycle (the protocol's ReconnectReason, plus client-only reasons). */ +/** What triggered the reconnect cycle. */ export const ATTR_LK_RECONNECT_REASON = "lk.reconnect.reason" as const; -/** The signaling socket closed. */ +/** The signaling socket closed (RR_SIGNAL_DISCONNECTED). */ export const LK_RECONNECT_REASON_VALUE_SIGNAL_DISCONNECTED = "signal_disconnected" as const; -/** The publisher peer connection failed. */ +/** 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. */ +/** The subscriber peer connection failed (RR_SUBSCRIBER_FAILED). */ export const LK_RECONNECT_REASON_VALUE_SUBSCRIBER_FAILED = "subscriber_failed" as const; -/** A peer connection failed; the platform did not say which. */ +/** 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. */ +/** 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. */ +/** The device moved to another network (client only). */ export const LK_RECONNECT_REASON_VALUE_NETWORK_CHANGED = "network_changed" as const; -/** A test or debug menu asked for it. */ +/** A test or a debug menu asked for it (client only). */ export const LK_RECONNECT_REASON_VALUE_DEBUG = "debug" as const; -/** Unknown reason. */ +/** RR_UNKNOWN or a number the client does not know. */ export const LK_RECONNECT_REASON_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned room session SID (`RM_...`). */ export const ATTR_LK_ROOM_SID = "lk.room_sid" as const; -/** Average audio level in the window, 0-1, audio. Unit: `1`. */ +/** 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, audio. Unit: `1`. */ +/** 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, audio. Unit: `1`. */ +/** 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`. */ @@ -257,13 +257,13 @@ export const ATTR_LK_RTC_JITTER_BUFFER_DELAY_MS = "lk.rtc.jitter_buffer_delay_ms /** 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. Unit: `ms`. */ +/** 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. Unit: `ms`. */ +/** 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. Unit: `ms`. */ +/** 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}`. */ @@ -293,10 +293,10 @@ 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 (remote-inbound for outbound, candidate pair for inbound). Unit: `ms`. */ +/** 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 in the window. Unit: `{reading}`. */ +/** 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}`. */ @@ -311,10 +311,19 @@ 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; -/** Batches waiting in the cache now (a gauge). Unit: `{batch}`. */ +/** Batches waiting in the cache now. Unit: `{batch}`. */ export const ATTR_LK_TELEMETRY_CACHE_BATCHES = "lk.telemetry.cache.batches" as const; -/** Batches the disk refused, kept in memory. Unit: `{batch}`. */ +/** 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}`. */ @@ -326,10 +335,10 @@ export const ATTR_LK_TELEMETRY_DROPPED_CACHE_FULL = "lk.telemetry.dropped.cache_ /** 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 24 h age limit. Unit: `{record}`. */ +/** 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 or attributes over the limits (rejected). Unit: `{record}`. */ +/** 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}`. */ @@ -341,13 +350,16 @@ export const ATTR_LK_TELEMETRY_DROPPED_QUEUE_FULL = "lk.telemetry.dropped.queue_ /** 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, partial success). Unit: `{record}`. */ +/** 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; -/** Soft upload holds that reached the 60 s cap. Unit: `{hold}`. */ +/** 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`. */ @@ -367,9 +379,9 @@ export const ATTR_LK_TELEMETRY_UPLOADS_UNAUTHORIZED = "lk.telemetry.uploads.unau /** Whether the stats describe sent or received media. */ export const ATTR_LK_TRACK_DIRECTION = "lk.track.direction" as const; -/** Received media. */ +/** Received media (inbound-rtp). */ export const LK_TRACK_DIRECTION_VALUE_INBOUND = "inbound" as const; -/** Sent media. */ +/** Sent media (outbound-rtp). */ export const LK_TRACK_DIRECTION_VALUE_OUTBOUND = "outbound" as const; /** Media kind of the track. */ @@ -379,17 +391,17 @@ export const LK_TRACK_KIND_VALUE_AUDIO = "audio" as const; /** Video. */ export const LK_TRACK_KIND_VALUE_VIDEO = "video" as const; -/** Source of the track (the protocol's TrackSource, snake_case). */ +/** Source of the track. */ export const ATTR_LK_TRACK_SOURCE = "lk.track.source" as const; -/** Camera. */ +/** Camera (TrackSource CAMERA). */ export const LK_TRACK_SOURCE_VALUE_CAMERA = "camera" as const; -/** Microphone. */ +/** Microphone (TrackSource MICROPHONE). */ export const LK_TRACK_SOURCE_VALUE_MICROPHONE = "microphone" as const; -/** Screen share video. */ +/** Screen share video (TrackSource SCREEN_SHARE). */ export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE = "screen_share" as const; -/** Screen share audio. */ +/** Screen share audio (TrackSource SCREEN_SHARE_AUDIO). */ export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE_AUDIO = "screen_share_audio" as const; -/** Unknown source. */ +/** Unknown source (TrackSource UNKNOWN). */ export const LK_TRACK_SOURCE_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned track SID (`TR_...`). */ @@ -397,31 +409,31 @@ export const ATTR_LK_TRACK_SID = "lk.track_sid" as const; /** Namespace of app-defined events. The core emits `custom.` with the app's own attributes. */ export const EVENT_CUSTOM = "custom" as const; -/** App moved to the foreground or background (and its initial value). */ +/** The app moved to the foreground or background. */ export const EVENT_LK_DEVICE_APP_STATE_CHANGED = "lk.device.app_state.changed" as const; -/** Audio session interruption began or ended. */ +/** An audio session interruption began or ended. */ export const EVENT_LK_DEVICE_AUDIO_INTERRUPTION = "lk.device.audio.interruption" as const; -/** Audio route changed. */ +/** 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. Never per percent. */ +/** 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. Severity WARN. */ +/** A capture device failed. */ export const EVENT_LK_DEVICE_CAPTURE_FAILED = "lk.device.capture.failed" as const; -/** Low Power Mode changed (and its initial value). */ +/** Low Power Mode changed. */ export const EVENT_LK_DEVICE_LOW_POWER_CHANGED = "lk.device.low_power.changed" as const; -/** Memory pressure changed (and its initial value). */ +/** Memory pressure changed. */ export const EVENT_LK_DEVICE_MEMORY_CHANGED = "lk.device.memory.changed" as const; -/** Network path changed (and its initial value). */ +/** The network path changed. */ export const EVENT_LK_DEVICE_NETWORK_CHANGED = "lk.device.network.changed" as const; -/** Thermal state changed (and its initial value). */ +/** Thermal state changed. */ export const EVENT_LK_DEVICE_THERMAL_CHANGED = "lk.device.thermal.changed" as const; -/** Pipeline smoke test. Never emitted in production paths. */ +/** Pipeline smoke test. */ export const EVENT_LK_PING = "lk.ping" as const; -/** The Room left connected for good (never on a reconnect). INFO when the client hung up, WARN otherwise. */ +/** 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, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. */ +/** Self-telemetry; counts since the previous report. */ export const EVENT_LK_TELEMETRY_REPORT = "lk.telemetry.report" as const; /** One connect attempt. Kind: client. */ @@ -430,5 +442,5 @@ export const SPAN_LK_CONNECT = "lk.connect" as const; 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 (or timed_out after 30 s). Kind: internal. */ +/** One subscription, from the intent to subscribe to the first media. Kind: internal. */ export const SPAN_LK_SUBSCRIBE = "lk.subscribe" as const; diff --git a/otel/model/client/groups.yaml b/otel/model/client/groups.yaml index a3cf19a33..559437490 100644 --- a/otel/model/client/groups.yaml +++ b/otel/model/client/groups.yaml @@ -1,4 +1,4 @@ -# Attribute sets the client core puts on every resource / record. They exist so live-check +# 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. @@ -7,55 +7,90 @@ attribute_groups: - id: livekit.client.resource visibility: public stability: development - brief: Resource attributes of the client pipeline (one per process). + 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 - examples: ["livekit-client-swift"] + 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 the core stamps on every record and span of a session. + brief: Attributes the core stamps 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`) are never 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, also on records outside a room. + note: The session's trace id (32 hex characters), also on the process's own records. + examples: ["4bf92f3577b34da6a3ce929d0e0e4736"] - ref: otel.event.name - requirement_level: recommended - note: Duplicates the log record's `event_name` for backends that do not surface it. + 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: recommended + requirement_level: {conditionally_required: Once the client has joined the room.} - ref: lk.pii.room_name - requirement_level: recommended + requirement_level: {conditionally_required: Once the client has joined the room.} - ref: lk.participant_id - requirement_level: recommended + requirement_level: {conditionally_required: Once the client has joined the room.} - ref: lk.pii.participant_identity - requirement_level: recommended + 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 the core copies from Rust + (`lk.log.source = ffi`), JWT-shaped substrings are masked as ``. attributes: - ref: lk.log.source requirement_level: required @@ -63,7 +98,10 @@ attribute_groups: requirement_level: required - ref: code.function.name requirement_level: recommended + examples: ["connect(url:token:)"] - ref: code.file.path requirement_level: recommended + examples: ["Sources/LiveKit/Core/Room.swift"] - ref: code.line.number requirement_level: recommended + examples: [42] diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml index 381ab3998..337542039 100644 --- a/otel/model/client/registry.yaml +++ b/otel/model/client/registry.yaml @@ -1,45 +1,56 @@ -# Client-owned attributes, under the names the client core emits today (rust-sdks -# livekit-telemetry SPEC.md). The Client team approves changes here alone, unless one touches -# a shared concept (otel/model/shared). Durations and RTT/jitter in RTC stats stay in ms, the -# W3C webrtc-stats unit, through an explicit `unit_exception`. +# 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). Durations and RTT/jitter in +# RTC stats stay in ms, the W3C webrtc-stats unit, through an explicit `unit_exception`. groups: - id: registry.lk.device type: attribute_group display_name: LiveKit client device state brief: Device conditions the host SDK observes and the core reports on change. + note: > + The platform SDK reads the OS; the core turns changes into `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: The platform's `unknown` is no reading and is never emitted. + 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: No thermal pressure., stability: development} + - {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 core doubles its cadence., stability: development} - - {id: critical, value: critical, brief: Critical; the core quadruples its cadence., 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; forces a flush., 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: "Android `onTrimMemory`: RUNNING_LOW/BACKGROUND map to `warning`, RUNNING_CRITICAL/COMPLETE to `critical`." + 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} @@ -51,6 +62,7 @@ groups: type: boolean stability: development brief: The network is metered (cellular, personal hotspot). + note: Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. examples: [false] annotations: livekit: {owner: client, sensitivity: none} @@ -58,6 +70,7 @@ groups: type: boolean stability: development brief: The user asked for less traffic (Low Data Mode, Data Saver). + note: Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. examples: [false] annotations: livekit: {owner: client, sensitivity: none} @@ -65,7 +78,10 @@ groups: type: int stability: development brief: Battery charge in percent, 0-100. - note: SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. + note: > + Only where the level is known (iOS, Android; not desktops or tvOS); never from the JS + port. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this + key. Open decision. examples: [42] annotations: livekit: {owner: client, sensitivity: none, unit: "%"} @@ -79,7 +95,8 @@ groups: livekit: {owner: client, sensitivity: none} - id: lk.device.audio_route.reason stability: development - brief: Why the audio route changed (AVAudioSession route change reasons). + 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} @@ -95,24 +112,27 @@ groups: - id: lk.device.audio_route.outputs type: string stability: development - brief: > - Comma-separated active outputs, each one of speaker, receiver, wired_headset, bluetooth, - car_audio, air_play, hdmi, usb, other. + 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., stability: development} + - {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} @@ -122,7 +142,10 @@ groups: livekit: {owner: client, sensitivity: none} - id: lk.device.capture.reason stability: development - brief: Why capture failed (the getUserMedia failure taxonomy). + 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} @@ -141,6 +164,7 @@ groups: - 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} @@ -149,14 +173,17 @@ groups: livekit: {owner: client, sensitivity: none} - id: lk.track.source stability: development - brief: Source of the track (the protocol's TrackSource, snake_case). + brief: Source of the track. + note: > + The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead + (`lk.track_source = SOURCE_MICROPHONE`). 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 share video., stability: development} - - {id: screen_share_audio, value: screen_share_audio, brief: Screen share audio., stability: development} - - {id: unknown, value: unknown, brief: Unknown source., stability: development} + - {id: camera, value: camera, brief: Camera (TrackSource CAMERA)., stability: development} + - {id: microphone, value: microphone, brief: Microphone (TrackSource MICROPHONE)., stability: development} + - {id: screen_share, value: screen_share, brief: Screen share video (TrackSource SCREEN_SHARE)., stability: development} + - {id: screen_share_audio, value: screen_share_audio, brief: Screen share audio (TrackSource SCREEN_SHARE_AUDIO)., stability: development} + - {id: unknown, value: unknown, brief: Unknown source (TrackSource UNKNOWN)., stability: development} annotations: livekit: owner: client @@ -166,10 +193,11 @@ groups: - 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., stability: development} - - {id: outbound, value: outbound, brief: Sent media., stability: development} + - {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} @@ -179,11 +207,16 @@ groups: 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. + note: > + Simulcast layers of one track are folded into one monotonic series before windowing. W3C + durations in seconds become integer milliseconds (truncated); RTT and jitter become double + milliseconds. 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} @@ -191,13 +224,15 @@ groups: type: int stability: development brief: Actual length of the stats window. + note: From the window's start to its close, on the core's clock (the JS port uses wall-clock time). examples: [60000] annotations: livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} - id: lk.rtc.samples type: int stability: development - brief: Stats readings in the window. + brief: Stats readings folded into the window. + note: Denominator for the gauges' averages. examples: [2] annotations: livekit: {owner: client, sensitivity: none, unit: "{reading}"} @@ -205,6 +240,7 @@ groups: 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} @@ -212,6 +248,7 @@ groups: 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}"} @@ -219,6 +256,7 @@ groups: 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}"} @@ -226,6 +264,7 @@ groups: type: int stability: development brief: Video freezes, inbound video (cumulative). + note: Inbound `freezeCount`. examples: [0] annotations: livekit: {owner: client, sensitivity: none, unit: "{freeze}"} @@ -233,6 +272,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -240,6 +280,7 @@ groups: type: int stability: development brief: Video pauses, inbound video (cumulative). + note: Inbound `pauseCount`. examples: [0] annotations: livekit: {owner: client, sensitivity: none, unit: "{pause}"} @@ -247,6 +288,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -254,6 +296,7 @@ groups: type: int stability: development brief: Concealed audio samples, inbound audio (cumulative). + note: Inbound `concealedSamples`. examples: [480] annotations: livekit: {owner: client, sensitivity: none, unit: "{sample}"} @@ -261,6 +304,7 @@ groups: 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}"} @@ -268,6 +312,7 @@ groups: type: int stability: development brief: Concealment events, inbound audio (cumulative). + note: Inbound `concealmentEvents`. examples: [1] annotations: livekit: {owner: client, sensitivity: none, unit: "{event}"} @@ -275,6 +320,7 @@ groups: type: int stability: development brief: Audio interruptions, inbound audio (cumulative). + note: Inbound `interruptionCount`. examples: [0] annotations: livekit: {owner: client, sensitivity: none, unit: "{interruption}"} @@ -282,6 +328,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -289,6 +336,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -296,6 +344,7 @@ groups: 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}"} @@ -303,6 +352,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -310,6 +360,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -317,34 +368,41 @@ groups: 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 keep webrtc-stats milliseconds} - id: lk.rtc.jitter_ms.min type: double stability: development - brief: Minimum jitter in the window. + 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 keep webrtc-stats milliseconds} - id: lk.rtc.jitter_ms.max type: double stability: development - brief: Maximum jitter in the window. + 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 keep webrtc-stats milliseconds} - id: lk.rtc.jitter_ms.avg type: double stability: development - brief: Average jitter in the window. + brief: Average jitter in the window, inbound. + note: Inbound `jitter` (seconds × 1000), averaged over `lk.rtc.samples` readings. examples: [1.8] annotations: livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} - id: lk.rtc.rtt_ms.min type: double stability: development - brief: Minimum round-trip time in the window (remote-inbound for outbound, candidate pair for inbound). + 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 keep webrtc-stats milliseconds} @@ -352,6 +410,7 @@ groups: 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 keep webrtc-stats milliseconds} @@ -359,6 +418,7 @@ groups: type: double stability: development brief: Average round-trip time in the window. + note: Same sources as `lk.rtc.rtt_ms.min`, averaged over `lk.rtc.samples` readings. examples: [25.5] annotations: livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} @@ -366,6 +426,7 @@ groups: 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"} @@ -373,6 +434,7 @@ groups: 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"} @@ -380,27 +442,31 @@ groups: type: double stability: development brief: Average frame rate in the window, video. + note: "`framesPerSecond`, either direction, averaged over `lk.rtc.samples` readings." 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, audio. + 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, audio. + 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, audio. + brief: Average audio level in the window, 0-1, inbound audio. + note: Inbound `audioLevel`, averaged over `lk.rtc.samples` readings. examples: [0.05] annotations: livekit: {owner: client, sensitivity: none, unit: "1"} @@ -412,50 +478,58 @@ groups: attributes: - id: lk.outcome stability: development - brief: How the attempt ended. Always present on a span; rollups read this, never the span status. - note: The Notion conventions name `lk.client.connection.result` (success|failure|cancelled) for connects only. Open decision. + brief: How the attempt ended. Rollups read this, never the span status. + note: > + Exactly once on every LiveKit span, written by the core from how the span ended; an + attribute of this name set any other way is dropped. Status follows it: `Error` for + `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name + `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. type: members: - - {id: ok, value: ok, brief: Succeeded., stability: development} - - {id: error, value: error, brief: Failed; `error.type` names why., stability: development} - - {id: cancelled, value: cancelled, brief: Cancelled by the caller or a newer attempt., stability: development} + - {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; 1 is the user-initiated connect. + 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 (the protocol's ReconnectReason, plus client-only reasons). + 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., stability: development} - - {id: publisher_failed, value: publisher_failed, brief: The publisher peer connection failed., stability: development} - - {id: subscriber_failed, value: subscriber_failed, brief: The subscriber peer connection failed., stability: development} - - {id: transport_failed, value: transport_failed, brief: A peer connection failed; the platform did not say which., stability: development} - - {id: switch_candidate, value: switch_candidate, brief: The server asked for a candidate switch., stability: development} - - {id: network_changed, value: network_changed, brief: The device moved to another network., stability: development} - - {id: debug, value: debug, brief: A test or debug menu asked for it., stability: development} - - {id: unknown, value: unknown, brief: Unknown reason., stability: development} + - {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: ICE restart / resume., stability: development} - - {id: full, value: full, brief: Full reconnect., stability: development} + - {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} @@ -467,27 +541,28 @@ groups: attributes: - id: lk.disconnect.reason stability: development - brief: Why the session ended (the protocol's DisconnectReason, plus the client giving up). + brief: Why the session ended. + note: The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). type: members: - - {id: client_initiated, value: client_initiated, brief: The client 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 shut down., stability: development} - - {id: participant_removed, value: participant_removed, brief: The participant was removed., stability: development} - - {id: room_deleted, value: room_deleted, brief: The room was deleted., stability: development} - - {id: state_mismatch, value: state_mismatch, brief: Client and server state diverged., stability: development} - - {id: join_failure, value: join_failure, brief: The join failed., stability: development} - - {id: migration, value: migration, brief: The session migrated., stability: development} - - {id: signal_close, value: signal_close, brief: The signaling connection closed., stability: development} - - {id: room_closed, value: room_closed, brief: The room closed., stability: development} - - {id: user_unavailable, value: user_unavailable, brief: SIP callee unavailable., stability: development} - - {id: user_rejected, value: user_rejected, brief: SIP callee rejected the call., stability: development} - - {id: sip_trunk_failure, value: sip_trunk_failure, brief: SIP trunk failure., stability: development} - - {id: connection_timeout, value: connection_timeout, brief: The connection timed out., stability: development} - - {id: media_failure, value: media_failure, brief: Media failed., stability: development} - - {id: agent_error, value: agent_error, brief: The agent failed., stability: development} - - {id: reconnect_failed, value: reconnect_failed, brief: The reconnect policy ran out of attempts., stability: development} - - {id: unknown, value: unknown, brief: Unknown reason., stability: development} + - {id: client_initiated, value: client_initiated, brief: The client hung up (CLIENT_INITIATED)., stability: development} + - {id: duplicate_identity, value: duplicate_identity, brief: Another participant joined with the same identity (DUPLICATE_IDENTITY)., stability: development} + - {id: server_shutdown, value: server_shutdown, brief: The server instance shut down (SERVER_SHUTDOWN)., stability: development} + - {id: participant_removed, value: participant_removed, brief: RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED)., stability: development} + - {id: room_deleted, value: room_deleted, brief: RoomService.DeleteRoom was called (ROOM_DELETED)., stability: development} + - {id: state_mismatch, value: state_mismatch, brief: The server did not know the session the client tried to resume (STATE_MISMATCH)., stability: development} + - {id: join_failure, value: join_failure, brief: The client could not connect fully (JOIN_FAILURE)., stability: development} + - {id: migration, value: migration, brief: "The server asked the participant to migrate (MIGRATION, Cloud only).", stability: development} + - {id: signal_close, value: signal_close, brief: The signal WebSocket closed unexpectedly (SIGNAL_CLOSE)., stability: development} + - {id: room_closed, value: room_closed, brief: The room closed after its participants left (ROOM_CLOSED)., stability: development} + - {id: user_unavailable, value: user_unavailable, brief: The SIP callee did not answer in time (USER_UNAVAILABLE)., stability: development} + - {id: user_rejected, value: user_rejected, brief: The SIP callee rejected the call (USER_REJECTED)., stability: development} + - {id: sip_trunk_failure, value: sip_trunk_failure, brief: SIP protocol failure (SIP_TRUNK_FAILURE)., stability: development} + - {id: connection_timeout, value: connection_timeout, brief: The server timed out the session (CONNECTION_TIMEOUT)., stability: development} + - {id: media_failure, value: media_failure, brief: Media failed or timed out (MEDIA_FAILURE)., stability: development} + - {id: agent_error, value: agent_error, brief: The agent failed (AGENT_ERROR)., stability: development} + - {id: reconnect_failed, value: reconnect_failed, brief: The client ran out of reconnect attempts (client only)., stability: development} + - {id: unknown, value: unknown, brief: UNKNOWN_REASON or a number the client does not know., stability: development} annotations: livekit: {owner: client, sensitivity: none} @@ -499,10 +574,11 @@ groups: - 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., 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} @@ -510,6 +586,7 @@ groups: 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} @@ -518,11 +595,15 @@ groups: 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} @@ -530,6 +611,7 @@ groups: 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}"} @@ -537,6 +619,7 @@ groups: 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} @@ -544,6 +627,7 @@ groups: 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}"} @@ -551,6 +635,7 @@ groups: 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}"} @@ -558,27 +643,31 @@ groups: 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 the 60 s cap. + 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 (a gauge). + 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. + brief: Batches the disk refused, kept in memory instead. + note: Only when non-zero. examples: [1] annotations: livekit: {owner: client, sensitivity: none, unit: "{batch}"} @@ -586,6 +675,7 @@ groups: 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}"} @@ -593,6 +683,7 @@ groups: 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}"} @@ -600,13 +691,15 @@ groups: 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 24 h age limit. + 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}"} @@ -614,20 +707,23 @@ groups: 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 or attributes over the limits (rejected). + 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, partial success). + 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}"} @@ -635,6 +731,7 @@ groups: 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}"} @@ -642,6 +739,7 @@ groups: 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}"} @@ -649,6 +747,65 @@ groups: 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. Not OTel keys: OTel 1.44.0 names this `user_agent.name` / + `user_agent.version`, and owns the `browser.*` namespace. Open decision. + 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 index ba439d1c6..ae8fa7005 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -1,12 +1,25 @@ -# Client signals, under the names the client core emits today (rust-sdks livekit-telemetry -# SPEC.md). Events are OTLP log records with `event_name`; spans are one attempt each. +# 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. Never emitted in production paths. + 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 @@ -15,7 +28,18 @@ groups: type: event name: lk.telemetry.report stability: development - brief: Self-telemetry, appended to the next upload after a loss, failure, refusal, capped hold or disk error, and once at shutdown. + 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 @@ -50,15 +74,28 @@ groups: - ref: lk.telemetry.dropped.oversized requirement_level: {conditionally_required: When non-zero.} - ref: lk.telemetry.dropped.throttled - requirement_level: {conditionally_required: When non-zero.} + 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 (and its initial value). + 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 @@ -67,7 +104,10 @@ groups: type: event name: lk.device.low_power.changed stability: development - brief: Low Power Mode changed (and its initial value). + 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 @@ -76,7 +116,10 @@ groups: type: event name: lk.device.app_state.changed stability: development - brief: App moved to the foreground or background (and its initial value). + 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 @@ -85,7 +128,10 @@ groups: type: event name: lk.device.memory.changed stability: development - brief: Memory pressure changed (and its initial value). + 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 @@ -94,11 +140,19 @@ groups: type: event name: lk.device.network.changed stability: development - brief: Network path changed (and its initial value). + brief: The network path changed. + note: > + Sent once with the initial state, then when any of its attributes changes. iOS, macOS and + Android; on the web, Chromium only. + 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: The client also sends `vpn`, `bluetooth` and `other`, which are not upstream members. + 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: required - ref: lk.device.network.constrained @@ -108,7 +162,10 @@ groups: type: event name: lk.device.battery.changed stability: development - brief: Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent. + 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 @@ -119,7 +176,10 @@ groups: type: event name: lk.device.audio_route.changed stability: development - brief: Audio route changed. + 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 @@ -130,7 +190,10 @@ groups: type: event name: lk.device.audio.interruption stability: development - brief: Audio session interruption began or ended. + 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 @@ -139,7 +202,10 @@ groups: type: event name: lk.device.capture.failed stability: development - brief: A capture device failed. Severity WARN. + 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 @@ -151,7 +217,15 @@ groups: name: lk.rtc.stats.sample stability: development brief: One RTC stats window for one track and direction. - note: Only the track, kind, direction, window and sample count are always present; counters and gauges a platform or direction lacks are omitted. + 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 @@ -218,17 +292,20 @@ groups: - ref: lk.rtc.fps.avg requirement_level: {conditionally_required: Video.} - ref: lk.rtc.audio_level.min - requirement_level: {conditionally_required: Audio.} + requirement_level: {conditionally_required: Inbound audio.} - ref: lk.rtc.audio_level.max - requirement_level: {conditionally_required: Audio.} + requirement_level: {conditionally_required: Inbound audio.} - ref: lk.rtc.audio_level.avg - requirement_level: {conditionally_required: Audio.} + 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 (never on a reconnect). INFO when the client hung up, WARN otherwise. + 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 @@ -238,31 +315,70 @@ groups: name: custom stability: development brief: Namespace of app-defined events. The core emits `custom.` with the app's own attributes. - note: App attribute keys may not use `lk.*`, `otel.*` or `session.id`. + 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`), + may not use the reserved keys, and stay within the limits; anything else is rejected, never + truncated, and counted 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 --------------------- + # --- 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: > - Checkpoints, in order: ws_open, signal, join_recv, pc_created (required on every platform), - then engine, pc_connected, offer_sent, answer_sent, room_connected (best effort). + 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: When `lk.outcome` is `error`.} + 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`. Written only by the core, at most once. + examples: ["LiveKitError.12", "signal_close"] - id: span.lk.reconnect type: span span_kind: client stability: development brief: One reconnect cycle. - note: Checkpoints are named `attempt `, one per attempt (dynamic; open decision). + note: > + `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint + named `attempt `: a dynamic name (the Notion conventions keep dynamic data out of + names; open decision), 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 @@ -273,31 +389,53 @@ groups: - ref: lk.outcome requirement_level: required - ref: error.type - requirement_level: {conditionally_required: When `lk.outcome` is `error`.} + 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. Written only by the core, 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.} + requirement_level: {conditionally_required: On success; the server assigns it.} - ref: lk.outcome requirement_level: required - ref: error.type - requirement_level: {conditionally_required: When `lk.outcome` is `error`.} + 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. Written only by the core, 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 (or timed_out after 30 s). - note: "Checkpoints: subscribed, first_media." + 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 @@ -310,4 +448,6 @@ groups: - ref: lk.outcome requirement_level: required - ref: error.type - requirement_level: {conditionally_required: When `lk.outcome` is `error`.} + 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 index 7d7b09ee4..467705ca7 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.1.0 +schema_url: https://livekit.io/schemas/telemetry/0.2.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned signals). dependencies: diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml index 3c996ea86..f3a90d2a5 100644 --- a/otel/model/shared/registry.yaml +++ b/otel/model/shared/registry.yaml @@ -6,12 +6,18 @@ groups: 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: Omit before join. High cardinality, never a default metric dimension. + note: > + On every record and span of a session once the client has joined; absent before join. + High cardinality, never a default metric dimension. The client core sends `lk.room.sid` + until the shared rename lands. examples: ["RM_5Fq8xQ2ZbXwN"] annotations: livekit: @@ -26,8 +32,11 @@ groups: - id: lk.participant_id type: string stability: development - brief: Server-assigned participant SID (`PA_...`). - note: High cardinality, never a default metric dimension. + 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. The client core sends `lk.participant.sid` until the + shared rename lands. examples: ["PA_9kLm2NpQ4rTs"] annotations: livekit: {owner: shared, sensitivity: none, authority: cloud} @@ -35,7 +44,10 @@ groups: type: string stability: development brief: Server-assigned track SID (`TR_...`). - note: High cardinality, never a default metric dimension. + note: > + On publish and subscribe spans and RTC stats windows. A publish learns it only on + success. High cardinality, never a default metric dimension. The client core sends + `lk.track.sid` until the shared rename lands. examples: ["TR_AMkTyV3Zx8Qp"] annotations: livekit: {owner: shared, sensitivity: none, authority: cloud} @@ -43,7 +55,9 @@ groups: type: string stability: development brief: Agent job ID (`AJ_...`). - note: High cardinality; Agents' job-scoped metrics are the one exception. + 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: @@ -60,11 +74,18 @@ groups: 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. The client core sends + `lk.room.name` until the shared rename lands, which the collector cannot redact. examples: ["my-room"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -72,6 +93,9 @@ groups: 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. The client core sends + `lk.participant.identity` until the shared rename lands. examples: ["identity-0"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -79,7 +103,9 @@ groups: type: string stability: development brief: Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). - note: Name TBD, needs sign-off; the Notion conventions name no key for it. + note: > + Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends + `lk.participant.remote_identity` until then. examples: ["identity-1"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -96,6 +122,9 @@ groups: type: string stability: development brief: LiveKit SDK name. + note: > + Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet + (it sends `service.name = livekit-client-`); the vocabulary is open. examples: ["swift", "android", "flutter", "react-native", "unity", "rust", "js"] annotations: livekit: {owner: shared, sensitivity: none} @@ -103,6 +132,7 @@ groups: type: string stability: development brief: LiveKit SDK version. + note: Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. examples: ["2.17.0"] annotations: livekit: {owner: shared, sensitivity: none} From 54f73a5d96dcd3ef631c1b7a8b5835c6e6bed827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:33:02 +0200 Subject: [PATCH 11/36] docs(otel): generate the conventions spec from the registry gen/docs/semconv.md now reads as the spec: the naming rules (flat shared identity, the lk.pii.* redaction contract, seconds with the documented RTC ms exception, keys the SDK owns, custom.* and its limits), the Resource, the attributes on every record, plain log records, each event with its severity, body and requirement table, each span with its kind, requirement table, checkpoints and limits, an attribute reference with where each key is used, enum tables, the bindings and the open naming decisions. Rust and TypeScript also get constants for browser.* and the checkpoint names. OTel refs used for logs get short notes of their own. --- otel/README.md | 27 +- otel/gen/docs/semconv.md | 876 ++++++++++++------ otel/gen/rust/semconv.rs | 35 + otel/gen/ts/semconv.ts | 31 + otel/model/client/groups.yaml | 3 + .../templates/registry/markdown/semconv.md.j2 | 208 ++++- otel/templates/registry/markdown/weaver.yaml | 2 +- otel/templates/registry/rust/semconv.rs.j2 | 7 + otel/templates/registry/rust/weaver.yaml | 2 +- .../registry/typescript/semconv.ts.j2 | 3 + .../templates/registry/typescript/weaver.yaml | 2 +- 11 files changed, 888 insertions(+), 308 deletions(-) diff --git a/otel/README.md b/otel/README.md index e7e12beb7..b1b006f05 100644 --- a/otel/README.md +++ b/otel/README.md @@ -7,8 +7,12 @@ upstream keys (`service.*`, `os.*`, `device.*`, `network.connection.type`, `erro `session.id`, `code.*`) are referenced with `ref:`, never redefined. Status: prototype, `development` stability. It registers the shared identity, PII and SDK keys, -and what the client core (rust-sdks `livekit-telemetry`) emits today, under its current names. -Agents-owned keys are not registered yet. +and what the client cores (rust-sdks `livekit-telemetry` and the JS port) emit today, under their +current names. Agents-owned keys are not registered yet. + +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 | |---|---| @@ -24,8 +28,9 @@ Agents-owned keys are not registered yet. | `live-check/` | Live-check config, advice rules and the OTLP/JSON converter | | `tests/` | Policy negatives, live-check samples, codegen escaping, schema-version rule | -The generated docs, [`gen/docs/semconv.md`](gen/docs/semconv.md), list every key with its type, -owner, sensitivity and unit. +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, bindings, and the open naming decisions. ## Ownership @@ -63,6 +68,20 @@ nothing validates it. | `bindings` | map of producer/store → name | informational | | `cardinality_exception` | `true`, on a metric | allows a high-cardinality ID as a metric dimension | + Events and spans carry their own `annotations.livekit`, rendered in the docs: + + | 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. diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 3b9f19217..cd9dd1110 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -1,113 +1,116 @@ -# LiveKit telemetry conventions - -Schema: `https://livekit.io/schemas/telemetry/0.2.0`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. - -## Attributes - -| Key | Type | Owner | Sensitivity | Unit | Description | -| --- | --- | --- | --- | --- | --- | -| `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. | -| `lk.device.app_state` | enum: `foreground`, `background` | client | none | | Whether the app is in front of the user. All platforms; the web maps page visibility. | -| `lk.device.audio.interruption` | enum: `began`, `ended` | client | none | | Phase of an audio session interruption. iOS audio session interruptions; Android audio focus loss and gain. | -| `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. | -| `lk.device.audio_route.reason` | enum: `new_device`, `old_device_unavailable`, `category_change`, `override`, `wake_from_sleep`, `no_suitable_route`, `route_configuration_change`, `unknown` | 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.battery.charging` | boolean | client | none | | The device is charging. SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. | -| `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. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | -| `lk.device.capture.device` | enum: `camera`, `microphone`, `screen_share` | client | none | | Capture device that failed. All platforms. | -| `lk.device.capture.reason` | enum: `permission_denied`, `not_found`, `in_use`, `disconnected`, `other` | 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.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. | -| `lk.device.memory.pressure` | enum: `normal`, `warning`, `critical` | 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.network.constrained` | boolean | client | none | | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. | -| `lk.device.network.expensive` | boolean | client | none | | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. | -| `lk.device.thermal.state` | enum: `nominal`, `fair`, `serious`, `critical` | 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.disconnect.reason` | enum: `client_initiated`, `duplicate_identity`, `server_shutdown`, `participant_removed`, `room_deleted`, `state_mismatch`, `join_failure`, `migration`, `signal_close`, `room_closed`, `user_unavailable`, `user_rejected`, `sip_trunk_failure`, `connection_timeout`, `media_failure`, `agent_error`, `reconnect_failed`, `unknown` | client | none | | Why the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | -| `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. | -| `lk.log.logger` | string | client | none | | Logger, type, module or file that wrote the line. Free text from the platform's logger. | -| `lk.log.source` | enum: `sdk`, `ffi`, `webrtc` | client | none | | Which layer wrote the log line. Only WARN and ERROR records are sent; WebRTC lines only at ERROR. | -| `lk.outcome` | enum: `ok`, `error`, `cancelled` | client | none | | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | -| `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. The client core sends `lk.participant.sid` until the shared rename lands. | -| `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. The client core sends `lk.participant.identity` until the shared rename lands. | -| `lk.pii.remote_participant_identity` | string | shared | pii | | Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends `lk.participant.remote_identity` until then. | -| `lk.pii.room_name` | string | shared | pii | | Customer-supplied room name. On every record and span of a session once the client has joined. The client core sends `lk.room.name` until the shared rename lands, which the collector cannot redact. | -| `lk.ping.seq` | int | client | none | | Monotonically increasing sequence number per pipeline. Optional; on `lk.ping` only. | -| `lk.reconnect.attempts` | int | client | none | | Reconnect attempts made in the cycle. Equals `` of the last `attempt ` checkpoint. | -| `lk.reconnect.mode` | enum: `quick`, `full` | client | none | | Mode of the last reconnect attempt. Updated with each `attempt ` checkpoint; matches the last one. | -| `lk.reconnect.reason` | enum: `signal_disconnected`, `publisher_failed`, `subscriber_failed`, `transport_failed`, `switch_candidate`, `network_changed`, `debug`, `unknown` | client | none | | What triggered the reconnect cycle. The protocol's `ReconnectReason` (`RR_*`), plus reasons only a client knows. | -| `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. The client core sends `lk.room.sid` until the shared rename lands. | -| `lk.rtc.audio_level.avg` | double | client | none | `1` | Average audio level in the window, 0-1, inbound audio. Inbound `audioLevel`, averaged over `lk.rtc.samples` readings. | -| `lk.rtc.audio_level.max` | double | client | none | `1` | Maximum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | -| `lk.rtc.audio_level.min` | double | client | none | `1` | Minimum audio level in the window, 0-1, inbound audio. Inbound `audioLevel`. | -| `lk.rtc.bytes` | int | client | none | `By` | Bytes sent or received (cumulative). Outbound `bytesSent`, inbound `bytesReceived`. | -| `lk.rtc.codec` | string | client | none | | Codec mimeType, when known. The `codec` stats entry the RTP stream's `codecId` names. | -| `lk.rtc.concealed_samples` | int | client | none | `{sample}` | Concealed audio samples, inbound audio (cumulative). Inbound `concealedSamples`. | -| `lk.rtc.concealment_events` | int | client | none | `{event}` | Concealment events, inbound audio (cumulative). Inbound `concealmentEvents`. | -| `lk.rtc.fps.avg` | double | client | none | `{frame}/s` | Average frame rate in the window, video. `framesPerSecond`, either direction, averaged over `lk.rtc.samples` readings. | -| `lk.rtc.fps.max` | double | client | none | `{frame}/s` | Maximum frame rate in the window, video. `framesPerSecond`, either direction. | -| `lk.rtc.fps.min` | double | client | none | `{frame}/s` | Minimum frame rate in the window, video. `framesPerSecond`, either direction. | -| `lk.rtc.freeze_count` | int | client | none | `{freeze}` | Video freezes, inbound video (cumulative). Inbound `freezeCount`. | -| `lk.rtc.freezes_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total freeze time, inbound video (cumulative). Inbound `totalFreezesDuration`. | -| `lk.rtc.interruption_count` | int | client | none | `{interruption}` | Audio interruptions, inbound audio (cumulative). Inbound `interruptionCount`. | -| `lk.rtc.interruptions_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total interruption time, inbound audio (cumulative). Inbound `totalInterruptionDuration`. | -| `lk.rtc.jitter_buffer_delay_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Jitter buffer delay, inbound (cumulative). Inbound `jitterBufferDelay`. Average delay = jitter_buffer_delay_ms / jitter_buffer_emitted_count. | -| `lk.rtc.jitter_buffer_emitted_count` | int | client | none | `{sample}` | Samples or frames emitted by the jitter buffer, inbound (cumulative). Inbound `jitterBufferEmittedCount`. | -| `lk.rtc.jitter_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average jitter in the window, inbound. Inbound `jitter` (seconds × 1000), averaged over `lk.rtc.samples` readings. | -| `lk.rtc.jitter_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | -| `lk.rtc.jitter_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Minimum jitter in the window, inbound. Inbound `jitter` (seconds × 1000). | -| `lk.rtc.packets` | int | client | none | `{packet}` | Packets sent or received (cumulative). Outbound `packetsSent`, inbound `packetsReceived`. | -| `lk.rtc.packets_lost` | int | client | none | `{packet}` | Packets lost, inbound (cumulative). Inbound `packetsLost`. Loss ratio = packets_lost / (packets + packets_lost). | -| `lk.rtc.pause_count` | int | client | none | `{pause}` | Video pauses, inbound video (cumulative). Inbound `pauseCount`. | -| `lk.rtc.pauses_duration_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Total pause time, inbound video (cumulative). Inbound `totalPausesDuration`. | -| `lk.rtc.quality_limitation.bandwidth_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by bandwidth, outbound video (cumulative). Outbound `qualityLimitationDurations.bandwidth`. | -| `lk.rtc.quality_limitation.cpu_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited by CPU, outbound video (cumulative). Outbound `qualityLimitationDurations.cpu`. | -| `lk.rtc.quality_limitation.other_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Time the encoder was limited for other reasons, outbound video (cumulative). Outbound `qualityLimitationDurations.other`. | -| `lk.rtc.rtt_ms.avg` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Average round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`, averaged over `lk.rtc.samples` readings. | -| `lk.rtc.rtt_ms.max` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Maximum round-trip time in the window. Same sources as `lk.rtc.rtt_ms.min`. | -| `lk.rtc.rtt_ms.min` | double | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | 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.samples` | int | client | none | `{reading}` | Stats readings folded into the window. Denominator for the gauges' averages. | -| `lk.rtc.silent_concealed_samples` | int | client | none | `{sample}` | Concealed samples that were silent, inbound audio (cumulative). Inbound `silentConcealedSamples`. | -| `lk.rtc.window_ms` | int | client | none | `ms` (exception: RTC stats keep webrtc-stats milliseconds) | Actual length of the stats window. From the window's start to its close, on the core's clock (the JS port uses wall-clock time). | -| `lk.sdk.name` | string | shared | none | | LiveKit SDK name. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet (it sends `service.name = livekit-client-`); the vocabulary is open. | -| `lk.sdk.version` | string | shared | none | | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. | -| `lk.telemetry.cache.batches` | int | client | none | `{batch}` | Batches waiting in the cache now. A gauge, not a count since the previous report. Always present on a report. | -| `lk.telemetry.cache.delete_errors` | int | client | none | `{batch}` | 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 | client | none | `{attempt}` | 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 | client | none | `{batch}` | 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 | client | none | `{batch}` | Batches the disk refused, kept in memory instead. Only when non-zero. | -| `lk.telemetry.dropped.cache_error` | int | client | none | `{record}` | Records no cache, not even memory, could take. Only when non-zero. | -| `lk.telemetry.dropped.cache_full` | int | client | none | `{record}` | Records evicted by the cache's size or file-count bound. Only when non-zero. | -| `lk.telemetry.dropped.corrupt` | int | client | none | `{record}` | Records in cached batches that failed their CRC. Only when non-zero. | -| `lk.telemetry.dropped.expired` | int | client | none | `{record}` | Records in batches past the cache's age limit. Only when non-zero. | -| `lk.telemetry.dropped.invalid` | int | client | none | `{record}` | 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 | client | none | `{record}` | Single records larger than the collector accepts (413). Only when non-zero. | -| `lk.telemetry.dropped.queue_full` | int | client | none | `{record}` | Records evicted from the in-memory queue. Only when non-zero. | -| `lk.telemetry.dropped.rate_limited` | int | client | none | `{record}` | 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 | client | none | `{record}` | Records the collector rejected (final 4xx/5xx, OTLP partial success). Only when non-zero. | -| `lk.telemetry.dropped.throttled` | int | client | none | `{record}` | 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 | client | none | `{record}` | 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 | client | none | `{hold}` | Soft upload holds that reached their cap. Only when non-zero. | -| `lk.telemetry.uploads.bytes` | int | client | none | `By` | Compressed bytes the collector accepted. What telemetry cost the uplink. Always present on a report, even when 0. | -| `lk.telemetry.uploads.failed` | int | client | none | `{attempt}` | Upload attempts that failed transiently (no answer, 429, 5xx). Always present on a report, even when 0. | -| `lk.telemetry.uploads.sent` | int | client | none | `{batch}` | Batches the collector accepted. Always present on a report, even when 0. | -| `lk.telemetry.uploads.timeouts` | int | client | none | `{attempt}` | Upload attempts that hit the export timeout. Only when non-zero. | -| `lk.telemetry.uploads.unauthorized` | int | client | none | `{attempt}` | 401/403 answers (tokens the collector refused). Only when non-zero. | -| `lk.track.direction` | enum: `inbound`, `outbound` | client | none | | Whether the stats describe sent or received media. On RTC stats windows only. | -| `lk.track.kind` | enum: `audio`, `video` | client | none | | Media kind of the track. On publish and subscribe spans and RTC stats windows. | -| `lk.track.source` | enum: `camera`, `microphone`, `screen_share`, `screen_share_audio`, `unknown` | client | none | | Source of the track. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | -| `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. The client core sends `lk.track.sid` until the shared rename lands. | - -## Bindings - -Physical names of a concept in other producers and stores. Carried for reference; nothing validates them. - -| Key | Binding | 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` | -| `lk.track.source` | agents_otlp | `lk.track_source` | +# LiveKit telemetry semantic conventions + +The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. +Schema `https://livekit.io/schemas/telemetry/0.2.0`, 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) · [Open decisions](#open-decisions) + +## 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). + +**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: `lk.pii.participant_identity`, `lk.pii.remote_participant_identity`, `lk.pii.room_name`. + +**Units.** Durations are seconds (OTel), on a monotonic clock; timestamps are the record's own +OTLP time. One documented exception: RTC stats keep the W3C webrtc-stats milliseconds, in keys +ending in `_ms`: +`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 never set these, on their own events or as session attributes: +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 stay within these limits: +attributes per event 64, attributes per session 64, key bytes 128, name bytes 128, string value bytes 1024. +Anything over a limit is rejected, never truncated, and counted 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. The core writes both; a span attribute of either name set any other way is +dropped. Checkpoints are span events without attributes, named from a fixed list per span. Past a +span's limits, later checkpoints are dropped and counted in OTLP `dropped_events_count`; app +correlation attributes are dropped 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. Not sent by the client core yet (it sends `service.name = livekit-client-`); the vocabulary is open. | +| `lk.sdk.version` | string | Recommended | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. | +| `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 the core stamps 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`) are never 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. The client core sends `lk.participant.sid` until the shared rename lands. | +| `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. The client core sends `lk.participant.identity` until the shared rename lands. | +| `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. The client core sends `lk.room.name` until the shared rename lands, which the collector cannot redact. | +| `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. The client core sends `lk.room.sid` until the shared rename lands. | +| `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 the core copies from Rust (`lk.log.source = ffi`), JWT-shaped substrings are 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 @@ -115,261 +118,588 @@ Physical names of a concept in other producers and stores. Carried for reference Namespace of app-defined events. The core emits `custom.` with 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`), may not use the reserved keys, and stay within the limits; anything else is rejected, never truncated, and counted in `lk.telemetry.dropped.invalid`. An event's own attributes override the session's. +- Severity: `INFO` +- Body: The event name (`custom.`). + ### `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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.app_state` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.audio.interruption` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.audio_route.outputs` | required | -| `lk.device.audio_route.reason` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.battery.charging` | required | -| `lk.device.battery.level` | required | +- 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. SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. | +| `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. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | ### `lk.device.capture.failed` A capture device failed. All platforms. -| Attribute | Requirement | -| --- | --- | -| `lk.device.capture.device` | required | -| `lk.device.capture.reason` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.low_power.enabled` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.memory.pressure` | required | +- 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. iOS, macOS and Android; on the web, Chromium only. -| Attribute | Requirement | -| --- | --- | -| `lk.device.network.constrained` | required | -| `lk.device.network.expensive` | required | -| `network.connection.type` | required | +- Severity: `INFO` +- Body: Display line, e.g. `network: cell, expensive` (Rust core); the event name (JS port). + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `lk.device.network.constrained` | boolean | Required | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. | +| `lk.device.network.expensive` | boolean | Required | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. | +| `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.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. -| Attribute | Requirement | -| --- | --- | -| `lk.device.thermal.state` | required | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.ping.seq` | recommended | +- 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. -| Attribute | Requirement | -| --- | --- | -| `lk.disconnect.reason` | required | +- 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 the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | ### `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. -| Attribute | Requirement | -| --- | --- | -| `lk.rtc.audio_level.avg` | conditionally_required: Inbound audio. | -| `lk.rtc.audio_level.max` | conditionally_required: Inbound audio. | -| `lk.rtc.audio_level.min` | conditionally_required: Inbound audio. | -| `lk.rtc.bytes` | conditionally_required: When the stats report it. | -| `lk.rtc.codec` | conditionally_required: When known. | -| `lk.rtc.concealed_samples` | conditionally_required: Inbound audio. | -| `lk.rtc.concealment_events` | conditionally_required: Inbound audio. | -| `lk.rtc.fps.avg` | conditionally_required: Video. | -| `lk.rtc.fps.max` | conditionally_required: Video. | -| `lk.rtc.fps.min` | conditionally_required: Video. | -| `lk.rtc.freeze_count` | conditionally_required: Inbound video. | -| `lk.rtc.freezes_duration_ms` | conditionally_required: Inbound video. | -| `lk.rtc.interruption_count` | conditionally_required: Inbound audio. | -| `lk.rtc.interruptions_duration_ms` | conditionally_required: Inbound audio. | -| `lk.rtc.jitter_buffer_delay_ms` | conditionally_required: Inbound. | -| `lk.rtc.jitter_buffer_emitted_count` | conditionally_required: Inbound. | -| `lk.rtc.jitter_ms.avg` | conditionally_required: Inbound. | -| `lk.rtc.jitter_ms.max` | conditionally_required: Inbound. | -| `lk.rtc.jitter_ms.min` | conditionally_required: Inbound. | -| `lk.rtc.packets` | conditionally_required: When the stats report it. | -| `lk.rtc.packets_lost` | conditionally_required: Inbound. | -| `lk.rtc.pause_count` | conditionally_required: Inbound video. | -| `lk.rtc.pauses_duration_ms` | conditionally_required: Inbound video. | -| `lk.rtc.quality_limitation.bandwidth_ms` | conditionally_required: Outbound video. | -| `lk.rtc.quality_limitation.cpu_ms` | conditionally_required: Outbound video. | -| `lk.rtc.quality_limitation.other_ms` | conditionally_required: Outbound video. | -| `lk.rtc.rtt_ms.avg` | conditionally_required: When the window has readings. | -| `lk.rtc.rtt_ms.max` | conditionally_required: When the window has readings. | -| `lk.rtc.rtt_ms.min` | conditionally_required: When the window has readings. | -| `lk.rtc.samples` | required | -| `lk.rtc.silent_concealed_samples` | conditionally_required: Inbound audio. | -| `lk.rtc.window_ms` | required | -| `lk.track.direction` | required | -| `lk.track.kind` | required | -| `lk.track_sid` | required | +- 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. Denominator for the gauges' averages. | +| `lk.rtc.window_ms` | int, `ms` | Required | Actual length of the stats window. From the window's start to its close, on the core's clock (the JS port uses wall-clock time). | +| `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. The client core sends `lk.track.sid` until the shared rename lands. | +| `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 `lk.rtc.samples` readings. | +| `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 `lk.rtc.samples` readings. | +| `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 `lk.rtc.samples` readings. | +| `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 `lk.rtc.samples` readings. | +| `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. -| Attribute | Requirement | -| --- | --- | -| `lk.telemetry.cache.batches` | required | -| `lk.telemetry.cache.delete_errors` | conditionally_required: When non-zero (JS port only). | -| `lk.telemetry.cache.list_errors` | conditionally_required: When non-zero (JS port only). | -| `lk.telemetry.cache.read_errors` | conditionally_required: When non-zero (JS port only). | -| `lk.telemetry.cache.write_errors` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.cache_error` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.cache_full` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.corrupt` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.expired` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.invalid` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.oversized` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.queue_full` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.rate_limited` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.rejected` | conditionally_required: When non-zero. | -| `lk.telemetry.dropped.throttled` | conditionally_required: When non-zero (Rust core only). | -| `lk.telemetry.dropped.unattributed` | conditionally_required: When non-zero (JS port only). | -| `lk.telemetry.holds.capped` | conditionally_required: When non-zero. | -| `lk.telemetry.uploads.bytes` | required | -| `lk.telemetry.uploads.failed` | required | -| `lk.telemetry.uploads.sent` | required | -| `lk.telemetry.uploads.timeouts` | conditionally_required: When non-zero. | -| `lk.telemetry.uploads.unauthorized` | conditionally_required: When non-zero. | +- 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 -### `lk.connect` (client) +### `lk.connect` -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. +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 | Requirement | -| --- | --- | -| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | -| `lk.connect.attempt` | required | -| `lk.outcome` | required | +| 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `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`. Written only by the core, 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` -### `lk.publish` (internal) +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. -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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | +| `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. Written only by the core, 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. The client core sends `lk.track.sid` until the shared rename lands. | -| Attribute | Requirement | +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 `: a dynamic name (the Notion conventions keep dynamic data out of names; open decision), 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `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. Written only by the core, 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | +| `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. The client core sends `lk.track.sid` until the shared rename lands. | +| `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). Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends `lk.participant.remote_identity` until then. | + +| 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. + +## Attribute reference + +Every key this registry defines; 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 | Examples | Used on | +| --- | --- | --- | --- | --- | --- | +| `browser.name` | string | client | none | `Chrome`, `Safari`, `Firefox` | resource | +| `browser.version` | string | client | none | `124.0` | resource | +| `lk.connect.attempt` | int | client | none | `1` | `lk.connect` | +| `lk.device.app_state` | enum | client | none | | `lk.device.app_state.changed` | +| `lk.device.audio.interruption` | enum | client | none | | `lk.device.audio.interruption` | +| `lk.device.audio_route.outputs` | string | client | none | `bluetooth`, `speaker,usb` | `lk.device.audio_route.changed` | +| `lk.device.audio_route.reason` | enum | client | none | | `lk.device.audio_route.changed` | +| `lk.device.battery.charging` | boolean | client | none | `false` | `lk.device.battery.changed` | +| `lk.device.battery.level` | int, `%` | client | none | `42` | `lk.device.battery.changed` | +| `lk.device.capture.device` | enum | client | none | | `lk.device.capture.failed` | +| `lk.device.capture.reason` | enum | client | none | | `lk.device.capture.failed` | +| `lk.device.low_power.enabled` | boolean | client | none | `true` | `lk.device.low_power.changed` | +| `lk.device.memory.pressure` | enum | client | none | | `lk.device.memory.changed` | +| `lk.device.network.constrained` | boolean | client | none | `false` | `lk.device.network.changed` | +| `lk.device.network.expensive` | boolean | client | none | `false` | `lk.device.network.changed` | +| `lk.device.thermal.state` | enum | client | none | | `lk.device.thermal.changed` | +| `lk.disconnect.reason` | enum | client | none | | `lk.room.disconnected` | +| `lk.job_id` | string | shared | none | `AJ_kVbMBWtnVHvT` | | +| `lk.log.logger` | string | client | none | `Room`, `livekit::rtc_engine` | log | +| `lk.log.source` | enum | client | none | | log | +| `lk.outcome` | enum | client | none | | `lk.connect` `lk.publish` `lk.reconnect` `lk.subscribe` | +| `lk.participant_id` | string | shared | none | `PA_9kLm2NpQ4rTs` | record | +| `lk.pii.participant_identity` | string | shared | pii | `identity-0` | record | +| `lk.pii.remote_participant_identity` | string | shared | pii | `identity-1` | `lk.subscribe` | +| `lk.pii.room_name` | string | shared | pii | `my-room` | record | +| `lk.ping.seq` | int | client | none | `1` | `lk.ping` | +| `lk.reconnect.attempts` | int | client | none | `2` | `lk.reconnect` | +| `lk.reconnect.mode` | enum | client | none | | `lk.reconnect` | +| `lk.reconnect.reason` | enum | client | none | | `lk.reconnect` | +| `lk.room_sid` | string | shared | none | `RM_5Fq8xQ2ZbXwN` | record | +| `lk.rtc.audio_level.avg` | double, `1` | client | none | `0.05` | `lk.rtc.stats.sample` | +| `lk.rtc.audio_level.max` | double, `1` | client | none | `0.3` | `lk.rtc.stats.sample` | +| `lk.rtc.audio_level.min` | double, `1` | client | none | `0.0` | `lk.rtc.stats.sample` | +| `lk.rtc.bytes` | int, `By` | client | none | `1048576` | `lk.rtc.stats.sample` | +| `lk.rtc.codec` | string | client | none | `audio/opus`, `video/VP8` | `lk.rtc.stats.sample` | +| `lk.rtc.concealed_samples` | int, `{sample}` | client | none | `480` | `lk.rtc.stats.sample` | +| `lk.rtc.concealment_events` | int, `{event}` | client | none | `1` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.avg` | double, `{frame}/s` | client | none | `29.7` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.max` | double, `{frame}/s` | client | none | `30.0` | `lk.rtc.stats.sample` | +| `lk.rtc.fps.min` | double, `{frame}/s` | client | none | `24.0` | `lk.rtc.stats.sample` | +| `lk.rtc.freeze_count` | int, `{freeze}` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.freezes_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.interruption_count` | int, `{interruption}` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.interruptions_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_buffer_delay_ms` | int, `ms` | client | none | `1200` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_buffer_emitted_count` | int, `{sample}` | client | none | `48000` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.avg` | double, `ms` | client | none | `1.8` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.max` | double, `ms` | client | none | `4.2` | `lk.rtc.stats.sample` | +| `lk.rtc.jitter_ms.min` | double, `ms` | client | none | `0.5` | `lk.rtc.stats.sample` | +| `lk.rtc.packets` | int, `{packet}` | client | none | `3000` | `lk.rtc.stats.sample` | +| `lk.rtc.packets_lost` | int, `{packet}` | client | none | `3` | `lk.rtc.stats.sample` | +| `lk.rtc.pause_count` | int, `{pause}` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.pauses_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.bandwidth_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.cpu_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.quality_limitation.other_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.avg` | double, `ms` | client | none | `25.5` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.max` | double, `ms` | client | none | `80.0` | `lk.rtc.stats.sample` | +| `lk.rtc.rtt_ms.min` | double, `ms` | client | none | `12.0` | `lk.rtc.stats.sample` | +| `lk.rtc.samples` | int, `{reading}` | client | none | `2` | `lk.rtc.stats.sample` | +| `lk.rtc.silent_concealed_samples` | int, `{sample}` | client | none | `0` | `lk.rtc.stats.sample` | +| `lk.rtc.window_ms` | int, `ms` | client | none | `60000` | `lk.rtc.stats.sample` | +| `lk.sdk.name` | string | shared | none | `swift`, `android`, `flutter`, `react-native`, `unity`, `rust`, `js` | resource | +| `lk.sdk.version` | string | shared | none | `2.17.0` | resource | +| `lk.telemetry.cache.batches` | int, `{batch}` | client | none | `0` | `lk.telemetry.report` | +| `lk.telemetry.cache.delete_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.list_errors` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.read_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.cache.write_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.cache_error` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.cache_full` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.corrupt` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.expired` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.invalid` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.oversized` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.queue_full` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.rate_limited` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.rejected` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.throttled` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.dropped.unattributed` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.holds.capped` | int, `{hold}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.uploads.bytes` | int, `By` | client | none | `20480` | `lk.telemetry.report` | +| `lk.telemetry.uploads.failed` | int, `{attempt}` | client | none | `0` | `lk.telemetry.report` | +| `lk.telemetry.uploads.sent` | int, `{batch}` | client | none | `4` | `lk.telemetry.report` | +| `lk.telemetry.uploads.timeouts` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | +| `lk.telemetry.uploads.unauthorized` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | +| `lk.track.direction` | enum | client | none | | `lk.rtc.stats.sample` | +| `lk.track.kind` | enum | client | none | | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` | +| `lk.track.source` | enum | client | none | | `lk.publish` `lk.subscribe` | +| `lk.track_sid` | string | shared | none | `TR_AMkTyV3Zx8Qp` | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` | + +## Enum values + +### `lk.device.app_state` + +| Value | Meaning | | --- | --- | -| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | -| `lk.outcome` | required | -| `lk.track.kind` | required | -| `lk.track.source` | required | -| `lk.track_sid` | conditionally_required: On success; the server assigns it. | +| `foreground` | In front of the user. | +| `background` | In the background. | -### `lk.reconnect` (client) +### `lk.device.audio.interruption` -One reconnect cycle. `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint named `attempt `: a dynamic name (the Notion conventions keep dynamic data out of names; open decision), mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. +| Value | Meaning | +| --- | --- | +| `began` | Interruption began (another app or a call took the audio session). | +| `ended` | Interruption ended. | + +### `lk.device.audio_route.reason` -| Attribute | Requirement | +| 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 | | --- | --- | -| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | -| `lk.outcome` | required | -| `lk.reconnect.attempts` | conditionally_required: After the first attempt. | -| `lk.reconnect.mode` | conditionally_required: After the first attempt. | -| `lk.reconnect.reason` | required | +| `camera` | Camera. | +| `microphone` | Microphone. | +| `screen_share` | Screen capture. | -### `lk.subscribe` (internal) +### `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. | -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. +### `lk.device.memory.pressure` -| Attribute | Requirement | +| Value | Meaning | | --- | --- | -| `error.type` | conditionally_required: Only when `lk.outcome` is `error` and the failure has a type. | -| `lk.outcome` | required | -| `lk.pii.remote_participant_identity` | recommended | -| `lk.track.kind` | required | -| `lk.track.source` | required | -| `lk.track_sid` | required | +| `normal` | No pressure. | +| `warning` | Low memory. | +| `critical` | Critically low memory. | -## Attribute sets +### `lk.device.thermal.state` -### `livekit.client.log` +| Value | Meaning | +| --- | --- | +| `nominal` | Within normal limits. | +| `fair` | Slightly elevated. | +| `serious` | High; the system reduces performance. | +| `critical` | Critical; the device needs to cool down. | -Attributes of a plain log record (no event name). +### `lk.disconnect.reason` -| Attribute | Requirement | +| Value | Meaning | +| --- | --- | +| `client_initiated` | The client hung up (CLIENT_INITIATED). | +| `duplicate_identity` | Another participant joined with the same identity (DUPLICATE_IDENTITY). | +| `server_shutdown` | The server instance shut down (SERVER_SHUTDOWN). | +| `participant_removed` | RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). | +| `room_deleted` | RoomService.DeleteRoom was called (ROOM_DELETED). | +| `state_mismatch` | The server did not know the session the client tried to resume (STATE_MISMATCH). | +| `join_failure` | The client could not connect fully (JOIN_FAILURE). | +| `migration` | The server asked the participant to migrate (MIGRATION, Cloud only). | +| `signal_close` | The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). | +| `room_closed` | The room closed after its participants left (ROOM_CLOSED). | +| `user_unavailable` | The SIP callee did not answer in time (USER_UNAVAILABLE). | +| `user_rejected` | The SIP callee rejected the call (USER_REJECTED). | +| `sip_trunk_failure` | SIP protocol failure (SIP_TRUNK_FAILURE). | +| `connection_timeout` | The server timed out the session (CONNECTION_TIMEOUT). | +| `media_failure` | Media failed or timed out (MEDIA_FAILURE). | +| `agent_error` | The agent failed (AGENT_ERROR). | +| `reconnect_failed` | The client ran out of reconnect attempts (client only). | +| `unknown` | UNKNOWN_REASON or a number the client does not know. | + +### `lk.log.source` + +| Value | Meaning | | --- | --- | -| `code.file.path` | recommended | -| `code.function.name` | recommended | -| `code.line.number` | recommended | -| `lk.log.logger` | required | -| `lk.log.source` | required | +| `sdk` | The platform SDK. | +| `ffi` | The Rust core, copied by the log forwarder with JWT-shaped substrings masked as ``. | +| `webrtc` | WebRTC. | + +### `lk.outcome` -### `livekit.client.record` +| 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. | -Attributes the core stamps on every log record and span of a session. +### `lk.reconnect.mode` -| Attribute | Requirement | +| Value | Meaning | | --- | --- | -| `lk.participant_id` | conditionally_required: Once the client has joined the room. | -| `lk.pii.participant_identity` | conditionally_required: Once the client has joined the room. | -| `lk.pii.room_name` | conditionally_required: Once the client has joined the room. | -| `lk.room_sid` | conditionally_required: Once the client has joined the room. | -| `otel.event.name` | conditionally_required: On event records; duplicates the record's `event_name`. | -| `session.id` | required | +| `quick` | Resume the session (ICE restart). | +| `full` | Reconnect from scratch. | -### `livekit.client.resource` +### `lk.reconnect.reason` -Resource attributes of the client pipeline, one Resource per process. +| 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). | -| Attribute | Requirement | +### `lk.track.kind` + +| Value | Meaning | | --- | --- | -| `browser.name` | conditionally_required: The JS SDK running in a browser. | -| `browser.version` | conditionally_required: The JS SDK running in a browser. | -| `device.model.identifier` | recommended | -| `lk.sdk.name` | recommended | -| `lk.sdk.version` | recommended | -| `os.name` | recommended | -| `os.version` | recommended | -| `service.name` | required | -| `service.version` | required | -| `telemetry.sdk.language` | required | -| `telemetry.sdk.name` | required | -| `telemetry.sdk.version` | required | +| `audio` | Audio. | +| `video` | Video. | + +### `lk.track.source` + +| Value | Meaning | +| --- | --- | +| `camera` | Camera (TrackSource CAMERA). | +| `microphone` | Microphone (TrackSource MICROPHONE). | +| `screen_share` | Screen share video (TrackSource SCREEN_SHARE). | +| `screen_share_audio` | Screen share audio (TrackSource SCREEN_SHARE_AUDIO). | +| `unknown` | Unknown source (TrackSource UNKNOWN). | + +## Bindings + +The physical name of a concept in other producers and stores. Carried for reference, to be +confirmed by each owner; nothing validates it. + +| Key | Producer or store | Name there | +| --- | --- | --- | +| `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` | +| `lk.track.source` | agents_otlp | `lk.track_source` | + +`agents_otlp` lists Agents' span key first, then its Resource and metric-point key. +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID` only, so no +client key fills it today. + +## Open decisions + +Naming questions this registry records without resolving them. Current names stay until each is +decided. + +1. **`lk.pii.remote_participant_identity`** is a proposed name for the remote participant's + identity on subscribe spans; the Notion conventions name no key for it. +2. **`lk.room_sid` and the collector's `room_id` column.** The rename alone leaves the client's + `room_id` column empty: either clients also send `room_id`, or ingest falls back to + `lk.room_sid`. +3. **`lk.client.*` / `client.*` namespaces.** The Notion conventions put client-owned attributes + under `lk.client.*` and client event names under `client.*`; no client key or event does yet. +4. **RTC units.** The `lk.rtc.*` `_ms` keys keep milliseconds through a documented exception; the + Notion conventions ask for seconds. +5. **Battery.** Clients send `lk.device.battery.level` (percent) and `lk.device.battery.charging`; + SPEC.md specifies OTel `hw.battery.charge` (0-1) and `hw.battery.state`. +6. **`lk.outcome`** (`ok`, `error`, `cancelled`, on every span) vs the Notion + `lk.client.connection.result` (`success`, `failure`, `cancelled`, on connects). +7. **`lk.connect.attempt`** vs the Notion `lk.client.connect_attempt`, plus the missing + `lk.client.connect_id`. +8. **`attempt `** puts dynamic data in a checkpoint name. +9. **`session.id`** is the client's session key; the Notion conventions want no separate client + session id. +10. **`otel.event.name`** duplicates `event_name` on every client event. +11. **`network.connection.type`** gets `vpn`, `bluetooth` and `other` from clients, which are not + OTel members. +12. **Track source vocabulary:** clients send `lk.track.source = microphone`, Agents + `lk.track_source = SOURCE_MICROPHONE`. +13. **`lk.sdk.name` values** are not fixed, and no client sends `lk.sdk.*` yet. +14. **`custom`** is registered as an event name because the registry has no wildcard events; the + events on the wire are `custom.`. +15. **Schema URL.** `https://livekit.io/schemas/telemetry/...` does not resolve yet. +16. **`browser.name` / `browser.version`** (JS port) are not OTel keys and sit in OTel's + `browser.*` namespace; OTel names them `user_agent.name` / `user_agent.version`. +17. **Event bodies.** The Rust core sends display lines, the JS port the event name. \ No newline at end of file diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 6c8fd6429..9950bf416 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -4,6 +4,12 @@ /// Telemetry schema version; set as `schema_url` on every OTLP scope. pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.2.0"; +/// Browser name. +pub const BROWSER_NAME: &str = "browser.name"; + +/// Browser version. +pub const BROWSER_VERSION: &str = "browser.version"; + /// 1-based connect attempt. pub const LK_CONNECT_ATTEMPT: &str = "lk.connect.attempt"; @@ -511,3 +517,32 @@ pub mod span { /// One subscription, from the intent to subscribe to the first media. Kind: internal. pub const LK_SUBSCRIBE: &str = "lk.subscribe"; } + +/// Checkpoint names: span events of the spans above. `lk.reconnect` names its attempts +/// `attempt ` instead. +pub mod checkpoint { + /// 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 first inbound stats reading with bytes; the span ends ok here. + pub const FIRST_MEDIA: &str = "first_media"; + /// 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 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"; + /// 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 platform reported the remote track subscribed. + pub const SUBSCRIBED: &str = "subscribed"; + /// The signaling WebSocket opened. + pub const WS_OPEN: &str = "ws_open"; +} diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 2eeed4ea3..d4cf0697b 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -3,6 +3,12 @@ /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.2.0" as const; +/** Browser name. */ +export const ATTR_BROWSER_NAME = "browser.name" as const; + +/** Browser version. */ +export const ATTR_BROWSER_VERSION = "browser.version" as const; + /** 1-based connect attempt. */ export const ATTR_LK_CONNECT_ATTEMPT = "lk.connect.attempt" as const; @@ -444,3 +450,28 @@ export const SPAN_LK_PUBLISH = "lk.publish" as const; 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; + +/** 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 first inbound stats reading with bytes; the span ends ok here. */ +export const CHECKPOINT_FIRST_MEDIA = "first_media" 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 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): 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 platform reported the remote track subscribed. */ +export const CHECKPOINT_SUBSCRIBED = "subscribed" as const; +/** Checkpoint (span event): The signaling WebSocket opened. */ +export const CHECKPOINT_WS_OPEN = "ws_open" as const; diff --git a/otel/model/client/groups.yaml b/otel/model/client/groups.yaml index 559437490..e326c91ab 100644 --- a/otel/model/client/groups.yaml +++ b/otel/model/client/groups.yaml @@ -98,10 +98,13 @@ attribute_groups: 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/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index b605d2e63..fc907a117 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -1,49 +1,201 @@ +{#- 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 -%}{{ a.requirement_level }}{%- else -%}{% for k, v in a.requirement_level | items %}{{ k }}: {{ v | trim }}{% endfor %}{%- endif -%} +{%- 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 table(attrs) -%} -| Attribute | Requirement | -| --- | --- | -{% for a in attrs %}| `{{ a.key }}` | {{ req(a) }} | +{%- 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 conventions +# LiveKit telemetry semantic conventions -Schema: `{{ ctx.schema_url }}`. Upstream OTel keys (OTel semconv v1.44.0) are referenced, not listed here. +The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. +Schema `{{ ctx.schema_url }}`, 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. -## Attributes +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. -| Key | Type | Owner | Sensitivity | Unit | Description | -| --- | --- | --- | --- | --- | --- | -{% for a in ctx.attributes %}| `{{ a.key }}` | {% if a.type is mapping %}enum: {% for m in a.type.members %}`{{ m.value | replace("\r", " ") | replace("\n", " ") | replace("|", "\\|") }}`{% if not loop.last %}, {% endif %}{% endfor %}{% else %}{{ a.type }}{% endif %} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% if a.annotations.livekit.unit %}`{{ a.annotations.livekit.unit }}`{% if a.annotations.livekit.unit_exception %} (exception: {{ a.annotations.livekit.unit_exception }}){% endif %}{% endif %} | {{ a.brief | trim | replace("\n", " ") | replace("|", "\\|") }}{% if a.note %} {{ a.note | trim | replace("\n", " ") | replace("|", "\\|") }}{% endif %} | -{% endfor %} -## Bindings +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) · [Open decisions](#open-decisions) -Physical names of a concept in other producers and stores. Carried for reference; nothing validates them. +## Conventions -| Key | Binding | Name | -| --- | --- | --- | -{% 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 %} +**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). + +**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: RTC stats keep the W3C webrtc-stats milliseconds, in keys +ending in `_ms`: +{% for a in ctx.attributes | selectattr("annotations.livekit.unit_exception") %}`{{ a.key }}`{% if not loop.last %}, {% endif %}{% endfor %}. + +**Keys the SDK owns.** Apps never set these, on their own events or as session attributes: +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 stay within these limits: +{% for k, v in custom.annotations.livekit.limits | items %}{{ k | replace("_", " ") }} {{ v }}{% if not loop.last %}, {% endif %}{% endfor %}. +Anything over a limit is rejected, never truncated, and counted 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. The core writes both; a span attribute of either name set any other way is +dropped. Checkpoints are span events without attributes, named from a fixed list per span. Past a +span's limits, later checkpoints are dropped and counted in OTLP `dropped_events_count`; app +correlation attributes are dropped 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 %} -{{ table(e.attributes) }}{% endif %}{% endfor %} +{{ attr_table(e.attributes) }}{% endif %}{% endfor %} ## Spans -{% for s in ctx.spans %} -### `{{ s.type }}` ({{ s.kind }}) +{% for s in ctx.spans %}{% set lk = s.annotations.livekit %} +### `{{ s.type }}` + +Kind `{{ s.kind }}`. {{ s.brief | trim }}{% if s.note %} {{ s.note | trim }}{% endif %} -{{ s.brief | trim }}{% if s.note %} {{ s.note | trim | replace("\n", " ") }}{% 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 %} +Limits: {% for k, v in lk.limits | items %}{{ v }} {{ k | replace("_", " ") }}{% if not loop.last %}, {% endif %}{% endfor %} per span. +{% endfor %} +## Attribute reference + +Every key this registry defines; 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 | Examples | Used on | +| --- | --- | --- | --- | --- | --- | +{% for a in ctx.attributes %}| `{{ a.key }}` | {{ ty(a) }} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% 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, to be +confirmed by each owner; 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. +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID` only, so no +client key fills it today. -{{ table(s.attributes) }}{% endfor %} -## Attribute sets -{% for g in ctx.groups %} -### `{{ g.id }}` +## Open decisions -{{ g.brief | trim }} +Naming questions this registry records without resolving them. Current names stay until each is +decided. -{{ table(g.attributes) }}{% endfor %} +1. **`lk.pii.remote_participant_identity`** is a proposed name for the remote participant's + identity on subscribe spans; the Notion conventions name no key for it. +2. **`lk.room_sid` and the collector's `room_id` column.** The rename alone leaves the client's + `room_id` column empty: either clients also send `room_id`, or ingest falls back to + `lk.room_sid`. +3. **`lk.client.*` / `client.*` namespaces.** The Notion conventions put client-owned attributes + under `lk.client.*` and client event names under `client.*`; no client key or event does yet. +4. **RTC units.** The `lk.rtc.*` `_ms` keys keep milliseconds through a documented exception; the + Notion conventions ask for seconds. +5. **Battery.** Clients send `lk.device.battery.level` (percent) and `lk.device.battery.charging`; + SPEC.md specifies OTel `hw.battery.charge` (0-1) and `hw.battery.state`. +6. **`lk.outcome`** (`ok`, `error`, `cancelled`, on every span) vs the Notion + `lk.client.connection.result` (`success`, `failure`, `cancelled`, on connects). +7. **`lk.connect.attempt`** vs the Notion `lk.client.connect_attempt`, plus the missing + `lk.client.connect_id`. +8. **`attempt `** puts dynamic data in a checkpoint name. +9. **`session.id`** is the client's session key; the Notion conventions want no separate client + session id. +10. **`otel.event.name`** duplicates `event_name` on every client event. +11. **`network.connection.type`** gets `vpn`, `bluetooth` and `other` from clients, which are not + OTel members. +12. **Track source vocabulary:** clients send `lk.track.source = microphone`, Agents + `lk.track_source = SOURCE_MICROPHONE`. +13. **`lk.sdk.name` values** are not fixed, and no client sends `lk.sdk.*` yet. +14. **`custom`** is registered as an event name because the registry has no wildcard events; the + events on the wire are `custom.`. +15. **Schema URL.** `https://livekit.io/schemas/telemetry/...` does not resolve yet. +16. **`browser.name` / `browser.version`** (JS port) are not OTel keys and sit in OTel's + `browser.*` namespace; OTel names them `user_agent.name` / `user_agent.version`. +17. **Event bodies.** The Rust core sends display lines, the JS port the event name. diff --git a/otel/templates/registry/markdown/weaver.yaml b/otel/templates/registry/markdown/weaver.yaml index 3a461601b..3fb8f61da 100644 --- a/otel/templates/registry/markdown/weaver.yaml +++ b/otel/templates/registry/markdown/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.md.j2 - filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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)}' + filter: '{schema_url, 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/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 index f96231ec2..efe079a28 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -36,6 +36,13 @@ 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 { diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml index 43b217188..aa8cb61d2 100644 --- a/otel/templates/registry/rust/weaver.yaml +++ b/otel/templates/registry/rust/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.rs.j2 - filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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)}' + filter: '{schema_url, 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.rs diff --git a/otel/templates/registry/typescript/semconv.ts.j2 b/otel/templates/registry/typescript/semconv.ts.j2 index 59b2102a8..7c20b6271 100644 --- a/otel/templates/registry/typescript/semconv.ts.j2 +++ b/otel/templates/registry/typescript/semconv.ts.j2 @@ -21,6 +21,9 @@ export const EVENT_{{ e.name | screaming_snake_case }} = {{ e.name | tojson }} a {% 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; diff --git a/otel/templates/registry/typescript/weaver.yaml b/otel/templates/registry/typescript/weaver.yaml index 85cfa40eb..69823d5be 100644 --- a/otel/templates/registry/typescript/weaver.yaml +++ b/otel/templates/registry/typescript/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.ts.j2 - filter: '{schema_url, attributes: [.registry.attributes[] | select(.key | startswith("lk."))] | 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)}' + filter: '{schema_url, 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 From a65ef502582863738931b4d352b690f1902392f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:34:31 +0200 Subject: [PATCH 12/36] feat(otel): check severities, checkpoints and span outcomes in live-check The event and span annotations are now enforced on matched samples: livekit_severity flags an event sent at a severity it is not declared with, livekit_checkpoint a span event its span does not list (or that misses the attempt pattern), and livekit_span_outcome a span whose status disagrees with lk.outcome or that has error.type without an error outcome. The live-check test plants one of each. The reference session still reports only the six identity/PII renames. --- otel/README.md | 11 +++++++---- otel/live-check/livekit.rego | 37 ++++++++++++++++++++++++++++++++++++ otel/tests/live-check.sh | 16 +++++++++++----- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/otel/README.md b/otel/README.md index b1b006f05..26405b20a 100644 --- a/otel/README.md +++ b/otel/README.md @@ -68,7 +68,8 @@ nothing validates it. | `bindings` | map of producer/store → name | informational | | `cardinality_exception` | `true`, on a metric | allows a high-cardinality ID as a metric dimension | - Events and spans carry their own `annotations.livekit`, rendered in the docs: + Events and spans carry their own `annotations.livekit`, rendered in the docs; live-check + enforces `severity`, `checkpoints` and `checkpoint_pattern`: | Annotation | On | Meaning | |---|---|---| @@ -156,7 +157,10 @@ record against `model/client/groups.yaml`. It drops two kinds of finding: `not_s is `development`) and absent `conditionally_required` attributes (their conditions are prose). `live-check/livekit.rego` adds `livekit_flat_identity` and `livekit_pii_unmarked`, each naming the key to send instead, and `livekit_unknown_span` for an `lk.*` span no matcher places (weaver -alone reports nothing for an unmatched span that has no attributes). +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 expected shared identity / PII renames: @@ -179,8 +183,7 @@ Known limits: `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`. -- Span event names (connect checkpoints, `attempt `) are not checked; weaver compares - span events only by attributes. +- 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. diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index 0807e5466..eb8f0cc27 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -51,3 +51,40 @@ deny contains lk_advice("livekit_unknown_span", {"span_name": name}, sprintf("sp 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 its checkpoint pattern. +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 regex.match(object.get(input, ["registry_group", "annotations", "livekit", "checkpoint_pattern", "pattern"], "^$"), name) +} + +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 + not log.severity_text in declared("severity") +} + +# Status follows lk.outcome (Error only for `error`), and error.type comes only with `error`. +deny contains lk_advice("livekit_span_outcome", {"span_name": span.name, "outcome": outcome, "status": span.status.code}, sprintf("span '%s': lk.outcome=%s but status=%s", [span.name, outcome, span.status.code])) if { + span := input.sample.span + startswith(span.name, "lk.") + some outcome in span_attr("lk.outcome") + (outcome == "error") != (span.status.code == "error") +} + +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/tests/live-check.sh b/otel/tests/live-check.sh index f38632caf..0e6eb7bda 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,7 +1,8 @@ #!/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; the same smoke sample with the client's current -# identity/PII keys and an unregistered lk.* span must be flagged. +# findings, and so must a sparse RTC window. The same smoke sample with the client's current +# identity/PII keys, an unregistered lk.* span, an unlisted checkpoint, an outcome that disagrees +# with the span status and an event at the wrong severity must be flagged. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -16,10 +17,15 @@ echo "$sparse" [[ $sparse == *'"advice_level_counts": {}'* ]] || { echo "FAIL sparse RTC sample has findings" >&2; exit 1; } 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"/' tests/live-sample.json > "$tmp/legacy.json" + -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"/' \ + 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_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name" "livekit_unknown_span: lk.bogus"; do +for want in "livekit_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name" "livekit_unknown_span: lk.bogus" \ + "livekit_checkpoint: lk.connect" "livekit_span_outcome: lk.connect" "livekit_severity: lk.device.network.changed"; do [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } done -echo "PASS live-check: smoke and sparse samples clean, legacy keys and unknown span flagged" +echo "PASS live-check: smoke and sparse samples clean, every planted violation flagged" From 4abc50181d697465b8e169fe4877bb167d478670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:41:36 +0200 Subject: [PATCH 13/36] docs(otel): state the conventions as shipped, without open questions The registry and the generated spec now read as the conventions as they are: the TBD, open-decision and pending-rename wording is gone from the model notes, the advice and live-check rules, the README and the docs, and the spec drops its Open decisions section. The names and choices stay as they were; the questions behind them are kept out of the spec. Schema 0.2.1 (documentation-only model change). --- otel/README.md | 4 +- otel/advice/livekit.rego | 6 +- otel/gen/docs/semconv.md | 82 +++++-------------- otel/gen/rust/semconv.rs | 2 +- otel/gen/ts/semconv.ts | 2 +- otel/live-check/livekit.rego | 2 +- otel/model/client/registry.yaml | 12 ++- otel/model/client/signals.yaml | 3 +- otel/model/manifest.yaml | 2 +- otel/model/shared/registry.yaml | 26 +++--- .../templates/registry/markdown/semconv.md.j2 | 46 +---------- 11 files changed, 51 insertions(+), 136 deletions(-) diff --git a/otel/README.md b/otel/README.md index 26405b20a..acb8f60d4 100644 --- a/otel/README.md +++ b/otel/README.md @@ -30,7 +30,7 @@ policy, storage and the app API are not conventions and live with each SDK. 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, bindings, and the open naming decisions. +requirement table, enum values and bindings. ## Ownership @@ -172,7 +172,7 @@ On the reference session (Swift 2.17.0 with the core at rust-sdks `a9ec8d52`: 6 | `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_pii_unmarked` ×10 each | | `lk.participant.identity` | `lk.pii.participant_identity` | same, ×10 each | -| `lk.participant.remote_identity` | `lk.pii.remote_participant_identity` (name TBD) | same, ×2 each, plus `recommended_attribute_not_present` ×2 | +| `lk.participant.remote_identity` | `lk.pii.remote_participant_identity` | same, ×2 each, plus `recommended_attribute_not_present` ×2 | With those six keys renamed in the same capture, live-check reports nothing and exits 0. `tests/live-sample.json` is part of that capture, anonymized. diff --git a/otel/advice/livekit.rego b/otel/advice/livekit.rego index 886ed997b..af103c87a 100644 --- a/otel/advice/livekit.rego +++ b/otel/advice/livekit.rego @@ -19,9 +19,9 @@ lk_attrs contains object.union(attr, {"id": attr.key}) if { not attr.deprecated } -# The Notion conventions put client-owned attributes under lk.client.*. The client core ships -# its current names until that decision is made; this lists what the rule would move. -deny contains {"id": "livekit_client_namespace", "level": "improvement", "message": sprintf("%s: client-owned; the Notion convention would move it under lk.client.*", [attr.id])} if { +# 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/gen/docs/semconv.md b/otel/gen/docs/semconv.md index cd9dd1110..140486624 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.2.0`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.2.1`, 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. @@ -13,8 +13,7 @@ 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) · [Open decisions](#open-decisions) +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) ## Conventions @@ -82,8 +81,8 @@ Resource attributes of the client pipeline, one Resource per process. Set once p | `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. Not sent by the client core yet (it sends `service.name = livekit-client-`); the vocabulary is open. | -| `lk.sdk.version` | string | Recommended | LiveKit SDK version. Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. | +| `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. | @@ -94,10 +93,10 @@ Attributes the core stamps on every log record and span of a session. One sessio | 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. The client core sends `lk.participant.sid` until the shared rename lands. | -| `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. The client core sends `lk.participant.identity` until the shared rename lands. | -| `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. The client core sends `lk.room.name` until the shared rename lands, which the collector cannot redact. | -| `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. The client core sends `lk.room.sid` until the shared rename lands. | +| `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 @@ -164,8 +163,8 @@ Charging changed, or the level crossed 20 % or 10 % unplugged. Never per percent | Attribute | Type | Requirement | Description | | --- | --- | --- | --- | -| `lk.device.battery.charging` | boolean | Required | The device is charging. SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. | -| `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. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this key. Open decision. | +| `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` @@ -260,7 +259,7 @@ One RTC stats window for one track and direction. One per track and direction pe | `lk.rtc.window_ms` | int, `ms` | Required | Actual length of the stats window. From the window's start to its close, on the core's clock (the JS port uses wall-clock time). | | `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. The client core sends `lk.track.sid` until the shared rename lands. | +| `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 `lk.rtc.samples` readings. | | `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`. | @@ -333,7 +332,7 @@ Kind `client`. One connect attempt. Dashboards compute connect phases only from | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. 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`. Written only by the core, at most once. | | Checkpoint | Requirement | Meaning | @@ -357,11 +356,11 @@ Kind `internal`. One track publish. A child of the ambient span when there is on | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. 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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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. Written only by the core, 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. The client core sends `lk.track.sid` until the shared rename lands. | +| `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. @@ -369,11 +368,11 @@ 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 `: a dynamic name (the Notion conventions keep dynamic data out of names; open decision), mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. +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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. 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. Written only by the core, 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. | @@ -391,12 +390,12 @@ Kind `internal`. One subscription, from the intent to subscribe to the first med | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. | +| `lk.outcome` | enum | Required | How the attempt ended. Rollups read this, never the span status. Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. 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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | -| `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. The client core sends `lk.track.sid` until the shared rename lands. | +| `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. | | `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). Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends `lk.participant.remote_identity` until then. | +| `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 | | --- | --- | --- | @@ -652,8 +651,8 @@ attribute sets, events and spans that reference the key. ## Bindings -The physical name of a concept in other producers and stores. Carried for reference, to be -confirmed by each owner; nothing validates it. +The physical name of a concept in other producers and stores. Carried for reference; nothing +validates it. | Key | Producer or store | Name there | | --- | --- | --- | @@ -665,41 +664,4 @@ confirmed by each owner; nothing validates it. | `lk.track.source` | agents_otlp | `lk.track_source` | `agents_otlp` lists Agents' span key first, then its Resource and metric-point key. -`collector_column: room_id` is materialized from the attributes `room_id` and `roomID` only, so no -client key fills it today. - -## Open decisions - -Naming questions this registry records without resolving them. Current names stay until each is -decided. - -1. **`lk.pii.remote_participant_identity`** is a proposed name for the remote participant's - identity on subscribe spans; the Notion conventions name no key for it. -2. **`lk.room_sid` and the collector's `room_id` column.** The rename alone leaves the client's - `room_id` column empty: either clients also send `room_id`, or ingest falls back to - `lk.room_sid`. -3. **`lk.client.*` / `client.*` namespaces.** The Notion conventions put client-owned attributes - under `lk.client.*` and client event names under `client.*`; no client key or event does yet. -4. **RTC units.** The `lk.rtc.*` `_ms` keys keep milliseconds through a documented exception; the - Notion conventions ask for seconds. -5. **Battery.** Clients send `lk.device.battery.level` (percent) and `lk.device.battery.charging`; - SPEC.md specifies OTel `hw.battery.charge` (0-1) and `hw.battery.state`. -6. **`lk.outcome`** (`ok`, `error`, `cancelled`, on every span) vs the Notion - `lk.client.connection.result` (`success`, `failure`, `cancelled`, on connects). -7. **`lk.connect.attempt`** vs the Notion `lk.client.connect_attempt`, plus the missing - `lk.client.connect_id`. -8. **`attempt `** puts dynamic data in a checkpoint name. -9. **`session.id`** is the client's session key; the Notion conventions want no separate client - session id. -10. **`otel.event.name`** duplicates `event_name` on every client event. -11. **`network.connection.type`** gets `vpn`, `bluetooth` and `other` from clients, which are not - OTel members. -12. **Track source vocabulary:** clients send `lk.track.source = microphone`, Agents - `lk.track_source = SOURCE_MICROPHONE`. -13. **`lk.sdk.name` values** are not fixed, and no client sends `lk.sdk.*` yet. -14. **`custom`** is registered as an event name because the registry has no wildcard events; the - events on the wire are `custom.`. -15. **Schema URL.** `https://livekit.io/schemas/telemetry/...` does not resolve yet. -16. **`browser.name` / `browser.version`** (JS port) are not OTel keys and sit in OTel's - `browser.*` namespace; OTel names them `user_agent.name` / `user_agent.version`. -17. **Event bodies.** The Rust core sends display lines, the JS port the event name. \ No newline at end of file +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID`. \ No newline at end of file diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 9950bf416..0beca88f7 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.2.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.2.1"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index d4cf0697b..2b95bc152 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.2.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.2.1" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index eb8f0cc27..d78d70f4c 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -20,7 +20,7 @@ canonical_identity := { canonical_pii := { "lk.room.name": "lk.pii.room_name", "lk.participant.identity": "lk.pii.participant_identity", - "lk.participant.remote_identity": "lk.pii.remote_participant_identity", # name TBD + "lk.participant.remote_identity": "lk.pii.remote_participant_identity", } lk_advice(type, ctx, msg) := { diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml index 337542039..9a985331e 100644 --- a/otel/model/client/registry.yaml +++ b/otel/model/client/registry.yaml @@ -80,8 +80,7 @@ groups: 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. SPEC.md specifies OTel `hw.battery.charge` (ratio 0-1) instead; the code ships this - key. Open decision. + port. An integer percentage, not a 0-1 ratio. examples: [42] annotations: livekit: {owner: client, sensitivity: none, unit: "%"} @@ -89,7 +88,7 @@ groups: type: boolean stability: development brief: The device is charging. - note: SPEC.md specifies OTel `hw.battery.state` instead; the code ships this key. Open decision. + note: Sent together with `lk.device.battery.level`. examples: [false] annotations: livekit: {owner: client, sensitivity: none} @@ -482,8 +481,7 @@ groups: note: > Exactly once on every LiveKit span, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for - `error`, `Unset` for `ok` and `cancelled`. The Notion conventions name - `lk.client.connection.result` (success, failure, cancelled) for connects only. Open decision. + `error`, `Unset` for `ok` and `cancelled`. type: members: - {id: ok, value: ok, brief: Succeeded; span status Unset., stability: development} @@ -790,8 +788,8 @@ groups: 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. Not OTel keys: OTel 1.44.0 names this `user_agent.name` / - `user_agent.version`, and owns the `browser.*` namespace. Open decision. + supplies its own identity. LiveKit-defined keys: OTel 1.44.0 defines no `browser.name` or + `browser.version`. attributes: - id: browser.name type: string diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml index ae8fa7005..3ea0bb863 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -372,8 +372,7 @@ groups: brief: One reconnect cycle. note: > `cancelled` when `disconnect()` or a newer reconnect wins. Each attempt is a checkpoint - named `attempt `: a dynamic name (the Notion conventions keep dynamic data out of - names; open decision), mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. + named `attempt `, mirrored by `lk.reconnect.attempts` and `lk.reconnect.mode`. annotations: livekit: checkpoints: [] diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index 467705ca7..474cf4d37 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.2.0 +schema_url: https://livekit.io/schemas/telemetry/0.2.1 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned signals). dependencies: diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml index f3a90d2a5..de30911a3 100644 --- a/otel/model/shared/registry.yaml +++ b/otel/model/shared/registry.yaml @@ -16,8 +16,7 @@ groups: 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. The client core sends `lk.room.sid` - until the shared rename lands. + High cardinality, never a default metric dimension. examples: ["RM_5Fq8xQ2ZbXwN"] annotations: livekit: @@ -28,15 +27,14 @@ groups: 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 # TBC by Observability + 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. The client core sends `lk.participant.sid` until the - shared rename lands. + never a default metric dimension. examples: ["PA_9kLm2NpQ4rTs"] annotations: livekit: {owner: shared, sensitivity: none, authority: cloud} @@ -46,8 +44,7 @@ groups: 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. The client core sends - `lk.track.sid` until the shared rename lands. + success. High cardinality, never a default metric dimension. examples: ["TR_AMkTyV3Zx8Qp"] annotations: livekit: {owner: shared, sensitivity: none, authority: cloud} @@ -84,8 +81,7 @@ groups: stability: development brief: Customer-supplied room name. note: > - On every record and span of a session once the client has joined. The client core sends - `lk.room.name` until the shared rename lands, which the collector cannot redact. + On every record and span of a session once the client has joined. examples: ["my-room"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -94,8 +90,7 @@ groups: stability: development brief: Customer-supplied identity of the local participant. note: > - On every record and span of a session once the client has joined. The client core sends - `lk.participant.identity` until the shared rename lands. + On every record and span of a session once the client has joined. examples: ["identity-0"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -104,8 +99,7 @@ groups: stability: development brief: Customer-supplied identity of the remote participant a record is about (the publisher, on subscribe). note: > - Name TBD, needs sign-off; the Notion conventions name no key for it. The client core sends - `lk.participant.remote_identity` until then. + On subscribe spans: the identity of the participant publishing the track. examples: ["identity-1"] annotations: livekit: {owner: shared, sensitivity: pii} @@ -123,8 +117,8 @@ groups: stability: development brief: LiveKit SDK name. note: > - Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet - (it sends `service.name = livekit-client-`); the vocabulary is open. + 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} @@ -132,7 +126,7 @@ groups: type: string stability: development brief: LiveKit SDK version. - note: Resource attribute. A low-cardinality metric dimension. Not sent by the client core yet. + note: Resource attribute. A low-cardinality metric dimension. examples: ["2.17.0"] annotations: livekit: {owner: shared, sensitivity: none} diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index fc907a117..a559a4717 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -39,8 +39,7 @@ 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) · [Open decisions](#open-decisions) +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) ## Conventions @@ -153,49 +152,12 @@ attribute sets, events and spans that reference the key. {% endfor %}{% endif %}{% endfor %} ## Bindings -The physical name of a concept in other producers and stores. Carried for reference, to be -confirmed by each owner; nothing validates it. +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. -`collector_column: room_id` is materialized from the attributes `room_id` and `roomID` only, so no -client key fills it today. - -## Open decisions - -Naming questions this registry records without resolving them. Current names stay until each is -decided. - -1. **`lk.pii.remote_participant_identity`** is a proposed name for the remote participant's - identity on subscribe spans; the Notion conventions name no key for it. -2. **`lk.room_sid` and the collector's `room_id` column.** The rename alone leaves the client's - `room_id` column empty: either clients also send `room_id`, or ingest falls back to - `lk.room_sid`. -3. **`lk.client.*` / `client.*` namespaces.** The Notion conventions put client-owned attributes - under `lk.client.*` and client event names under `client.*`; no client key or event does yet. -4. **RTC units.** The `lk.rtc.*` `_ms` keys keep milliseconds through a documented exception; the - Notion conventions ask for seconds. -5. **Battery.** Clients send `lk.device.battery.level` (percent) and `lk.device.battery.charging`; - SPEC.md specifies OTel `hw.battery.charge` (0-1) and `hw.battery.state`. -6. **`lk.outcome`** (`ok`, `error`, `cancelled`, on every span) vs the Notion - `lk.client.connection.result` (`success`, `failure`, `cancelled`, on connects). -7. **`lk.connect.attempt`** vs the Notion `lk.client.connect_attempt`, plus the missing - `lk.client.connect_id`. -8. **`attempt `** puts dynamic data in a checkpoint name. -9. **`session.id`** is the client's session key; the Notion conventions want no separate client - session id. -10. **`otel.event.name`** duplicates `event_name` on every client event. -11. **`network.connection.type`** gets `vpn`, `bluetooth` and `other` from clients, which are not - OTel members. -12. **Track source vocabulary:** clients send `lk.track.source = microphone`, Agents - `lk.track_source = SOURCE_MICROPHONE`. -13. **`lk.sdk.name` values** are not fixed, and no client sends `lk.sdk.*` yet. -14. **`custom`** is registered as an event name because the registry has no wildcard events; the - events on the wire are `custom.`. -15. **Schema URL.** `https://livekit.io/schemas/telemetry/...` does not resolve yet. -16. **`browser.name` / `browser.version`** (JS port) are not OTel keys and sit in OTel's - `browser.*` namespace; OTel names them `user_agent.name` / `user_agent.version`. -17. **Event bodies.** The Rust core sends display lines, the JS port the event name. +`collector_column: room_id` is materialized from the attributes `room_id` and `roomID`. From 75e7497d72584d1c8cccb2ae389122cf350664f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:37:27 +0200 Subject: [PATCH 14/36] fix(otel): enforce the policies under --v2 and gate generation Weaver skips before_resolution policies with --v2 and hands after_resolution a different representation, so with --v2 (the mode generate.sh uses) no LiveKit rule ran: the metric, override and every per-file probe passed. The rules now live in policies/lib.rego. livekit.rego applies them per file in default mode; resolved.rego applies them to the resolved registry in both representations (default groups, --v2 registry and refinements), including metric refinements and ref overrides compared with each key's definition. check.sh checks both modes, negatives.sh runs every fixture in both, a refinement fixture is added, and generate.sh only replaces gen/ when every target passes. --- otel/README.md | 14 +-- otel/check.sh | 7 +- otel/generate.sh | 7 +- otel/policies/lib.rego | 94 +++++++++++++++++++ otel/policies/livekit.rego | 90 +++--------------- otel/policies/resolved.rego | 85 +++++++++++++---- otel/tests/negatives.sh | 20 ++-- .../negatives/cardinality-refinement.yaml | 15 +++ 8 files changed, 215 insertions(+), 117 deletions(-) create mode 100644 otel/policies/lib.rego create mode 100644 otel/tests/negatives/cardinality-refinement.yaml diff --git a/otel/README.md b/otel/README.md index acb8f60d4..4dc3071ea 100644 --- a/otel/README.md +++ b/otel/README.md @@ -21,7 +21,7 @@ policy, storage and the app API are not conventions and live with each SDK. | `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) | -| `policies/` | LiveKit rules, per file (`livekit.rego`) and on the resolved registry (`resolved.rego`); any finding fails `check.sh` | +| `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,markdown}/` | Codegen templates | | `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `docs/semconv.md` | @@ -116,8 +116,8 @@ prints 34 warnings: 33 about upstream's own files, and one because `client/group not-yet-stable `definition/2` format. ```sh -otel/check.sh # registry + LiveKit policies, then advice -otel/tests/negatives.sh # each negative fixture fails with its named finding +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 otel/generate.sh && git status otel/gen # regenerate; no diff expected otel/tests/live-check.sh # smoke and sparse samples clean, legacy keys flagged otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc and tsc @@ -136,12 +136,14 @@ npx -p typescript@5.9.3 tsc --strict --noEmit --target es2020 otel/gen/ts/semcon | `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*` | | `livekit_flat_identity` | yes | no `lk.room.sid`, `lk.participant.sid`, `lk.track.sid`, `lk.job.id` | | `livekit_seconds`, `livekit_unit` | yes | seconds, unless `unit_exception` | -| `livekit_metric_unit` | yes | metric units are not `ms` (resolved: v1, `definition/2` and `extends`) | -| `livekit_cardinality` | yes | high-cardinality IDs are not metric dimensions (resolved, so inherited ones count) | +| `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 an `lk.*` key may not override its `annotations` (owner, sensitivity, unit) | | `livekit_client_namespace` | no (advice) | client-owned keys under `lk.client.*` | -Weaver itself rejects duplicate keys, unknown fields and unresolved references. +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 diff --git a/otel/check.sh b/otel/check.sh index 0c6e34add..efdf87670 100755 --- a/otel/check.sh +++ b/otel/check.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash -# Checks otel/model against OTel semconv v1.44.0 and the LiveKit policies (fails on any -# violation), then prints the non-failing advice from otel/advice. +# 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] set -euo pipefail cd "$(dirname "$0")" W=${WEAVER:-weaver} "$W" registry check -r model -p policies "$@" +# 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 "$@" # 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. diff --git a/otel/generate.sh b/otel/generate.sh index bfa360f6b..d971aa6d0 100755 --- a/otel/generate.sh +++ b/otel/generate.sh @@ -1,9 +1,12 @@ #!/usr/bin/env bash -# Regenerates otel/gen from otel/model (Rust, TypeScript, markdown docs). CI fails when the +# Regenerates otel/gen from otel/model (Rust, TypeScript, markdown docs). Generation runs the +# LiveKit policies (--v2 mode); gen/ is only replaced when every target passes. CI fails when the # committed outputs differ from a fresh run. set -euo pipefail cd "$(dirname "$0")" W=${WEAVER:-weaver} +tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT for target in rust:rust typescript:ts markdown:docs; do - "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "gen/${target#*:}" + "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "$tmp/${target#*:}" done +cp -R "$tmp"/. gen/ diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego new file mode 100644 index 000000000..73b0a988f --- /dev/null +++ b/otel/policies/lib.rego @@ -0,0 +1,94 @@ +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_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") != regex.match(pii_segment, 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) +} + +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) diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego index 14a5b38ab..21a4b866f 100644 --- a/otel/policies/livekit.rego +++ b/otel/policies/livekit.rego @@ -1,30 +1,10 @@ package before_resolution -# LiveKit rules per definition file (violations), run by otel/check.sh. Each finding id is -# documented in otel/README.md; otel/tests/negatives has a failing fixture for each. +# 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 -# 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", -} - -owners := {"shared", "client", "agents", "cloud"} - -# Mirrors protocol protobufs/logger/options.proto Sensitivity (UNSPECIFIED/PII/SECRET). -sensitivities := {"none", "pii", "secret"} - # LiveKit attribute definitions in this file (refs and deprecated keys are exempt). lk_attrs contains attr if { some group in input.groups @@ -40,64 +20,16 @@ lk_attrs contains object.union(attr, {"id": attr.key}) if { not attr.deprecated } -lk(obj, field) := object.get(obj, ["annotations", "livekit", field], null) - -# A documented reason to keep milliseconds (e.g. RTC stats in webrtc-stats units). -ms_exception(attr) if { - lk(attr, "unit") == "ms" - is_string(lk(attr, "unit_exception")) - lk(attr, "unit_exception") != "" -} - -finding(id, msg) := {"id": id, "level": "violation", "message": msg} - -deny contains finding("livekit_owner", sprintf("%s: annotations.livekit.owner must be one of %v", [attr.id, owners])) if { - some attr in lk_attrs - not owners[lk(attr, "owner")] -} - -deny contains finding("livekit_sensitivity", sprintf("%s: annotations.livekit.sensitivity must be one of %v (protocol logger.Sensitivity)", [attr.id, sensitivities])) if { +deny contains f if { some attr in lk_attrs - not sensitivities[lk(attr, "sensitivity")] + some f in data.livekit.lib.attr_findings(attr) } -deny contains finding("livekit_secret", sprintf("%s: SECRET values are never emitted as telemetry", [attr.id])) if { - some attr in lk_attrs - lk(attr, "sensitivity") == "secret" +# 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) } - -deny contains 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")])) if { - some attr in lk_attrs - sensitivities[lk(attr, "sensitivity")] - (lk(attr, "sensitivity") == "pii") != regex.match(pii_segment, attr.id) -} - -deny contains finding("livekit_pii_heuristic", sprintf("%s looks like PII; name it lk.pii.* so the collector redacts it", [attr.id])) if { - some attr in lk_attrs - regex.match(pii_looking, attr.id) - not regex.match(pii_segment, attr.id) -} - -deny contains finding("livekit_flat_identity", sprintf("%s: shared identity keys are flat, use %s", [attr.id, canonical_identity[attr.id]])) if { - some attr in lk_attrs - canonical_identity[attr.id] -} - -deny contains finding("livekit_seconds", sprintf("%s: LiveKit durations are seconds; a *_ms key needs annotations.livekit.unit: ms plus a unit_exception reason", [attr.id])) if { - some attr in lk_attrs - regex.match(`_ms($|\.)`, attr.id) - not ms_exception(attr) -} - -# 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]+))?$` - -deny contains finding("livekit_unit", sprintf("%s: duration-like attribute must declare annotations.livekit.unit: s (or unit: ms with a unit_exception)", [attr.id])) if { - some attr in lk_attrs - regex.match(duration_like, attr.id) - lk(attr, "unit") != "s" - not ms_exception(attr) -} - -# Metric rules (units, cardinality) and ref overrides live in otel/policies/resolved.rego. diff --git a/otel/policies/resolved.rego b/otel/policies/resolved.rego index aa3c7a9e7..7e1c40786 100644 --- a/otel/policies/resolved.rego +++ b/otel/policies/resolved.rego @@ -1,36 +1,81 @@ package after_resolution -# LiveKit rules on the resolved registry, run by otel/check.sh with otel/policies/livekit.rego. -# They see what each group ends up with after refs, `extends` and definition/2 files are -# resolved, which the per-file rules cannot. +# 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 -high_cardinality := {"lk.room_sid", "lk.participant_id", "lk.track_sid", "lk.job_id", "lk.client.connect_id"} +# --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"], []) + startswith(attr.key, "lk.") + not attr.deprecated + some f in data.livekit.lib.attr_findings(object.union(attr, {"id": attr.key})) +} -finding(id, msg) := {"id": id, "level": "violation", "message": msg} +# 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) +} -where(group) := object.get(group, ["lineage", "provenance", "path"], group.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 finding("livekit_metric_unit", sprintf("%s (%s): metric unit must be s, not ms", [group.metric_name, where(group)])) if { - some group in input.groups - group.type == "metric" - group.unit == "ms" +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 finding("livekit_cardinality", sprintf("%s (%s): %s is high-cardinality, not a default metric dimension", [group.metric_name, where(group), attr.name])) if { - some group in input.groups - group.type == "metric" - some attr in group.attributes - high_cardinality[attr.name] - object.get(group, ["annotations", "livekit", "cardinality_exception"], false) != true # Agents' job-scoped exception +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. -deny contains finding("livekit_ref_override", sprintf("%s in %s (%s): a ref may not override the annotations of a LiveKit key", [key, group.id, where(group)])) if { - some group in input.groups +# 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: 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"], {}) startswith(key, "lk.") "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", []) + startswith(a.key, "lk.") + 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)) } diff --git a/otel/tests/negatives.sh b/otel/tests/negatives.sh index 11ccdaa60..e993ecfc5 100755 --- a/otel/tests/negatives.sh +++ b/otel/tests/negatives.sh @@ -1,6 +1,7 @@ #!/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. +# 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} @@ -10,12 +11,15 @@ 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" - out=$("$W" registry check -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 fails with: $expect" - else - echo "FAIL $fixture (exit $status), expected: $expect"; echo "$out" | tail -20; failed=1 - fi + 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-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 From cece9e80026b4625b1b506d553e0c0c74c3e9c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:38:22 +0200 Subject: [PATCH 15/36] fix(otel): the network flags are conditional on the network event The JS port sends lk.device.network.changed on every browser, and without the Network Information API it sends only network.connection.type (unknown or unavailable), so requiring lk.device.network.expensive and .constrained made that event a live-check violation, and the model called the event Chromium-only. The flags are now conditionally required, the event is described for all platforms, and a fixture of the JS shape must pass live-check. Schema 0.3.0 (requirement change; the README rule now names it). --- otel/README.md | 4 ++-- otel/gen/docs/semconv.md | 8 ++++---- otel/gen/rust/semconv.rs | 2 +- otel/gen/ts/semconv.ts | 2 +- otel/model/client/registry.yaml | 4 ++-- otel/model/client/signals.yaml | 9 +++++---- otel/model/manifest.yaml | 2 +- otel/tests/live-check.sh | 14 ++++++++++---- otel/tests/live-js-network.json | 5 +++++ 9 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 otel/tests/live-js-network.json diff --git a/otel/README.md b/otel/README.md index 4dc3071ea..0a9a3fa32 100644 --- a/otel/README.md +++ b/otel/README.md @@ -99,8 +99,8 @@ column. It is versioned independently of the protocol package. Every PR that changes `model/` bumps it: CI fails unless the version at the end of `schema_url` (MAJOR.MINOR.PATCH) is strictly higher than the base branch's (`tests/schema-bump.sh`; a comment or quotes on an unchanged URL, or a downgrade, do not count). While the -registry is `0.x`, bump the minor version for added, renamed or removed keys, events or spans, and -the patch version for documentation-only changes. To rename a key that has shipped, keep the old +registry is `0.x`, bump the minor version for added, renamed or removed keys, events or spans and +for changed types or requirement levels, and the patch version for documentation-only changes. To rename a key that has shipped, keep the old key with `deprecated: {reason: renamed, renamed_to: }` instead of deleting it; `weaver registry diff` and live-check both report the rename. diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 140486624..14c0440e4 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.2.1`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.3.0`, 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. @@ -202,16 +202,16 @@ Memory pressure changed. Sent once with the initial reading, then on each change ### `lk.device.network.changed` -The network path changed. Sent once with the initial state, then when any of its attributes changes. iOS, macOS and Android; on the web, Chromium only. +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 | | --- | --- | --- | --- | -| `lk.device.network.constrained` | boolean | Required | The user asked for less traffic (Low Data Mode, Data Saver). Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. | -| `lk.device.network.expensive` | boolean | Required | The network is metered (cellular, personal hotspot). Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. | | `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` diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 0beca88f7..c41090928 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.2.1"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.3.0"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 2b95bc152..21b1c654a 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.2.1" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.3.0" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml index 9a985331e..6482355b9 100644 --- a/otel/model/client/registry.yaml +++ b/otel/model/client/registry.yaml @@ -62,7 +62,7 @@ groups: type: boolean stability: development brief: The network is metered (cellular, personal hotspot). - note: Apple `NWPath.isExpensive`, Android metered networks; web Chromium only. + 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} @@ -70,7 +70,7 @@ groups: type: boolean stability: development brief: The user asked for less traffic (Low Data Mode, Data Saver). - note: Apple `NWPath.isConstrained`, Android Data Saver, web `navigator.connection.saveData`. + 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} diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml index 3ea0bb863..344835829 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -142,8 +142,9 @@ groups: stability: development brief: The network path changed. note: > - Sent once with the initial state, then when any of its attributes changes. iOS, macOS and - Android; on the web, Chromium only. + 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: @@ -154,9 +155,9 @@ groups: when the platform cannot tell, `unavailable` when offline. examples: ["wifi", "cell"] - ref: lk.device.network.expensive - requirement_level: required + requirement_level: {conditionally_required: When the platform reports it.} - ref: lk.device.network.constrained - requirement_level: required + requirement_level: {conditionally_required: When the platform reports it.} - id: event.lk.device.battery.changed type: event diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index 474cf4d37..e64719d27 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.2.1 +schema_url: https://livekit.io/schemas/telemetry/0.3.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned signals). dependencies: diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh index 0e6eb7bda..88b006e7e 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,8 +1,9 @@ #!/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. The same smoke sample with the client's current -# identity/PII keys, an unregistered lk.* span, an unlisted checkpoint, an outcome that disagrees -# with the span status and an event at the wrong severity must be flagged. +# findings, and so must a sparse RTC window and a JS network event without its flags. The same +# smoke sample with the client's current identity/PII keys, an unregistered lk.* span, an unlisted +# checkpoint, an outcome that disagrees with the span status and an event at the wrong severity +# must be flagged. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -16,6 +17,11 @@ 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; } + 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"/' \ @@ -28,4 +34,4 @@ for want in "livekit_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room. "livekit_checkpoint: lk.connect" "livekit_span_outcome: lk.connect" "livekit_severity: lk.device.network.changed"; do [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } done -echo "PASS live-check: smoke and sparse samples clean, every planted violation flagged" +echo "PASS live-check: smoke, sparse and JS samples clean, every planted violation flagged" 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"}]}} +] From 67520b579dad95f45e0dd5450bbdf61963b1f49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:40:01 +0200 Subject: [PATCH 16/36] fix(otel): close three contradictions the live-check rules accepted An empty checkpoint name matched the fallback pattern ^$ on spans that declare no pattern; lk.outcome=ok with status Ok passed although the model requires Unset; an INFO-only event sent at severity number 17 with text INFO passed because only the text was compared. A checkpoint must now be listed or match an explicitly declared pattern, status must be exactly the one lk.outcome maps to, and the severity number must fall in a declared severity's range (the text, when present, is still checked). The live-check test plants one of each. --- otel/live-check/livekit.rego | 35 ++++++++++++++++++++++++++++++----- otel/tests/live-check.sh | 17 +++++++++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index d78d70f4c..1c0b25400 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -57,28 +57,53 @@ declared(field) := object.get(input, ["registry_group", "annotations", "livekit" 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 its checkpoint pattern. +# 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 regex.match(object.get(input, ["registry_group", "annotations", "livekit", "checkpoint_pattern", "pattern"], "^$"), name) + 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 + object.get(log, "severity_text", "") != "" not log.severity_text in declared("severity") } -# Status follows lk.outcome (Error only for `error`), and error.type comes only with `error`. -deny contains lk_advice("livekit_span_outcome", {"span_name": span.name, "outcome": outcome, "status": span.status.code}, sprintf("span '%s': lk.outcome=%s but status=%s", [span.name, outcome, span.status.code])) if { +# 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") - (outcome == "error") != (span.status.code == "error") + 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 { diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh index 88b006e7e..846b9f4c9 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,9 +1,9 @@ #!/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 and a JS network event without its flags. The same -# smoke sample with the client's current identity/PII keys, an unregistered lk.* span, an unlisted -# checkpoint, an outcome that disagrees with the span status and an event at the wrong severity -# must be flagged. +# smoke sample with the client's current identity/PII 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. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -27,11 +27,16 @@ sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.na -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/' \ 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_flat_identity: lk.room.sid" "livekit_pii_unmarked: lk.room.name" "livekit_unknown_span: lk.bogus" \ - "livekit_checkpoint: lk.connect" "livekit_span_outcome: lk.connect" "livekit_severity: lk.device.network.changed"; do - [[ $legacy == *"$want"* ]] || { echo "FAIL legacy keys not flagged: $want" >&2; exit 1; } +for want in "livekit_flat_identity lk.room.sid" "livekit_pii_unmarked 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"; do + id=${want% *} key=${want#* } + grep -q -E -- "${id}: .*${key//./\\.}" <<<"$legacy" || { echo "FAIL not flagged: $id on $key" >&2; exit 1; } done echo "PASS live-check: smoke, sparse and JS samples clean, every planted violation flagged" From 30b874263a64caa29a8820c2fb777763bf216443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:41:58 +0200 Subject: [PATCH 17/36] docs(otel): normative stage-1 rules, gauge averages, ms provenance The spec stated the reserved keys, app limits and span caps as what producers do; they are requirements, and not every producer meets them yet, so they are now worded as rules (must) with no claim about a producer's behaviour. Gauge averages divide by the readings that reported the gauge, not by lk.rtc.samples. The RTC _ms keys carry milliseconds converted from the W3C stats' seconds; the spec no longer calls ms the W3C unit or names a rounding. The attribute reference gains a description column, so a key no signal references (lk.job_id) keeps its meaning. --- otel/README.md | 2 +- otel/gen/docs/semconv.md | 245 +++++++++--------- otel/gen/rust/semconv.rs | 2 +- otel/gen/ts/semconv.ts | 2 +- otel/model/client/groups.yaml | 12 +- otel/model/client/registry.yaml | 64 ++--- otel/model/client/signals.yaml | 14 +- .../templates/registry/markdown/semconv.md.j2 | 33 +-- 8 files changed, 191 insertions(+), 183 deletions(-) diff --git a/otel/README.md b/otel/README.md index 0a9a3fa32..ade0eee32 100644 --- a/otel/README.md +++ b/otel/README.md @@ -64,7 +64,7 @@ nothing validates it. | `owner` | `shared`, `client`, `agents`, `cloud` | required | | `sensitivity` | `none`, `pii`, `secret` (protocol `logger.Sensitivity`) | required; `pii` iff the key has a `pii` segment; `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` (RTC stats keep webrtc-stats milliseconds) | + | `unit_exception` | a reason | allows a `*_ms` key with `unit: ms` (the RTC stats keys, 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 | diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 14c0440e4..e59913203 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -39,18 +39,20 @@ keeps everything else. Log bodies and metrics are never redacted, so PII must no PII keys: `lk.pii.participant_identity`, `lk.pii.remote_participant_identity`, `lk.pii.room_name`. **Units.** Durations are seconds (OTel), on a monotonic clock; timestamps are the record's own -OTLP time. One documented exception: RTC stats keep the W3C webrtc-stats milliseconds, in keys -ending in `_ms`: +OTLP time. One documented exception: the RTC stats keys ending in `_ms` carry milliseconds, +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 never set these, on their own events or as session attributes: +**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 stay within these limits: +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. -Anything over a limit is rejected, never truncated, and counted in `lk.telemetry.dropped.invalid`. +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 @@ -61,11 +63,12 @@ taken when it is captured. Every record and span of a room session carries the 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. The core writes both; a span attribute of either name set any other way is -dropped. Checkpoints are span events without attributes, named from a fixed list per span. Past a -span's limits, later checkpoints are dropped and counted in OTLP `dropped_events_count`; app -correlation attributes are dropped first, then the span's own tail, counted in -`dropped_attributes_count`; the session's attributes, `lk.outcome` and `error.type` always ship. +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 @@ -88,7 +91,7 @@ Resource attributes of the client pipeline, one Resource per process. Set once p ## Every record -Attributes the core stamps 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`) are never 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 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 | | --- | --- | --- | --- | @@ -101,7 +104,7 @@ Attributes the core stamps on every log record and span of a session. One sessio ## 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 the core copies from Rust (`lk.log.source = ffi`), JWT-shaped substrings are masked as ``. +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 | | --- | --- | --- | --- | @@ -115,7 +118,7 @@ Attributes of a plain log record (no event name). The body is the log message. O ### `custom` -Namespace of app-defined events. The core emits `custom.` with 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`), may not use the reserved keys, and stay within the limits; anything else is rejected, never truncated, and counted in `lk.telemetry.dropped.invalid`. An event's own attributes override the session's. +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.`). @@ -255,19 +258,19 @@ One RTC stats window for one track and direction. One per track and direction pe | Attribute | Type | Requirement | Description | | --- | --- | --- | --- | -| `lk.rtc.samples` | int, `{reading}` | Required | Stats readings folded into the window. Denominator for the gauges' averages. | -| `lk.rtc.window_ms` | int, `ms` | Required | Actual length of the stats window. From the window's start to its close, on the core's clock (the JS port uses wall-clock time). | +| `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 `lk.rtc.samples` readings. | +| `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 `lk.rtc.samples` readings. | +| `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`. | @@ -276,7 +279,7 @@ One RTC stats window for one track and direction. One per track and direction pe | `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 `lk.rtc.samples` readings. | +| `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`. | @@ -286,7 +289,7 @@ One RTC stats window for one track and direction. One per track and direction pe | `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 `lk.rtc.samples` readings. | +| `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`. | @@ -332,8 +335,8 @@ Kind `client`. One connect attempt. Dashboards compute connect phases only from | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. 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`. Written only by the core, at most once. | +| `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 | | --- | --- | --- | @@ -356,10 +359,10 @@ Kind `internal`. One track publish. A child of the ambient span when there is on | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | -| `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. Written only by the core, at most once. | +| `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. @@ -372,9 +375,9 @@ Kind `client`. One reconnect cycle. `cancelled` when `disconnect()` or a newer r | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `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. Written only by the core, at most once. | +| `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. | @@ -390,7 +393,7 @@ Kind `internal`. One subscription, from the intent to subscribe to the first med | 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, written by the core from how the span ended; an attribute of this name set any other way is dropped. Status follows it: `Error` for `error`, `Unset` for `ok` and `cancelled`. | +| `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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. | @@ -406,101 +409,101 @@ Limits: 128 attributes, 128 span events per span. ## Attribute reference -Every key this registry defines; 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 | Examples | Used on | -| --- | --- | --- | --- | --- | --- | -| `browser.name` | string | client | none | `Chrome`, `Safari`, `Firefox` | resource | -| `browser.version` | string | client | none | `124.0` | resource | -| `lk.connect.attempt` | int | client | none | `1` | `lk.connect` | -| `lk.device.app_state` | enum | client | none | | `lk.device.app_state.changed` | -| `lk.device.audio.interruption` | enum | client | none | | `lk.device.audio.interruption` | -| `lk.device.audio_route.outputs` | string | client | none | `bluetooth`, `speaker,usb` | `lk.device.audio_route.changed` | -| `lk.device.audio_route.reason` | enum | client | none | | `lk.device.audio_route.changed` | -| `lk.device.battery.charging` | boolean | client | none | `false` | `lk.device.battery.changed` | -| `lk.device.battery.level` | int, `%` | client | none | `42` | `lk.device.battery.changed` | -| `lk.device.capture.device` | enum | client | none | | `lk.device.capture.failed` | -| `lk.device.capture.reason` | enum | client | none | | `lk.device.capture.failed` | -| `lk.device.low_power.enabled` | boolean | client | none | `true` | `lk.device.low_power.changed` | -| `lk.device.memory.pressure` | enum | client | none | | `lk.device.memory.changed` | -| `lk.device.network.constrained` | boolean | client | none | `false` | `lk.device.network.changed` | -| `lk.device.network.expensive` | boolean | client | none | `false` | `lk.device.network.changed` | -| `lk.device.thermal.state` | enum | client | none | | `lk.device.thermal.changed` | -| `lk.disconnect.reason` | enum | client | none | | `lk.room.disconnected` | -| `lk.job_id` | string | shared | none | `AJ_kVbMBWtnVHvT` | | -| `lk.log.logger` | string | client | none | `Room`, `livekit::rtc_engine` | log | -| `lk.log.source` | enum | client | none | | log | -| `lk.outcome` | enum | client | none | | `lk.connect` `lk.publish` `lk.reconnect` `lk.subscribe` | -| `lk.participant_id` | string | shared | none | `PA_9kLm2NpQ4rTs` | record | -| `lk.pii.participant_identity` | string | shared | pii | `identity-0` | record | -| `lk.pii.remote_participant_identity` | string | shared | pii | `identity-1` | `lk.subscribe` | -| `lk.pii.room_name` | string | shared | pii | `my-room` | record | -| `lk.ping.seq` | int | client | none | `1` | `lk.ping` | -| `lk.reconnect.attempts` | int | client | none | `2` | `lk.reconnect` | -| `lk.reconnect.mode` | enum | client | none | | `lk.reconnect` | -| `lk.reconnect.reason` | enum | client | none | | `lk.reconnect` | -| `lk.room_sid` | string | shared | none | `RM_5Fq8xQ2ZbXwN` | record | -| `lk.rtc.audio_level.avg` | double, `1` | client | none | `0.05` | `lk.rtc.stats.sample` | -| `lk.rtc.audio_level.max` | double, `1` | client | none | `0.3` | `lk.rtc.stats.sample` | -| `lk.rtc.audio_level.min` | double, `1` | client | none | `0.0` | `lk.rtc.stats.sample` | -| `lk.rtc.bytes` | int, `By` | client | none | `1048576` | `lk.rtc.stats.sample` | -| `lk.rtc.codec` | string | client | none | `audio/opus`, `video/VP8` | `lk.rtc.stats.sample` | -| `lk.rtc.concealed_samples` | int, `{sample}` | client | none | `480` | `lk.rtc.stats.sample` | -| `lk.rtc.concealment_events` | int, `{event}` | client | none | `1` | `lk.rtc.stats.sample` | -| `lk.rtc.fps.avg` | double, `{frame}/s` | client | none | `29.7` | `lk.rtc.stats.sample` | -| `lk.rtc.fps.max` | double, `{frame}/s` | client | none | `30.0` | `lk.rtc.stats.sample` | -| `lk.rtc.fps.min` | double, `{frame}/s` | client | none | `24.0` | `lk.rtc.stats.sample` | -| `lk.rtc.freeze_count` | int, `{freeze}` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.freezes_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.interruption_count` | int, `{interruption}` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.interruptions_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.jitter_buffer_delay_ms` | int, `ms` | client | none | `1200` | `lk.rtc.stats.sample` | -| `lk.rtc.jitter_buffer_emitted_count` | int, `{sample}` | client | none | `48000` | `lk.rtc.stats.sample` | -| `lk.rtc.jitter_ms.avg` | double, `ms` | client | none | `1.8` | `lk.rtc.stats.sample` | -| `lk.rtc.jitter_ms.max` | double, `ms` | client | none | `4.2` | `lk.rtc.stats.sample` | -| `lk.rtc.jitter_ms.min` | double, `ms` | client | none | `0.5` | `lk.rtc.stats.sample` | -| `lk.rtc.packets` | int, `{packet}` | client | none | `3000` | `lk.rtc.stats.sample` | -| `lk.rtc.packets_lost` | int, `{packet}` | client | none | `3` | `lk.rtc.stats.sample` | -| `lk.rtc.pause_count` | int, `{pause}` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.pauses_duration_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.quality_limitation.bandwidth_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.quality_limitation.cpu_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.quality_limitation.other_ms` | int, `ms` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.rtt_ms.avg` | double, `ms` | client | none | `25.5` | `lk.rtc.stats.sample` | -| `lk.rtc.rtt_ms.max` | double, `ms` | client | none | `80.0` | `lk.rtc.stats.sample` | -| `lk.rtc.rtt_ms.min` | double, `ms` | client | none | `12.0` | `lk.rtc.stats.sample` | -| `lk.rtc.samples` | int, `{reading}` | client | none | `2` | `lk.rtc.stats.sample` | -| `lk.rtc.silent_concealed_samples` | int, `{sample}` | client | none | `0` | `lk.rtc.stats.sample` | -| `lk.rtc.window_ms` | int, `ms` | client | none | `60000` | `lk.rtc.stats.sample` | -| `lk.sdk.name` | string | shared | none | `swift`, `android`, `flutter`, `react-native`, `unity`, `rust`, `js` | resource | -| `lk.sdk.version` | string | shared | none | `2.17.0` | resource | -| `lk.telemetry.cache.batches` | int, `{batch}` | client | none | `0` | `lk.telemetry.report` | -| `lk.telemetry.cache.delete_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.cache.list_errors` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.cache.read_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.cache.write_errors` | int, `{batch}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.cache_error` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.cache_full` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.corrupt` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.expired` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.invalid` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.oversized` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.queue_full` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.rate_limited` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.rejected` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.throttled` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.dropped.unattributed` | int, `{record}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.holds.capped` | int, `{hold}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.uploads.bytes` | int, `By` | client | none | `20480` | `lk.telemetry.report` | -| `lk.telemetry.uploads.failed` | int, `{attempt}` | client | none | `0` | `lk.telemetry.report` | -| `lk.telemetry.uploads.sent` | int, `{batch}` | client | none | `4` | `lk.telemetry.report` | -| `lk.telemetry.uploads.timeouts` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | -| `lk.telemetry.uploads.unauthorized` | int, `{attempt}` | client | none | `1` | `lk.telemetry.report` | -| `lk.track.direction` | enum | client | none | | `lk.rtc.stats.sample` | -| `lk.track.kind` | enum | client | none | | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` | -| `lk.track.source` | enum | client | none | | `lk.publish` `lk.subscribe` | -| `lk.track_sid` | string | shared | none | `TR_AMkTyV3Zx8Qp` | `lk.rtc.stats.sample` `lk.publish` `lk.subscribe` | +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 | +| --- | --- | --- | --- | --- | --- | --- | +| `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 | +| `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.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 | client | none | Why the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | | `lk.room.disconnected` | +| `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` | | +| `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.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.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 | +| `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 | +| `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.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 | +| `lk.ping.seq` | int | client | none | Monotonically increasing sequence number per pipeline. Optional; on `lk.ping` only. | `1` | `lk.ping` | +| `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.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 | +| `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.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.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.source` | enum | client | none | Source of the track. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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` | ## Enum values diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index c41090928..4551672e5 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -476,7 +476,7 @@ pub const LK_TRACK_SID: &str = "lk.track_sid"; /// Event names (OTLP `LogRecord.event_name`). pub mod event { - /// Namespace of app-defined events. The core emits `custom.` with the app's own attributes. + /// Namespace of app-defined events, named `custom.` and carrying the app's own attributes. pub const CUSTOM: &str = "custom"; /// The app moved to the foreground or background. pub const LK_DEVICE_APP_STATE_CHANGED: &str = "lk.device.app_state.changed"; diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 21b1c654a..d44ae27af 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -413,7 +413,7 @@ export const LK_TRACK_SOURCE_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned track SID (`TR_...`). */ export const ATTR_LK_TRACK_SID = "lk.track_sid" as const; -/** Namespace of app-defined events. The core emits `custom.` with the app's own attributes. */ +/** Namespace of app-defined events, named `custom.` and carrying the app's own attributes. */ export const EVENT_CUSTOM = "custom" 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; diff --git a/otel/model/client/groups.yaml b/otel/model/client/groups.yaml index e326c91ab..2363bc89e 100644 --- a/otel/model/client/groups.yaml +++ b/otel/model/client/groups.yaml @@ -56,15 +56,15 @@ attribute_groups: - id: livekit.client.record visibility: public stability: development - brief: Attributes the core stamps on every log record and span of a session. + 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`) are never set by an - app; an app's correlation attributes (e.g. `app.call_id`, `enduser.id`) ride along, within the - `custom` event's limits. + 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 @@ -89,8 +89,8 @@ attribute_groups: 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 the core copies from Rust - (`lk.log.source = ffi`), JWT-shaped substrings are masked as ``. + 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 diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml index 6482355b9..32ee598d4 100644 --- a/otel/model/client/registry.yaml +++ b/otel/model/client/registry.yaml @@ -1,14 +1,15 @@ # 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). Durations and RTT/jitter in -# RTC stats stay in ms, the W3C webrtc-stats unit, through an explicit `unit_exception`. +# 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 host SDK observes and the core reports on change. + brief: Device conditions the platform observes, reported on change. note: > - The platform SDK reads the OS; the core turns changes into `lk.device.*` events. A platform + 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 @@ -205,11 +206,12 @@ groups: 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. + 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. W3C - durations in seconds become integer milliseconds (truncated); RTT and jitter become double - milliseconds. A field the platform or direction lacks is omitted, never sent as 0. + 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 @@ -223,15 +225,15 @@ groups: type: int stability: development brief: Actual length of the stats window. - note: From the window's start to its close, on the core's clock (the JS port uses wall-clock time). + 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 keep webrtc-stats milliseconds} + 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: Denominator for the gauges' averages. + 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}"} @@ -274,7 +276,7 @@ groups: note: Inbound `totalFreezesDuration`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.pause_count type: int stability: development @@ -290,7 +292,7 @@ groups: note: Inbound `totalPausesDuration`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.concealed_samples type: int stability: development @@ -330,7 +332,7 @@ groups: note: Inbound `totalInterruptionDuration`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + 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 @@ -338,7 +340,7 @@ groups: 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 keep webrtc-stats milliseconds} + 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 @@ -354,7 +356,7 @@ groups: note: Outbound `qualityLimitationDurations.bandwidth`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + 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 @@ -362,7 +364,7 @@ groups: note: Outbound `qualityLimitationDurations.cpu`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + 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 @@ -370,7 +372,7 @@ groups: note: Outbound `qualityLimitationDurations.other`. examples: [0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.jitter_ms.min type: double stability: development @@ -378,7 +380,7 @@ groups: note: Inbound `jitter` (seconds × 1000). examples: [0.5] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.jitter_ms.max type: double stability: development @@ -386,15 +388,15 @@ groups: note: Inbound `jitter` (seconds × 1000). examples: [4.2] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + 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 `lk.rtc.samples` readings. + 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 keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.rtt_ms.min type: double stability: development @@ -404,7 +406,7 @@ groups: candidate pair's `currentRoundTripTime` (seconds × 1000). examples: [12.0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.rtt_ms.max type: double stability: development @@ -412,15 +414,15 @@ groups: note: Same sources as `lk.rtc.rtt_ms.min`. examples: [80.0] annotations: - livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keep webrtc-stats milliseconds} + 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 `lk.rtc.samples` readings. + 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 keep webrtc-stats milliseconds} + livekit: {owner: client, sensitivity: none, unit: ms, unit_exception: RTC stats keys report milliseconds} - id: lk.rtc.fps.min type: double stability: development @@ -441,7 +443,7 @@ groups: type: double stability: development brief: Average frame rate in the window, video. - note: "`framesPerSecond`, either direction, averaged over `lk.rtc.samples` readings." + 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"} @@ -465,7 +467,7 @@ groups: type: double stability: development brief: Average audio level in the window, 0-1, inbound audio. - note: Inbound `audioLevel`, averaged over `lk.rtc.samples` readings. + note: Inbound `audioLevel`, averaged over the window's readings that report it. examples: [0.05] annotations: livekit: {owner: client, sensitivity: none, unit: "1"} @@ -473,14 +475,14 @@ groups: - id: registry.lk.span type: attribute_group display_name: LiveKit client span details - brief: Attributes on the core's spans. + 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, written by the core from how the span ended; an - attribute of this name set any other way is dropped. Status follows it: `Error` for + 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: diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml index 344835829..251cbcb80 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -315,13 +315,13 @@ groups: type: event name: custom stability: development - brief: Namespace of app-defined events. The core emits `custom.` with the app's own attributes. + 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`), - may not use the reserved keys, and stay within the limits; anything else is rejected, never - truncated, and counted in `lk.telemetry.dropped.invalid`. An event's own attributes override - the session's. + 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] @@ -363,7 +363,7 @@ groups: 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`. Written only by the core, at most once. + group by it per `service.name`. Set only by the SDK, at most once. examples: ["LiveKitError.12", "signal_close"] - id: span.lk.reconnect @@ -390,7 +390,7 @@ groups: 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. Written only by the core, at most once. + 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 @@ -416,7 +416,7 @@ groups: 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. Written only by the core, at most once. + 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 diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index a559a4717..8134f1be4 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -65,18 +65,20 @@ keeps everything else. Log bodies and metrics are never redacted, so PII must no 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: RTC stats keep the W3C webrtc-stats milliseconds, in keys -ending in `_ms`: +OTLP time. One documented exception: the RTC stats keys ending in `_ms` carry milliseconds, +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 never set these, on their own events or as session attributes: +**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 stay within these limits: +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 %}. -Anything over a limit is rejected, never truncated, and counted in `lk.telemetry.dropped.invalid`. +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 @@ -87,11 +89,12 @@ taken when it is captured. Every record and span of a room session carries the 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. The core writes both; a span attribute of either name set any other way is -dropped. Checkpoints are span events without attributes, named from a fixed list per span. Past a -span's limits, later checkpoints are dropped and counted in OTLP `dropped_events_count`; app -correlation attributes are dropped first, then the span's own tail, counted in -`dropped_attributes_count`; the session's attributes, `lk.outcome` and `error.type` always ship. +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 @@ -135,12 +138,12 @@ Limits: {% for k, v in lk.limits | items %}{{ v }} {{ k | replace("_", " ") }}{% {% endfor %} ## Attribute reference -Every key this registry defines; the OTel keys used above are defined by OTel. "Used on" names the -attribute sets, events and spans that reference the key. +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 | Examples | Used on | -| --- | --- | --- | --- | --- | --- | -{% for a in ctx.attributes %}| `{{ a.key }}` | {{ ty(a) }} | {{ a.annotations.livekit.owner }} | {{ a.annotations.livekit.sensitivity }} | {% 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 %}| +| 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 %} From 541aa125bd04cff4f050793ab9c52b2f4f81e16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:05:25 +0200 Subject: [PATCH 18/36] fix(otel): an omitted severity text is not a severity violation severity_text is optional in OTLP. Omitted, it reaches the live-check rule as null, which passed the != "" test and was then not among the declared texts, so a valid INFO record (severity number 9, no text) was flagged. The text is now compared only when it is a non-empty string; the number is still checked. Two positive fixtures, as weaver samples and as OTLP/JSON lines, must pass live-check. --- otel/live-check/livekit.rego | 6 ++++-- otel/tests/live-check.sh | 19 ++++++++++++++----- otel/tests/live-no-severity-text.json | 3 +++ otel/tests/live-no-severity-text.jsonl | 1 + 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 otel/tests/live-no-severity-text.json create mode 100644 otel/tests/live-no-severity-text.jsonl diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index 1c0b25400..4553f4235 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -92,8 +92,10 @@ deny contains lk_advice("livekit_severity", {"event_name": log.event_name, "seve 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 - object.get(log, "severity_text", "") != "" - not log.severity_text in declared("severity") + 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`. diff --git a/otel/tests/live-check.sh b/otel/tests/live-check.sh index 846b9f4c9..d6a054f4b 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,9 +1,10 @@ #!/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 and a JS network event without its flags. The same -# smoke sample with the client's current identity/PII 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. +# findings, and so must a sparse RTC window, a JS network event without its flags and a record +# without severity text. The same smoke sample with the client's current identity/PII 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. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -22,6 +23,14 @@ 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; } +# 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"/' \ @@ -39,4 +48,4 @@ for want in "livekit_flat_identity lk.room.sid" "livekit_pii_unmarked lk.room.na id=${want% *} key=${want#* } grep -q -E -- "${id}: .*${key//./\\.}" <<<"$legacy" || { echo "FAIL not flagged: $id on $key" >&2; exit 1; } done -echo "PASS live-check: smoke, sparse and JS samples clean, every planted violation flagged" +echo "PASS live-check: smoke, sparse, JS and no-severity-text samples clean, every planted violation flagged" 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"}}]}]}]}]} From a87d861ef51e635ce22562d2039de9aa50da1a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:05:54 +0200 Subject: [PATCH 19/36] docs(otel): qualify where the RTC milliseconds come from The spec and README said every RTC _ms key is converted from the W3C stats' seconds, but lk.rtc.window_ms is the window's length as the producer measures it. They now say so; key names and units are unchanged. --- otel/README.md | 2 +- otel/gen/docs/semconv.md | 5 +++-- otel/templates/registry/markdown/semconv.md.j2 | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/otel/README.md b/otel/README.md index ade0eee32..f92791ec2 100644 --- a/otel/README.md +++ b/otel/README.md @@ -64,7 +64,7 @@ nothing validates it. | `owner` | `shared`, `client`, `agents`, `cloud` | required | | `sensitivity` | `none`, `pii`, `secret` (protocol `logger.Sensitivity`) | required; `pii` iff the key has a `pii` segment; `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, converted from the W3C stats' 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 | diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index e59913203..d31607c7a 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -39,8 +39,9 @@ keeps everything else. Log bodies and metrics are never redacted, so PII must no PII keys: `lk.pii.participant_identity`, `lk.pii.remote_participant_identity`, `lk.pii.room_name`. **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, -converted from the seconds the W3C stats report: +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, diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index 8134f1be4..cafe95010 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -65,8 +65,9 @@ keeps everything else. Log bodies and metrics are never redacted, so PII must no 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, -converted from the seconds the W3C stats report: +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, From 85645efab44995a98fec6059ca71d863383d32d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:52:47 +0200 Subject: [PATCH 20/36] docs(otel): note live-check as the SDKs' integration-test gate --- otel/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/otel/README.md b/otel/README.md index f92791ec2..ac8a2151f 100644 --- a/otel/README.md +++ b/otel/README.md @@ -189,3 +189,19 @@ Known limits: - 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 six identity/PII renames 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. From 672132064ab77de2dc5f9e099f9c02e8c959ed89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:18:31 +0200 Subject: [PATCH 21/36] feat(otel): register the agents conventions under their current names Adds otel/model/agents: the lk.* attributes LiveKit Agents 2.0 emits (Python trace_types.py, types.py, otel_metrics.py, observability.py and the Rust core), 50 spans as shipped (unprefixed), the 20 lk.agents.* metrics with their bare dimensions, and the gen_ai.*/LangFuse names no upstream registry defines. The OTel GenAI registry becomes a second dependency (pinned by commit, it has no tags) so the 46 current gen_ai.* keys are referenced, never redefined. Three keys that collide with a client spelling (lk.track_source, lk.disconnect_reason, lk.agent_name) are left out on purpose and stay literals in the producers until one spelling is picked. lk.rpc.caller_identity and lk.rpc.destination_identity carry a pii_heuristic_exception: shipped unmarked, documented rather than renamed. Codegen: a Python template (flat module of constants), an upstream section in the Rust and Python output for referenced OTel keys, a markdown template that renders spans without annotations, and the codegen test round-trips Python too. Schema 0.3.0 -> 0.4.0. --- otel/README.md | 30 +- otel/generate.sh | 2 +- otel/model/agents/registry.yaml | 250 +++++ otel/model/agents/signals.yaml | 885 ++++++++++++++++++ otel/model/manifest.yaml | 9 +- otel/policies/lib.rego | 9 + .../templates/registry/markdown/semconv.md.j2 | 4 +- otel/templates/registry/python/semconv.py.j2 | 49 + otel/templates/registry/python/weaver.yaml | 5 + otel/templates/registry/rust/semconv.rs.j2 | 27 +- otel/templates/registry/rust/weaver.yaml | 2 +- otel/tests/codegen.sh | 10 +- 12 files changed, 1255 insertions(+), 27 deletions(-) create mode 100644 otel/model/agents/registry.yaml create mode 100644 otel/model/agents/signals.yaml create mode 100644 otel/templates/registry/python/semconv.py.j2 create mode 100644 otel/templates/registry/python/weaver.yaml diff --git a/otel/README.md b/otel/README.md index ac8a2151f..7ebfd22c5 100644 --- a/otel/README.md +++ b/otel/README.md @@ -7,8 +7,11 @@ upstream keys (`service.*`, `os.*`, `device.*`, `network.connection.type`, `erro `session.id`, `code.*`) are referenced with `ref:`, never redefined. Status: prototype, `development` stability. It registers the shared identity, PII and SDK keys, -and what the client cores (rust-sdks `livekit-telemetry` and the JS port) emit today, under their -current names. Agents-owned keys are not registered yet. +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 @@ -21,10 +24,12 @@ policy, storage and the app API are not conventions and live with each SDK. | `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, and the `gen_ai.*`/LangFuse keys no upstream registry defines | +| `model/agents/signals.yaml` | Agents spans (50, unprefixed as shipped), the 20 `lk.agents.*` metrics, and the GenAI span events and metrics deprecated upstream | | `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,markdown}/` | Codegen templates | -| `gen/` | Generated outputs, committed: `rust/semconv.rs`, `ts/semconv.ts`, `docs/semconv.md` | +| `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, schema-version rule | @@ -40,8 +45,11 @@ Every LiveKit key carries `annotations.livekit.owner`: Observability review. - **`client`** (`model/client/`): the Client team approves alone, unless the change touches or duplicates a shared concept. -- `agents` and `cloud` are valid owners for when those teams register their keys, in their own - directories. +- **`agents`** (`model/agents/`): the Agents team approves alone, unless the change touches or + duplicates a shared concept. Registered as emitted; the three keys that collide with a client + spelling (`lk.track_source`, `lk.disconnect_reason`, `lk.agent_name`) are deliberately left + out until the two teams pick one, and stay literals in the Agents shims. +- `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, and never @@ -67,6 +75,7 @@ nothing validates it. | `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 | + | `pii_heuristic_exception` | a reason | a shipped key the PII heuristic would flag (`lk.rpc.caller_identity`); documents it, the collector still does not redact it | Events and spans carry their own `annotations.livekit`, rendered in the docs; live-check enforces `severity`, `checkpoints` and `checkpoint_pattern`: @@ -120,20 +129,25 @@ otel/check.sh # registry + LiveKit policies, default an otel/tests/negatives.sh # each negative fixture fails with its named finding, both modes otel/generate.sh && git status otel/gen # regenerate; no diff expected otel/tests/live-check.sh # smoke and sparse samples clean, legacy keys flagged -otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc and tsc +otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc, tsc and python3 otel/tests/schema-bump.sh origin/main # schema_url version strictly higher if model/ changed otel/tests/schema-bump-test.sh # that rule, against comment-only changes and downgrades 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 -m py_compile 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. + | 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 | -| `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*` | +| `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*`, unless `pii_heuristic_exception` gives a reason | | `livekit_flat_identity` | yes | no `lk.room.sid`, `lk.participant.sid`, `lk.track.sid`, `lk.job.id` | | `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) | diff --git a/otel/generate.sh b/otel/generate.sh index d971aa6d0..949bfb9c8 100755 --- a/otel/generate.sh +++ b/otel/generate.sh @@ -6,7 +6,7 @@ set -euo pipefail cd "$(dirname "$0")" W=${WEAVER:-weaver} tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT -for target in rust:rust typescript:ts markdown:docs; do +for target in rust:rust typescript:ts python:python markdown:docs; do "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "$tmp/${target#*:}" done cp -R "$tmp"/. gen/ diff --git a/otel/model/agents/registry.yaml b/otel/model/agents/registry.yaml new file mode 100644 index 000000000..7f869de10 --- /dev/null +++ b/otel/model/agents/registry.yaml @@ -0,0 +1,250 @@ +# 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.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}}} diff --git a/otel/model/agents/signals.yaml b/otel/model/agents/signals.yaml new file mode 100644 index 000000000..65a016315 --- /dev/null +++ b/otel/model/agents/signals.yaml @@ -0,0 +1,885 @@ +# 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. + 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.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. + 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.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. + attributes: + - ref: lk.track_sid + requirement_level: recommended + - 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. + attributes: + - ref: lk.track_sid + requirement_level: recommended + - 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.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. + 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. + 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. + 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. + 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`) -------------------------------------- + - {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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{character}", 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: "{request}", 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."} + - {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/manifest.yaml b/otel/model/manifest.yaml index e64719d27..8adc0227c 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,9 +2,14 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.3.0 +schema_url: https://livekit.io/schemas/telemetry/0.4.0 stability: development -description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned signals). +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/policies/lib.rego b/otel/policies/lib.rego index 73b0a988f..eab00f4cf 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -67,6 +67,15 @@ pii_marker(attr) := {finding("livekit_pii_marker", sprintf("%s: sensitivity=%v, 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) +} + +# A shipped key the heuristic would flag, registered as emitted with the reason it is not renamed +# (Agents' lk.rpc.caller_identity). The collector still does not redact it: this documents, it +# does not excuse. +heuristic_exception(attr) if { + 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]])) | diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index cafe95010..2729f00a4 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -123,7 +123,7 @@ and `error.type` always ship. {% if e.attributes %} {{ attr_table(e.attributes) }}{% endif %}{% endfor %} ## Spans -{% for s in ctx.spans %}{% set lk = s.annotations.livekit %} +{% 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 %} @@ -135,7 +135,7 @@ Kind `{{ s.kind }}`. {{ s.brief | trim }}{% if s.note %} {{ s.note | trim }}{% e {% 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 %} -Limits: {% for k, v in lk.limits | items %}{{ v }} {{ k | replace("_", " ") }}{% if not loop.last %}, {% endif %}{% endfor %} per span. +{% 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 diff --git a/otel/templates/registry/python/semconv.py.j2 b/otel/templates/registry/python/semconv.py.j2 new file mode 100644 index 000000000..e2fcec88d --- /dev/null +++ b/otel/templates/registry/python/semconv.py.j2 @@ -0,0 +1,49 @@ +{#- 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 %} +{%- endmacro -%} +# Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +"""LiveKit telemetry semantic conventions.""" + +#: Telemetry schema version; set as `schema_url` on every OTLP scope. +SCHEMA_URL = {{ lit(ctx.schema_url) }} + +{% 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 %} +# 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) }}`. +METRIC_{{ m.name | screaming_snake_case }} = {{ lit(m.name) }} +{% endfor %}{% endif %} diff --git a/otel/templates/registry/python/weaver.yaml b/otel/templates/registry/python/weaver.yaml new file mode 100644 index 000000000..664364cab --- /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 | {schema_url, 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)] | unique_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.py diff --git a/otel/templates/registry/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 index efe079a28..4145c911e 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -8,23 +8,32 @@ {{ s | trim | replace("\r", " ") | replace("\n", " ") }} {%- 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 %} +{{ 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 -%} -// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. -//! LiveKit telemetry semantic conventions. - -/// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; -{% for a in ctx.attributes %} +{#- 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 %} /// Values of [`{{ a.key | screaming_snake_case }}`]. pub mod {{ a.key | snake_case }} { -{% for m in a.type.members %} /// {{ line(m.brief) }} +{% for m in a.type.members %} /// {{ line(m.brief) or "`" ~ m.value ~ "`." }} pub const {{ m.id | screaming_snake_case }}: &str = {{ lit(m.value) }}; {% endfor %}} -{% endif %}{% endfor %} +{% endif %} +{%- endmacro -%} +// Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +//! LiveKit telemetry semantic conventions. + +/// Telemetry schema version; set as `schema_url` on every OTLP scope. +pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; +{% 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 %}} /// Event names (OTLP `LogRecord.event_name`). pub mod event { {% for e in ctx.events %} /// {{ line(e.brief) }} diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml index aa8cb61d2..1d601d24f 100644 --- a/otel/templates/registry/rust/weaver.yaml +++ b/otel/templates/registry/rust/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.rs.j2 - filter: '{schema_url, 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)}' + filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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)] | unique_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.rs diff --git a/otel/tests/codegen.sh b/otel/tests/codegen.sh index 9ee370f7d..163d6cb46 100755 --- a/otel/tests/codegen.sh +++ b/otel/tests/codegen.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Generates Rust and TypeScript from otel/model plus tests/codegen/escape.yaml, compiles both, -# and checks the tricky enum value comes back byte for byte from rustc and from tsc + node. +# 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} @@ -8,7 +8,7 @@ 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; do +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 @@ -22,4 +22,6 @@ rustc --edition 2021 -D warnings "$tmp/main.rs" -o "$tmp/main" 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; } -echo "PASS codegen: escaped literals round-trip through rustc and tsc" +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" From 1a424cf843d61de4589515f843e5958cb3e28981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:18:31 +0200 Subject: [PATCH 22/36] chore(otel): regenerate Rust, TypeScript, Python and docs --- otel/gen/docs/semconv.md | 1018 +++++++++++++++++++++++++- otel/gen/python/semconv.py | 1417 ++++++++++++++++++++++++++++++++++++ otel/gen/rust/semconv.rs | 909 ++++++++++++++++++++++- otel/gen/ts/semconv.ts | 653 ++++++++++++++++- 4 files changed, 3987 insertions(+), 10 deletions(-) create mode 100644 otel/gen/python/semconv.py diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index d31607c7a..4fd6c85ef 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.3.0`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.4.0`, 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. @@ -36,7 +36,7 @@ assigned them, and never default metric dimensions (high cardinality). **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: `lk.pii.participant_identity`, `lk.pii.remote_participant_identity`, `lk.pii.room_name`. +PII keys: `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.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 @@ -124,6 +124,41 @@ Namespace of app-defined events, named `custom.` and carrying the app's ow - 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. @@ -329,6 +364,246 @@ Self-telemetry; counts since the previous report. Sent after a loss, failure, re ## Spans +### `agent_session` + +Kind `internal`. The agent session, parent of every turn. + +| Attribute | Type | Requirement | Description | +| --- | --- | --- | --- | +| `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.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. | + +No checkpoints. + +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. | + +No checkpoints. + +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. | + +No checkpoints. + +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. | + +No checkpoints. + +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.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. | + +No checkpoints. + +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. @@ -408,6 +683,563 @@ Kind `internal`. One subscription, from the intent to subscribe to the first med 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. | + +No checkpoints. + +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.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. | + +No checkpoints. + +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. | + +No checkpoints. + +No limits declared. + ## Attribute reference Every key this registry defines, with its meaning; the OTel keys used above are defined by OTel. @@ -417,7 +1249,39 @@ Every key this registry defines, with its meaning; the OTel keys used above are | --- | --- | --- | --- | --- | --- | --- | | `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 | +| `connection_reused` | string | agents | none | Whether a pooled connection was reused, as the string `true`/`false`. | `true` | | +| `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.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` | @@ -432,19 +1296,120 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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 | client | none | Why the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | | `lk.room.disconnected` | -| `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` | | +| `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.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 | -| `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 | +| `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.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.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 | +| `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.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 | +| `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` | @@ -479,6 +1444,15 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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` | @@ -501,10 +1475,21 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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.source` | enum | client | none | Source of the track. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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` | +| `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.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` | | ## Enum values @@ -593,6 +1578,23 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `reconnect_failed` | The client ran out of reconnect attempts (client only). | | `unknown` | UNKNOWN_REASON or a number the client does not know. | +### `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 | diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py new file mode 100644 index 000000000..83a2b4d0a --- /dev/null +++ b/otel/gen/python/semconv.py @@ -0,0 +1,1417 @@ +# Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. +"""LiveKit telemetry semantic conventions.""" + +#: Telemetry schema version; set as `schema_url` on every OTLP scope. +SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.4.0" + +#: Browser name. +BROWSER_NAME = "browser.name" + +#: Browser version. +BROWSER_VERSION = "browser.version" + +#: Whether a pooled connection was reused, as the string `true`/`false`. +CONNECTION_REUSED = "connection_reused" + +#: 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" + +#: 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 the session ended. +LK_DISCONNECT_REASON = "lk.disconnect.reason" +#: The client hung up (CLIENT_INITIATED). +LK_DISCONNECT_REASON_CLIENT_INITIATED = "client_initiated" +#: Another participant joined with the same identity (DUPLICATE_IDENTITY). +LK_DISCONNECT_REASON_DUPLICATE_IDENTITY = "duplicate_identity" +#: The server instance shut down (SERVER_SHUTDOWN). +LK_DISCONNECT_REASON_SERVER_SHUTDOWN = "server_shutdown" +#: RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). +LK_DISCONNECT_REASON_PARTICIPANT_REMOVED = "participant_removed" +#: RoomService.DeleteRoom was called (ROOM_DELETED). +LK_DISCONNECT_REASON_ROOM_DELETED = "room_deleted" +#: The server did not know the session the client tried to resume (STATE_MISMATCH). +LK_DISCONNECT_REASON_STATE_MISMATCH = "state_mismatch" +#: The client could not connect fully (JOIN_FAILURE). +LK_DISCONNECT_REASON_JOIN_FAILURE = "join_failure" +#: The server asked the participant to migrate (MIGRATION, Cloud only). +LK_DISCONNECT_REASON_MIGRATION = "migration" +#: The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). +LK_DISCONNECT_REASON_SIGNAL_CLOSE = "signal_close" +#: The room closed after its participants left (ROOM_CLOSED). +LK_DISCONNECT_REASON_ROOM_CLOSED = "room_closed" +#: The SIP callee did not answer in time (USER_UNAVAILABLE). +LK_DISCONNECT_REASON_USER_UNAVAILABLE = "user_unavailable" +#: The SIP callee rejected the call (USER_REJECTED). +LK_DISCONNECT_REASON_USER_REJECTED = "user_rejected" +#: SIP protocol failure (SIP_TRUNK_FAILURE). +LK_DISCONNECT_REASON_SIP_TRUNK_FAILURE = "sip_trunk_failure" +#: The server timed out the session (CONNECTION_TIMEOUT). +LK_DISCONNECT_REASON_CONNECTION_TIMEOUT = "connection_timeout" +#: Media failed or timed out (MEDIA_FAILURE). +LK_DISCONNECT_REASON_MEDIA_FAILURE = "media_failure" +#: The agent failed (AGENT_ERROR). +LK_DISCONNECT_REASON_AGENT_ERROR = "agent_error" +#: The client ran out of reconnect attempts (client only). +LK_DISCONNECT_REASON_RECONNECT_FAILED = "reconnect_failed" +#: UNKNOWN_REASON or a number the client does not know. +LK_DISCONNECT_REASON_UNKNOWN = "unknown" + +#: 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" + +#: 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" + +#: Source of the track. +LK_TRACK_SOURCE = "lk.track.source" +#: Camera (TrackSource CAMERA). +LK_TRACK_SOURCE_CAMERA = "camera" +#: Microphone (TrackSource MICROPHONE). +LK_TRACK_SOURCE_MICROPHONE = "microphone" +#: Screen share video (TrackSource SCREEN_SHARE). +LK_TRACK_SOURCE_SCREEN_SHARE = "screen_share" +#: Screen share audio (TrackSource SCREEN_SHARE_AUDIO). +LK_TRACK_SOURCE_SCREEN_SHARE_AUDIO = "screen_share_audio" +#: Unknown source (TrackSource UNKNOWN). +LK_TRACK_SOURCE_UNKNOWN = "unknown" + +#: Server-assigned track SID (`TR_...`). +LK_TRACK_SID = "lk.track_sid" + +#: 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" + + +# 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" + +#: 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" + +#: 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" + + +# 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 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 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" +#: The join response arrived. +CHECKPOINT_JOIN_RECV = "join_recv" +#: The publisher's SDP offer went out. +CHECKPOINT_OFFER_SENT = "offer_sent" +#: 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" +#: 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 platform reported the remote track subscribed. +CHECKPOINT_SUBSCRIBED = "subscribed" +#: The signaling WebSocket opened. +CHECKPOINT_WS_OPEN = "ws_open" + +# Metric names. +#: 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" +#: 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" +#: Seconds to acquire a provider connection. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_CONNECTION_ACQUIRE_TIME = "lk.agents.connection.acquire_time" +#: 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" +#: End-to-end latency of a turn. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_E2E_LATENCY = "lk.agents.turn.e2e_latency" +#: 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" +#: Time to the first LLM token. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_LLM_TTFT = "lk.agents.turn.llm_ttft" +#: 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" +#: 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" +#: Time to the first TTS byte. Instrument: histogram, unit: `s`. +METRIC_LK_AGENTS_TURN_TTS_TTFB = "lk.agents.turn.tts_ttfb" +#: Interruption-detector requests. Instrument: counter, unit: `{request}`. +METRIC_LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS = "lk.agents.usage.interruption_num_requests" +#: Realtime-model audio input tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS = "lk.agents.usage.llm_input_audio_tokens" +#: LLM cached input tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS = "lk.agents.usage.llm_input_cached_tokens" +#: Realtime-model text input tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS = "lk.agents.usage.llm_input_text_tokens" +#: LLM input tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_INPUT_TOKENS = "lk.agents.usage.llm_input_tokens" +#: Realtime-model audio output tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS = "lk.agents.usage.llm_output_audio_tokens" +#: Realtime-model text output tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS = "lk.agents.usage.llm_output_text_tokens" +#: LLM output tokens. Instrument: counter, unit: `{token}`. +METRIC_LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS = "lk.agents.usage.llm_output_tokens" +#: Realtime-model session seconds. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_LLM_SESSION_DURATION = "lk.agents.usage.llm_session_duration" +#: Seconds of audio transcribed. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_STT_AUDIO_DURATION = "lk.agents.usage.stt_audio_duration" +#: Seconds of audio synthesized. Instrument: counter, unit: `s`. +METRIC_LK_AGENTS_USAGE_TTS_AUDIO_DURATION = "lk.agents.usage.tts_audio_duration" +#: Characters synthesized. Instrument: counter, unit: `{character}`. +METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS = "lk.agents.usage.tts_characters" diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 4551672e5..588e2c27e 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.3.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.4.0"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; @@ -10,9 +10,105 @@ pub const BROWSER_NAME: &str = "browser.name"; /// Browser version. pub const BROWSER_VERSION: &str = "browser.version"; +/// Whether a pooled connection was reused, as the string `true`/`false`. +pub const CONNECTION_REUSED: &str = "connection_reused"; + +/// 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"; + +/// 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"; @@ -177,9 +273,178 @@ pub mod lk_disconnect_reason { pub const UNKNOWN: &str = "unknown"; } +/// 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"; @@ -196,6 +461,18 @@ pub mod lk_log_source { 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"; @@ -209,21 +486,117 @@ pub mod lk_outcome { 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"; + +/// 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"; @@ -261,9 +634,57 @@ pub mod lk_reconnect_reason { 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"; @@ -366,6 +787,33 @@ 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"; @@ -432,6 +880,9 @@ 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"; @@ -474,10 +925,316 @@ pub mod lk_track_source { /// Server-assigned track SID (`TR_...`). pub const LK_TRACK_SID: &str = "lk.track_sid"; +/// 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"; + +/// 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"; + } + /// 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"; + /// 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"; +} /// 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. @@ -508,6 +1265,32 @@ pub mod event { /// 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. @@ -516,6 +1299,80 @@ pub mod span { 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 @@ -529,6 +1386,8 @@ pub mod checkpoint { pub const ENGINE: &str = "engine"; /// 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"; /// The join response arrived. pub const JOIN_RECV: &str = "join_recv"; /// The publisher's SDP offer went out. @@ -546,3 +1405,51 @@ pub mod checkpoint { /// The signaling WebSocket opened. pub const WS_OPEN: &str = "ws_open"; } + +/// Metric names. +pub mod metric { + /// 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"; + /// 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"; + /// Seconds to acquire a provider connection. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_CONNECTION_ACQUIRE_TIME: &str = "lk.agents.connection.acquire_time"; + /// 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"; + /// End-to-end latency of a turn. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_E2E_LATENCY: &str = "lk.agents.turn.e2e_latency"; + /// 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"; + /// Time to the first LLM token. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_LLM_TTFT: &str = "lk.agents.turn.llm_ttft"; + /// 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"; + /// 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"; + /// Time to the first TTS byte. Instrument: histogram, unit: `s`. + pub const LK_AGENTS_TURN_TTS_TTFB: &str = "lk.agents.turn.tts_ttfb"; + /// Interruption-detector requests. Instrument: counter, unit: `{request}`. + pub const LK_AGENTS_USAGE_INTERRUPTION_NUM_REQUESTS: &str = "lk.agents.usage.interruption_num_requests"; + /// Realtime-model audio input tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_INPUT_AUDIO_TOKENS: &str = "lk.agents.usage.llm_input_audio_tokens"; + /// LLM cached input tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_INPUT_CACHED_TOKENS: &str = "lk.agents.usage.llm_input_cached_tokens"; + /// Realtime-model text input tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_INPUT_TEXT_TOKENS: &str = "lk.agents.usage.llm_input_text_tokens"; + /// LLM input tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_INPUT_TOKENS: &str = "lk.agents.usage.llm_input_tokens"; + /// Realtime-model audio output tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_AUDIO_TOKENS: &str = "lk.agents.usage.llm_output_audio_tokens"; + /// Realtime-model text output tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TEXT_TOKENS: &str = "lk.agents.usage.llm_output_text_tokens"; + /// LLM output tokens. Instrument: counter, unit: `{token}`. + pub const LK_AGENTS_USAGE_LLM_OUTPUT_TOKENS: &str = "lk.agents.usage.llm_output_tokens"; + /// Realtime-model session seconds. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_LLM_SESSION_DURATION: &str = "lk.agents.usage.llm_session_duration"; + /// Seconds of audio transcribed. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_STT_AUDIO_DURATION: &str = "lk.agents.usage.stt_audio_duration"; + /// Seconds of audio synthesized. Instrument: counter, unit: `s`. + pub const LK_AGENTS_USAGE_TTS_AUDIO_DURATION: &str = "lk.agents.usage.tts_audio_duration"; + /// Characters synthesized. Instrument: counter, unit: `{character}`. + pub const LK_AGENTS_USAGE_TTS_CHARACTERS: &str = "lk.agents.usage.tts_characters"; +} diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index d44ae27af..941b791b7 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.3.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.4.0" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; @@ -9,9 +9,105 @@ export const ATTR_BROWSER_NAME = "browser.name" as const; /** Browser version. */ export const ATTR_BROWSER_VERSION = "browser.version" as const; +/** Whether a pooled connection was reused, as the string `true`/`false`. */ +export const ATTR_CONNECTION_REUSED = "connection_reused" 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; + +/** 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. */ @@ -144,9 +240,170 @@ export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = "reconnect_failed" as /** UNKNOWN_REASON or a number the client does not know. */ export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = "unknown" 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; @@ -159,6 +416,18 @@ 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. */ @@ -168,21 +437,117 @@ 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; + +/** 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; @@ -212,9 +577,57 @@ 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; @@ -317,6 +730,33 @@ 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; @@ -383,6 +823,9 @@ export const ATTR_LK_TELEMETRY_UPLOADS_TIMEOUTS = "lk.telemetry.uploads.timeouts /** 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). */ @@ -413,8 +856,48 @@ export const LK_TRACK_SOURCE_VALUE_UNKNOWN = "unknown" as const; /** Server-assigned track SID (`TR_...`). */ export const ATTR_LK_TRACK_SID = "lk.track_sid" 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; + /** 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. */ @@ -442,6 +925,32 @@ 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. */ @@ -450,6 +959,80 @@ export const SPAN_LK_PUBLISH = "lk.publish" as const; 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 subscriber's SDP answer went out. */ export const CHECKPOINT_ANSWER_SENT = "answer_sent" as const; @@ -459,6 +1042,8 @@ export const CHECKPOINT_EARLY_PC_CREATED = "early_pc_created" as const; export const CHECKPOINT_ENGINE = "engine" 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): The join response arrived. */ export const CHECKPOINT_JOIN_RECV = "join_recv" as const; /** Checkpoint (span event): The publisher's SDP offer went out. */ @@ -475,3 +1060,69 @@ export const CHECKPOINT_SIGNAL = "signal" as const; export const CHECKPOINT_SUBSCRIBED = "subscribed" as const; /** Checkpoint (span event): The signaling WebSocket opened. */ export const CHECKPOINT_WS_OPEN = "ws_open" 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; + +/** 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: `{request}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{token}`. */ +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: `{character}`. */ +export const METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS = "lk.agents.usage.tts_characters" as const; From 590344115b721b708e3e1d008d74cf7606873bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:54:49 +0200 Subject: [PATCH 23/36] fix(otel): agents review fixes: shipped units, span events, policy allowlist Metric units are now exactly what Agents ships (nine counters unitless, unit: ""), and the Rust/Python output carries a _UNIT constant per metric so the producers can pin them. Every span-event name Agents passes to add_event is registered as a checkpoint on its span (21 names on 8 spans) and lk.pii.interim_transcript is declared. pii_heuristic_exception only takes effect for keys on the policy's own allowlist (lib.rego), with a negative fixture proving an annotated third key still fails. Schema 0.4.0 -> 0.5.0. --- otel/README.md | 4 +- otel/gen/docs/semconv.md | 54 +++++++-- otel/gen/python/semconv.py | 111 ++++++++++++++++-- otel/gen/rust/semconv.rs | 111 ++++++++++++++++-- otel/gen/ts/semconv.ts | 67 +++++++++-- otel/model/agents/registry.yaml | 1 + otel/model/agents/signals.yaml | 72 ++++++++++-- otel/model/manifest.yaml | 2 +- otel/policies/lib.rego | 14 ++- otel/templates/registry/python/semconv.py.j2 | 4 +- otel/templates/registry/rust/semconv.rs.j2 | 4 +- .../negatives/pii-heuristic-exception.yaml | 14 +++ 12 files changed, 401 insertions(+), 57 deletions(-) create mode 100644 otel/tests/negatives/pii-heuristic-exception.yaml diff --git a/otel/README.md b/otel/README.md index 7ebfd22c5..33c3f50d9 100644 --- a/otel/README.md +++ b/otel/README.md @@ -75,7 +75,7 @@ nothing validates it. | `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 | - | `pii_heuristic_exception` | a reason | a shipped key the PII heuristic would flag (`lk.rpc.caller_identity`); documents it, the collector still does not redact it | + | `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`: @@ -147,7 +147,7 @@ never here, so a producer names them from one place. TypeScript does not yet. | `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 | -| `livekit_pii_heuristic` | yes | identity, email, phone, room/participant name outside `lk.pii.*`, unless `pii_heuristic_exception` gives a reason | +| `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_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) | diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 4fd6c85ef..3b7a32ace 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.4.0`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.5.0`, 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. @@ -36,7 +36,7 @@ assigned them, and never default metric dimensions (high cardinality). **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: `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.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`. +PII keys: `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 @@ -383,7 +383,13 @@ Kind `internal`. The agent session, parent of every turn. | `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. | -No checkpoints. +| 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`). | +| `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. @@ -425,7 +431,10 @@ Kind `internal`. One agent turn (generation), parent of the LLM and TTS nodes. | `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. | -No checkpoints. +| 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. @@ -470,7 +479,9 @@ Kind `internal`. One end-of-utterance inference. | `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. | -No checkpoints. +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `superseded` | Best effort | A newer detection superseded this one. | No limits declared. @@ -486,7 +497,10 @@ Kind `internal`. The endpointing wait, from the user's last speech to the turn d | `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. | -No checkpoints. +| 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. @@ -558,7 +572,16 @@ Kind `internal`. The job's entrypoint, root of a job's 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. | -No checkpoints. +| 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. @@ -846,7 +869,9 @@ Kind `internal`. The `on_user_turn_completed` hook. | --- | --- | --- | --- | | `lk.on_user_turn_completed_delay` | double, `s` | Recommended | Seconds the `on_user_turn_completed` hook took. | -No checkpoints. +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `stop_response` | Best effort | The hook stopped the response. | No limits declared. @@ -1196,6 +1221,7 @@ Kind `internal`. One user turn, from speech to the turn 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. @@ -1211,7 +1237,11 @@ Kind `internal`. Waiting for the participant's audio track. | `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. | -No checkpoints. +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `track_subscribed` | Best effort | The track was subscribed. | +| `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. @@ -1236,7 +1266,10 @@ Kind `internal`. Waiting for the participant's video track. | `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. | -No checkpoints. +| Checkpoint | Requirement | Meaning | +| --- | --- | --- | +| `track_subscribed` | Best effort | The track was subscribed. | +| `first_frame` | Best effort | The first frame arrived. | No limits declared. @@ -1366,6 +1399,7 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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` | diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py index 83a2b4d0a..3696c2bfa 100644 --- a/otel/gen/python/semconv.py +++ b/otel/gen/python/semconv.py @@ -2,7 +2,7 @@ """LiveKit telemetry semantic conventions.""" #: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.4.0" +SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.5.0" #: Browser name. BROWSER_NAME = "browser.name" @@ -477,6 +477,9 @@ #: 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" @@ -1343,75 +1346,163 @@ # 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 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. +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. #: 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" -#: Interruption-detector requests. Instrument: counter, unit: `{request}`. +#: 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" -#: Realtime-model audio input tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: LLM cached input tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: Realtime-model text input tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: LLM input tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: Realtime-model audio output tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: Realtime-model text output tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: LLM output tokens. Instrument: counter, unit: `{token}`. +#: 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" -#: Characters synthesized. Instrument: counter, unit: `{character}`. +#: 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 index 588e2c27e..9ae892235 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.4.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.5.0"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; @@ -525,6 +525,9 @@ 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"; @@ -1378,30 +1381,74 @@ pub mod span { /// 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 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. + 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"; } @@ -1410,46 +1457,90 @@ pub mod checkpoint { pub mod metric { /// 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"; - /// Interruption-detector requests. Instrument: counter, unit: `{request}`. + /// 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"; - /// Realtime-model audio input tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// LLM cached input tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// Realtime-model text input tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// LLM input tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// Realtime-model audio output tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// Realtime-model text output tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// LLM output tokens. Instrument: counter, unit: `{token}`. + /// 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"; - /// Characters synthesized. Instrument: counter, unit: `{character}`. + /// 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 index 941b791b7..df22ef2b7 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.4.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.5.0" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; @@ -476,6 +476,9 @@ 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; @@ -1034,30 +1037,74 @@ 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 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. */ +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; @@ -1091,28 +1138,28 @@ export const METRIC_LK_AGENTS_TURN_TRANSCRIPTION_DELAY = "lk.agents.turn.transcr /** 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: `{request}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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: `{token}`. */ +/** 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`. */ @@ -1124,5 +1171,5 @@ export const METRIC_LK_AGENTS_USAGE_STT_AUDIO_DURATION = "lk.agents.usage.stt_au /** 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: `{character}`. */ +/** Characters synthesized. Instrument: counter, unit: ``. */ export const METRIC_LK_AGENTS_USAGE_TTS_CHARACTERS = "lk.agents.usage.tts_characters" as const; diff --git a/otel/model/agents/registry.yaml b/otel/model/agents/registry.yaml index 7f869de10..9706a3df5 100644 --- a/otel/model/agents/registry.yaml +++ b/otel/model/agents/registry.yaml @@ -145,6 +145,7 @@ groups: - {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}}} diff --git a/otel/model/agents/signals.yaml b/otel/model/agents/signals.yaml index 65a016315..562870ee0 100644 --- a/otel/model/agents/signals.yaml +++ b/otel/model/agents/signals.yaml @@ -15,6 +15,17 @@ groups: 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 @@ -102,6 +113,14 @@ groups: 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: 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 @@ -169,6 +188,12 @@ groups: 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."} + - {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 @@ -179,6 +204,11 @@ groups: 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."} + - {name: first_frame, requirement: best_effort, brief: "The first frame arrived."} attributes: - ref: lk.track_sid requirement_level: recommended @@ -371,6 +401,9 @@ groups: 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 @@ -398,6 +431,10 @@ groups: 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 @@ -416,6 +453,11 @@ groups: 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 @@ -432,6 +474,10 @@ groups: 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 @@ -440,6 +486,11 @@ groups: 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 @@ -815,24 +866,27 @@ groups: 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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: "{token}", 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_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: "{character}", stability: development, brief: "Characters synthesized.", 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: "{request}", stability: development, brief: "Interruption-detector requests.", 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."} diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index 8adc0227c..9d8c484de 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.4.0 +schema_url: https://livekit.io/schemas/telemetry/0.5.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). dependencies: diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego index eab00f4cf..6c40c7390 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -70,10 +70,18 @@ pii_heuristic(attr) := {finding("livekit_pii_heuristic", sprintf("%s looks like not heuristic_exception(attr) } -# A shipped key the heuristic would flag, registered as emitted with the reason it is not renamed -# (Agents' lk.rpc.caller_identity). The collector still does not redact it: this documents, it -# does not excuse. +# 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") != "" } diff --git a/otel/templates/registry/python/semconv.py.j2 b/otel/templates/registry/python/semconv.py.j2 index e2fcec88d..bfe862a60 100644 --- a/otel/templates/registry/python/semconv.py.j2 +++ b/otel/templates/registry/python/semconv.py.j2 @@ -44,6 +44,8 @@ SPAN_{{ s.type | screaming_snake_case }} = {{ lit(s.type) }} 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) }}`. +{% 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/rust/semconv.rs.j2 b/otel/templates/registry/rust/semconv.rs.j2 index 4145c911e..1f346c939 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -55,7 +55,9 @@ pub mod checkpoint { {% if ctx.metrics %} /// Metric names. pub mod metric { -{% for m in ctx.metrics %} /// {{ line(m.brief) }} Instrument: {{ m.instrument }}, unit: `{{ line(m.unit) }}`. +{% 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/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"} From 208c2d7fce23b37a14881a24172aec6ee96f2d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:58:08 +0200 Subject: [PATCH 24/36] feat(otel): share track source, disconnect reason and agent name under the Agents spelling Clients and Agents named three concepts differently. The Agents spelling and value set is now the shared key in otel/model/shared, and the client is the side that changes: - lk.track_source, values the protobuf TrackSource names (SOURCE_CAMERA, SOURCE_MICROPHONE, ...), replaces the client's lk.track.source (camera, microphone, ...) - lk.disconnect_reason, values the protobuf DisconnectReason names (CLIENT_INITIATED, UNKNOWN_REASON, ...), replaces the client's lk.disconnect.reason; RECONNECT_FAILED stays as the one client-only value - lk.agent_name is the shared agent key; the participant attribute lk.agent.name and the session report's bare agent_name are bindings The client spellings follow the pattern of the six identity/PII renames: not registered, rejected by livekit_shared_rename in the policies (new negative, both modes) and flagged by the same finding in live-check, which names the key to send. Client spans and the disconnect event now reference the shared keys; the Agents job, session and track-wait spans reference them too, with the participant_disconnected checkpoint added. On the reference client session live-check now reports 136 violations and 62 improvements (was 118 and 56), every one from the eight renames; with them applied it reports nothing. A new Agents fixture with the shared keys as Agents sends them goes from 5 findings to 0. Schema 0.5.0 -> 0.6.0. --- otel/README.md | 29 +++-- otel/gen/docs/semconv.md | 84 +++++++------ otel/gen/python/semconv.py | 111 +++++++++-------- otel/gen/rust/semconv.rs | 115 +++++++++--------- otel/gen/ts/semconv.ts | 111 +++++++++-------- otel/live-check/livekit.rego | 11 ++ otel/model/agents/signals.yaml | 18 ++- otel/model/client/registry.yaml | 53 +------- otel/model/client/signals.yaml | 8 +- otel/model/manifest.yaml | 2 +- otel/model/shared/registry.yaml | 71 +++++++++++ otel/policies/lib.rego | 13 +- .../templates/registry/markdown/semconv.md.j2 | 6 + otel/tests/live-agents.json | 13 ++ otel/tests/live-check.sh | 22 ++-- otel/tests/live-sample.json | 6 +- otel/tests/negatives/shared-rename.yaml | 12 ++ 17 files changed, 410 insertions(+), 275 deletions(-) create mode 100644 otel/tests/live-agents.json create mode 100644 otel/tests/negatives/shared-rename.yaml diff --git a/otel/README.md b/otel/README.md index 33c3f50d9..ff62fd798 100644 --- a/otel/README.md +++ b/otel/README.md @@ -20,7 +20,7 @@ policy, storage and the app API are not conventions and live with each SDK. | Path | What | |---|---| | `model/manifest.yaml` | Registry version (`schema_url`) and the pinned OTel dependency | -| `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.*`) | +| `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) | @@ -46,9 +46,14 @@ Every LiveKit key carries `annotations.livekit.owner`: - **`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; the three keys that collide with a client - spelling (`lk.track_source`, `lk.disconnect_reason`, `lk.agent_name`) are deliberately left - out until the two teams pick one, and stay literals in the Agents shims. + 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 protobuf enum names as values (`SOURCE_MICROPHONE`, +`CLIENT_INITIATED`), and `lk.agent_name` is the key a client uses for the agent. The client's +current spellings are rename targets, like the six identity/PII keys: they are not registered, a +policy rejects defining them, and live-check names the shared key to send instead. - `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 @@ -149,6 +154,7 @@ never here, so a producer names them from one place. TypeScript does not yet. | `livekit_pii_marker` | yes | `sensitivity: pii` iff a `pii` key segment | | `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 `lk.track.source`, `lk.disconnect.reason` (the client spellings of shared keys) | | `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) | @@ -171,15 +177,16 @@ It takes OTLP/JSON lines from an otelcol `file` exporter (the SDK e2e setup), or 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_flat_identity` and `livekit_pii_unmarked`, each naming -the key to send instead, and `livekit_unknown_span` for an `lk.*` span no matcher places (weaver +`live-check/livekit.rego` adds `livekit_flat_identity`, `livekit_pii_unmarked` and +`livekit_shared_rename`, each naming the key to send instead, 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 expected shared identity / PII renames: +349 attributes), every finding is one of the eight expected renames to shared keys (136 +violations, 62 improvements): | Emitted today | Registered key | Findings | |---|---|---| @@ -189,8 +196,12 @@ On the reference session (Swift 2.17.0 with the core at rust-sdks `a9ec8d52`: 6 | `lk.room.name` | `lk.pii.room_name` | `missing_attribute`, `unexpected_attribute`, `livekit_pii_unmarked` ×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 six keys renamed in the same capture, live-check reports nothing and exits 0. +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. `tests/live-sample.json` is part of that capture, anonymized. Known limits: @@ -215,7 +226,7 @@ otel/live-check.sh .jsonl # names, types, enums, se otel/live-check.sh .jsonl . --fail-on none # report only ``` -Rollout: run report-only until the six identity/PII renames land in a core, then fail the +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/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 3b7a32ace..1f4dad183 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.5.0`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.6.0`, 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. @@ -33,6 +33,11 @@ owner is in the [attribute reference](#attribute-reference). 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 protobuf enum names as values +(`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The other +spelling is never registered. + **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. @@ -276,14 +281,14 @@ Pipeline smoke test. Never emitted in production paths. Not sent by the JS port. ### `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. +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 the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | +| `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` @@ -370,6 +375,7 @@ 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. | @@ -379,6 +385,7 @@ Kind `internal`. The agent session, parent of every turn. | `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. | @@ -388,6 +395,7 @@ Kind `internal`. The agent session, parent of every turn. | `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. | @@ -555,6 +563,7 @@ 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. | @@ -660,7 +669,7 @@ Kind `internal`. One track publish. A child of the ambient span when there is on | --- | --- | --- | --- | | `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | +| `lk.track_source` | enum | Required | Source of the track, as the protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | | `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. | @@ -694,8 +703,8 @@ Kind `internal`. One subscription, from the intent to subscribe to the first med | --- | --- | --- | --- | | `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. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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 protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | | `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. | @@ -1236,10 +1245,11 @@ Kind `internal`. Waiting for the participant's audio track. | --- | --- | --- | --- | | `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 protobuf `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | | Checkpoint | Requirement | Meaning | | --- | --- | --- | -| `track_subscribed` | Best effort | The track was subscribed. | +| `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`. | @@ -1265,10 +1275,11 @@ Kind `internal`. Waiting for the participant's video track. | --- | --- | --- | --- | | `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 protobuf `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | | Checkpoint | Requirement | Meaning | | --- | --- | --- | -| `track_subscribed` | Best effort | The track was subscribed. | +| `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. @@ -1292,6 +1303,7 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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` | @@ -1328,7 +1340,7 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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 | client | none | Why the session ended. The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). | | `lk.room.disconnected` | +| `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` | @@ -1512,8 +1524,8 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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.source` | enum | client | none | Source of the track. The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead (`lk.track_source = SOURCE_MICROPHONE`). | | `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 protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | | `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` | @@ -1589,28 +1601,28 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `serious` | High; the system reduces performance. | | `critical` | Critical; the device needs to cool down. | -### `lk.disconnect.reason` +### `lk.disconnect_reason` | Value | Meaning | | --- | --- | -| `client_initiated` | The client hung up (CLIENT_INITIATED). | -| `duplicate_identity` | Another participant joined with the same identity (DUPLICATE_IDENTITY). | -| `server_shutdown` | The server instance shut down (SERVER_SHUTDOWN). | -| `participant_removed` | RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). | -| `room_deleted` | RoomService.DeleteRoom was called (ROOM_DELETED). | -| `state_mismatch` | The server did not know the session the client tried to resume (STATE_MISMATCH). | -| `join_failure` | The client could not connect fully (JOIN_FAILURE). | -| `migration` | The server asked the participant to migrate (MIGRATION, Cloud only). | -| `signal_close` | The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). | -| `room_closed` | The room closed after its participants left (ROOM_CLOSED). | -| `user_unavailable` | The SIP callee did not answer in time (USER_UNAVAILABLE). | -| `user_rejected` | The SIP callee rejected the call (USER_REJECTED). | -| `sip_trunk_failure` | SIP protocol failure (SIP_TRUNK_FAILURE). | -| `connection_timeout` | The server timed out the session (CONNECTION_TIMEOUT). | -| `media_failure` | Media failed or timed out (MEDIA_FAILURE). | -| `agent_error` | The agent failed (AGENT_ERROR). | -| `reconnect_failed` | The client ran out of reconnect attempts (client only). | -| `unknown` | UNKNOWN_REASON or a number the client does not know. | +| `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` @@ -1679,15 +1691,15 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `audio` | Audio. | | `video` | Video. | -### `lk.track.source` +### `lk.track_source` | Value | Meaning | | --- | --- | -| `camera` | Camera (TrackSource CAMERA). | -| `microphone` | Microphone (TrackSource MICROPHONE). | -| `screen_share` | Screen share video (TrackSource SCREEN_SHARE). | -| `screen_share_audio` | Screen share audio (TrackSource SCREEN_SHARE_AUDIO). | -| `unknown` | Unknown source (TrackSource UNKNOWN). | +| `SOURCE_UNKNOWN` | Unknown source. | +| `SOURCE_CAMERA` | Camera. | +| `SOURCE_MICROPHONE` | Microphone. | +| `SOURCE_SCREENSHARE` | Screen share video. | +| `SOURCE_SCREENSHARE_AUDIO` | Screen share audio. | ## Bindings @@ -1696,12 +1708,14 @@ 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` | -| `lk.track.source` | agents_otlp | `lk.track_source` | `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`. \ No newline at end of file diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py index 3696c2bfa..5d7ce5b66 100644 --- a/otel/gen/python/semconv.py +++ b/otel/gen/python/semconv.py @@ -2,7 +2,7 @@ """LiveKit telemetry semantic conventions.""" #: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.5.0" +SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.0" #: Browser name. BROWSER_NAME = "browser.name" @@ -40,6 +40,9 @@ #: 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" @@ -202,44 +205,44 @@ #: Critical; the device needs to cool down. LK_DEVICE_THERMAL_STATE_CRITICAL = "critical" -#: Why the session ended. -LK_DISCONNECT_REASON = "lk.disconnect.reason" -#: The client hung up (CLIENT_INITIATED). -LK_DISCONNECT_REASON_CLIENT_INITIATED = "client_initiated" -#: Another participant joined with the same identity (DUPLICATE_IDENTITY). -LK_DISCONNECT_REASON_DUPLICATE_IDENTITY = "duplicate_identity" -#: The server instance shut down (SERVER_SHUTDOWN). -LK_DISCONNECT_REASON_SERVER_SHUTDOWN = "server_shutdown" -#: RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). -LK_DISCONNECT_REASON_PARTICIPANT_REMOVED = "participant_removed" -#: RoomService.DeleteRoom was called (ROOM_DELETED). -LK_DISCONNECT_REASON_ROOM_DELETED = "room_deleted" -#: The server did not know the session the client tried to resume (STATE_MISMATCH). -LK_DISCONNECT_REASON_STATE_MISMATCH = "state_mismatch" -#: The client could not connect fully (JOIN_FAILURE). -LK_DISCONNECT_REASON_JOIN_FAILURE = "join_failure" -#: The server asked the participant to migrate (MIGRATION, Cloud only). -LK_DISCONNECT_REASON_MIGRATION = "migration" -#: The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). -LK_DISCONNECT_REASON_SIGNAL_CLOSE = "signal_close" -#: The room closed after its participants left (ROOM_CLOSED). -LK_DISCONNECT_REASON_ROOM_CLOSED = "room_closed" -#: The SIP callee did not answer in time (USER_UNAVAILABLE). -LK_DISCONNECT_REASON_USER_UNAVAILABLE = "user_unavailable" -#: The SIP callee rejected the call (USER_REJECTED). -LK_DISCONNECT_REASON_USER_REJECTED = "user_rejected" -#: SIP protocol failure (SIP_TRUNK_FAILURE). -LK_DISCONNECT_REASON_SIP_TRUNK_FAILURE = "sip_trunk_failure" -#: The server timed out the session (CONNECTION_TIMEOUT). -LK_DISCONNECT_REASON_CONNECTION_TIMEOUT = "connection_timeout" -#: Media failed or timed out (MEDIA_FAILURE). -LK_DISCONNECT_REASON_MEDIA_FAILURE = "media_failure" -#: The agent failed (AGENT_ERROR). -LK_DISCONNECT_REASON_AGENT_ERROR = "agent_error" -#: The client ran out of reconnect attempts (client only). -LK_DISCONNECT_REASON_RECONNECT_FAILED = "reconnect_failed" -#: UNKNOWN_REASON or a number the client does not know. -LK_DISCONNECT_REASON_UNKNOWN = "unknown" +#: 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" @@ -844,22 +847,22 @@ #: Video. LK_TRACK_KIND_VIDEO = "video" -#: Source of the track. -LK_TRACK_SOURCE = "lk.track.source" -#: Camera (TrackSource CAMERA). -LK_TRACK_SOURCE_CAMERA = "camera" -#: Microphone (TrackSource MICROPHONE). -LK_TRACK_SOURCE_MICROPHONE = "microphone" -#: Screen share video (TrackSource SCREEN_SHARE). -LK_TRACK_SOURCE_SCREEN_SHARE = "screen_share" -#: Screen share audio (TrackSource SCREEN_SHARE_AUDIO). -LK_TRACK_SOURCE_SCREEN_SHARE_AUDIO = "screen_share_audio" -#: Unknown source (TrackSource UNKNOWN). -LK_TRACK_SOURCE_UNKNOWN = "unknown" - #: Server-assigned track SID (`TR_...`). LK_TRACK_SID = "lk.track_sid" +#: Source of the track, as the protobuf `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" @@ -1384,6 +1387,8 @@ 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. @@ -1408,7 +1413,7 @@ CHECKPOINT_SUBSCRIBED = "subscribed" #: A newer detection superseded this one. CHECKPOINT_SUPERSEDED = "superseded" -#: The track was subscribed. +#: 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" diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 9ae892235..76772ef18 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.5.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.6.0"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; @@ -40,6 +40,9 @@ pub const LANGFUSE_OBSERVATION_COMPLETION_START_TIME: &str = "langfuse.observati /// 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"; @@ -230,47 +233,47 @@ pub mod lk_device_thermal_state { pub const CRITICAL: &str = "critical"; } -/// Why the session ended. -pub const LK_DISCONNECT_REASON: &str = "lk.disconnect.reason"; +/// 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 { - /// The client hung up (CLIENT_INITIATED). - pub const CLIENT_INITIATED: &str = "client_initiated"; - /// Another participant joined with the same identity (DUPLICATE_IDENTITY). - pub const DUPLICATE_IDENTITY: &str = "duplicate_identity"; - /// The server instance shut down (SERVER_SHUTDOWN). - pub const SERVER_SHUTDOWN: &str = "server_shutdown"; - /// RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). - pub const PARTICIPANT_REMOVED: &str = "participant_removed"; - /// RoomService.DeleteRoom was called (ROOM_DELETED). - pub const ROOM_DELETED: &str = "room_deleted"; - /// The server did not know the session the client tried to resume (STATE_MISMATCH). - pub const STATE_MISMATCH: &str = "state_mismatch"; - /// The client could not connect fully (JOIN_FAILURE). - pub const JOIN_FAILURE: &str = "join_failure"; - /// The server asked the participant to migrate (MIGRATION, Cloud only). - pub const MIGRATION: &str = "migration"; - /// The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). - pub const SIGNAL_CLOSE: &str = "signal_close"; - /// The room closed after its participants left (ROOM_CLOSED). - pub const ROOM_CLOSED: &str = "room_closed"; - /// The SIP callee did not answer in time (USER_UNAVAILABLE). - pub const USER_UNAVAILABLE: &str = "user_unavailable"; - /// The SIP callee rejected the call (USER_REJECTED). - pub const USER_REJECTED: &str = "user_rejected"; - /// SIP protocol failure (SIP_TRUNK_FAILURE). - pub const SIP_TRUNK_FAILURE: &str = "sip_trunk_failure"; - /// The server timed out the session (CONNECTION_TIMEOUT). - pub const CONNECTION_TIMEOUT: &str = "connection_timeout"; - /// Media failed or timed out (MEDIA_FAILURE). - pub const MEDIA_FAILURE: &str = "media_failure"; - /// The agent failed (AGENT_ERROR). - pub const AGENT_ERROR: &str = "agent_error"; - /// The client ran out of reconnect attempts (client only). - pub const RECONNECT_FAILED: &str = "reconnect_failed"; - /// UNKNOWN_REASON or a number the client does not know. - pub const UNKNOWN: &str = "unknown"; + /// 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. @@ -908,26 +911,26 @@ pub mod lk_track_kind { pub const VIDEO: &str = "video"; } -/// Source of the track. -pub const LK_TRACK_SOURCE: &str = "lk.track.source"; +/// Server-assigned track SID (`TR_...`). +pub const LK_TRACK_SID: &str = "lk.track_sid"; + +/// Source of the track, as the protobuf `TrackSource` enum name. +pub const LK_TRACK_SOURCE: &str = "lk.track_source"; /// Values of [`LK_TRACK_SOURCE`]. pub mod lk_track_source { - /// Camera (TrackSource CAMERA). - pub const CAMERA: &str = "camera"; - /// Microphone (TrackSource MICROPHONE). - pub const MICROPHONE: &str = "microphone"; - /// Screen share video (TrackSource SCREEN_SHARE). - pub const SCREEN_SHARE: &str = "screen_share"; - /// Screen share audio (TrackSource SCREEN_SHARE_AUDIO). - pub const SCREEN_SHARE_AUDIO: &str = "screen_share_audio"; - /// Unknown source (TrackSource UNKNOWN). - pub const UNKNOWN: &str = "unknown"; + /// 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"; } -/// Server-assigned track SID (`TR_...`). -pub const LK_TRACK_SID: &str = "lk.track_sid"; - /// Confidence of the final transcript. pub const LK_TRANSCRIPT_CONFIDENCE: &str = "lk.transcript_confidence"; @@ -1419,6 +1422,8 @@ pub mod checkpoint { 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. @@ -1443,7 +1448,7 @@ pub mod checkpoint { pub const SUBSCRIBED: &str = "subscribed"; /// A newer detection superseded this one. pub const SUPERSEDED: &str = "superseded"; - /// The track was subscribed. + /// 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"; diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index df22ef2b7..b2074966c 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.5.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.0" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; @@ -39,6 +39,9 @@ export const ATTR_LANGFUSE_OBSERVATION_COMPLETION_START_TIME = "langfuse.observa /** 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; @@ -201,44 +204,44 @@ 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 the session ended. */ -export const ATTR_LK_DISCONNECT_REASON = "lk.disconnect.reason" as const; -/** The client hung up (CLIENT_INITIATED). */ -export const LK_DISCONNECT_REASON_VALUE_CLIENT_INITIATED = "client_initiated" as const; -/** Another participant joined with the same identity (DUPLICATE_IDENTITY). */ -export const LK_DISCONNECT_REASON_VALUE_DUPLICATE_IDENTITY = "duplicate_identity" as const; -/** The server instance shut down (SERVER_SHUTDOWN). */ -export const LK_DISCONNECT_REASON_VALUE_SERVER_SHUTDOWN = "server_shutdown" as const; -/** RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED). */ -export const LK_DISCONNECT_REASON_VALUE_PARTICIPANT_REMOVED = "participant_removed" as const; -/** RoomService.DeleteRoom was called (ROOM_DELETED). */ -export const LK_DISCONNECT_REASON_VALUE_ROOM_DELETED = "room_deleted" as const; -/** The server did not know the session the client tried to resume (STATE_MISMATCH). */ -export const LK_DISCONNECT_REASON_VALUE_STATE_MISMATCH = "state_mismatch" as const; -/** The client could not connect fully (JOIN_FAILURE). */ -export const LK_DISCONNECT_REASON_VALUE_JOIN_FAILURE = "join_failure" as const; -/** The server asked the participant to migrate (MIGRATION, Cloud only). */ -export const LK_DISCONNECT_REASON_VALUE_MIGRATION = "migration" as const; -/** The signal WebSocket closed unexpectedly (SIGNAL_CLOSE). */ -export const LK_DISCONNECT_REASON_VALUE_SIGNAL_CLOSE = "signal_close" as const; -/** The room closed after its participants left (ROOM_CLOSED). */ -export const LK_DISCONNECT_REASON_VALUE_ROOM_CLOSED = "room_closed" as const; -/** The SIP callee did not answer in time (USER_UNAVAILABLE). */ -export const LK_DISCONNECT_REASON_VALUE_USER_UNAVAILABLE = "user_unavailable" as const; -/** The SIP callee rejected the call (USER_REJECTED). */ -export const LK_DISCONNECT_REASON_VALUE_USER_REJECTED = "user_rejected" as const; -/** SIP protocol failure (SIP_TRUNK_FAILURE). */ -export const LK_DISCONNECT_REASON_VALUE_SIP_TRUNK_FAILURE = "sip_trunk_failure" as const; -/** The server timed out the session (CONNECTION_TIMEOUT). */ -export const LK_DISCONNECT_REASON_VALUE_CONNECTION_TIMEOUT = "connection_timeout" as const; -/** Media failed or timed out (MEDIA_FAILURE). */ -export const LK_DISCONNECT_REASON_VALUE_MEDIA_FAILURE = "media_failure" as const; -/** The agent failed (AGENT_ERROR). */ -export const LK_DISCONNECT_REASON_VALUE_AGENT_ERROR = "agent_error" as const; -/** The client ran out of reconnect attempts (client only). */ -export const LK_DISCONNECT_REASON_VALUE_RECONNECT_FAILED = "reconnect_failed" as const; -/** UNKNOWN_REASON or a number the client does not know. */ -export const LK_DISCONNECT_REASON_VALUE_UNKNOWN = "unknown" 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; @@ -843,22 +846,22 @@ export const LK_TRACK_KIND_VALUE_AUDIO = "audio" as const; /** Video. */ export const LK_TRACK_KIND_VALUE_VIDEO = "video" as const; -/** Source of the track. */ -export const ATTR_LK_TRACK_SOURCE = "lk.track.source" as const; -/** Camera (TrackSource CAMERA). */ -export const LK_TRACK_SOURCE_VALUE_CAMERA = "camera" as const; -/** Microphone (TrackSource MICROPHONE). */ -export const LK_TRACK_SOURCE_VALUE_MICROPHONE = "microphone" as const; -/** Screen share video (TrackSource SCREEN_SHARE). */ -export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE = "screen_share" as const; -/** Screen share audio (TrackSource SCREEN_SHARE_AUDIO). */ -export const LK_TRACK_SOURCE_VALUE_SCREEN_SHARE_AUDIO = "screen_share_audio" as const; -/** Unknown source (TrackSource UNKNOWN). */ -export const LK_TRACK_SOURCE_VALUE_UNKNOWN = "unknown" as const; - /** Server-assigned track SID (`TR_...`). */ export const ATTR_LK_TRACK_SID = "lk.track_sid" as const; +/** Source of the track, as the protobuf `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; @@ -1075,6 +1078,8 @@ export const CHECKPOINT_JOB_STARTED_ON_SERVER = "job_started_on_server" as const 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. */ @@ -1099,7 +1104,7 @@ export const CHECKPOINT_STOP_RESPONSE = "stop_response" as const; 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. */ +/** 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; diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index 4553f4235..71dc0dcec 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -17,6 +17,12 @@ canonical_identity := { "lk.job.id": "lk.job_id", } +# Client spellings of shared keys whose Agents spelling and value set won (otel/model/shared). +renamed_shared := { + "lk.track.source": "lk.track_source", + "lk.disconnect.reason": "lk.disconnect_reason", +} + canonical_pii := { "lk.room.name": "lk.pii.room_name", "lk.participant.identity": "lk.pii.participant_identity", @@ -39,6 +45,11 @@ deny contains lk_advice("livekit_pii_unmarked", {"attribute_key": key, "expected hint := object.get({k: sprintf("; send '%s'", [v]) | some k, v in canonical_pii}, key, "") } +deny contains lk_advice("livekit_shared_rename", {"attribute_key": key, "expected": want}, sprintf("'%s' is the client spelling of the shared key; send '%s' with its protobuf enum name as the value", [key, want])) if { + key := input.sample.attribute.name + want := renamed_shared[key] +} + deny contains lk_advice("livekit_flat_identity", {"attribute_key": key, "expected": want}, sprintf("'%s' must be emitted as '%s'", [key, want])) if { key := input.sample.attribute.name want := canonical_identity[key] diff --git a/otel/model/agents/signals.yaml b/otel/model/agents/signals.yaml index 562870ee0..5592877b0 100644 --- a/otel/model/agents/signals.yaml +++ b/otel/model/agents/signals.yaml @@ -33,6 +33,8 @@ groups: 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 @@ -119,6 +121,7 @@ groups: - {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: @@ -129,6 +132,11 @@ groups: 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 @@ -191,12 +199,15 @@ groups: annotations: livekit: checkpoints: - - {name: track_subscribed, requirement: best_effort, brief: "The track was subscribed."} + - {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 @@ -207,11 +218,14 @@ groups: annotations: livekit: checkpoints: - - {name: track_subscribed, requirement: best_effort, brief: "The track was subscribed."} + - {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 diff --git a/otel/model/client/registry.yaml b/otel/model/client/registry.yaml index 32ee598d4..7f6c0af0f 100644 --- a/otel/model/client/registry.yaml +++ b/otel/model/client/registry.yaml @@ -159,7 +159,7 @@ groups: - 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 is shared (`lk.track_sid`). + 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 @@ -171,25 +171,6 @@ groups: - {id: video, value: video, brief: Video., stability: development} annotations: livekit: {owner: client, sensitivity: none} - - id: lk.track.source - stability: development - brief: Source of the track. - note: > - The protocol's `TrackSource` in snake_case. Agents sends the FFI enum names instead - (`lk.track_source = SOURCE_MICROPHONE`). - type: - members: - - {id: camera, value: camera, brief: Camera (TrackSource CAMERA)., stability: development} - - {id: microphone, value: microphone, brief: Microphone (TrackSource MICROPHONE)., stability: development} - - {id: screen_share, value: screen_share, brief: Screen share video (TrackSource SCREEN_SHARE)., stability: development} - - {id: screen_share_audio, value: screen_share_audio, brief: Screen share audio (TrackSource SCREEN_SHARE_AUDIO)., stability: development} - - {id: unknown, value: unknown, brief: Unknown source (TrackSource UNKNOWN)., stability: development} - annotations: - livekit: - owner: client - sensitivity: none - bindings: - agents_otlp: lk.track_source # values are the FFI enum names, e.g. SOURCE_MICROPHONE - id: lk.track.direction stability: development brief: Whether the stats describe sent or received media. @@ -534,38 +515,6 @@ groups: annotations: livekit: {owner: client, sensitivity: none} - - id: registry.lk.session - type: attribute_group - display_name: LiveKit client session - brief: Session lifecycle attributes. - attributes: - - id: lk.disconnect.reason - stability: development - brief: Why the session ended. - note: The protocol's `DisconnectReason`, plus the client giving up (`reconnect_failed`). - type: - members: - - {id: client_initiated, value: client_initiated, brief: The client hung up (CLIENT_INITIATED)., stability: development} - - {id: duplicate_identity, value: duplicate_identity, brief: Another participant joined with the same identity (DUPLICATE_IDENTITY)., stability: development} - - {id: server_shutdown, value: server_shutdown, brief: The server instance shut down (SERVER_SHUTDOWN)., stability: development} - - {id: participant_removed, value: participant_removed, brief: RoomService.RemoveParticipant was called (PARTICIPANT_REMOVED)., stability: development} - - {id: room_deleted, value: room_deleted, brief: RoomService.DeleteRoom was called (ROOM_DELETED)., stability: development} - - {id: state_mismatch, value: state_mismatch, brief: The server did not know the session the client tried to resume (STATE_MISMATCH)., stability: development} - - {id: join_failure, value: join_failure, brief: The client could not connect fully (JOIN_FAILURE)., stability: development} - - {id: migration, value: migration, brief: "The server asked the participant to migrate (MIGRATION, Cloud only).", stability: development} - - {id: signal_close, value: signal_close, brief: The signal WebSocket closed unexpectedly (SIGNAL_CLOSE)., stability: development} - - {id: room_closed, value: room_closed, brief: The room closed after its participants left (ROOM_CLOSED)., stability: development} - - {id: user_unavailable, value: user_unavailable, brief: The SIP callee did not answer in time (USER_UNAVAILABLE)., stability: development} - - {id: user_rejected, value: user_rejected, brief: The SIP callee rejected the call (USER_REJECTED)., stability: development} - - {id: sip_trunk_failure, value: sip_trunk_failure, brief: SIP protocol failure (SIP_TRUNK_FAILURE)., stability: development} - - {id: connection_timeout, value: connection_timeout, brief: The server timed out the session (CONNECTION_TIMEOUT)., stability: development} - - {id: media_failure, value: media_failure, brief: Media failed or timed out (MEDIA_FAILURE)., stability: development} - - {id: agent_error, value: agent_error, brief: The agent failed (AGENT_ERROR)., stability: development} - - {id: reconnect_failed, value: reconnect_failed, brief: The client ran out of reconnect attempts (client only)., stability: development} - - {id: unknown, value: unknown, brief: UNKNOWN_REASON or a number the client does not know., stability: development} - annotations: - livekit: {owner: client, sensitivity: none} - - id: registry.lk.log type: attribute_group display_name: LiveKit client log records diff --git a/otel/model/client/signals.yaml b/otel/model/client/signals.yaml index 251cbcb80..b4ea45da9 100644 --- a/otel/model/client/signals.yaml +++ b/otel/model/client/signals.yaml @@ -304,11 +304,11 @@ groups: 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. + 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 + - ref: lk.disconnect_reason requirement_level: required - id: event.custom @@ -408,7 +408,7 @@ groups: attributes: - ref: lk.track.kind requirement_level: required - - ref: lk.track.source + - ref: lk.track_source requirement_level: required - ref: lk.track_sid requirement_level: {conditionally_required: On success; the server assigns it.} @@ -441,7 +441,7 @@ groups: requirement_level: required - ref: lk.track.kind requirement_level: required - - ref: lk.track.source + - ref: lk.track_source requirement_level: required - ref: lk.pii.remote_participant_identity requirement_level: recommended diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index 9d8c484de..19c63fa43 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.5.0 +schema_url: https://livekit.io/schemas/telemetry/0.6.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). dependencies: diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml index de30911a3..7e3a154ea 100644 --- a/otel/model/shared/registry.yaml +++ b/otel/model/shared/registry.yaml @@ -130,3 +130,74 @@ groups: 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 protobuf enum names as values. + attributes: + - id: lk.track_source + stability: development + brief: Source of the track, as the protobuf `TrackSource` enum name. + note: > + On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. + The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside + SCREENSHARE). + 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} + - 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} + - 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 index 6c40c7390..ccda4c2f7 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -22,6 +22,13 @@ canonical_identity := { "lk.job.id": "lk.job_id", } +# Client spellings of shared keys whose Agents spelling and value set won (otel/model/shared). +# Keep in sync with otel/live-check/livekit.rego. +renamed_shared := { + "lk.track.source": "lk.track_source", + "lk.disconnect.reason": "lk.disconnect_reason", +} + high_cardinality := {"lk.room_sid", "lk.participant_id", "lk.track_sid", "lk.job_id", "lk.client.connect_id"} owners := {"shared", "client", "agents", "cloud"} @@ -45,7 +52,7 @@ ms_exception(attr) if { } # 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) +attr_findings(attr) := (((((((owner(attr) | sensitivity(attr)) | secret(attr)) | pii_marker(attr)) | pii_heuristic(attr)) | flat_identity(attr)) | shared_rename(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")] @@ -86,6 +93,10 @@ heuristic_exception(attr) if { lk(attr, "pii_heuristic_exception") != "" } +shared_rename(attr) := {finding("livekit_shared_rename", sprintf("%s: the shared key is %s (the Agents spelling); do not define the client spelling", [attr.id, renamed_shared[attr.id]])) | + renamed_shared[attr.id] +} + 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] } diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index 2729f00a4..9f82740a8 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -59,6 +59,11 @@ owner is in the [attribute reference](#attribute-reference). 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 protobuf enum names as values +(`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The other +spelling is never registered. + **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. @@ -164,4 +169,5 @@ validates it. {% 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`. 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 index d6a054f4b..403f2d4bb 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -1,10 +1,10 @@ #!/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 and a record -# without severity text. The same smoke sample with the client's current identity/PII 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. +# 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. set -euo pipefail cd "$(dirname "$0")/.." tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT @@ -23,6 +23,11 @@ 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 @@ -39,13 +44,16 @@ sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.na -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"/' \ 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_flat_identity lk.room.sid" "livekit_pii_unmarked 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"; do + "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"; do id=${want% *} key=${want#* } grep -q -E -- "${id}: .*${key//./\\.}" <<<"$legacy" || { echo "FAIL not flagged: $id on $key" >&2; exit 1; } done -echo "PASS live-check: smoke, sparse, JS and no-severity-text samples clean, every planted violation flagged" +echo "PASS live-check: smoke, sparse, JS, Agents and no-severity-text samples clean, every planted violation flagged" diff --git a/otel/tests/live-sample.json b/otel/tests/live-sample.json index 43a673e8d..60457def8 100644 --- a/otel/tests/live-sample.json +++ b/otel/tests/live-sample.json @@ -1,11 +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": "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": "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": []}}, + {"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"}]}} + {"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/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}} From c3ea8d70785bcf0b424bae872e7e6ea09066974c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:31:03 +0200 Subject: [PATCH 25/36] fix(otel): record the eight renames in the model and enforce them whatever the deprecation Codex round 5. A deprecated old spelling slipped past livekit_shared_rename: the rule ran only on non-deprecated attributes, so defining lk.track.source as deprecated and requiring it from a span passed both registry-check modes and policy-gated generation. Each shared key now records the spelling it replaces in annotations.livekit.renamed_from: the old key for all eight renames (the six identity/PII keys, lk.track.source, lk.disconnect.reason) and, for the two enums, the map of old values to members (5 track source, 18 disconnect reason). The old spellings stay unregistered. - policies: livekit_shared_rename moves to resolved.rego, derives the map from renamed_from, and fires on any definition of an old spelling, deprecated or not, in both modes; livekit_rename_value rejects a mapped value that is not a member of the target enum. New negatives shared-rename-deprecated (Codex's probe) and rename-value, both modes. - live-check: live-check/renames.jq builds the map from the model; one livekit_shared_rename finding per old spelling names the key and value to send. The hard-coded identity, PII and shared maps are gone. The real session stays at 136 violations / 62 improvements. - docs: the spec lists every old spelling from renamed_from; the track source values are the Agents RTC FFI names, not the signal protocol's, so clients map explicitly; README drops a duplicated sentence. Schema 0.6.0 -> 0.6.1 (annotations and notes only). --- otel/README.md | 29 ++++++--- otel/gen/docs/semconv.md | 62 +++++++++++++++---- otel/gen/python/semconv.py | 4 +- otel/gen/rust/semconv.rs | 4 +- otel/gen/ts/semconv.ts | 4 +- otel/live-check.sh | 3 +- otel/live-check/livekit.rego | 51 +++++++-------- otel/live-check/renames.jq | 6 ++ otel/model/manifest.yaml | 2 +- otel/model/shared/registry.yaml | 52 ++++++++++++---- otel/policies/lib.rego | 13 +--- otel/policies/resolved.rego | 32 ++++++++++ .../templates/registry/markdown/semconv.md.j2 | 22 +++++-- otel/tests/live-check.sh | 10 ++- otel/tests/negatives/rename-value.yaml | 17 +++++ .../negatives/shared-rename-deprecated.yaml | 22 +++++++ 16 files changed, 247 insertions(+), 86 deletions(-) create mode 100644 otel/live-check/renames.jq create mode 100644 otel/tests/negatives/rename-value.yaml create mode 100644 otel/tests/negatives/shared-rename-deprecated.yaml diff --git a/otel/README.md b/otel/README.md index ff62fd798..cbfcfc2d2 100644 --- a/otel/README.md +++ b/otel/README.md @@ -50,10 +50,16 @@ Every LiveKit key carries `annotations.livekit.owner`: 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 protobuf enum names as values (`SOURCE_MICROPHONE`, -`CLIENT_INITIATED`), and `lk.agent_name` is the key a client uses for the agent. The client's -current spellings are rename targets, like the six identity/PII keys: they are not registered, a -policy rejects defining them, and live-check names the shared key to send instead. +`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 @@ -80,6 +86,7 @@ nothing validates it. | `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 @@ -154,7 +161,8 @@ never here, so a producer names them from one place. TypeScript does not yet. | `livekit_pii_marker` | yes | `sensitivity: pii` iff a `pii` key segment | | `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 `lk.track.source`, `lk.disconnect.reason` (the client spellings of shared keys) | +| `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) | @@ -177,8 +185,10 @@ It takes OTLP/JSON lines from an otelcol `file` exporter (the SDK e2e setup), or 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_flat_identity`, `livekit_pii_unmarked` and -`livekit_shared_rename`, each naming the key to send instead, and `livekit_unknown_span` for an `lk.*` span no matcher places (weaver +`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 @@ -190,10 +200,10 @@ violations, 62 improvements): | Emitted today | Registered key | Findings | |---|---|---| -| `lk.room.sid` | `lk.room_sid` | `missing_attribute`, `unexpected_attribute`, `livekit_flat_identity` ×10 each | +| `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_pii_unmarked` ×10 each | +| `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 | @@ -202,7 +212,6 @@ violations, 62 improvements): 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. -`tests/live-sample.json` is part of that capture, anonymized. Known limits: diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 1f4dad183..97dfcfcf2 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.6.0`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.6.1`, 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. @@ -13,7 +13,8 @@ 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) +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) · +[Old spellings](#old-spellings) ## Conventions @@ -34,9 +35,10 @@ They are record and span attributes, never Resource attributes, omitted until th 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 protobuf enum names as values -(`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The other -spelling is never registered. +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). **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 @@ -669,7 +671,7 @@ Kind `internal`. One track publish. A child of the ambient span when there is on | --- | --- | --- | --- | | `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 protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | +| `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. | @@ -704,7 +706,7 @@ Kind `internal`. One subscription, from the intent to subscribe to the first med | `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 protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | +| `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. | @@ -1245,7 +1247,7 @@ Kind `internal`. Waiting for the participant's audio track. | --- | --- | --- | --- | | `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 protobuf `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | +| `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 | | --- | --- | --- | @@ -1275,7 +1277,7 @@ Kind `internal`. Waiting for the participant's video track. | --- | --- | --- | --- | | `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 protobuf `TrackSource` enum name. Sent on the `track_subscribed` checkpoint, not on the span. | +| `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 | | --- | --- | --- | @@ -1525,7 +1527,7 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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 protobuf `TrackSource` enum name. On client publish and subscribe spans, and on the Agents `track_subscribed` checkpoint. The value is the enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside SCREENSHARE). | | `lk.publish` `lk.subscribe` `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` | @@ -1718,4 +1720,42 @@ validates it. `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`. \ No newline at end of file +`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 index 5d7ce5b66..ed75ad891 100644 --- a/otel/gen/python/semconv.py +++ b/otel/gen/python/semconv.py @@ -2,7 +2,7 @@ """LiveKit telemetry semantic conventions.""" #: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.0" +SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.1" #: Browser name. BROWSER_NAME = "browser.name" @@ -850,7 +850,7 @@ #: Server-assigned track SID (`TR_...`). LK_TRACK_SID = "lk.track_sid" -#: Source of the track, as the protobuf `TrackSource` enum name. +#: 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" diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 76772ef18..6d747ff46 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -2,7 +2,7 @@ //! LiveKit telemetry semantic conventions. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.6.0"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.6.1"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; @@ -914,7 +914,7 @@ pub mod lk_track_kind { /// Server-assigned track SID (`TR_...`). pub const LK_TRACK_SID: &str = "lk.track_sid"; -/// Source of the track, as the protobuf `TrackSource` enum name. +/// 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`]. diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index b2074966c..171556575 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,7 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.0" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.1" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; @@ -849,7 +849,7 @@ 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 protobuf `TrackSource` enum name. */ +/** 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; diff --git a/otel/live-check.sh b/otel/live-check.sh index 6a4aa8e98..c1f59d0a4 100755 --- a/otel/live-check.sh +++ b/otel/live-check.sh @@ -18,7 +18,8 @@ fi status=0 "$W" registry live-check --config "$here/live-check/weaver.toml" -r "$here/model" --v2 \ - --advice-policies "$here/live-check" --input-source "$samples" --input-format json \ + --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=$? python3 -I "$here/live-check/summarize.py" "$out/live_check.json" echo "report: $out/live_check.json" diff --git a/otel/live-check/livekit.rego b/otel/live-check/livekit.rego index 71dc0dcec..ba5f1cfac 100644 --- a/otel/live-check/livekit.rego +++ b/otel/live-check/livekit.rego @@ -1,7 +1,7 @@ package live_check_advice -# Same contract as otel/policies/livekit.rego, applied to emitted telemetry. Each finding names -# the registered key a producer should send instead. +# 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 @@ -9,25 +9,9 @@ pii_segment := `(^|\.)pii(\.|$)` 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", -} - -# Client spellings of shared keys whose Agents spelling and value set won (otel/model/shared). -renamed_shared := { - "lk.track.source": "lk.track_source", - "lk.disconnect.reason": "lk.disconnect_reason", -} - -canonical_pii := { - "lk.room.name": "lk.pii.room_name", - "lk.participant.identity": "lk.pii.participant_identity", - "lk.participant.remote_identity": "lk.pii.remote_participant_identity", -} +# 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", @@ -37,22 +21,33 @@ lk_advice(type, ctx, msg) := { "message": msg, } -deny contains lk_advice("livekit_pii_unmarked", {"attribute_key": key, "expected": object.get(canonical_pii, key, null)}, sprintf("'%s' looks like PII but has no `pii` key segment, so the collector will not redact it%s", [key, hint])) if { +# 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) - hint := object.get({k: sprintf("; send '%s'", [v]) | some k, v in canonical_pii}, key, "") + not renames[key] } -deny contains lk_advice("livekit_shared_rename", {"attribute_key": key, "expected": want}, sprintf("'%s' is the client spelling of the shared key; send '%s' with its protobuf enum name as the value", [key, want])) if { +# 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 - want := renamed_shared[key] + to := renames[key] + value := object.get(to.values, input.sample.attribute.value, null) + hint := rename_value_hint(to, value) } -deny contains lk_advice("livekit_flat_identity", {"attribute_key": key, "expected": want}, sprintf("'%s' must be emitted as '%s'", [key, want])) if { - key := input.sample.attribute.name - want := canonical_identity[key] +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 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/model/manifest.yaml b/otel/model/manifest.yaml index 19c63fa43..a55571a91 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.6.0 +schema_url: https://livekit.io/schemas/telemetry/0.6.1 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). dependencies: diff --git a/otel/model/shared/registry.yaml b/otel/model/shared/registry.yaml index 7e3a154ea..74247c208 100644 --- a/otel/model/shared/registry.yaml +++ b/otel/model/shared/registry.yaml @@ -23,6 +23,7 @@ groups: 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 @@ -37,7 +38,7 @@ groups: never a default metric dimension. examples: ["PA_9kLm2NpQ4rTs"] annotations: - livekit: {owner: shared, sensitivity: none, authority: cloud} + livekit: {owner: shared, sensitivity: none, authority: cloud, renamed_from: {key: lk.participant.sid}} - id: lk.track_sid type: string stability: development @@ -47,7 +48,7 @@ groups: success. High cardinality, never a default metric dimension. examples: ["TR_AMkTyV3Zx8Qp"] annotations: - livekit: {owner: shared, sensitivity: none, authority: cloud} + livekit: {owner: shared, sensitivity: none, authority: cloud, renamed_from: {key: lk.track.sid}} - id: lk.job_id type: string stability: development @@ -84,7 +85,7 @@ groups: On every record and span of a session once the client has joined. examples: ["my-room"] annotations: - livekit: {owner: shared, sensitivity: pii} + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.room.name}} - id: lk.pii.participant_identity type: string stability: development @@ -93,7 +94,7 @@ groups: On every record and span of a session once the client has joined. examples: ["identity-0"] annotations: - livekit: {owner: shared, sensitivity: pii} + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.participant.identity}} - id: lk.pii.remote_participant_identity type: string stability: development @@ -102,7 +103,7 @@ groups: On subscribe spans: the identity of the participant publishing the track. examples: ["identity-1"] annotations: - livekit: {owner: shared, sensitivity: pii} + livekit: {owner: shared, sensitivity: pii, renamed_from: {key: lk.participant.remote_identity}} - id: registry.lk.sdk type: attribute_group @@ -137,15 +138,16 @@ groups: 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 protobuf enum names as values. + 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 protobuf `TrackSource` enum name. + 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 enum name exactly, e.g. `SOURCE_SCREENSHARE` (no underscore inside - SCREENSHARE). + 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} @@ -154,7 +156,12 @@ groups: - {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} + 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. @@ -183,7 +190,30 @@ groups: - {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} + 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 diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego index ccda4c2f7..6c40c7390 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -22,13 +22,6 @@ canonical_identity := { "lk.job.id": "lk.job_id", } -# Client spellings of shared keys whose Agents spelling and value set won (otel/model/shared). -# Keep in sync with otel/live-check/livekit.rego. -renamed_shared := { - "lk.track.source": "lk.track_source", - "lk.disconnect.reason": "lk.disconnect_reason", -} - high_cardinality := {"lk.room_sid", "lk.participant_id", "lk.track_sid", "lk.job_id", "lk.client.connect_id"} owners := {"shared", "client", "agents", "cloud"} @@ -52,7 +45,7 @@ ms_exception(attr) if { } # 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)) | shared_rename(attr)) | seconds(attr)) | unit(attr) +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")] @@ -93,10 +86,6 @@ heuristic_exception(attr) if { lk(attr, "pii_heuristic_exception") != "" } -shared_rename(attr) := {finding("livekit_shared_rename", sprintf("%s: the shared key is %s (the Agents spelling); do not define the client spelling", [attr.id, renamed_shared[attr.id]])) | - renamed_shared[attr.id] -} - 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] } diff --git a/otel/policies/resolved.rego b/otel/policies/resolved.rego index 7e1c40786..6c72416a8 100644 --- a/otel/policies/resolved.rego +++ b/otel/policies/resolved.rego @@ -15,6 +15,38 @@ deny contains f if { 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", []) diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index 9f82740a8..1dd28eaff 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -39,7 +39,8 @@ 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) +[Attribute reference](#attribute-reference) · [Enum values](#enum-values) · [Bindings](#bindings) · +[Old spellings](#old-spellings) ## Conventions @@ -60,9 +61,10 @@ They are record and span attributes, never Resource attributes, omitted until th 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 protobuf enum names as values -(`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_INITIATED`). The other -spelling is never registered. +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). **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 @@ -171,3 +173,15 @@ validates it. `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/tests/live-check.sh b/otel/tests/live-check.sh index 403f2d4bb..8f866d5e5 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -46,14 +46,20 @@ sed -e 's/"lk\.room_sid"/"lk.room.sid"/g' -e 's/"lk\.pii\.room_name"/"lk.room.na -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_flat_identity lk.room.sid" "livekit_pii_unmarked lk.room.name" "livekit_unknown_span lk.bogus" \ +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"; do + "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 echo "PASS live-check: smoke, sparse, JS, Agents and no-severity-text samples clean, every planted violation flagged" 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/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 From 948b07e707657c46efca72d02593506eeff2a38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:29:23 +0200 Subject: [PATCH 26/36] fix(otel): accept --v2 in check.sh without passing it twice check.sh always checks the default mode and --v2, so `otel/check.sh --v2` gave weaver --v2 twice and exited 2. A --v2 (or --v2=...) among the arguments is now dropped; other arguments still reach both checks, and plain `otel/check.sh` is unchanged. --- otel/check.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/otel/check.sh b/otel/check.sh index efdf87670..301fae0c1 100755 --- a/otel/check.sh +++ b/otel/check.sh @@ -2,14 +2,20 @@ # 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} -"$W" registry check -r model -p policies "$@" +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 "$@" +"$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. From 411663175fb56f44d763c497c0185a3911fe5466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:48:18 +0200 Subject: [PATCH 27/36] feat(otel): generate the remaining Agents literals from the registry Agents still wrote five groups of telemetry names as literals. Four of them now have generated constants, with no wire change: - GenAI metrics: model/agents/groups.yaml imports gen_ai.invoke_agent.duration and gen_ai.execute_tool.duration from the GenAI registry, and the Rust and Python upstream sections list them with their units. Weaver 0.28.0 refuses to import gen_ai.client.operation.duration (semconv v1.44.0 keeps a deprecated copy excluded from dependency resolution), so signals.yaml declares it as emitted, next to the other GenAI metrics declared that way. - Values beyond upstream: a ref to an upstream key can list annotations.livekit.shipped_values. gen_ai.provider.name gains mistral_ai and gen_ai.response.finish_reasons its six values, each generated next to the upstream values. The new policy livekit_shipped_value checks refs and definitions in both modes. It rejects a list on an lk.* key or on a key that is not a string, string[] or all-string enum; an entry whose id is not lowercase snake_case (so the uppercase id is exactly the constant name); a value that upstream already lists; an id that repeats another entry or an upstream member id read as lowercase snake_case; and a second distinct list for the same key anywhere in the registry, since codegen merges them. Eight negative fixtures cover it, and the CI Rust compile is the backstop for any collision it misses. - Session report: the ten attributes Agents sends on its chat_history and evaluations log records are registered as emitted, owner agents; the three session.* keys carry upstream_namespace: session. The Rust log kv keys need no registry change: a parenthesized constant is a valid log 0.4 key, and the generated Rust header says so. README and the generated spec describe each mechanism. Schema 0.6.1 -> 0.7.0. --- otel/README.md | 41 +++++++++-- otel/gen/docs/semconv.md | 39 ++++++++++- otel/gen/python/semconv.py | 60 +++++++++++++++- otel/gen/rust/semconv.rs | 69 ++++++++++++++++++- otel/gen/ts/semconv.ts | 41 ++++++++++- otel/model/agents/groups.yaml | 37 ++++++++++ otel/model/agents/registry.yaml | 22 ++++++ otel/model/agents/signals.yaml | 5 ++ otel/model/manifest.yaml | 2 +- otel/policies/lib.rego | 64 +++++++++++++++++ otel/policies/resolved.rego | 35 ++++++++++ .../templates/registry/markdown/semconv.md.j2 | 19 +++++ otel/templates/registry/python/semconv.py.j2 | 12 +++- otel/templates/registry/python/weaver.yaml | 2 +- otel/templates/registry/rust/semconv.rs.j2 | 19 ++++- otel/templates/registry/rust/weaver.yaml | 2 +- .../shipped-value-camelcase-duplicate.yaml | 13 ++++ .../negatives/shipped-value-camelcase-id.yaml | 13 ++++ .../negatives/shipped-value-definition.yaml | 17 +++++ .../negatives/shipped-value-duplicate-id.yaml | 13 ++++ .../shipped-value-normalized-id.yaml | 13 ++++ .../negatives/shipped-value-numeric-enum.yaml | 13 ++++ .../negatives/shipped-value-split-groups.yaml | 22 ++++++ .../negatives/shipped-value-upstream.yaml | 13 ++++ 24 files changed, 568 insertions(+), 18 deletions(-) create mode 100644 otel/model/agents/groups.yaml create mode 100644 otel/tests/negatives/shipped-value-camelcase-duplicate.yaml create mode 100644 otel/tests/negatives/shipped-value-camelcase-id.yaml create mode 100644 otel/tests/negatives/shipped-value-definition.yaml create mode 100644 otel/tests/negatives/shipped-value-duplicate-id.yaml create mode 100644 otel/tests/negatives/shipped-value-normalized-id.yaml create mode 100644 otel/tests/negatives/shipped-value-numeric-enum.yaml create mode 100644 otel/tests/negatives/shipped-value-split-groups.yaml create mode 100644 otel/tests/negatives/shipped-value-upstream.yaml diff --git a/otel/README.md b/otel/README.md index cbfcfc2d2..e26860d8a 100644 --- a/otel/README.md +++ b/otel/README.md @@ -24,8 +24,9 @@ policy, storage and the app API are not conventions and live with each SDK. | `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, and the `gen_ai.*`/LangFuse keys no upstream registry defines | -| `model/agents/signals.yaml` | Agents spans (50, unprefixed as shipped), the 20 `lk.agents.*` metrics, and the GenAI span events and metrics deprecated upstream | +| `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 | @@ -70,6 +71,13 @@ How a concept is named in other producers or stores (Agents' `room_id`, the coll 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 @@ -133,8 +141,9 @@ that touch `otel/**` or the workflow file itself, and on manual dispatch. 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 -prints 34 warnings: 33 about upstream's own files, and one because `client/groups.yaml` uses the -not-yet-stable `definition/2` format. +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 @@ -152,7 +161,28 @@ python3 -I -m py_compile 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. +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 | |---|---|---| @@ -167,6 +197,7 @@ never here, so a producer names them from one place. TypeScript does not yet. | `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 an `lk.*` key 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.*` | Weaver itself rejects duplicate keys, unknown fields and unresolved references. Every rule runs in diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index 97dfcfcf2..d9dbd53b2 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.6.1`, stability `development`. Built on +Schema `https://livekit.io/schemas/telemetry/0.7.0`, 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. @@ -40,10 +40,35 @@ values (`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_IN 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: `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`. +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 @@ -1293,9 +1318,12 @@ Every key this registry defines, with its meaning; the OTel keys used above are | 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` | | @@ -1538,6 +1566,13 @@ Every key this registry defines, with its meaning; the OTel keys used above are | `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 diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py index ed75ad891..5a44eb405 100644 --- a/otel/gen/python/semconv.py +++ b/otel/gen/python/semconv.py @@ -2,7 +2,10 @@ """LiveKit telemetry semantic conventions.""" #: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.1" +SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.7.0" + +#: The agent's name. +AGENT_NAME = "agent_name" #: Browser name. BROWSER_NAME = "browser.name" @@ -10,9 +13,15 @@ #: 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" @@ -893,6 +902,27 @@ #: 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. @@ -1036,6 +1066,8 @@ 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" @@ -1045,6 +1077,18 @@ #: 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" @@ -1197,6 +1241,16 @@ 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" @@ -1423,6 +1477,10 @@ 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). diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index 6d747ff46..bec0f9201 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -1,8 +1,14 @@ // 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`. /// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.6.1"; +pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.7.0"; + +/// The agent's name. +pub const AGENT_NAME: &str = "agent_name"; /// Browser name. pub const BROWSER_NAME: &str = "browser.name"; @@ -10,9 +16,15 @@ 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"; @@ -961,6 +973,27 @@ 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 { @@ -1099,6 +1132,8 @@ pub mod upstream { 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"; @@ -1106,6 +1141,22 @@ pub mod upstream { 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. @@ -1226,6 +1277,18 @@ pub mod upstream { 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 { @@ -1460,6 +1523,10 @@ pub mod checkpoint { /// 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). diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index 171556575..f51b891c2 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,7 +1,10 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. /** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.6.1" as const; +export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.7.0" as const; + +/** The agent's name. */ +export const ATTR_AGENT_NAME = "agent_name" as const; /** Browser name. */ export const ATTR_BROWSER_NAME = "browser.name" as const; @@ -9,9 +12,15 @@ 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; @@ -892,6 +901,27 @@ 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`. */ @@ -1113,12 +1143,21 @@ 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; 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 index 9706a3df5..2ecb57e3a 100644 --- a/otel/model/agents/registry.yaml +++ b/otel/model/agents/registry.yaml @@ -249,3 +249,25 @@ groups: - {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 index 5592877b0..4108f8b83 100644 --- a/otel/model/agents/signals.yaml +++ b/otel/model/agents/signals.yaml @@ -949,5 +949,10 @@ groups: 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/manifest.yaml b/otel/model/manifest.yaml index a55571a91..b30ba9a7a 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -2,7 +2,7 @@ file_format: definition_manifest/2.0 # The registry version IS the telemetry schema version: producers stamp this URL on # OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). # Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.6.1 +schema_url: https://livekit.io/schemas/telemetry/0.7.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). dependencies: diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego index 6c40c7390..6630f73f6 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -109,3 +109,67 @@ cardinality(name, where, keys, exception) := {finding("livekit_cardinality", spr } 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/resolved.rego b/otel/policies/resolved.rego index 6c72416a8..18b7687ca 100644 --- a/otel/policies/resolved.rego +++ b/otel/policies/resolved.rego @@ -111,3 +111,38 @@ deny contains data.livekit.lib.finding("livekit_ref_override", msg) if { 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 index 1dd28eaff..3af189bc8 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -66,6 +66,25 @@ values (`lk.track_source = SOURCE_MICROPHONE`, `lk.disconnect_reason = CLIENT_IN 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. diff --git a/otel/templates/registry/python/semconv.py.j2 b/otel/templates/registry/python/semconv.py.j2 index bfe862a60..1ff46ca89 100644 --- a/otel/templates/registry/python/semconv.py.j2 +++ b/otel/templates/registry/python/semconv.py.j2 @@ -16,7 +16,9 @@ {{ 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 %} +{% 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.""" @@ -29,7 +31,13 @@ SCHEMA_URL = {{ lit(ctx.schema_url) }} # 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 %} +{% 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) }} diff --git a/otel/templates/registry/python/weaver.yaml b/otel/templates/registry/python/weaver.yaml index 664364cab..410604805 100644 --- a/otel/templates/registry/python/weaver.yaml +++ b/otel/templates/registry/python/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.py.j2 - filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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)] | unique_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)}' + filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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 index 1f346c939..bb05385bb 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -14,16 +14,21 @@ {%- macro attr(a) -%} /// {{ doc(a) }} pub const {{ a.key | screaming_snake_case }}: &str = {{ lit(a.key) }}; -{% if a.type is mapping %} +{% 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 %} /// {{ line(m.brief) or "`" ~ m.value ~ "`." }} +{% 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`. /// Telemetry schema version; set as `schema_url` on every OTLP scope. pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; @@ -33,7 +38,15 @@ pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; /// never here), so a producer can name them from one place. pub mod upstream { {% for a in ctx.upstream %} {{ attr(a) | indent(4) }} -{% endfor %}} +{% 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) }} diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml index 1d601d24f..159a3cbb2 100644 --- a/otel/templates/registry/rust/weaver.yaml +++ b/otel/templates/registry/rust/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.rs.j2 - filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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)] | unique_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)}' + filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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/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}] From 25657ad30b803095b8fd3c80db02dad859493d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:35:53 +0200 Subject: [PATCH 28/36] refactor(otel): drop the schema version until a producer emits it No producer sets the OTLP scope schema_url: the Rust core, the JS core and Agents all leave it empty. A version that nothing emits only costs a bump in every model change, so the registry is unversioned for now. - model/manifest.yaml keeps the fixed placeholder https://livekit.io/schemas/telemetry/0.0.0, because Weaver requires a schema_url with a version segment. It does not change with the model. - The Rust, TypeScript and Python templates no longer generate SCHEMA_URL, the docs no longer print the schema URL, and no template filter passes schema_url. - tests/schema-bump.sh and tests/schema-bump-test.sh are gone, with their two CI steps and the full-history checkout only they needed. - The README replaces the bump rule with a Versioning section: the registry stays unversioned until a producer needs schema_url, and then the release step stamps the protocol release version, as OTel semantic-conventions does. --- .github/workflows/otel.yaml | 12 ------- otel/README.md | 25 +++++--------- otel/gen/docs/semconv.md | 2 +- otel/gen/python/semconv.py | 3 -- otel/gen/rust/semconv.rs | 3 -- otel/gen/ts/semconv.ts | 3 -- otel/model/manifest.yaml | 7 ++-- .../templates/registry/markdown/semconv.md.j2 | 2 +- otel/templates/registry/markdown/weaver.yaml | 2 +- otel/templates/registry/python/semconv.py.j2 | 3 -- otel/templates/registry/python/weaver.yaml | 2 +- otel/templates/registry/rust/semconv.rs.j2 | 3 -- otel/templates/registry/rust/weaver.yaml | 2 +- .../registry/typescript/semconv.ts.j2 | 3 -- .../templates/registry/typescript/weaver.yaml | 2 +- otel/tests/schema-bump-test.sh | 29 ---------------- otel/tests/schema-bump.sh | 34 ------------------- 17 files changed, 17 insertions(+), 120 deletions(-) delete mode 100755 otel/tests/schema-bump-test.sh delete mode 100755 otel/tests/schema-bump.sh diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml index 57af7c094..f8aacdadc 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -31,8 +31,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # the schema-version rule diffs against the base branch - name: Set up weaver uses: open-telemetry/weaver/.github/actions/setup-weaver@51a5bf95bdbb2e20fb6e16638e6bbe7dde911e93 # v0.28.0 @@ -61,13 +59,3 @@ jobs: - name: Live-check the smoke sample run: otel/tests/live-check.sh - - - name: Schema-version rule rejects comment-only changes and downgrades - run: otel/tests/schema-bump-test.sh - - # The same script as locally: a changed otel/model needs a strictly higher schema_url version. - - name: Schema version bumped when the model changed - if: github.event_name == 'pull_request' - env: - BASE_REF: ${{ github.base_ref }} - run: otel/tests/schema-bump.sh "origin/$BASE_REF" diff --git a/otel/README.md b/otel/README.md index e26860d8a..102696e50 100644 --- a/otel/README.md +++ b/otel/README.md @@ -19,7 +19,7 @@ policy, storage and the app API are not conventions and live with each SDK. | Path | What | |---|---| -| `model/manifest.yaml` | Registry version (`schema_url`) and the pinned OTel dependency | +| `model/manifest.yaml` | The pinned OTel dependencies, and a placeholder `schema_url` (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 | @@ -32,7 +32,7 @@ policy, storage and the app API are not conventions and live with each SDK. | `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, schema-version rule | +| `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 @@ -117,21 +117,14 @@ map, and its fields (such as `reason` in `outcome`) are payload, not attribute k 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 `otel/generate.sh`, and commit `gen/` with the change. -6. Bump the schema version (below). -## Bumping the schema version +## Versioning -`schema_url` in `model/manifest.yaml` is the telemetry schema version. Producers set it as the -OTLP scope `schema_url` (generated as `SCHEMA_URL`), which the collector already stores as a -column. It is versioned independently of the protocol package. - -Every PR that changes `model/` bumps it: CI fails unless the version at the end of `schema_url` -(MAJOR.MINOR.PATCH) is strictly higher than the base branch's (`tests/schema-bump.sh`; a comment -or quotes on an unchanged URL, or a downgrade, do not count). While the -registry is `0.x`, bump the minor version for added, renamed or removed keys, events or spans and -for changed types or requirement levels, and the patch version for documentation-only changes. To rename a key that has shipped, keep the old -key with `deprecated: {reason: renamed, renamed_to: }` instead of deleting it; -`weaver registry diff` and live-check both report the rename. +The registry is unversioned until a producer needs to emit `schema_url`. No producer sets it +today, so `model/manifest.yaml` carries the fixed placeholder +`https://livekit.io/schemas/telemetry/0.0.0` that Weaver requires. When a producer needs it, 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 @@ -151,8 +144,6 @@ otel/tests/negatives.sh # each negative fixture fails with its na otel/generate.sh && git status otel/gen # regenerate; no diff expected otel/tests/live-check.sh # smoke and sparse samples clean, legacy keys flagged otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc, tsc and python3 -otel/tests/schema-bump.sh origin/main # schema_url version strictly higher if model/ changed -otel/tests/schema-bump-test.sh # that rule, against comment-only changes and downgrades 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 diff --git a/otel/gen/docs/semconv.md b/otel/gen/docs/semconv.md index d9dbd53b2..3c24c52bd 100644 --- a/otel/gen/docs/semconv.md +++ b/otel/gen/docs/semconv.md @@ -2,7 +2,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `https://livekit.io/schemas/telemetry/0.7.0`, stability `development`. Built on +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. diff --git a/otel/gen/python/semconv.py b/otel/gen/python/semconv.py index 5a44eb405..fd61a50c0 100644 --- a/otel/gen/python/semconv.py +++ b/otel/gen/python/semconv.py @@ -1,9 +1,6 @@ # Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. """LiveKit telemetry semantic conventions.""" -#: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.7.0" - #: The agent's name. AGENT_NAME = "agent_name" diff --git a/otel/gen/rust/semconv.rs b/otel/gen/rust/semconv.rs index bec0f9201..2d7f3e16e 100644 --- a/otel/gen/rust/semconv.rs +++ b/otel/gen/rust/semconv.rs @@ -4,9 +4,6 @@ //! 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`. -/// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = "https://livekit.io/schemas/telemetry/0.7.0"; - /// The agent's name. pub const AGENT_NAME: &str = "agent_name"; diff --git a/otel/gen/ts/semconv.ts b/otel/gen/ts/semconv.ts index f51b891c2..fa3d74856 100644 --- a/otel/gen/ts/semconv.ts +++ b/otel/gen/ts/semconv.ts @@ -1,8 +1,5 @@ // Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. -/** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = "https://livekit.io/schemas/telemetry/0.7.0" as const; - /** The agent's name. */ export const ATTR_AGENT_NAME = "agent_name" as const; diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index b30ba9a7a..131fcccee 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -1,8 +1,7 @@ file_format: definition_manifest/2.0 -# The registry version IS the telemetry schema version: producers stamp this URL on -# OTLP ScopeSpans/ScopeLogs/ScopeMetrics.schema_url (generated as SCHEMA_URL). -# Bump it in the same PR as any change under otel/model (see otel/README.md). -schema_url: https://livekit.io/schemas/telemetry/0.7.0 +# A placeholder: Weaver requires a schema_url with a version segment. Producers do not emit it, +# and it does not change when the model changes (see otel/README.md, Versioning). +schema_url: https://livekit.io/schemas/telemetry/0.0.0 stability: development description: LiveKit telemetry semantic conventions (shared identity, PII, SDK identity, client-owned and agents-owned signals). dependencies: diff --git a/otel/templates/registry/markdown/semconv.md.j2 b/otel/templates/registry/markdown/semconv.md.j2 index 3af189bc8..306561153 100644 --- a/otel/templates/registry/markdown/semconv.md.j2 +++ b/otel/templates/registry/markdown/semconv.md.j2 @@ -28,7 +28,7 @@ # LiveKit telemetry semantic conventions The attribute keys, events and spans LiveKit producers send over OTLP, and what they mean. -Schema `{{ ctx.schema_url }}`, stability `development`. Built on +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. diff --git a/otel/templates/registry/markdown/weaver.yaml b/otel/templates/registry/markdown/weaver.yaml index 3fb8f61da..3cde97bbc 100644 --- a/otel/templates/registry/markdown/weaver.yaml +++ b/otel/templates/registry/markdown/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.md.j2 - filter: '{schema_url, 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)}' + 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 index 1ff46ca89..eb19a3ffc 100644 --- a/otel/templates/registry/python/semconv.py.j2 +++ b/otel/templates/registry/python/semconv.py.j2 @@ -23,9 +23,6 @@ # Code generated by weaver from livekit/protocol otel/model. DO NOT EDIT. """LiveKit telemetry semantic conventions.""" -#: Telemetry schema version; set as `schema_url` on every OTLP scope. -SCHEMA_URL = {{ lit(ctx.schema_url) }} - {% for a in ctx.attributes %}{{ attr(a) }} {% endfor %} # Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, diff --git a/otel/templates/registry/python/weaver.yaml b/otel/templates/registry/python/weaver.yaml index 410604805..eb93a2900 100644 --- a/otel/templates/registry/python/weaver.yaml +++ b/otel/templates/registry/python/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.py.j2 - filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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)}' + 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 index bb05385bb..14e0cf373 100644 --- a/otel/templates/registry/rust/semconv.rs.j2 +++ b/otel/templates/registry/rust/semconv.rs.j2 @@ -29,9 +29,6 @@ pub mod {{ a.key | snake_case }} { //! //! 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`. - -/// Telemetry schema version; set as `schema_url` on every OTLP scope. -pub const SCHEMA_URL: &str = {{ lit(ctx.schema_url) }}; {% for a in ctx.attributes %} {{ attr(a) }}{% endfor %} /// Upstream OTel keys that LiveKit signals reference (defined by the registry's dependencies, diff --git a/otel/templates/registry/rust/weaver.yaml b/otel/templates/registry/rust/weaver.yaml index 159a3cbb2..3e6f0e5a3 100644 --- a/otel/templates/registry/rust/weaver.yaml +++ b/otel/templates/registry/rust/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.rs.j2 - filter: '(.registry.attributes | map(.key)) as $local | {schema_url, 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)}' + 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 index 7c20b6271..6c8618ba5 100644 --- a/otel/templates/registry/typescript/semconv.ts.j2 +++ b/otel/templates/registry/typescript/semconv.ts.j2 @@ -7,9 +7,6 @@ {{ 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. - -/** Telemetry schema version; set as `schemaUrl` on every OTLP scope. */ -export const SCHEMA_URL = {{ ctx.schema_url | tojson }} as const; {% for a in ctx.attributes %} /** {{ doc(a) }} */ export const ATTR_{{ a.key | screaming_snake_case }} = {{ a.key | tojson }} as const; diff --git a/otel/templates/registry/typescript/weaver.yaml b/otel/templates/registry/typescript/weaver.yaml index 69823d5be..7b17b873f 100644 --- a/otel/templates/registry/typescript/weaver.yaml +++ b/otel/templates/registry/typescript/weaver.yaml @@ -1,5 +1,5 @@ templates: - template: semconv.ts.j2 - filter: '{schema_url, 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)}' + 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/schema-bump-test.sh b/otel/tests/schema-bump-test.sh deleted file mode 100755 index a78178c47..000000000 --- a/otel/tests/schema-bump-test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Exercises otel/tests/schema-bump.sh in a throwaway repo: a changed model passes only with a -# strictly higher schema_url version. -set -uo pipefail -bump=$(cd "$(dirname "$0")" && pwd)/schema-bump.sh -tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT -cd "$tmp" && git init -q && mkdir -p otel/model -url() { printf 'file_format: definition_manifest/2.0\nschema_url: %s\n' "$1" > otel/model/manifest.yaml; } -url https://livekit.io/schemas/telemetry/0.1.0 && echo 'groups: []' > otel/model/a.yaml -git add -A && git -c user.name=t -c user.email=t@t commit -qm base - -failed=0 -case_() { # case_ - git checkout -q -- . && url "$3" - [[ $4 == 1 ]] && echo '# changed' >> otel/model/a.yaml - "$bump" HEAD > out.txt 2>&1 - local got=$? - if [[ $got == "$2" ]]; then echo "PASS $1 (exit $got)"; else echo "FAIL $1 (exit $got, want $2)"; cat out.txt; failed=1; fi -} -case_ "unchanged" 0 https://livekit.io/schemas/telemetry/0.1.0 0 -case_ "changed, not bumped" 1 https://livekit.io/schemas/telemetry/0.1.0 1 -case_ "changed, only a comment on the URL" 1 "https://livekit.io/schemas/telemetry/0.1.0 # bumped?" 1 -case_ "changed, downgraded" 1 https://livekit.io/schemas/telemetry/0.0.1 1 -case_ "changed, same version quoted" 1 '"https://livekit.io/schemas/telemetry/0.1.0"' 1 -case_ "changed, version not semver" 1 https://livekit.io/schemas/telemetry/next 1 -case_ "changed, patch bump" 0 https://livekit.io/schemas/telemetry/0.1.1 1 -case_ "changed, minor bump with a comment" 0 "https://livekit.io/schemas/telemetry/0.2.0 # renames" 1 -case_ "changed, major bump" 0 https://livekit.io/schemas/telemetry/1.0.0 1 -exit "$failed" diff --git a/otel/tests/schema-bump.sh b/otel/tests/schema-bump.sh deleted file mode 100755 index ab02852d3..000000000 --- a/otel/tests/schema-bump.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# Fails when otel/model differs from but the schema_url version did not strictly increase. -# usage: otel/tests/schema-bump.sh (CI passes origin/) -# otel/tests/schema-bump-test.sh exercises it. -set -euo pipefail -cd "$(git rev-parse --show-toplevel)" -base=$(git merge-base "$1" HEAD) - -if git diff --quiet "$base" -- otel/model; then - echo "otel/model unchanged since $base"; exit 0 -fi -python3 -I - "$base" otel/model/manifest.yaml <<'PY' -import re, subprocess, sys - -base, path = sys.argv[1:] - -def version(text, where): - """schema_url as YAML reads it (quotes, trailing comment dropped) and its MAJOR.MINOR.PATCH.""" - m = re.search(r"""^schema_url:[ \t]*(["']?)([^"'#\s]+)\1[ \t]*(#.*)?$""", text, re.M) - v = m and re.fullmatch(r".*/(\d+)\.(\d+)\.(\d+)", m.group(2)) - if not v: - sys.exit(f"{where}: schema_url must end in a MAJOR.MINOR.PATCH version") - return m.group(2), tuple(int(n) for n in v.groups()) - -new_url, new = version(open(path).read(), path) -old = subprocess.run(["git", "show", f"{base}:{path}"], capture_output=True, text=True) -if old.returncode: - print(f"new registry: {new_url}") - sys.exit() -old_url, old_v = version(old.stdout, f"{base}:{path}") -if new <= old_v: - sys.exit(f"otel/model changed since {base[:12]}, but {new_url} is not a higher version than {old_url}: bump it") -print(f"schema version bumped: {old_url} -> {new_url}") -PY From 5a8b62adcbfcb03bfff6efd8e127851410869e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:16:33 +0200 Subject: [PATCH 29/36] chore(otel): point schema_url at this registry Weaver requires a schema_url and reads its last path segment as the version. It now points at the registry itself, https://github.com/livekit/protocol/tree/main/otel, instead of a livekit.io placeholder that never resolves. Producers do not emit it, and the link resolves once otel/ is on main. The generated code does not change: it no longer carries the schema URL. --- otel/README.md | 11 ++++++----- otel/model/manifest.yaml | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/otel/README.md b/otel/README.md index 102696e50..84d2bde8e 100644 --- a/otel/README.md +++ b/otel/README.md @@ -19,7 +19,7 @@ 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 placeholder `schema_url` (see [Versioning](#versioning)) | +| `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 | @@ -120,10 +120,11 @@ map, and its fields (such as `reason` in `outcome`) are payload, not attribute k ## Versioning -The registry is unversioned until a producer needs to emit `schema_url`. No producer sets it -today, so `model/manifest.yaml` carries the fixed placeholder -`https://livekit.io/schemas/telemetry/0.0.0` that Weaver requires. When a producer needs it, the -plan is to stamp the protocol release version into the manifest in the release step, as OTel +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 diff --git a/otel/model/manifest.yaml b/otel/model/manifest.yaml index 131fcccee..19e0ab7a0 100644 --- a/otel/model/manifest.yaml +++ b/otel/model/manifest.yaml @@ -1,7 +1,7 @@ file_format: definition_manifest/2.0 -# A placeholder: Weaver requires a schema_url with a version segment. Producers do not emit it, -# and it does not change when the model changes (see otel/README.md, Versioning). -schema_url: https://livekit.io/schemas/telemetry/0.0.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: From 38530e93e4989c210155745e6354529a9938c99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:19:46 +0200 Subject: [PATCH 30/36] ci(otel): regenerate otel/gen in the Generate workflow `mage otel` runs otel/generate.sh next to `mage proto`. It is a separate, opt-in target: `mage proto` (the default) and the JS generation are unchanged and never need weaver, so only a change under otel/ does. The Generate workflow installs weaver 0.28.0 with the same pinned setup action as otel.yaml, runs `mage otel` after the protobuf and JS generation, and commits otel/gen with the rest, the same way it treats the generated protobuf code. Generate already runs on every push to a non-main branch with no path filter. The weaver setup and generation steps are continue-on-error: a weaver install failure, a registry failure or a network error while cloning the upstream registries leaves otel/gen untouched and never blocks the protobuf and JS commit. otel.yaml keeps its freshness check as the independent gate: a commit whose otel/gen is stale fails it. Generate's 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. --- .github/workflows/generate.yaml | 16 +++++++++++++++- .github/workflows/otel.yaml | 4 ++++ magefile.go | 7 +++++++ otel/README.md | 13 +++++++++++-- 4 files changed, 37 insertions(+), 3 deletions(-) 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 index f8aacdadc..19b516e32 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -43,6 +43,10 @@ jobs: - 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. - name: Generated outputs are current run: | otel/generate.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 index 84d2bde8e..11d7399d8 100644 --- a/otel/README.md +++ b/otel/README.md @@ -116,7 +116,8 @@ map, and its fields (such as `reason` in `outcome`) are payload, not attribute k 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 `otel/generate.sh`, and commit `gen/` with the change. +5. Run `otel/check.sh`, then `go tool mage otel` (it runs `otel/generate.sh`), and commit `gen/` + with the change. ## Versioning @@ -132,6 +133,14 @@ semantic-conventions does. 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 @@ -142,7 +151,7 @@ for each LiveKit file in the not-yet-stable `definition/2` format (`client/group ```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 -otel/generate.sh && git status otel/gen # regenerate; no diff expected +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 otel/tests/codegen.sh # a value with quotes, backslashes, newlines round-trips through rustc, tsc and python3 From d3f75f4413796065a00422f6ac93964910bc939b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:19:52 +0200 Subject: [PATCH 31/36] chore(otel): add a changeset for the telemetry conventions registry --- .changeset/telemetry-conventions-registry.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/telemetry-conventions-registry.md 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/` From 044450acc61c5fd7c37bf6301be701b873067df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:15:29 +0200 Subject: [PATCH 32/36] fix(otel): replace otel/gen on generate and catch deleted outputs in CI generate.sh copied the staged outputs over otel/gen, so a file that generation no longer writes stayed behind. It now removes otel/gen and moves the staged tree in, after every target succeeds. When a target fails, otel/gen stays unchanged. The CI freshness check also missed a deleted output: `git add --intent-to-add` stages the deletion, so `git diff` against the index stayed clean. The check now stages with `git add --all` and compares the index with the commit, so an obsolete output fails it. otel/tests/generate.sh runs generate.sh against a stub weaver: a failing target leaves otel/gen byte-identical, and a successful run removes an obsolete file and directory. CI runs it in the freshness step. --- .github/workflows/otel.yaml | 9 ++++++--- otel/README.md | 1 + otel/generate.sh | 11 ++++++----- otel/tests/generate.sh | 31 +++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100755 otel/tests/generate.sh diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml index 19b516e32..00fe5c025 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -46,12 +46,15 @@ jobs: # 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. + # 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 --intent-to-add otel/gen - git diff --exit-code -- otel/gen + git add --all otel/gen + git diff --cached --exit-code -- otel/gen - name: Generated outputs compile run: | diff --git a/otel/README.md b/otel/README.md index 11d7399d8..40970821b 100644 --- a/otel/README.md +++ b/otel/README.md @@ -153,6 +153,7 @@ otel/check.sh # registry + LiveKit policies, default an 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 +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 diff --git a/otel/generate.sh b/otel/generate.sh index 949bfb9c8..2eb56c336 100755 --- a/otel/generate.sh +++ b/otel/generate.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash -# Regenerates otel/gen from otel/model (Rust, TypeScript, markdown docs). Generation runs the -# LiveKit policies (--v2 mode); gen/ is only replaced when every target passes. CI fails when the -# committed outputs differ from a fresh run. +# 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/${target#*:}" + "$W" registry generate --v2 -r model -p policies -t templates "${target%%:*}" "$tmp/gen/${target#*:}" done -cp -R "$tmp"/. gen/ +rm -rf gen && mv "$tmp/gen" gen 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" From 582b26cd689af2a7a76bdaea8b4d0ef037b39474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:15:36 +0200 Subject: [PATCH 33/36] fix(otel): report weaver's error when live-check writes no report When weaver failed before it wrote a report, live-check.sh ran the summary anyway: the user got a Python traceback and exit 1, not weaver's error or exit status. When the out-dir held a report from an earlier run, the script printed that old report as this run's result. live-check.sh now deletes only the previous live_check.json before the run, because the out-dir can be the caller's and hold other files. If weaver writes no report, the script prints the weaver log and exits with weaver's status, or 1 if weaver exited 0. A valid report is summarized whatever weaver's status is. tests/live-check.sh adds four cases against a stub weaver: no report with status 7 and with status 0, a valid report with status 1, and a reused out-dir whose earlier report is not summarized while an unrelated file stays. --- otel/README.md | 2 +- otel/live-check.sh | 10 +++++++++- otel/tests/live-check.sh | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/otel/README.md b/otel/README.md index 40970821b..6eaed9e05 100644 --- a/otel/README.md +++ b/otel/README.md @@ -152,7 +152,7 @@ for each LiveKit file in the not-yet-stable `definition/2` format (`client/group 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 +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 diff --git a/otel/live-check.sh b/otel/live-check.sh index c1f59d0a4..9dea94439 100755 --- a/otel/live-check.sh +++ b/otel/live-check.sh @@ -3,12 +3,15 @@ # 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). +# 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 @@ -21,6 +24,11 @@ status=0 --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/tests/live-check.sh b/otel/tests/live-check.sh index 8f866d5e5..37bea9821 100755 --- a/otel/tests/live-check.sh +++ b/otel/tests/live-check.sh @@ -4,7 +4,9 @@ # 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. +# 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 @@ -62,4 +64,32 @@ done 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 -echo "PASS live-check: smoke, sparse, JS, Agents and no-severity-text samples clean, every planted violation flagged" + +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" From e1d0b949b4a9d52ec102a0624a2a0573db0a31fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:15:54 +0200 Subject: [PATCH 34/36] ci(otel): run the committed Python constants Agents imports the generated Python directly, but the compile step ran only rustc and tsc on the committed files. It now also runs `python3 -I -W error otel/gen/python/semconv.py`, the counterpart of `rustc -D warnings`: an invalid escape sequence fails it. The README uses the same command. The codegen step name now says that the step runs rustc, tsc and python3. --- .github/workflows/otel.yaml | 3 ++- otel/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/otel.yaml b/.github/workflows/otel.yaml index 00fe5c025..d10a1bc8b 100644 --- a/.github/workflows/otel.yaml +++ b/.github/workflows/otel.yaml @@ -60,8 +60,9 @@ jobs: 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 and tsc) + - name: Codegen escapes literals (round-trip through rustc, tsc and python3) run: otel/tests/codegen.sh - name: Live-check the smoke sample diff --git a/otel/README.md b/otel/README.md index 6eaed9e05..786a867fc 100644 --- a/otel/README.md +++ b/otel/README.md @@ -158,7 +158,7 @@ otel/tests/codegen.sh # a value with quotes, backslashes, newli 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 -m py_compile otel/gen/python/semconv.py +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 From f2831d47a41276064eca2bdab3fa3b56c61ebfc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:19:40 +0200 Subject: [PATCH 35/36] fix(otel): apply the attribute rules to every registered key Every attribute rule, the ref-override rule and the client-namespace advice selected keys by the `lk.` prefix, so 23 keys defined in otel/model without that prefix skipped them. The rules now cover every attribute definition in otel/model, `lk.*` or not: a definition, not a ref, in the default mode, and every entry of the resolved registry with --v2. lk_key_shipped and the live-check rules keep the prefix. The ref-override rule covers every key with a definition here, in both modes, and takes that membership from the definition, not from the ref's copy. In the default mode, a definition's copy has no lineage entry and a ref's copy has one, so a ref that drops or nulls the owner, or empties the annotations, still fails. With --v2 the rule compares each ref's copy with the definition. Of the 23 keys, only chat.item broke a rule: it is PII without a `pii` key segment. The collector drops every record that carries chat.item by that exact name (pii-redaction/redact/otellogs.go:19), and Agents sends that name, so it keeps it: lib.rego names it in pii_by_name, used only by the pii_marker rule. The advice now also lists browser.name and browser.version. New negatives, each run in both modes: unprefixed-owner.yaml (an unprefixed key with no owner), unprefixed-pii.yaml (a bare key with sensitivity pii), and four refs that re-label a LiveKit key: ref-override-unprefixed.yaml (chat.item as non-PII), ref-override-drop-owner.yaml (chat.item without its owner), ref-override-empty-annotations.yaml (chat.item with no annotations) and ref-override-null-owner.yaml (lk.pii.room_name with a null owner). otel/model and otel/gen do not change. --- otel/README.md | 11 ++++++----- otel/advice/livekit.rego | 5 ++--- otel/policies/lib.rego | 12 +++++++++++- otel/policies/livekit.rego | 5 ++--- otel/policies/resolved.rego | 13 ++++++++++--- otel/tests/negatives/ref-override-drop-owner.yaml | 12 ++++++++++++ .../negatives/ref-override-empty-annotations.yaml | 12 ++++++++++++ otel/tests/negatives/ref-override-null-owner.yaml | 12 ++++++++++++ otel/tests/negatives/ref-override-unprefixed.yaml | 11 +++++++++++ otel/tests/negatives/unprefixed-owner.yaml | 12 ++++++++++++ otel/tests/negatives/unprefixed-pii.yaml | 13 +++++++++++++ 11 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 otel/tests/negatives/ref-override-drop-owner.yaml create mode 100644 otel/tests/negatives/ref-override-empty-annotations.yaml create mode 100644 otel/tests/negatives/ref-override-null-owner.yaml create mode 100644 otel/tests/negatives/ref-override-unprefixed.yaml create mode 100644 otel/tests/negatives/unprefixed-owner.yaml create mode 100644 otel/tests/negatives/unprefixed-pii.yaml diff --git a/otel/README.md b/otel/README.md index 786a867fc..a7cb15598 100644 --- a/otel/README.md +++ b/otel/README.md @@ -64,8 +64,8 @@ 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, and never -fails on it. +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; @@ -89,7 +89,7 @@ map, and its fields (such as `reason` in `outcome`) are payload, not attribute k | 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; `secret` is never emitted | + | `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 | @@ -190,7 +190,7 @@ add to it: |---|---|---| | `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 | +| `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) | @@ -198,10 +198,11 @@ add to it: | `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 an `lk.*` key may not override its `annotations` (owner, sensitivity, unit) | +| `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. diff --git a/otel/advice/livekit.rego b/otel/advice/livekit.rego index af103c87a..8e8912809 100644 --- a/otel/advice/livekit.rego +++ b/otel/advice/livekit.rego @@ -5,17 +5,16 @@ package before_resolution import rego.v1 -# LiveKit attribute definitions, from v1 `groups:` files and definition/2 files alike. +# 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 - startswith(attr.id, "lk.") + 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 - startswith(attr.key, "lk.") not attr.deprecated } diff --git a/otel/policies/lib.rego b/otel/policies/lib.rego index 6630f73f6..8043ec7d0 100644 --- a/otel/policies/lib.rego +++ b/otel/policies/lib.rego @@ -59,9 +59,19 @@ secret(attr) := {finding("livekit_secret", sprintf("%s: SECRET values are never 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") != regex.match(pii_segment, attr.id) + (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])) | diff --git a/otel/policies/livekit.rego b/otel/policies/livekit.rego index 21a4b866f..8c4536bb3 100644 --- a/otel/policies/livekit.rego +++ b/otel/policies/livekit.rego @@ -5,18 +5,17 @@ package before_resolution import rego.v1 -# LiveKit attribute definitions in this file (refs and deprecated keys are exempt). +# 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 - startswith(attr.id, "lk.") + 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 - startswith(attr.key, "lk.") not attr.deprecated } diff --git a/otel/policies/resolved.rego b/otel/policies/resolved.rego index 18b7687ca..eb9bcdf7a 100644 --- a/otel/policies/resolved.rego +++ b/otel/policies/resolved.rego @@ -10,7 +10,6 @@ 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"], []) - startswith(attr.key, "lk.") not attr.deprecated some f in data.livekit.lib.attr_findings(object.union(attr, {"id": attr.key})) } @@ -81,11 +80,19 @@ deny contains f if { # 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"], {}) - startswith(key, "lk.") + defined[key] "annotations" in object.get(lineage, "locally_overridden_fields", []) msg := override_msg(key, group.id, object.get(group, ["lineage", "provenance", "path"], group.id)) } @@ -106,7 +113,7 @@ v2_signals contains s if { deny contains data.livekit.lib.finding("livekit_ref_override", msg) if { some s in v2_signals some a in object.get(s, "attributes", []) - startswith(a.key, "lk.") + 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)) 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/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}} From 5f7a50ace896f01511ecb74240b13defd5d457c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:19:45 +0200 Subject: [PATCH 36/36] chore: mark otel/gen as generated otel/gen holds the generated Rust, TypeScript, Python and docs, about 6,200 of the PR's added lines. Marking it linguist-generated lets GitHub collapse it in the diff, as it already does for the generated JS and Go. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) 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