Skip to content

Commit c311682

Browse files
test: add OTEL span content leakage integration tests (#485)
* test: add integration tests asserting no user content in OTEL spans COE Finding: User prompts and agent responses must never leak into exported OTEL span attributes, events, or resource attributes. These tests send requests with unique sentinel strings through the full request lifecycle and verify they are absent from all exported span data. Covers: BedrockAgentCoreApp (sync + streaming), A2A, AG-UI. * fix: resolve lint errors in OTEL test (unused var, undefined name) * refactor: remove A2A and AG-UI tests from OTEL content leakage suite * style: remove trailing blank lines * style: move opentelemetry.trace import to top level
1 parent 76c8d89 commit c311682

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Integration tests: assert user content is NOT present in OTEL spans.
2+
3+
COE Finding: User prompts and agent responses must never leak into exported
4+
OTEL span attributes, events, or resource attributes.
5+
"""
6+
7+
import json
8+
9+
import opentelemetry.trace
10+
import pytest
11+
from opentelemetry.sdk.trace import TracerProvider
12+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
13+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
14+
from starlette.testclient import TestClient
15+
16+
from bedrock_agentcore.runtime import BedrockAgentCoreApp
17+
from bedrock_agentcore.runtime import tracing as tracing_mod
18+
19+
SENTINEL_PROMPT = "SENSITIVE_USER_PROMPT_a1b2c3d4e5"
20+
SENTINEL_RESPONSE = "SENSITIVE_AGENT_RESPONSE_f6g7h8i9j0"
21+
22+
23+
def _assert_no_sentinel_in_spans(spans, sentinels):
24+
"""Assert none of the sentinel strings appear anywhere in exported span data."""
25+
for span in spans:
26+
for sentinel in sentinels:
27+
assert sentinel not in span.name, f"Sentinel found in span name: {span.name}"
28+
29+
for key, value in (span.attributes or {}).items():
30+
serialized = json.dumps(value) if not isinstance(value, str) else value
31+
assert sentinel not in str(key), f"Sentinel in attribute key: {key}"
32+
assert sentinel not in serialized, f"Sentinel in attribute value for key={key}: {serialized}"
33+
34+
for event in span.events:
35+
assert sentinel not in event.name, f"Sentinel in event name: {event.name}"
36+
for key, value in (event.attributes or {}).items():
37+
serialized = json.dumps(value) if not isinstance(value, str) else value
38+
assert sentinel not in str(key), f"Sentinel in event attribute key: {key}"
39+
assert sentinel not in serialized, f"Sentinel in event attribute value for key={key}: {serialized}"
40+
41+
if hasattr(span, "resource") and span.resource:
42+
for key, value in (span.resource.attributes or {}).items():
43+
serialized = json.dumps(value) if not isinstance(value, str) else value
44+
assert sentinel not in str(key), f"Sentinel in resource attribute key: {key}"
45+
assert sentinel not in serialized, (
46+
f"Sentinel in resource attribute value for key={key}: {serialized}"
47+
)
48+
49+
50+
@pytest.fixture()
51+
def otel_exporter():
52+
"""Set up TracerProvider with InMemorySpanExporter for span capture."""
53+
exporter = InMemorySpanExporter()
54+
provider = TracerProvider()
55+
provider.add_span_processor(SimpleSpanProcessor(exporter))
56+
57+
original_provider = opentelemetry.trace.get_tracer_provider()
58+
opentelemetry.trace.set_tracer_provider(provider)
59+
tracing_mod._registered_on = None
60+
61+
yield exporter
62+
63+
provider.shutdown()
64+
opentelemetry.trace.set_tracer_provider(original_provider)
65+
66+
67+
class TestBedrockAgentCoreAppNoUserContent:
68+
"""Verify BedrockAgentCoreApp does not leak user content into OTEL spans."""
69+
70+
def test_sync_invocation_no_user_content_in_spans(self, otel_exporter):
71+
app = BedrockAgentCoreApp()
72+
73+
@app.entrypoint
74+
def handler(payload):
75+
return {"response": SENTINEL_RESPONSE, "echo": payload.get("prompt")}
76+
77+
client = TestClient(app)
78+
response = client.post(
79+
"/invocations",
80+
json={"prompt": SENTINEL_PROMPT},
81+
headers={"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "test-session"},
82+
)
83+
84+
assert response.status_code == 200
85+
spans = otel_exporter.get_finished_spans()
86+
_assert_no_sentinel_in_spans(spans, [SENTINEL_PROMPT, SENTINEL_RESPONSE])
87+
88+
def test_streaming_invocation_no_user_content_in_spans(self, otel_exporter):
89+
app = BedrockAgentCoreApp()
90+
91+
@app.entrypoint
92+
async def handler(payload):
93+
yield f"chunk1: {SENTINEL_RESPONSE}"
94+
yield f"chunk2: {payload.get('prompt')}"
95+
96+
client = TestClient(app)
97+
response = client.post(
98+
"/invocations",
99+
json={"prompt": SENTINEL_PROMPT},
100+
headers={
101+
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": "test-session",
102+
"Accept": "text/event-stream",
103+
},
104+
)
105+
106+
assert response.status_code == 200
107+
spans = otel_exporter.get_finished_spans()
108+
_assert_no_sentinel_in_spans(spans, [SENTINEL_PROMPT, SENTINEL_RESPONSE])

0 commit comments

Comments
 (0)