Skip to content

Commit 02ada61

Browse files
authored
feat: auto-detect and mutually exclude native vs third-party agentic instrumentors (#729)
## Summary - Remove `AWS_AGENTIC_OBSERVABILITY_OPT_IN` logic and related code introduced in 0.17, restore v0.15 `is_agent_observability_enabled()` as the sole legacy toggle - When `AGENT_OBSERVABILITY_ENABLED=true`, auto-detect registered third-party agentic instrumentors (e.g. OpenInference) and mutually exclude them with AWS native ones to prevent double instrumentation - Add `AWS_AGENTIC_INSTRUMENTATION_OPT_IN` env var to let users override auto-detection and force AWS native instrumentors ## Use Cases ### Case 1: New user, only ADOT installed (no OpenInference) - Sets `AGENT_OBSERVABILITY_ENABLED=true` - No third-party instrumentors detected → AWS native (`aws_langchain`, `aws_crewai`, etc.) auto-enabled - Zero additional configuration needed ### Case 2: Existing user with ADOT + OpenInference - Sets `AGENT_OBSERVABILITY_ENABLED=true` - Auto-detect finds `langchain`, `crewai` etc. entry points from OpenInference - AWS native equivalents (`aws_langchain`, `aws_crewai`) are disabled automatically - OpenInference continues to work, no breaking change ### Case 3: User wants to migrate from OpenInference to AWS native - Sets `AGENT_OBSERVABILITY_ENABLED=true` + `AWS_AGENTIC_INSTRUMENTATION_OPT_IN=true` - Even though OpenInference is still installed, third-party instrumentors are disabled - AWS native instrumentors take over - User can uninstall OpenInference at their convenience ### Case 4: Non-agentic user - Does not set `AGENT_OBSERVABILITY_ENABLED` → entire agentic logic skipped, no impact ### Case 5: User with custom OTEL_PYTHON_DISABLED_INSTRUMENTATIONS - User-provided disabled list is preserved (setdefault) - Mutual exclusion disables are appended on top of user's value ## Test plan - [x] Unit tests: 936 passed, 0 failures - [x] Coverage: `aws_opentelemetry_distro.py` at 100% - [x] Lint: black, flake8, isort all pass - [x] Contract tests - [x] Manual validation with OpenInference installed
1 parent 314aa88 commit 02ada61

13 files changed

Lines changed: 218 additions & 265 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pip-log.txt
3030
# Unit test / coverage reports
3131
coverage.xml
3232
.coverage
33+
.coverage.*
3334
.nox
3435
.tox
3536
.cache

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: auto-detect and mutually exclude AWS native vs third-party agentic instrumentors; add `AWS_AGENTIC_INSTRUMENTATION_OPT_IN` env var to override auto-detection
16+
([#729](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/729))
1517
- fix(lambda-layer): align context propagation with JS — delegate to global propagator so W3C traceparent is no longer ignored when X-Ray active tracing is enabled
1618
([#727](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/727))
1719
## v0.17.0 - 2026-04-08

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010

1111
_logger: Logger = getLogger(__name__)
1212

13-
# Maintained for backwards compatibility. New users should use AWS_AGENTIC_OBSERVABILITY_OPT_IN instead.
1413
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
15-
AWS_AGENTIC_OBSERVABILITY_OPT_IN = "AWS_AGENTIC_OBSERVABILITY_OPT_IN"
1614
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS = "OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS"
1715

1816

@@ -37,20 +35,10 @@ def is_installed(req: str) -> bool:
3735

3836

3937
def is_agent_observability_enabled() -> bool:
40-
# Maintained for backwards compatibility. New users should use AWS_AGENTIC_OBSERVABILITY_OPT_IN instead.
38+
"""Is the Agentic AI monitoring flag set to true?"""
4139
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"
4240

4341

44-
def is_aws_agentic_observability_opt_in() -> bool:
45-
"""Is the AI observability opt-in flag set to true?"""
46-
return os.environ.get(AWS_AGENTIC_OBSERVABILITY_OPT_IN, "false").lower() == "true"
47-
48-
49-
def is_agentic_observability_enabled() -> bool:
50-
"""Returns True if either AGENT_OBSERVABILITY_ENABLED or AWS_AGENTIC_OBSERVABILITY_OPT_IN is set to true."""
51-
return is_agent_observability_enabled() or is_aws_agentic_observability_opt_in()
52-
53-
5442
def should_add_application_signals_dimensions() -> bool:
5543
"""Should Service and Environment Application Signals dimensions be added to EMF logs?"""
5644
return os.environ.get(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "true").lower() == "true"

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE, AWS_SERVICE_TYPE
1919
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
20-
from amazon.opentelemetry.distro._utils import get_aws_session, is_agentic_observability_enabled
20+
from amazon.opentelemetry.distro._utils import get_aws_session, is_agent_observability_enabled
2121
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
2222
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
2323
AttributePropagatingSpanProcessorBuilder,
@@ -205,7 +205,7 @@ def _initialize_components():
205205
AwsEksResourceDetector(),
206206
AwsEcsResourceDetector(),
207207
]
208-
if not (_is_lambda_environment() or is_agentic_observability_enabled())
208+
if not (_is_lambda_environment() or is_agent_observability_enabled())
209209
else []
210210
)
211211

@@ -327,7 +327,7 @@ def _export_unsampled_span_for_lambda(trace_provider: TracerProvider, resource:
327327

328328

329329
def _export_unsampled_span_for_agent_observability(trace_provider: TracerProvider, resource: Resource = None):
330-
if not is_agentic_observability_enabled():
330+
if not is_agent_observability_enabled():
331331
return
332332

333333
traces_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
@@ -468,7 +468,7 @@ def _customize_log_record_processor(logger_provider: LoggerProvider, log_exporte
468468
if not log_exporter:
469469
return
470470

471-
if is_agentic_observability_enabled():
471+
if is_agent_observability_enabled():
472472
# pylint: disable=import-outside-toplevel
473473
from amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor import (
474474
AwsCloudWatchOtlpBatchLogRecordProcessor,
@@ -527,7 +527,7 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource, sam
527527
# AI applications typically have low throughput traffic patterns and require
528528
# comprehensive monitoring to catch subtle failure modes like hallucinations
529529
# and quality degradation that sampling could miss.
530-
if is_agentic_observability_enabled():
530+
if is_agent_observability_enabled():
531531
_export_unsampled_span_for_agent_observability(provider, resource)
532532
provider.add_span_processor(GenAiNestedClientSpanProcessor())
533533
baggage_keys.add("session.id")
@@ -635,7 +635,7 @@ def _customize_resource(resource: Resource) -> Resource:
635635

636636
custom_attributes = {AWS_LOCAL_SERVICE: service_name}
637637

638-
if is_agentic_observability_enabled():
638+
if is_agent_observability_enabled():
639639
# Add aws.service.type if it doesn't exist in the resource
640640
if resource and resource.attributes.get(AWS_SERVICE_TYPE) is None:
641641
# Set a default agent type for AI agent observability
@@ -921,7 +921,7 @@ def _create_aws_otlp_exporter(endpoint: str, service: str, region: str):
921921
from amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter import OTLPAwsSpanExporter
922922

923923
if service == XRAY_SERVICE:
924-
if is_agentic_observability_enabled():
924+
if is_agent_observability_enabled():
925925
# Span exporter needs an instance of logger provider in ai agent
926926
# observability case because we need to split input/output prompts
927927
# from span attributes and send them to the logs pipeline per

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

Lines changed: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def _check_otel_version_compatibility():
7070
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS,
7171
get_aws_region,
7272
is_agent_observability_enabled,
73-
is_aws_agentic_observability_opt_in,
7473
is_installed,
7574
)
7675
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import APPLICATION_SIGNALS_ENABLED_CONFIG
@@ -95,28 +94,36 @@ def _check_otel_version_compatibility():
9594
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED as OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
9695
)
9796
from opentelemetry.sdk.environment_variables import (
98-
OTEL_EXPORTER_OTLP_ENDPOINT,
9997
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
10098
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
10199
OTEL_EXPORTER_OTLP_PROTOCOL,
102100
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
103101
)
104-
from opentelemetry.util._importlib_metadata import EntryPoint
102+
from opentelemetry.util._importlib_metadata import EntryPoint, entry_points
105103

106104
_logger: Logger = getLogger(__name__)
107105
# Suppress configurator warnings from auto-instrumentation
108106
_load._logger.setLevel(LEVELS.get(os.environ.get(OTEL_PYTHON_LOG_LEVEL, "error").lower(), ERROR))
109107

110108

111-
AGENT_OBSERVABILITY_DISABLED_INSTRUMENTATIONS = (
112-
"sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
113-
"system_metrics,google-genai,jinja2,aws_crewai,aws_langchain,aws_llama-index,aws_mcp,aws_openai_agents"
114-
)
109+
# When set to "true", opt in to AWS agentic instrumentors over third-party ones (e.g. OpenInference),
110+
# even if both are installed. By default, auto-detection prefers third-party when present.
111+
AWS_AGENTIC_INSTRUMENTATION_OPT_IN = "AWS_AGENTIC_INSTRUMENTATION_OPT_IN"
115112

116-
AWS_AGENTIC_OBSERVABILITY_DISABLED_INSTRUMENTATIONS = (
117-
"sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
118-
"system_metrics,google-genai,jinja2,crewai,langchain,llama-index,llama_index,mcp,openai_agents"
119-
)
113+
# Maps third-party instrumentor entry point names to their AWS native equivalents.
114+
# Used for mutual exclusion: only one side instruments each library at a time.
115+
_THIRDPARTY_TO_AWS_NATIVE = {
116+
"crewai": "aws_crewai",
117+
"langchain": "aws_langchain",
118+
"llama-index": "aws_llama-index",
119+
"llama_index": "aws_llama-index",
120+
"mcp": "aws_mcp",
121+
"openai_agents": "aws_openai_agents",
122+
}
123+
124+
# Dist names owned by ADOT that register entry points with the same names as third-party ones.
125+
# These are excluded from third-party detection to avoid false positives.
126+
_ADOT_OWNED_DISTS = {"opentelemetry-instrumentation-openai-agents-v2"}
120127

121128

122129
class AwsOpenTelemetryDistro(OpenTelemetryDistro):
@@ -192,66 +199,74 @@ def _configure(self, **kwargs):
192199
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "base2_exponential_bucket_histogram"
193200
)
194201

195-
if is_aws_agentic_observability_opt_in():
196-
_logger.info("AWS Agentic Observability enabled.")
197-
self._configure_common_agent_observability(AWS_AGENTIC_OBSERVABILITY_DISABLED_INSTRUMENTATIONS)
198-
os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp")
199-
os.environ.setdefault("CREWAI_DISABLE_TELEMETRY", "true")
200-
elif is_agent_observability_enabled():
201-
# Maintained for backwards compatibility. New users should use AWS_AGENTIC_OBSERVABILITY_OPT_IN instead.
202-
_logger.info(
203-
"AGENT_OBSERVABILITY_ENABLED is set. Consider using AWS_AGENTIC_OBSERVABILITY_OPT_IN for ADOT Agentic Observability."
204-
)
205-
self._configure_common_agent_observability(AGENT_OBSERVABILITY_DISABLED_INSTRUMENTATIONS)
202+
if is_agent_observability_enabled():
203+
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
204+
os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp")
206205
os.environ.setdefault(OTEL_METRICS_EXPORTER, "awsemf")
206+
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true")
207+
207208
region = get_aws_region()
208-
if not os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT):
209-
if region:
210-
os.environ.setdefault(
211-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces"
212-
)
213-
os.environ.setdefault(
214-
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs"
215-
)
216-
else:
217-
_logger.warning(
218-
"AWS region could not be determined. OTLP endpoints will not be automatically configured. "
219-
"Please set AWS_REGION environment variable or configure OTLP endpoints manually."
220-
)
209+
if region:
210+
os.environ.setdefault(
211+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces"
212+
)
213+
os.environ.setdefault(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs")
214+
else:
215+
_logger.warning(
216+
"AWS region could not be determined. OTLP endpoints will not be automatically configured. "
217+
"Please set AWS_REGION environment variable or configure OTLP endpoints manually."
218+
)
219+
220+
os.environ.setdefault(
221+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
222+
"http,sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
223+
"urllib3,requests,system_metrics,google-genai,jinja2",
224+
)
225+
os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true")
226+
os.environ.setdefault(OTEL_PYTHON_LOG_CORRELATION, "true")
227+
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
228+
os.environ.setdefault(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "false")
229+
os.environ.setdefault("CREWAI_DISABLE_TELEMETRY", "true")
221230

222231
super(AwsOpenTelemetryDistro, self)._configure()
223232

224233
if kwargs.get("apply_patches", True):
225234
apply_instrumentation_patches()
226235

227236
def load_instrumentor(self, entry_point: EntryPoint, **kwargs):
228-
if self._should_skip_instrumentor(entry_point):
237+
"""Mutual exclusion between AWS native and third-party agentic instrumentors.
238+
239+
When agent observability is enabled:
240+
- Skip ADOT-owned dists that duplicate an aws_* entry point (e.g. openai-agents-v2).
241+
- Auto-detect: if a third-party instrumentor is registered for the same library,
242+
skip the AWS native one. If not, skip the third-party one.
243+
- AWS_AGENTIC_INSTRUMENTATION_OPT_IN=true overrides auto-detection and forces
244+
AWS native instrumentors, skipping third-party ones.
245+
"""
246+
if is_agent_observability_enabled() and self._should_skip_instrumentor(entry_point):
229247
return
230248
super().load_instrumentor(entry_point, **kwargs)
231249

232250
@staticmethod
233-
def _should_skip_instrumentor(entry_point: EntryPoint) -> bool:
234-
# Some third-party SDKs register the same entry point name as the upstream
235-
# OTel packages that we depend on. For Agentic Observability legacy mode, skip our bundled
236-
# OTel instrumentation so that existing third-party setups are not brokens.
237-
if (
238-
is_agent_observability_enabled()
239-
and not is_aws_agentic_observability_opt_in()
240-
and entry_point.dist
241-
and entry_point.name == "openai_agents"
242-
and entry_point.dist.name == "opentelemetry-instrumentation-openai-agents-v2"
243-
):
251+
def _should_skip_instrumentor(entry_point):
252+
if entry_point.dist and entry_point.dist.name in _ADOT_OWNED_DISTS:
244253
return True
245-
# TODO: add additional skip conditions here as needed
246-
return False
247254

248-
@staticmethod
249-
def _configure_common_agent_observability(disabled_instrumentations: str) -> None:
250-
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true")
251-
os.environ.setdefault(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, disabled_instrumentations)
252-
os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true")
253-
os.environ.setdefault(OTEL_PYTHON_LOG_CORRELATION, "true")
254-
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
255-
os.environ.setdefault(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "false")
256-
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
257-
os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp")
255+
prefer_native = os.environ.get(AWS_AGENTIC_INSTRUMENTATION_OPT_IN, "false").lower() == "true"
256+
third_party_names = {
257+
ep.name
258+
for ep in entry_points(group="opentelemetry_instrumentor")
259+
if not (ep.dist and ep.dist.name in _ADOT_OWNED_DISTS)
260+
}
261+
262+
if entry_point.name in _THIRDPARTY_TO_AWS_NATIVE.values() and not prefer_native:
263+
for tp_name, aws_name in _THIRDPARTY_TO_AWS_NATIVE.items():
264+
if entry_point.name == aws_name and tp_name in third_party_names:
265+
_logger.debug("Skipping %s: third-party %s is registered", aws_name, tp_name)
266+
return True
267+
268+
if entry_point.name in _THIRDPARTY_TO_AWS_NATIVE and prefer_native:
269+
_logger.debug("Skipping third-party %s: AWS native preferred", entry_point.name)
270+
return True
271+
272+
return False

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/otlp/aws/common/_aws_http_headers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
4+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
55
from amazon.opentelemetry.distro.version import __version__
66

77

88
def build_user_agent() -> str:
99
user_agent = f"ADOT-OTLP-Exporter-Python/{__version__}"
1010

11-
if is_agentic_observability_enabled():
11+
if is_agent_observability_enabled():
1212
user_agent = f"ADOT-GenAI-OTLP-Exporter-Python/{__version__}"
1313
return user_agent
1414

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

Lines changed: 3 additions & 3 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_agent_observability_enabled
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
@@ -61,7 +61,7 @@ def __init__(
6161

6262
def _ensure_llo_handler(self):
6363
"""Lazily initialize LLO handler when needed to avoid initialization order issues"""
64-
if self._llo_handler is None and is_agentic_observability_enabled():
64+
if self._llo_handler is None and is_agent_observability_enabled():
6565
if self._logger_provider is None:
6666
try:
6767
self._logger_provider = get_logger_provider()
@@ -77,7 +77,7 @@ 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 is_agent_observability_enabled() and self._ensure_llo_handler():
8181
llo_processed_spans = self._llo_handler.process_spans(spans)
8282
return super().export(llo_processed_spans)
8383
except Exception: # pylint: disable=broad-exception-caught

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_starlette_patches.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
44
from logging import Logger, getLogger
55

6-
from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
6+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
77

88
_logger: Logger = getLogger(__name__)
99

@@ -25,7 +25,7 @@ def _apply_starlette_instrumentation_patches() -> None:
2525
#
2626
# Issue for tracking a feature to customize this setting within Starlette:
2727
# https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3725
28-
if is_agentic_observability_enabled():
28+
if is_agent_observability_enabled():
2929
original_init = OpenTelemetryMiddleware.__init__
3030

3131
def patched_init(self, app, **kwargs):

0 commit comments

Comments
 (0)