Skip to content

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

Open
davidzhao wants to merge 4 commits into
dz/telemetry-startup-shutdownfrom
dz/telemetry-rpc
Open

feat(telemetry): trace RPC calls made and handled#7134
davidzhao wants to merge 4 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). On the current livekit==1.1.17 pin install is a no-op with one debug log, so this can land first; tracing activates once the pin moves to the SDK release carrying the hook.

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 2 times, most recently from 0fea92f to 8dc4304 Compare September 7, 2026 06:26
davidzhao and others added 4 commits September 6, 2026 23:47
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>


def _truncate(payload: str) -> str:
return payload if len(payload) <= MAX_PAYLOAD_ATTR_LEN else payload[:MAX_PAYLOAD_ATTR_LEN]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

payload[:MAX_PAYLOAD_ATTR_LEN] alone should be enough.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also, should we use byte length check here?

return attrs


def _handler_parent_context() -> otel_context.Context | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should reuse session_context.session_root_context

) as span:
try:
response = await next(invocation)
except rtc.RpcError as e:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

found by Codex:

The SDK enforces the response deadline by cancelling the incoming interceptor chain. This produces CancelledError here; the SDK converts it to RpcError(RESPONSE_TIMEOUT) only after this interceptor has unwound. Since CancelledError also bypasses OpenTelemetry’s except Exception, the handler span ends with UNSET status and no error code even though the caller receives RESPONSE_TIMEOUT.

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.

2 participants