feat: add enduser.id OTel span attribute from request identity - #628
Open
gingeekrishna wants to merge 2 commits into
Open
feat: add enduser.id OTel span attribute from request identity#628gingeekrishna wants to merge 2 commits into
gingeekrishna wants to merge 2 commits into
Conversation
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>
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
gingeekrishna
requested a deployment
to
manual-approval
August 8, 2026 18:49 — with
GitHub Actions
Waiting
There was a problem hiding this comment.
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_HEADERand a newBedrockAgentCoreContextContextVar (enduser_id) with getters/setters. - Adds
extract_sub_from_bearer()to pullsubfrom an (unsigned/unchecked) Bearer JWT and wires it into request-context builders (app/A2A/AG-UI). - Updates
BaggageSpanProcessorto stampenduser.idand 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the request in #592 — automatically populate
enduser.idon every OpenTelemetry span for the lifetime of a request.enduser.idflows into Datadog, Grafana, CloudWatch, etc. without any agent-code change.How it works
Sources checked in priority order per request:
X-Amzn-Bedrock-AgentCore-Runtime-User-Idheadersubclaim inAuthorization: Bearer <JWT>enduser.idattribute is omitted (no-op)The value is stored in a
BedrockAgentCoreContext.enduser_idContextVar (same pattern assession_id/ routing experiment), so concurrent requests never bleed user IDs into each other.BaggageSpanProcessor.on_startreads the ContextVar and stampsenduser.idon every new span, alongside the existing routing-experiment attributes.Files changed
runtime/models.pyUSER_ID_HEADERconstantruntime/context.py_enduser_idContextVar +set_enduser_id/get_enduser_idruntime/utils.pyextract_sub_from_bearerJWT-payload helperruntime/tracing.pyenduser.idinBaggageSpanProcessor.on_startruntime/app.pyenduser_idin_build_request_contextruntime/a2a.pyruntime/ag_ui.pytests/…/test_utils.pyextract_sub_from_bearertests/…/test_tracing.pyTest plan
extract_sub_from_bearer: valid JWT, absent sub, non-Bearer scheme, malformed token, numeric sub coercionBaggageSpanProcessor: enduser.id set when ContextVar present; absent when None; no interference with routing-experiment attributes; concurrent requests get independent valuesTestClient: explicit header wins, JWT fallback, header-over-JWT priority, neither present → NoneFixes #592