Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pip-log.txt
# Unit test / coverage reports
coverage.xml
.coverage
.coverage.*
.nox
.tox
.cache
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t

## Unreleased

- 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
([#729](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/729))
- 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
([#727](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/727))
## v0.17.0 - 2026-04-08
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

_logger: Logger = getLogger(__name__)

# Maintained for backwards compatibility. New users should use AWS_AGENTIC_OBSERVABILITY_OPT_IN instead.
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
AWS_AGENTIC_OBSERVABILITY_OPT_IN = "AWS_AGENTIC_OBSERVABILITY_OPT_IN"
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS = "OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS"


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


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


def is_aws_agentic_observability_opt_in() -> bool:
"""Is the AI observability opt-in flag set to true?"""
return os.environ.get(AWS_AGENTIC_OBSERVABILITY_OPT_IN, "false").lower() == "true"


def is_agentic_observability_enabled() -> bool:
"""Returns True if either AGENT_OBSERVABILITY_ENABLED or AWS_AGENTIC_OBSERVABILITY_OPT_IN is set to true."""
return is_agent_observability_enabled() or is_aws_agentic_observability_opt_in()


def should_add_application_signals_dimensions() -> bool:
"""Should Service and Environment Application Signals dimensions be added to EMF logs?"""
return os.environ.get(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "true").lower() == "true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE, AWS_SERVICE_TYPE
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
from amazon.opentelemetry.distro._utils import get_aws_session, is_agentic_observability_enabled
from amazon.opentelemetry.distro._utils import get_aws_session, is_agent_observability_enabled
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
AttributePropagatingSpanProcessorBuilder,
Expand Down Expand Up @@ -205,7 +205,7 @@ def _initialize_components():
AwsEksResourceDetector(),
AwsEcsResourceDetector(),
]
if not (_is_lambda_environment() or is_agentic_observability_enabled())
if not (_is_lambda_environment() or is_agent_observability_enabled())
else []
)

Expand Down Expand Up @@ -327,7 +327,7 @@ def _export_unsampled_span_for_lambda(trace_provider: TracerProvider, resource:


def _export_unsampled_span_for_agent_observability(trace_provider: TracerProvider, resource: Resource = None):
if not is_agentic_observability_enabled():
if not is_agent_observability_enabled():
return

traces_endpoint = os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
Expand Down Expand Up @@ -468,7 +468,7 @@ def _customize_log_record_processor(logger_provider: LoggerProvider, log_exporte
if not log_exporter:
return

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

custom_attributes = {AWS_LOCAL_SERVICE: service_name}

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

if service == XRAY_SERVICE:
if is_agentic_observability_enabled():
if is_agent_observability_enabled():
# Span exporter needs an instance of logger provider in ai agent
# observability case because we need to split input/output prompts
# from span attributes and send them to the logs pipeline per
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def _check_otel_version_compatibility():
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS,
get_aws_region,
is_agent_observability_enabled,
is_aws_agentic_observability_opt_in,
is_installed,
)
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import APPLICATION_SIGNALS_ENABLED_CONFIG
Expand All @@ -95,28 +94,36 @@ def _check_otel_version_compatibility():
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED as OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
)
from opentelemetry.sdk.environment_variables import (
OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
)
from opentelemetry.util._importlib_metadata import EntryPoint
from opentelemetry.util._importlib_metadata import EntryPoint, entry_points

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


AGENT_OBSERVABILITY_DISABLED_INSTRUMENTATIONS = (
"sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
"system_metrics,google-genai,jinja2,aws_crewai,aws_langchain,aws_llama-index,aws_mcp,aws_openai_agents"
)
# When set to "true", opt in to AWS agentic instrumentors over third-party ones (e.g. OpenInference),
# even if both are installed. By default, auto-detection prefers third-party when present.
AWS_AGENTIC_INSTRUMENTATION_OPT_IN = "AWS_AGENTIC_INSTRUMENTATION_OPT_IN"
Comment thread
wangzlei marked this conversation as resolved.

AWS_AGENTIC_OBSERVABILITY_DISABLED_INSTRUMENTATIONS = (
"sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
"system_metrics,google-genai,jinja2,crewai,langchain,llama-index,llama_index,mcp,openai_agents"
)
# Maps third-party instrumentor entry point names to their AWS native equivalents.
# Used for mutual exclusion: only one side instruments each library at a time.
_THIRDPARTY_TO_AWS_NATIVE = {
Comment thread
wangzlei marked this conversation as resolved.
"crewai": "aws_crewai",
"langchain": "aws_langchain",
"llama-index": "aws_llama-index",
"llama_index": "aws_llama-index",
"mcp": "aws_mcp",
"openai_agents": "aws_openai_agents",
}

# Dist names owned by ADOT that register entry points with the same names as third-party ones.
# These are excluded from third-party detection to avoid false positives.
_ADOT_OWNED_DISTS = {"opentelemetry-instrumentation-openai-agents-v2"}


class AwsOpenTelemetryDistro(OpenTelemetryDistro):
Expand Down Expand Up @@ -192,66 +199,74 @@ def _configure(self, **kwargs):
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, "base2_exponential_bucket_histogram"
)

if is_aws_agentic_observability_opt_in():
_logger.info("AWS Agentic Observability enabled.")
self._configure_common_agent_observability(AWS_AGENTIC_OBSERVABILITY_DISABLED_INSTRUMENTATIONS)
os.environ.setdefault(OTEL_METRICS_EXPORTER, "otlp")
os.environ.setdefault("CREWAI_DISABLE_TELEMETRY", "true")
elif is_agent_observability_enabled():
# Maintained for backwards compatibility. New users should use AWS_AGENTIC_OBSERVABILITY_OPT_IN instead.
_logger.info(
"AGENT_OBSERVABILITY_ENABLED is set. Consider using AWS_AGENTIC_OBSERVABILITY_OPT_IN for ADOT Agentic Observability."
)
self._configure_common_agent_observability(AGENT_OBSERVABILITY_DISABLED_INSTRUMENTATIONS)
if is_agent_observability_enabled():
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp")
os.environ.setdefault(OTEL_METRICS_EXPORTER, "awsemf")
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true")

region = get_aws_region()
if not os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT):
if region:
os.environ.setdefault(
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces"
)
os.environ.setdefault(
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs"
)
else:
_logger.warning(
"AWS region could not be determined. OTLP endpoints will not be automatically configured. "
"Please set AWS_REGION environment variable or configure OTLP endpoints manually."
)
if region:
os.environ.setdefault(
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, f"https://xray.{region}.amazonaws.com/v1/traces"
)
os.environ.setdefault(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, f"https://logs.{region}.amazonaws.com/v1/logs")
else:
_logger.warning(
"AWS region could not be determined. OTLP endpoints will not be automatically configured. "
"Please set AWS_REGION environment variable or configure OTLP endpoints manually."
)

os.environ.setdefault(
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
"http,sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,"
"urllib3,requests,system_metrics,google-genai,jinja2",
)
os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true")
os.environ.setdefault(OTEL_PYTHON_LOG_CORRELATION, "true")
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
os.environ.setdefault(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "false")
os.environ.setdefault("CREWAI_DISABLE_TELEMETRY", "true")

super(AwsOpenTelemetryDistro, self)._configure()

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

def load_instrumentor(self, entry_point: EntryPoint, **kwargs):
if self._should_skip_instrumentor(entry_point):
"""Mutual exclusion between AWS native and third-party agentic instrumentors.

When agent observability is enabled:
- Skip ADOT-owned dists that duplicate an aws_* entry point (e.g. openai-agents-v2).
- Auto-detect: if a third-party instrumentor is registered for the same library,
skip the AWS native one. If not, skip the third-party one.
- AWS_AGENTIC_INSTRUMENTATION_OPT_IN=true overrides auto-detection and forces
AWS native instrumentors, skipping third-party ones.
"""
if is_agent_observability_enabled() and self._should_skip_instrumentor(entry_point):
return
super().load_instrumentor(entry_point, **kwargs)

@staticmethod
def _should_skip_instrumentor(entry_point: EntryPoint) -> bool:
Comment thread
wangzlei marked this conversation as resolved.
# Some third-party SDKs register the same entry point name as the upstream
# OTel packages that we depend on. For Agentic Observability legacy mode, skip our bundled
# OTel instrumentation so that existing third-party setups are not brokens.
if (
is_agent_observability_enabled()
and not is_aws_agentic_observability_opt_in()
and entry_point.dist
and entry_point.name == "openai_agents"
and entry_point.dist.name == "opentelemetry-instrumentation-openai-agents-v2"
):
def _should_skip_instrumentor(entry_point):
if entry_point.dist and entry_point.dist.name in _ADOT_OWNED_DISTS:
return True
# TODO: add additional skip conditions here as needed
return False

@staticmethod
def _configure_common_agent_observability(disabled_instrumentations: str) -> None:
os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true")
os.environ.setdefault(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, disabled_instrumentations)
os.environ.setdefault(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "true")
os.environ.setdefault(OTEL_PYTHON_LOG_CORRELATION, "true")
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
os.environ.setdefault(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "false")
os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp")
os.environ.setdefault(OTEL_LOGS_EXPORTER, "otlp")
prefer_native = os.environ.get(AWS_AGENTIC_INSTRUMENTATION_OPT_IN, "false").lower() == "true"
third_party_names = {
ep.name
for ep in entry_points(group="opentelemetry_instrumentor")
if not (ep.dist and ep.dist.name in _ADOT_OWNED_DISTS)
}

if entry_point.name in _THIRDPARTY_TO_AWS_NATIVE.values() and not prefer_native:
for tp_name, aws_name in _THIRDPARTY_TO_AWS_NATIVE.items():
if entry_point.name == aws_name and tp_name in third_party_names:
_logger.debug("Skipping %s: third-party %s is registered", aws_name, tp_name)
return True

if entry_point.name in _THIRDPARTY_TO_AWS_NATIVE and prefer_native:
_logger.debug("Skipping third-party %s: AWS native preferred", entry_point.name)
return True

return False
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
from amazon.opentelemetry.distro.version import __version__


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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from botocore.session import Session

from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
from amazon.opentelemetry.distro.exporter.otlp.aws.common._aws_http_headers import _OTLP_AWS_HTTP_HEADERS
from amazon.opentelemetry.distro.exporter.otlp.aws.common.aws_auth_session import AwsAuthSession
from amazon.opentelemetry.distro.llo_handler import LLOHandler
Expand Down Expand Up @@ -61,7 +61,7 @@ def __init__(

def _ensure_llo_handler(self):
"""Lazily initialize LLO handler when needed to avoid initialization order issues"""
if self._llo_handler is None and is_agentic_observability_enabled():
if self._llo_handler is None and is_agent_observability_enabled():
if self._logger_provider is None:
try:
self._logger_provider = get_logger_provider()
Expand All @@ -77,7 +77,7 @@ def _ensure_llo_handler(self):

def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
try:
if is_agentic_observability_enabled() and self._ensure_llo_handler():
if is_agent_observability_enabled() and self._ensure_llo_handler():
llo_processed_spans = self._llo_handler.process_spans(spans)
return super().export(llo_processed_spans)
except Exception: # pylint: disable=broad-exception-caught
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
from logging import Logger, getLogger

from amazon.opentelemetry.distro._utils import is_agentic_observability_enabled
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled

_logger: Logger = getLogger(__name__)

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

def patched_init(self, app, **kwargs):
Expand Down
Loading
Loading