Skip to content

Commit 78ce346

Browse files
authored
Merge branch 'main' into converse-contract-tests
2 parents 801e621 + aefb06e commit 78ce346

7 files changed

Lines changed: 141 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ 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))
1519
- fix(agent-observability): fall back to OTEL_EXPORTER_OTLP_ENDPOINT for unsampled spans; also export unsampled spans to non-AWS endpoints
1620
([#738](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/738))
1721
- feat: auto-detect and mutually exclude AWS native vs third-party agentic instrumentors; add `AWS_AGENTIC_INSTRUMENTATION_OPT_IN` env var to override auto-detection

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
1414
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS = "OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS"
15+
AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT = "AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT"
1516

1617

1718
def is_installed(req: str) -> bool:
@@ -39,6 +40,11 @@ def is_agent_observability_enabled() -> bool:
3940
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"
4041

4142

43+
def is_genai_content_extraction_opted_out() -> bool:
44+
"""Has the user opted out of GenAI content extraction from spans?"""
45+
return os.environ.get(AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT, "false").lower() == "true"
46+
47+
4248
def should_add_application_signals_dimensions() -> bool:
4349
"""Should Service and Environment Application Signals dimensions be added to EMF logs?"""
4450
return os.environ.get(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "true").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_agent_observability_enabled
9+
from amazon.opentelemetry.distro._utils import is_agent_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_agent_observability_enabled() and self._ensure_llo_handler():
80+
if (
81+
is_agent_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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,66 @@ def test_export_with_llo_processing_failure(
209209
result = exporter.export(spans)
210210

211211
self.assertEqual(result, SpanExportResult.FAILURE)
212+
213+
@patch(
214+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter."
215+
"is_genai_content_extraction_opted_out"
216+
)
217+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.is_agent_observability_enabled")
218+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.get_logger_provider")
219+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.LLOHandler")
220+
def test_export_skips_llo_when_content_extraction_opted_out(
221+
self, mock_llo_handler_class, mock_get_logger_provider, mock_is_enabled, mock_opted_out
222+
):
223+
mock_is_enabled.return_value = True
224+
mock_opted_out.return_value = True
225+
mock_logger_provider = MagicMock(spec=LoggerProvider)
226+
mock_get_logger_provider.return_value = mock_logger_provider
227+
228+
endpoint = "https://xray.us-east-1.amazonaws.com/v1/traces"
229+
exporter = OTLPAwsSpanExporter(session=get_aws_session(), aws_region="us-east-1", endpoint=endpoint)
230+
231+
original_spans = [MagicMock(spec=ReadableSpan)]
232+
233+
with patch.object(OTLPSpanExporter, "export") as mock_parent_export:
234+
mock_parent_export.return_value = SpanExportResult.SUCCESS
235+
236+
result = exporter.export(original_spans)
237+
238+
self.assertEqual(result, SpanExportResult.SUCCESS)
239+
mock_parent_export.assert_called_once_with(original_spans)
240+
mock_llo_handler_class.assert_not_called()
241+
242+
@patch(
243+
"amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter."
244+
"is_genai_content_extraction_opted_out"
245+
)
246+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.is_agent_observability_enabled")
247+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.get_logger_provider")
248+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter.LLOHandler")
249+
def test_export_processes_llo_when_content_extraction_not_opted_out(
250+
self, mock_llo_handler_class, mock_get_logger_provider, mock_is_enabled, mock_opted_out
251+
):
252+
mock_is_enabled.return_value = True
253+
mock_opted_out.return_value = False
254+
mock_logger_provider = MagicMock(spec=LoggerProvider)
255+
mock_get_logger_provider.return_value = mock_logger_provider
256+
257+
mock_llo_handler = MagicMock()
258+
mock_llo_handler_class.return_value = mock_llo_handler
259+
260+
endpoint = "https://xray.us-east-1.amazonaws.com/v1/traces"
261+
exporter = OTLPAwsSpanExporter(session=get_aws_session(), aws_region="us-east-1", endpoint=endpoint)
262+
263+
original_spans = [MagicMock(spec=ReadableSpan)]
264+
processed_spans = [MagicMock(spec=ReadableSpan)]
265+
mock_llo_handler.process_spans.return_value = processed_spans
266+
267+
with patch.object(OTLPSpanExporter, "export") as mock_parent_export:
268+
mock_parent_export.return_value = SpanExportResult.SUCCESS
269+
270+
result = exporter.export(original_spans)
271+
272+
self.assertEqual(result, SpanExportResult.SUCCESS)
273+
mock_llo_handler.process_spans.assert_called_once_with(original_spans)
274+
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
@@ -8,28 +8,33 @@
88

99
from amazon.opentelemetry.distro._utils import (
1010
AGENT_OBSERVABILITY_ENABLED,
11+
AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT,
1112
get_aws_region,
1213
get_aws_session,
1314
is_agent_observability_enabled,
15+
is_genai_content_extraction_opted_out,
1416
is_installed,
1517
)
1618

1719

1820
class TestUtils(TestCase):
1921
def setUp(self):
20-
# Store original env var if it exists
2122
self.original_env = os.environ.get(AGENT_OBSERVABILITY_ENABLED)
22-
# Clear it to ensure clean state
23+
self.original_opt_out_env = os.environ.get(AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT)
2324
if AGENT_OBSERVABILITY_ENABLED in os.environ:
2425
del os.environ[AGENT_OBSERVABILITY_ENABLED]
26+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
27+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
2528

2629
def tearDown(self):
27-
# First clear the env var
2830
if AGENT_OBSERVABILITY_ENABLED in os.environ:
2931
del os.environ[AGENT_OBSERVABILITY_ENABLED]
30-
# Then restore original if it existed
32+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
33+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
3134
if self.original_env is not None:
3235
os.environ[AGENT_OBSERVABILITY_ENABLED] = self.original_env
36+
if self.original_opt_out_env is not None:
37+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = self.original_opt_out_env
3338

3439
def test_is_installed_package_not_found(self):
3540
"""Test is_installed returns False when package is not found"""
@@ -173,3 +178,26 @@ def test_get_aws_region_with_aws_default_region_env(self):
173178
self.assertEqual(region, "eu-west-1")
174179

175180
os.environ.pop("AWS_DEFAULT_REGION", None)
181+
182+
def test_is_genai_content_extraction_opted_out_various_values(self):
183+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "true"
184+
self.assertTrue(is_genai_content_extraction_opted_out())
185+
186+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "True"
187+
self.assertTrue(is_genai_content_extraction_opted_out())
188+
189+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "TRUE"
190+
self.assertTrue(is_genai_content_extraction_opted_out())
191+
192+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "false"
193+
self.assertFalse(is_genai_content_extraction_opted_out())
194+
195+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = "False"
196+
self.assertFalse(is_genai_content_extraction_opted_out())
197+
198+
os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT] = ""
199+
self.assertFalse(is_genai_content_extraction_opted_out())
200+
201+
if AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT in os.environ:
202+
del os.environ[AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT]
203+
self.assertFalse(is_genai_content_extraction_opted_out())

0 commit comments

Comments
 (0)