Skip to content

Commit 7bf2836

Browse files
ezhang6811liustve
andauthored
feat: agentcore feature patches (#751)
*Issue #, if available:* *Description of changes:* Backpatches the following changes to the release/v0.17.x branch: * #741 * #748 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Steve Liu <liustve@amazon.com>
1 parent b8917a4 commit 7bf2836

7 files changed

Lines changed: 146 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t
1212

1313
## Unreleased
1414

15+
- feat(agent-observability): add `AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT` env var to allow disabling LLO content extraction from spans
16+
([#741](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/741))
17+
- fix(mcp-instrumentation): suppress MCP `/ping` spans when agent observability is enabled
18+
([#748](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/748))
19+
1520
## v0.17.0 - 2026-04-08
1621

1722
- fix(lambda-layer): Disable all agentic instrumentation in Lambda by default

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
1515
AWS_AGENTIC_OBSERVABILITY_OPT_IN = "AWS_AGENTIC_OBSERVABILITY_OPT_IN"
1616
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS = "OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS"
17+
AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT = "AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT"
1718

1819

1920
def is_installed(req: str) -> bool:
@@ -41,6 +42,11 @@ def is_agent_observability_enabled() -> bool:
4142
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"
4243

4344

45+
def is_genai_content_extraction_opted_out() -> bool:
46+
"""Has the user opted out of GenAI content extraction from spans?"""
47+
return os.environ.get(AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT, "false").lower() == "true"
48+
49+
4450
def is_aws_agentic_observability_opt_in() -> bool:
4551
"""Is the AI observability opt-in flag set to true?"""
4652
return os.environ.get(AWS_AGENTIC_OBSERVABILITY_OPT_IN, "false").lower() == "true"

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/traces/otlp_aws_span_exporter.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from botocore.session import Session
88

9-
from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
9+
from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled, is_genai_content_extraction_opted_out
1010
from amazon.opentelemetry.distro.exporter.otlp.aws.common._aws_http_headers import _OTLP_AWS_HTTP_HEADERS
1111
from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession
1212
from amazon.opentelemetry.distro.llo_handler import LLOHandler
@@ -77,7 +77,11 @@ def _ensure_llo_handler(self):
7777

7878
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
7979
try:
80-
if is_agentic_observability_enabled() and self._ensure_llo_handler():
80+
if (
81+
is_agentic_observability_enabled()
82+
and not is_genai_content_extraction_opted_out()
83+
and self._ensure_llo_handler()
84+
):
8185
llo_processed_spans = self._llo_handler.process_spans(spans)
8286
return super().export(llo_processed_spans)
8387
except Exception: # pylint: disable=broad-exception-caught

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/instrumentation/mcp/_wrappers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any, Callable, Coroutine, Dict, Optional, Tuple
99
from urllib.parse import urlparse
1010

11+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
1112
from amazon.opentelemetry.distro.instrumentation.common.instrumentation_utils import serialize_to_json_string
1213
from opentelemetry import context, trace
1314
from opentelemetry.instrumentation.utils import suppress_http_instrumentation
@@ -60,17 +61,22 @@ def __init__(self, tracer: trace.Tracer, **kwargs: Any) -> None:
6061
self._should_suppress_http_spans = (
6162
os.environ.get(OTEL_MCP_SUPPRESS_HTTP_INSTRUMENTATION, "true").lower() == "true"
6263
)
64+
self._agent_observability_enabled = is_agent_observability_enabled()
6365

64-
@staticmethod
65-
def _should_suppress_mcp_span(message: Any) -> bool:
66+
def _should_suppress_mcp_span(self, message: Any) -> bool:
6667
from mcp import types # pylint: disable=import-outside-toplevel
6768

6869
if isinstance(
6970
message, (types.ClientRequest, types.ClientNotification, types.ServerRequest, types.ServerNotification)
7071
):
7172
message = message.root
7273
# noisy spans most of the time
73-
return isinstance(message, (types.InitializeRequest, types.InitializedNotification))
74+
if isinstance(message, (types.InitializeRequest, types.InitializedNotification)):
75+
return True
76+
# MCP servers hosted on AgentCore get frequent /ping health checks
77+
if self._agent_observability_enabled and isinstance(message, types.PingRequest):
78+
return True
79+
return False
7480

7581
@staticmethod
7682
def _set_mcp_attributes(span: trace.Span, message: Any, request_id: Optional[int]) -> None:

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/exporter/otlp/aws/traces/test_otlp_aws_span_exporter.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,70 @@ def test_export_with_llo_processing_failure(
223223
result = exporter.export(spans)
224224

225225
self.assertEqual(result, SpanExportResult.FAILURE)
226+
227+
@patch(
228+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter."
229+
"is_genai_content_extraction_opted_out"
230+
)
231+
@patch(
232+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.is_agentic_observability_enabled"
233+
)
234+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.get_logger_provider")
235+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.LLOHandler")
236+
def test_export_skips_llo_when_content_extraction_opted_out(
237+
self, mock_llo_handler_class, mock_get_logger_provider, mock_is_enabled, mock_opted_out
238+
):
239+
mock_is_enabled.return_value = True
240+
mock_opted_out.return_value = True
241+
mock_logger_provider = MagicMock(spec=LoggerProvider)
242+
mock_get_logger_provider.return_value = mock_logger_provider
243+
244+
endpoint = "https://xray.us-east-1.amazonaws.com/v1/traces"
245+
exporter = OTLPAwsSpanExporter(session=get_aws_session(), aws_region="us-east-1", endpoint=endpoint)
246+
247+
original_spans = [MagicMock(spec=ReadableSpan)]
248+
249+
with patch.object(OTLPSpanExporter, "export") as mock_parent_export:
250+
mock_parent_export.return_value = SpanExportResult.SUCCESS
251+
252+
result = exporter.export(original_spans)
253+
254+
self.assertEqual(result, SpanExportResult.SUCCESS)
255+
mock_parent_export.assert_called_once_with(original_spans)
256+
mock_llo_handler_class.assert_not_called()
257+
258+
@patch(
259+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter."
260+
"is_genai_content_extraction_opted_out"
261+
)
262+
@patch(
263+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.is_agentic_observability_enabled"
264+
)
265+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.get_logger_provider")
266+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.LLOHandler")
267+
def test_export_processes_llo_when_content_extraction_not_opted_out(
268+
self, mock_llo_handler_class, mock_get_logger_provider, mock_is_enabled, mock_opted_out
269+
):
270+
mock_is_enabled.return_value = True
271+
mock_opted_out.return_value = False
272+
mock_logger_provider = MagicMock(spec=LoggerProvider)
273+
mock_get_logger_provider.return_value = mock_logger_provider
274+
275+
mock_llo_handler = MagicMock()
276+
mock_llo_handler_class.return_value = mock_llo_handler
277+
278+
endpoint = "https://xray.us-east-1.amazonaws.com/v1/traces"
279+
exporter = OTLPAwsSpanExporter(session=get_aws_session(), aws_region="us-east-1", endpoint=endpoint)
280+
281+
original_spans = [MagicMock(spec=ReadableSpan)]
282+
processed_spans = [MagicMock(spec=ReadableSpan)]
283+
mock_llo_handler.process_spans.return_value = processed_spans
284+
285+
with patch.object(OTLPSpanExporter, "export") as mock_parent_export:
286+
mock_parent_export.return_value = SpanExportResult.SUCCESS
287+
288+
result = exporter.export(original_spans)
289+
290+
self.assertEqual(result, SpanExportResult.SUCCESS)
291+
mock_llo_handler.process_spans.assert_called_once_with(original_spans)
292+
mock_parent_export.assert_called_once_with(processed_spans)

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/instrumentation/mcp/test_mcp_instrumentor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,27 @@ async def run(session):
473473
finally:
474474
HTTPXClientInstrumentor().uninstrument()
475475

476+
def test_ping_span_suppression(self):
477+
for agent_obs_enabled, expect_suppressed in [("true", True), ("false", False)]:
478+
with self.subTest(agent_observability=agent_obs_enabled):
479+
self.instrumentor.uninstrument()
480+
self.span_exporter.clear()
481+
with unittest.mock.patch.dict(os.environ, {"AGENT_OBSERVABILITY_ENABLED": agent_obs_enabled}):
482+
self.instrumentor.instrument(tracer_provider=self.tracer_provider, propagators=self.propagator)
483+
self.server = self._create_server()
484+
485+
async def run(session):
486+
await session.send_ping()
487+
488+
asyncio.run(self._run_inprocess(run))
489+
spans = self.span_exporter.get_finished_spans()
490+
491+
ping_spans = [s for s in spans if "ping" in s.name.lower()]
492+
if expect_suppressed:
493+
self.assertEqual(len(ping_spans), 0, "Ping spans should be suppressed")
494+
else:
495+
self.assertGreater(len(ping_spans), 0, "Ping spans should not be suppressed")
496+
476497
def test_mcp_respects_active_parent_span(self):
477498
tracer = get_tracer("test", tracer_provider=self.tracer_provider)
478499

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,35 @@
99
from amazon.opentelemetry.distro._utils import (
1010
AGENT_OBSERVABILITY_ENABLED,
1111
AWS_AGENTIC_OBSERVABILITY_OPT_IN,
12+
AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT,
1213
get_aws_region,
1314
get_aws_session,
1415
is_agent_observability_enabled,
1516
is_agentic_observability_enabled,
1617
is_aws_agentic_observability_opt_in,
18+
is_genai_content_extraction_opted_out,
1719
is_installed,
1820
)
1921

2022

2123
class TestUtils(TestCase):
2224
def setUp(self):
23-
# Store original env var if it exists
2425
self.original_env = os.environ.get(AGENT_OBSERVABILITY_ENABLED)
25-
# Clear it to ensure clean state
26+
self.original_opt_out_env = os.environ.get(AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT)
2627
if AGENT_OBSERVABILITY_ENABLED in os.environ:
2728
del os.environ[AGENT_OBSERVABILITY_ENABLED]
29+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
30+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
2831

2932
def tearDown(self):
30-
# First clear the env var
3133
if AGENT_OBSERVABILITY_ENABLED in os.environ:
3234
del os.environ[AGENT_OBSERVABILITY_ENABLED]
33-
# Then restore original if it existed
35+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
36+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
3437
if self.original_env is not None:
3538
os.environ[AGENT_OBSERVABILITY_ENABLED] = self.original_env
39+
if self.original_opt_out_env is not None:
40+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = self.original_opt_out_env
3641

3742
def test_is_installed_package_not_found(self):
3843
"""Test is_installed returns False when package is not found"""
@@ -211,3 +216,26 @@ def test_get_aws_region_with_aws_default_region_env(self):
211216
self.assertEqual(region, "eu-west-1")
212217

213218
os.environ.pop("AWS_DEFAULT_REGION", None)
219+
220+
def test_is_genai_content_extraction_opted_out_various_values(self):
221+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "true"
222+
self.assertTrue(is_genai_content_extraction_opted_out())
223+
224+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "True"
225+
self.assertTrue(is_genai_content_extraction_opted_out())
226+
227+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "TRUE"
228+
self.assertTrue(is_genai_content_extraction_opted_out())
229+
230+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "false"
231+
self.assertFalse(is_genai_content_extraction_opted_out())
232+
233+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "False"
234+
self.assertFalse(is_genai_content_extraction_opted_out())
235+
236+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = ""
237+
self.assertFalse(is_genai_content_extraction_opted_out())
238+
239+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
240+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
241+
self.assertFalse(is_genai_content_extraction_opted_out())

0 commit comments

Comments
 (0)