Skip to content

feat: add enduser.id OTel span attribute from request identity - #628

Open
gingeekrishna wants to merge 2 commits into
aws:mainfrom
gingeekrishna:feat/592-enduser-id-otel-span-attribute
Open

feat: add enduser.id OTel span attribute from request identity#628
gingeekrishna wants to merge 2 commits into
aws:mainfrom
gingeekrishna:feat/592-enduser-id-otel-span-attribute

Conversation

@gingeekrishna

Copy link
Copy Markdown

Summary

Implements the request in #592 — automatically populate enduser.id on every OpenTelemetry span for the lifetime of a request.

  • User-level observability: enduser.id flows into Datadog, Grafana, CloudWatch, etc. without any agent-code change.
  • Auditability & compliance: LLM invocations and tool calls are linked to the initiating user in trace exports.
  • Analytics: per-user token counts and latency histograms become trivial to build on top of existing OTel pipelines.

How it works

Sources checked in priority order per request:

Priority Source Behaviour
1 X-Amzn-Bedrock-AgentCore-Runtime-User-Id header Explicit override — use when the caller already has the user ID
2 sub claim in Authorization: Bearer <JWT> Auto-extraction from Cognito / any OIDC token (no signature validation — trust belongs to the inbound auth layer)
Neither present enduser.id attribute is omitted (no-op)

The value is stored in a BedrockAgentCoreContext.enduser_id ContextVar (same pattern as session_id / routing experiment), so concurrent requests never bleed user IDs into each other.

BaggageSpanProcessor.on_start reads the ContextVar and stamps enduser.id on every new span, alongside the existing routing-experiment attributes.

Files changed

File Change
runtime/models.py Add USER_ID_HEADER constant
runtime/context.py Add _enduser_id ContextVar + set_enduser_id / get_enduser_id
runtime/utils.py Add extract_sub_from_bearer JWT-payload helper
runtime/tracing.py Stamp enduser.id in BaggageSpanProcessor.on_start
runtime/app.py Extract and set enduser_id in _build_request_context
runtime/a2a.py Same for A2A path
runtime/ag_ui.py Same for AG-UI path
tests/…/test_utils.py 10 unit tests for extract_sub_from_bearer
tests/…/test_tracing.py 4 processor-unit tests + 5 end-to-end integration tests

Test plan

  • extract_sub_from_bearer: valid JWT, absent sub, non-Bearer scheme, malformed token, numeric sub coercion
  • BaggageSpanProcessor: enduser.id set when ContextVar present; absent when None; no interference with routing-experiment attributes; concurrent requests get independent values
  • End-to-end via Starlette TestClient: explicit header wins, JWT fallback, header-over-JWT priority, neither present → None
  • All 300 pre-existing tests still pass

Fixes #592

When extended thinking is enabled, assistant messages contain
reasoningContent blocks alongside toolUse blocks. The filter was
stripping toolUse/toolResult but leaving reasoningContent intact,
producing a partial assistant message that Bedrock rejects with:

  ValidationException: `thinking` or `redacted_thinking` blocks in
  the latest assistant message cannot be modified.

reasoningContent blocks are semantically coupled to the tool calls
that follow them — stripping the tool context without the accompanying
reasoning leaves an incoherent and API-rejected message. The fix adds
reasoningContent to the set of block types removed by the filter.

Fixes aws#621

Signed-off-by: gingeekrishna <gingeekrishna@gmail.com>
Populate the OpenTelemetry semantic-convention ``enduser.id`` attribute
on every span for the lifetime of a request, enabling user-level
observability, auditability, and per-user analytics without any changes
to agent code.

Sources (in priority order):
1. ``X-Amzn-Bedrock-AgentCore-Runtime-User-Id`` request header —
   explicit override, useful when the caller already knows the user ID.
2. ``sub`` claim extracted from a Bearer JWT in the ``Authorization``
   header — automatic extraction from Cognito or any OIDC-compatible
   token.  The JWT payload is decoded for claim extraction only; no
   signature validation is performed (trust decisions belong to the
   inbound auth layer).

The value is stored in a ``BedrockAgentCoreContext.enduser_id``
ContextVar so concurrent requests never see each other's user IDs.
``BaggageSpanProcessor.on_start`` reads the ContextVar and stamps
``enduser.id`` on every span, exactly as it already does for routing
experiment attributes.

All three entry paths are covered:
- ``BedrockAgentCoreApp._build_request_context`` (HTTP/SSE)
- ``BedrockCallContextBuilder.build`` (A2A)
- ``AGUIApp._build_request_context`` (AG-UI)

22 new tests; 300 previously-passing tests still pass.

Closes aws#592

Signed-off-by: gingeekrishna <gingeekrishna@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds request-scoped end-user identity propagation into tracing by extracting a user ID from inbound headers/JWT and stamping it as the OpenTelemetry semantic attribute enduser.id on newly created spans.

Changes:

  • Introduces USER_ID_HEADER and a new BedrockAgentCoreContext ContextVar (enduser_id) with getters/setters.
  • Adds extract_sub_from_bearer() to pull sub from an (unsigned/unchecked) Bearer JWT and wires it into request-context builders (app/A2A/AG-UI).
  • Updates BaggageSpanProcessor to stamp enduser.id and adds unit + integration tests around extraction and stamping behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/bedrock_agentcore/runtime/test_utils.py Adds unit tests for extract_sub_from_bearer() with malformed/edge-case coverage.
tests/bedrock_agentcore/runtime/test_tracing.py Adds span-processor tests and end-to-end request tests validating header/JWT priority and context isolation.
tests/bedrock_agentcore/memory/integrations/strands/test_agentcore_memory_session_manager.py Adds regression tests for filtering restored tool context with extended-thinking (reasoningContent).
src/bedrock_agentcore/runtime/utils.py Adds JWT payload helper extract_sub_from_bearer() (no validation) for enduser.id extraction.
src/bedrock_agentcore/runtime/tracing.py Stamps enduser.id on spans via BaggageSpanProcessor.on_start().
src/bedrock_agentcore/runtime/models.py Adds USER_ID_HEADER constant.
src/bedrock_agentcore/runtime/context.py Adds enduser_id ContextVar + set_enduser_id / get_enduser_id.
src/bedrock_agentcore/runtime/app.py Extracts and sets enduser_id during request context build.
src/bedrock_agentcore/runtime/a2a.py Extracts and sets enduser_id for A2A requests.
src/bedrock_agentcore/runtime/ag_ui.py Extracts and sets enduser_id for AG-UI requests/websockets.
src/bedrock_agentcore/memory/integrations/strands/session_manager.py Extends _filter_restored_tool_context to also strip reasoningContent blocks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 105 to 109
def on_start(self, span: object, parent_context: Optional[object] = None) -> None:
"""Set routing experiment attributes on every new span.
"""Set routing experiment and end-user identity attributes on every new span.

Primary source: ContextVars set by ``_build_request_context`` — covers
all spans created after request parsing (agent spans, tool spans, etc.).
Comment on lines +137 to +139
enduser_id = _context.get_enduser_id()
if enduser_id is not None:
span.set_attribute("enduser.id", enduser_id) # type: ignore[union-attr]
Comment on lines +802 to +806
"""Strip historical toolUse/toolResult context from restored messages.

Extended-thinking (reasoningContent) blocks are coupled to the tool
calls that follow them. Bedrock rejects an assistant message whose
reasoningContent blocks have been separated from their companion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add user identity (enduser.id) attribute to OpenTelemetry

2 participants