Skip to content

Commit aefb06e

Browse files
liustveezhang6811
andauthored
feat: add AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT env var to disable LLO processing (#741)
*Description of changes:* - Add `GENAI_CONTENT_EXTRACTION_OPT_OUT` environment variable to allow users to opt out of LLO content extraction from spans - When set to `true` the LLO handler is bypassed and spans are exported without content extraction - Content extraction remains enabled by default when agent observability is on 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: Eric Zhang <eric.zhang6811@gmail.com> Co-authored-by: Eric Zhang <zhaez@amazon.com>
1 parent ea1f4f4 commit aefb06e

5 files changed

Lines changed: 109 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ 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))
1517
- fix(mcp-instrumentation): suppress MCP `/ping` spans when agent observability is enabled
1618
([#748](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/748))
1719
- fix(agent-observability): fall back to OTEL_EXPORTER_OTLP_ENDPOINT for unsampled spans; also export unsampled spans to non-AWS endpoints

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/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/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)