Skip to content

feat(telemetry): trace RPC calls made and handled - #7134

Merged
davidzhao merged 6 commits into
dz/telemetry-startup-shutdownfrom
dz/telemetry-rpc
Sep 12, 2026
Merged

davidzhao merged 6 commits into
dz/telemetry-startup-shutdownfrom
dz/telemetry-rpc

Conversation

@davidzhao

@davidzhao davidzhao commented Sep 5, 2026 •

Copy link
Copy Markdown
Member

What

Neither user-registered RPC methods nor the avatar datastream RPCs appeared in a trace. This installs a tracing interceptor on the job's local participant and turns each RPC into a span.

Depends on the RpcInterceptor hook in livekit-rtc (livekit/python-sdks#806, released in 1.1.18); the stack pins livekit==1.1.18 in #7128. install still degrades to a no-op with one debug log on an older SDK.

Spans

Span Kind Parent
rpc_call CLIENT the current span, so an RPC issued from a tool nests under function_tool
rpc_handler SERVER the primary session's root span, so it lands on the session timeline

Attributes: rpc.method, lk.rpc.request_id, lk.rpc.caller_identity, lk.rpc.destination_identity, lk.rpc.payload_size, lk.rpc.response_size, lk.rpc.response_timeout, lk.rpc.error_code, lk.rpc.handler_registered (false when a client called a method the agent never registered). Request and response bodies are under PII keys, lk.pii.rpc.payload and lk.pii.rpc.response, truncated to 1 KiB. RpcError and handler exceptions set error status.

Where

  • telemetry/rpc.py: TracingRpcInterceptor (subclasses rtc.RpcInterceptor when present) and install(local_participant).
  • Installed from JobContext.connect() after the room connects, and from RoomIO.start() for a room connected elsewhere. One singleton interceptor; the SDK dedups registrations by identity, so repeated installs are no-ops.

Tests

tests/test_rpc_tracing.py drives the interceptor with fake continuations: attributes and parenting for both directions, request and response truncation, error codes and status, the unregistered-method flag, handler exceptions, and install registering once or degrading. Passes against both the pinned SDK and the interceptor-capable SDK source tree.

Stacked on #7131.

🤖 Generated with Claude Code

@davidzhao
davidzhao force-pushed the dz/telemetry-rpc branch 3 times, most recently from fe5b4a4 to 9fae149 Compare September 7, 2026 00:10
@davidzhao
davidzhao marked this pull request as ready for review September 7, 2026 03:38
@davidzhao
davidzhao requested a review from a team as a code owner September 7, 2026 03:38
devin-ai-integration[bot]

This comment was marked as resolved.

@davidzhao
davidzhao force-pushed the dz/telemetry-rpc branch 3 times, most recently from 8dc4304 to a3701b2 Compare September 7, 2026 06:50
Comment thread livekit-agents/livekit/agents/telemetry/rpc.py Outdated
Comment thread livekit-agents/livekit/agents/telemetry/rpc.py Outdated
Comment thread livekit-agents/livekit/agents/telemetry/rpc.py
@davidzhao
davidzhao force-pushed the dz/telemetry-rpc branch 2 times, most recently from 6e88aaa to 805a46d Compare September 8, 2026 04:26
devin-ai-integration[bot]

This comment was marked as resolved.

davidzhao and others added 6 commits September 11, 2026 15:01
Neither user-registered RPC methods nor the avatar datastream RPCs appeared
in a trace. livekit-rtc is gaining an RpcInterceptor hook (add_rpc_interceptor
on LocalParticipant) that wraps every perform_rpc call and every dispatched
handler invocation; this installs a tracing interceptor on the job's local
participant and turns each call into a span following the OpenTelemetry RPC
semantic conventions:

- rpc_call (SpanKind.CLIENT) for outgoing calls, parented to the current
  span so an RPC issued from a tool nests under function_tool;
- rpc_handler (SpanKind.SERVER) for incoming invocations, parented to the
  primary session's root span so it lands on the session timeline.

Attributes: rpc.system=livekit, rpc.method, lk.rpc.request_id,
lk.pii.rpc.caller_identity / lk.pii.rpc.destination_identity,
lk.rpc.payload_size, lk.pii.rpc.payload (truncated to 1 KiB),
lk.rpc.response_size, lk.rpc.response_timeout, lk.rpc.error_code, and
lk.rpc.handler_registered (false when a client called a method the agent
never registered). RpcError and handler exceptions set error status.

Installed from JobContext.connect() after the room connects, and from
RoomIO.start() for a room connected elsewhere. On an SDK without the hook
(the current livekit==1.1.17 pin) install is a no-op with one debug log, so
this can land ahead of the SDK release; tracing activates once the pin moves
to the release carrying RpcInterceptor.

Tests exercise the interceptor with fake continuations (attributes, parenting,
truncation, error codes, unregistered method, handler exceptions) and the
install/degrade behavior; they pass against both the pinned SDK and the
interceptor-capable SDK source tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rpc.system is always livekit here, so it carried no information. Responses
are now recorded like requests: lk.pii.rpc.response, truncated to 1 KiB,
alongside the existing size, so a client/agent disagreement about a message
format is readable from both sides of the span.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Participant identities are application identifiers, not end-user data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RoomIO.start only instrumented a room that was already connected; a room that
connected later (ctx.connect() after session.start(), or one the user connects)
got no RPC spans unless JobContext.connect() installed it. The install now lives
in the connection-state handler, so every ordering and reconnects are covered;
it is idempotent, one interceptor instance deduped by identity in the SDK.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…els the handler chain

The SDK cancels the incoming chain when the caller's deadline passes or the
room disconnects and maps it to an RpcError only after the interceptor has
unwound; CancelledError is not an Exception, so the handler span ended UNSET.
The interceptor now records RESPONSE_TIMEOUT or RECIPIENT_DISCONNECTED, by
elapsed time against the deadline, with ERROR status.

Also: the handler's parent comes from session_context.session_root_context
instead of a local copy, payload truncation is a plain slice, and comments are
trimmed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… elapsed time

The SDK's deadline clock starts before the interceptor's, so a deadline cancel
could read as a disconnect, and a CancelledError raised inside the chain (an
APPLICATION_ERROR to the caller) was labelled as either. The SDK now sets
invocation.cancel_reason before it cancels the chain; the interceptor records
that, APPLICATION_ERROR when it is unset, and no code on an SDK without the
field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants