From f6931f975946b06fdea2be1907bbad5562110b82 Mon Sep 17 00:00:00 2001 From: Michael He Date: Mon, 16 Jun 2025 17:36:45 +0000 Subject: [PATCH 01/15] Reapply "Bump OTel Dependency Versions to 1.33.0/0.54b0" (#397) This reverts commit a538775495db1f6a9a914a29784329a0a46ebf44. --- aws-opentelemetry-distro/pyproject.toml | 107 +++++++++--------- .../distro/patches/_bedrock_patches.py | 7 +- .../distro/patches/_botocore_patches.py | 17 ++- .../distro/test_instrumentation_patch.py | 5 +- 4 files changed, 71 insertions(+), 65 deletions(-) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 3d8eadbc1..bc98b7a27 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -24,62 +24,61 @@ classifiers = [ ] dependencies = [ - "opentelemetry-api == 1.27.0", - "opentelemetry-sdk == 1.27.0", - "opentelemetry-exporter-otlp-proto-grpc == 1.27.0", - "opentelemetry-exporter-otlp-proto-http == 1.27.0", - "opentelemetry-propagator-b3 == 1.27.0", - "opentelemetry-propagator-jaeger == 1.27.0", - "opentelemetry-exporter-otlp-proto-common == 1.27.0", + "opentelemetry-api == 1.33.0", + "opentelemetry-sdk == 1.33.0", + "opentelemetry-exporter-otlp-proto-grpc == 1.33.0", + "opentelemetry-exporter-otlp-proto-http == 1.33.0", + "opentelemetry-propagator-b3 == 1.33.0", + "opentelemetry-propagator-jaeger == 1.33.0", + "opentelemetry-exporter-otlp-proto-common == 1.33.0", "opentelemetry-sdk-extension-aws == 2.0.2", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-distro == 0.48b0", - "opentelemetry-processor-baggage == 0.48b0", - "opentelemetry-propagator-ot-trace == 0.48b0", - "opentelemetry-instrumentation == 0.48b0", - "opentelemetry-instrumentation-aws-lambda == 0.48b0", - "opentelemetry-instrumentation-aio-pika == 0.48b0", - "opentelemetry-instrumentation-aiohttp-client == 0.48b0", - "opentelemetry-instrumentation-aiopg == 0.48b0", - "opentelemetry-instrumentation-asgi == 0.48b0", - "opentelemetry-instrumentation-asyncpg == 0.48b0", - "opentelemetry-instrumentation-boto == 0.48b0", - "opentelemetry-instrumentation-boto3sqs == 0.48b0", - "opentelemetry-instrumentation-botocore == 0.48b0", - "opentelemetry-instrumentation-celery == 0.48b0", - "opentelemetry-instrumentation-confluent-kafka == 0.48b0", - "opentelemetry-instrumentation-dbapi == 0.48b0", - "opentelemetry-instrumentation-django == 0.48b0", - "opentelemetry-instrumentation-elasticsearch == 0.48b0", - "opentelemetry-instrumentation-falcon == 0.48b0", - "opentelemetry-instrumentation-fastapi == 0.48b0", - "opentelemetry-instrumentation-flask == 0.48b0", - "opentelemetry-instrumentation-grpc == 0.48b0", - "opentelemetry-instrumentation-httpx == 0.48b0", - "opentelemetry-instrumentation-jinja2 == 0.48b0", - "opentelemetry-instrumentation-kafka-python == 0.48b0", - "opentelemetry-instrumentation-logging == 0.48b0", - "opentelemetry-instrumentation-mysql == 0.48b0", - "opentelemetry-instrumentation-mysqlclient == 0.48b0", - "opentelemetry-instrumentation-pika == 0.48b0", - "opentelemetry-instrumentation-psycopg2 == 0.48b0", - "opentelemetry-instrumentation-pymemcache == 0.48b0", - "opentelemetry-instrumentation-pymongo == 0.48b0", - "opentelemetry-instrumentation-pymysql == 0.48b0", - "opentelemetry-instrumentation-pyramid == 0.48b0", - "opentelemetry-instrumentation-redis == 0.48b0", - "opentelemetry-instrumentation-remoulade == 0.48b0", - "opentelemetry-instrumentation-requests == 0.48b0", - "opentelemetry-instrumentation-sqlalchemy == 0.48b0", - "opentelemetry-instrumentation-sqlite3 == 0.48b0", - "opentelemetry-instrumentation-starlette == 0.48b0", - "opentelemetry-instrumentation-system-metrics == 0.48b0", - "opentelemetry-instrumentation-tornado == 0.48b0", - "opentelemetry-instrumentation-tortoiseorm == 0.48b0", - "opentelemetry-instrumentation-urllib == 0.48b0", - "opentelemetry-instrumentation-urllib3 == 0.48b0", - "opentelemetry-instrumentation-wsgi == 0.48b0", - "opentelemetry-instrumentation-cassandra == 0.48b0", + "opentelemetry-distro == 0.54b0", + "opentelemetry-propagator-ot-trace == 0.54b0", + "opentelemetry-instrumentation == 0.54b0", + "opentelemetry-instrumentation-aws-lambda == 0.54b0", + "opentelemetry-instrumentation-aio-pika == 0.54b0", + "opentelemetry-instrumentation-aiohttp-client == 0.54b0", + "opentelemetry-instrumentation-aiopg == 0.54b0", + "opentelemetry-instrumentation-asgi == 0.54b0", + "opentelemetry-instrumentation-asyncpg == 0.54b0", + "opentelemetry-instrumentation-boto == 0.54b0", + "opentelemetry-instrumentation-boto3sqs == 0.54b0", + "opentelemetry-instrumentation-botocore == 0.54b0", + "opentelemetry-instrumentation-celery == 0.54b0", + "opentelemetry-instrumentation-confluent-kafka == 0.54b0", + "opentelemetry-instrumentation-dbapi == 0.54b0", + "opentelemetry-instrumentation-django == 0.54b0", + "opentelemetry-instrumentation-elasticsearch == 0.54b0", + "opentelemetry-instrumentation-falcon == 0.54b0", + "opentelemetry-instrumentation-fastapi == 0.54b0", + "opentelemetry-instrumentation-flask == 0.54b0", + "opentelemetry-instrumentation-grpc == 0.54b0", + "opentelemetry-instrumentation-httpx == 0.54b0", + "opentelemetry-instrumentation-jinja2 == 0.54b0", + "opentelemetry-instrumentation-kafka-python == 0.54b0", + "opentelemetry-instrumentation-logging == 0.54b0", + "opentelemetry-instrumentation-mysql == 0.54b0", + "opentelemetry-instrumentation-mysqlclient == 0.54b0", + "opentelemetry-instrumentation-pika == 0.54b0", + "opentelemetry-instrumentation-psycopg2 == 0.54b0", + "opentelemetry-instrumentation-pymemcache == 0.54b0", + "opentelemetry-instrumentation-pymongo == 0.54b0", + "opentelemetry-instrumentation-pymysql == 0.54b0", + "opentelemetry-instrumentation-pyramid == 0.54b0", + "opentelemetry-instrumentation-redis == 0.54b0", + "opentelemetry-instrumentation-remoulade == 0.54b0", + "opentelemetry-instrumentation-requests == 0.54b0", + "opentelemetry-instrumentation-sqlalchemy == 0.54b0", + "opentelemetry-instrumentation-sqlite3 == 0.54b0", + "opentelemetry-instrumentation-starlette == 0.54b0", + "opentelemetry-instrumentation-system-metrics == 0.54b0", + "opentelemetry-instrumentation-tornado == 0.54b0", + "opentelemetry-instrumentation-tortoiseorm == 0.54b0", + "opentelemetry-instrumentation-urllib == 0.54b0", + "opentelemetry-instrumentation-urllib3 == 0.54b0", + "opentelemetry-instrumentation-wsgi == 0.54b0", + "opentelemetry-instrumentation-cassandra == 0.54b0", ] [project.optional-dependencies] diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py index a25e55330..d8c241ede 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py @@ -31,6 +31,7 @@ _AttributeMapT, _AwsSdkCallContext, _AwsSdkExtension, + _BotocoreInstrumentorContext, _BotoResultT, ) from opentelemetry.trace.span import Span @@ -192,7 +193,7 @@ def extract_attributes(self, attributes: _AttributeMapT): if request_param_value: attributes[attribute_key] = request_param_value - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): if self._operation_class is None: return @@ -229,7 +230,7 @@ class _BedrockExtension(_AwsSdkExtension): """ # pylint: disable=no-self-use - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): # _GUARDRAIL_ID can only be retrieved from the response, not from the request guardrail_id = result.get(_GUARDRAIL_ID) if guardrail_id: @@ -333,7 +334,7 @@ def _set_if_not_none(attributes, key, value): attributes[key] = value # pylint: disable=too-many-branches - def on_success(self, span: Span, result: Dict[str, Any]): + def on_success(self, span: Span, result: Dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext): model_id = self._call_context.params.get(_MODEL_ID) if not model_id: diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py index 0f4a77d1e..3cea9146f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py @@ -25,7 +25,12 @@ from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension from opentelemetry.instrumentation.botocore.extensions.sns import _SnsExtension from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension -from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension, _BotoResultT +from opentelemetry.instrumentation.botocore.extensions.types import ( + _AttributeMapT, + _AwsSdkExtension, + _BotocoreInstrumentorContext, + _BotoResultT, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.span import Span @@ -75,8 +80,8 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): old_on_success = _LambdaExtension.on_success - def patch_on_success(self, span: Span, result: _BotoResultT): - old_on_success(self, span, result) + def patch_on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): + old_on_success(self, span, result, instrumentor_context) lambda_configuration = result.get("Configuration", {}) function_arn = lambda_configuration.get("FunctionArn") if function_arn: @@ -180,8 +185,8 @@ def patch_extract_attributes(self, attributes: _AttributeMapT): old_on_success = _SqsExtension.on_success - def patch_on_success(self, span: Span, result: _BotoResultT): - old_on_success(self, span, result) + def patch_on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): + old_on_success(self, span, result, instrumentor_context) queue_url = result.get("QueueUrl") if queue_url: span.set_attribute(AWS_SQS_QUEUE_URL, queue_url) @@ -243,7 +248,7 @@ def extract_attributes(self, attributes: _AttributeMapT): attributes[AWS_SECRETSMANAGER_SECRET_ARN] = secret_id # pylint: disable=no-self-use - def on_success(self, span: Span, result: _BotoResultT): + def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _BotocoreInstrumentorContext): secret_arn = result.get("ARN") if secret_arn: span.set_attribute(AWS_SECRETSMANAGER_SECRET_ARN, secret_arn) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 87e6c4810..7b62cfb49 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -147,7 +147,7 @@ def _test_unpatched_botocore_instrumentation(self): ) # BedrockRuntime - self.assertFalse("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") + self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") # SecretsManager self.assertFalse("secretsmanager" in _KNOWN_EXTENSIONS, "Upstream has added a SecretsManager extension") @@ -678,6 +678,7 @@ def _do_on_success( ) -> Dict[str, str]: span_mock: Span = MagicMock() mock_call_context = MagicMock() + mock_instrumentor_context = MagicMock() span_attributes: Dict[str, str] = {} def set_side_effect(set_key, set_value): @@ -692,6 +693,6 @@ def set_side_effect(set_key, set_value): mock_call_context.params = params extension = _KNOWN_EXTENSIONS[service_name]()(mock_call_context) - extension.on_success(span_mock, result) + extension.on_success(span_mock, result, mock_instrumentor_context) return span_attributes From 82e9b7f492a3e3608813127b7c10ad67c614a72a Mon Sep 17 00:00:00 2001 From: Michael He Date: Mon, 16 Jun 2025 18:49:22 +0000 Subject: [PATCH 02/15] add otel baggage dependency --- aws-opentelemetry-distro/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index bc98b7a27..e269c53a3 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "opentelemetry-sdk-extension-aws == 2.0.2", "opentelemetry-propagator-aws-xray == 1.0.1", "opentelemetry-distro == 0.54b0", + "opentelemetry-processor-baggage == 0.54b0", "opentelemetry-propagator-ot-trace == 0.54b0", "opentelemetry-instrumentation == 0.54b0", "opentelemetry-instrumentation-aws-lambda == 0.54b0", From 4716d6b1372c42efe93a4e7f3abc428ee4377551 Mon Sep 17 00:00:00 2001 From: Michael He Date: Mon, 16 Jun 2025 21:49:14 +0000 Subject: [PATCH 03/15] graceful shutdown for meter provider in unit tests --- .../test_aws_opentelementry_configurator.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py index 13397a0d5..dbaee3c33 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py @@ -50,6 +50,7 @@ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.metrics import get_meter_provider from opentelemetry.processor.baggage import BaggageSpanProcessor from opentelemetry.sdk.environment_variables import OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG from opentelemetry.sdk.metrics._internal.export import PeriodicExportingMetricReader @@ -87,6 +88,22 @@ def setUpClass(cls): aws_otel_configurator.configure() cls.tracer_provider: TracerProvider = get_tracer_provider() + @classmethod + def tearDownClass(cls): + # Explicitly shut down meter provider to avoid I/O errors on Python 3.9 with gevent + # This ensures ConsoleMetricExporter is properly closed before Python cleanup + try: + meter_provider = get_meter_provider() + if hasattr(meter_provider, "force_flush"): + meter_provider.force_flush() + if hasattr(meter_provider, "shutdown"): + meter_provider.shutdown() + except (ValueError, RuntimeError): + # Ignore errors during cleanup: + # - ValueError: I/O operation on closed file (the exact error we're trying to prevent) + # - RuntimeError: Provider already shut down or threading issues + pass + def tearDown(self): os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", None) os.environ.pop("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", None) From 503236716210452de797b56f85b7a22c1e8cfaa4 Mon Sep 17 00:00:00 2001 From: Michael He Date: Tue, 17 Jun 2025 01:41:41 +0000 Subject: [PATCH 04/15] update contract tests otel dependencies to 1.33.0/0.54b0 --- .../images/applications/botocore/requirements.txt | 4 ++-- .../images/applications/django/requirements.txt | 8 ++++---- .../applications/mysql-connector/requirements.txt | 8 ++++---- .../images/applications/mysqlclient/requirements.txt | 6 +++--- .../images/applications/psycopg2/requirements.txt | 8 ++++---- .../images/applications/pymysql/requirements.txt | 8 ++++---- .../images/applications/requests/requirements.txt | 6 +++--- contract-tests/images/mock-collector/pyproject.toml | 10 +++++----- contract-tests/images/mock-collector/requirements.txt | 10 +++++----- contract-tests/tests/pyproject.toml | 6 +++--- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/contract-tests/images/applications/botocore/requirements.txt b/contract-tests/images/applications/botocore/requirements.txt index 9c92cb92f..25113e3f4 100644 --- a/contract-tests/images/applications/botocore/requirements.txt +++ b/contract-tests/images/applications/botocore/requirements.txt @@ -1,5 +1,5 @@ opentelemetry-distro==0.46b0 opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 +typing-extensions==4.12.2 botocore==1.34.143 -boto3==1.34.143 \ No newline at end of file +boto3==1.34.143 diff --git a/contract-tests/images/applications/django/requirements.txt b/contract-tests/images/applications/django/requirements.txt index acea2a85c..b8d27c9e9 100644 --- a/contract-tests/images/applications/django/requirements.txt +++ b/contract-tests/images/applications/django/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 -django==5.0.11 \ No newline at end of file +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 +django==5.0.11 diff --git a/contract-tests/images/applications/mysql-connector/requirements.txt b/contract-tests/images/applications/mysql-connector/requirements.txt index 2910612dc..96db30087 100644 --- a/contract-tests/images/applications/mysql-connector/requirements.txt +++ b/contract-tests/images/applications/mysql-connector/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 -mysql-connector-python~=9.1.0 \ No newline at end of file +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 +mysql-connector-python~=9.1.0 diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt index 43d1e7867..3b3a24a4d 100644 --- a/contract-tests/images/applications/mysqlclient/requirements.txt +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 mysqlclient==2.2.4 diff --git a/contract-tests/images/applications/psycopg2/requirements.txt b/contract-tests/images/applications/psycopg2/requirements.txt index 828f71c11..8db4c6f07 100644 --- a/contract-tests/images/applications/psycopg2/requirements.txt +++ b/contract-tests/images/applications/psycopg2/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 -psycopg2==2.9.9 \ No newline at end of file +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 +psycopg2==2.9.9 diff --git a/contract-tests/images/applications/pymysql/requirements.txt b/contract-tests/images/applications/pymysql/requirements.txt index 9f9b1041d..2aca7a152 100644 --- a/contract-tests/images/applications/pymysql/requirements.txt +++ b/contract-tests/images/applications/pymysql/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 -pymysql==1.1.1 \ No newline at end of file +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 +pymysql==1.1.1 diff --git a/contract-tests/images/applications/requests/requirements.txt b/contract-tests/images/applications/requests/requirements.txt index 26d8b1c8b..85a32820a 100644 --- a/contract-tests/images/applications/requests/requirements.txt +++ b/contract-tests/images/applications/requests/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 -typing-extensions==4.9.0 +opentelemetry-distro==0.54b0 +opentelemetry-exporter-otlp-proto-grpc==1.33.0 +typing-extensions==4.12.2 requests~=2.0 diff --git a/contract-tests/images/mock-collector/pyproject.toml b/contract-tests/images/mock-collector/pyproject.toml index ec502d256..9db408bee 100644 --- a/contract-tests/images/mock-collector/pyproject.toml +++ b/contract-tests/images/mock-collector/pyproject.toml @@ -10,11 +10,11 @@ license = "Apache-2.0" requires-python = ">=3.9" dependencies = [ - "grpcio ~= 1.60.0", - "opentelemetry-proto==1.25.0", - "opentelemetry-sdk==1.25.0", - "protobuf==4.25.2", - "typing-extensions==4.9.0" + "grpcio ~= 1.66.0", + "opentelemetry-proto==1.33.0", + "opentelemetry-sdk==1.33.0", + "protobuf==5.26.1", + "typing-extensions==4.12.2" ] [tool.hatch.build.targets.sdist] diff --git a/contract-tests/images/mock-collector/requirements.txt b/contract-tests/images/mock-collector/requirements.txt index e536e81f1..c77abc140 100644 --- a/contract-tests/images/mock-collector/requirements.txt +++ b/contract-tests/images/mock-collector/requirements.txt @@ -1,5 +1,5 @@ -grpcio==1.60.1 -opentelemetry-proto==1.25.0 -opentelemetry-sdk==1.25.0 -protobuf==4.25.2 -typing-extensions==4.9.0 \ No newline at end of file +grpcio==1.66.2 +opentelemetry-proto==1.33.0 +opentelemetry-sdk==1.33.0 +protobuf==5.26.1 +typing-extensions==4.12.2 diff --git a/contract-tests/tests/pyproject.toml b/contract-tests/tests/pyproject.toml index 48e2b3d7b..3a7e57abb 100644 --- a/contract-tests/tests/pyproject.toml +++ b/contract-tests/tests/pyproject.toml @@ -10,10 +10,10 @@ license = "Apache-2.0" requires-python = ">=3.9" dependencies = [ - "opentelemetry-proto==1.25.0", - "opentelemetry-sdk==1.25.0", + "opentelemetry-proto==1.33.0", + "opentelemetry-sdk==1.33.0", "testcontainers==3.7.1", - "grpcio==1.60.0", + "grpcio==1.66.2", "docker==7.1.0", "mock-collector==1.0.0", "requests==2.32.2" From 8081d7dba5ebb197ee00576df5e008754a71b830 Mon Sep 17 00:00:00 2001 From: Michael He Date: Tue, 17 Jun 2025 21:50:03 +0000 Subject: [PATCH 05/15] bump otel dependency versions to 1.33.1/0.54b1 --- aws-opentelemetry-distro/pyproject.toml | 108 +++++++++--------- .../applications/botocore/requirements.txt | 4 +- .../applications/django/requirements.txt | 4 +- .../mysql-connector/requirements.txt | 4 +- .../applications/mysqlclient/requirements.txt | 4 +- .../applications/psycopg2/requirements.txt | 4 +- .../applications/pymysql/requirements.txt | 4 +- .../applications/requests/requirements.txt | 4 +- .../images/mock-collector/pyproject.toml | 4 +- .../images/mock-collector/requirements.txt | 4 +- contract-tests/tests/pyproject.toml | 4 +- lambda-layer/src/tests/requirements.txt | 4 +- 12 files changed, 76 insertions(+), 76 deletions(-) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index e269c53a3..f8984854d 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -24,62 +24,62 @@ classifiers = [ ] dependencies = [ - "opentelemetry-api == 1.33.0", - "opentelemetry-sdk == 1.33.0", - "opentelemetry-exporter-otlp-proto-grpc == 1.33.0", - "opentelemetry-exporter-otlp-proto-http == 1.33.0", - "opentelemetry-propagator-b3 == 1.33.0", - "opentelemetry-propagator-jaeger == 1.33.0", - "opentelemetry-exporter-otlp-proto-common == 1.33.0", + "opentelemetry-api == 1.33.1", + "opentelemetry-sdk == 1.33.1", + "opentelemetry-exporter-otlp-proto-grpc == 1.33.1", + "opentelemetry-exporter-otlp-proto-http == 1.33.1", + "opentelemetry-propagator-b3 == 1.33.1", + "opentelemetry-propagator-jaeger == 1.33.1", + "opentelemetry-exporter-otlp-proto-common == 1.33.1", "opentelemetry-sdk-extension-aws == 2.0.2", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-distro == 0.54b0", - "opentelemetry-processor-baggage == 0.54b0", - "opentelemetry-propagator-ot-trace == 0.54b0", - "opentelemetry-instrumentation == 0.54b0", - "opentelemetry-instrumentation-aws-lambda == 0.54b0", - "opentelemetry-instrumentation-aio-pika == 0.54b0", - "opentelemetry-instrumentation-aiohttp-client == 0.54b0", - "opentelemetry-instrumentation-aiopg == 0.54b0", - "opentelemetry-instrumentation-asgi == 0.54b0", - "opentelemetry-instrumentation-asyncpg == 0.54b0", - "opentelemetry-instrumentation-boto == 0.54b0", - "opentelemetry-instrumentation-boto3sqs == 0.54b0", - "opentelemetry-instrumentation-botocore == 0.54b0", - "opentelemetry-instrumentation-celery == 0.54b0", - "opentelemetry-instrumentation-confluent-kafka == 0.54b0", - "opentelemetry-instrumentation-dbapi == 0.54b0", - "opentelemetry-instrumentation-django == 0.54b0", - "opentelemetry-instrumentation-elasticsearch == 0.54b0", - "opentelemetry-instrumentation-falcon == 0.54b0", - "opentelemetry-instrumentation-fastapi == 0.54b0", - "opentelemetry-instrumentation-flask == 0.54b0", - "opentelemetry-instrumentation-grpc == 0.54b0", - "opentelemetry-instrumentation-httpx == 0.54b0", - "opentelemetry-instrumentation-jinja2 == 0.54b0", - "opentelemetry-instrumentation-kafka-python == 0.54b0", - "opentelemetry-instrumentation-logging == 0.54b0", - "opentelemetry-instrumentation-mysql == 0.54b0", - "opentelemetry-instrumentation-mysqlclient == 0.54b0", - "opentelemetry-instrumentation-pika == 0.54b0", - "opentelemetry-instrumentation-psycopg2 == 0.54b0", - "opentelemetry-instrumentation-pymemcache == 0.54b0", - "opentelemetry-instrumentation-pymongo == 0.54b0", - "opentelemetry-instrumentation-pymysql == 0.54b0", - "opentelemetry-instrumentation-pyramid == 0.54b0", - "opentelemetry-instrumentation-redis == 0.54b0", - "opentelemetry-instrumentation-remoulade == 0.54b0", - "opentelemetry-instrumentation-requests == 0.54b0", - "opentelemetry-instrumentation-sqlalchemy == 0.54b0", - "opentelemetry-instrumentation-sqlite3 == 0.54b0", - "opentelemetry-instrumentation-starlette == 0.54b0", - "opentelemetry-instrumentation-system-metrics == 0.54b0", - "opentelemetry-instrumentation-tornado == 0.54b0", - "opentelemetry-instrumentation-tortoiseorm == 0.54b0", - "opentelemetry-instrumentation-urllib == 0.54b0", - "opentelemetry-instrumentation-urllib3 == 0.54b0", - "opentelemetry-instrumentation-wsgi == 0.54b0", - "opentelemetry-instrumentation-cassandra == 0.54b0", + "opentelemetry-distro == 0.54b1", + "opentelemetry-processor-baggage == 0.54b1", + "opentelemetry-propagator-ot-trace == 0.54b1", + "opentelemetry-instrumentation == 0.54b1", + "opentelemetry-instrumentation-aws-lambda == 0.54b1", + "opentelemetry-instrumentation-aio-pika == 0.54b1", + "opentelemetry-instrumentation-aiohttp-client == 0.54b1", + "opentelemetry-instrumentation-aiopg == 0.54b1", + "opentelemetry-instrumentation-asgi == 0.54b1", + "opentelemetry-instrumentation-asyncpg == 0.54b1", + "opentelemetry-instrumentation-boto == 0.54b1", + "opentelemetry-instrumentation-boto3sqs == 0.54b1", + "opentelemetry-instrumentation-botocore == 0.54b1", + "opentelemetry-instrumentation-celery == 0.54b1", + "opentelemetry-instrumentation-confluent-kafka == 0.54b1", + "opentelemetry-instrumentation-dbapi == 0.54b1", + "opentelemetry-instrumentation-django == 0.54b1", + "opentelemetry-instrumentation-elasticsearch == 0.54b1", + "opentelemetry-instrumentation-falcon == 0.54b1", + "opentelemetry-instrumentation-fastapi == 0.54b1", + "opentelemetry-instrumentation-flask == 0.54b1", + "opentelemetry-instrumentation-grpc == 0.54b1", + "opentelemetry-instrumentation-httpx == 0.54b1", + "opentelemetry-instrumentation-jinja2 == 0.54b1", + "opentelemetry-instrumentation-kafka-python == 0.54b1", + "opentelemetry-instrumentation-logging == 0.54b1", + "opentelemetry-instrumentation-mysql == 0.54b1", + "opentelemetry-instrumentation-mysqlclient == 0.54b1", + "opentelemetry-instrumentation-pika == 0.54b1", + "opentelemetry-instrumentation-psycopg2 == 0.54b1", + "opentelemetry-instrumentation-pymemcache == 0.54b1", + "opentelemetry-instrumentation-pymongo == 0.54b1", + "opentelemetry-instrumentation-pymysql == 0.54b1", + "opentelemetry-instrumentation-pyramid == 0.54b1", + "opentelemetry-instrumentation-redis == 0.54b1", + "opentelemetry-instrumentation-remoulade == 0.54b1", + "opentelemetry-instrumentation-requests == 0.54b1", + "opentelemetry-instrumentation-sqlalchemy == 0.54b1", + "opentelemetry-instrumentation-sqlite3 == 0.54b1", + "opentelemetry-instrumentation-starlette == 0.54b1", + "opentelemetry-instrumentation-system-metrics == 0.54b1", + "opentelemetry-instrumentation-tornado == 0.54b1", + "opentelemetry-instrumentation-tortoiseorm == 0.54b1", + "opentelemetry-instrumentation-urllib == 0.54b1", + "opentelemetry-instrumentation-urllib3 == 0.54b1", + "opentelemetry-instrumentation-wsgi == 0.54b1", + "opentelemetry-instrumentation-cassandra == 0.54b1", ] [project.optional-dependencies] diff --git a/contract-tests/images/applications/botocore/requirements.txt b/contract-tests/images/applications/botocore/requirements.txt index 25113e3f4..408eaffb2 100644 --- a/contract-tests/images/applications/botocore/requirements.txt +++ b/contract-tests/images/applications/botocore/requirements.txt @@ -1,5 +1,5 @@ -opentelemetry-distro==0.46b0 -opentelemetry-exporter-otlp-proto-grpc==1.25.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 botocore==1.34.143 boto3==1.34.143 diff --git a/contract-tests/images/applications/django/requirements.txt b/contract-tests/images/applications/django/requirements.txt index b8d27c9e9..bb2b5f6d5 100644 --- a/contract-tests/images/applications/django/requirements.txt +++ b/contract-tests/images/applications/django/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 django==5.0.11 diff --git a/contract-tests/images/applications/mysql-connector/requirements.txt b/contract-tests/images/applications/mysql-connector/requirements.txt index 96db30087..bb25864af 100644 --- a/contract-tests/images/applications/mysql-connector/requirements.txt +++ b/contract-tests/images/applications/mysql-connector/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 mysql-connector-python~=9.1.0 diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt index 3b3a24a4d..1293d78f1 100644 --- a/contract-tests/images/applications/mysqlclient/requirements.txt +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 mysqlclient==2.2.4 diff --git a/contract-tests/images/applications/psycopg2/requirements.txt b/contract-tests/images/applications/psycopg2/requirements.txt index 8db4c6f07..61f6b6780 100644 --- a/contract-tests/images/applications/psycopg2/requirements.txt +++ b/contract-tests/images/applications/psycopg2/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 psycopg2==2.9.9 diff --git a/contract-tests/images/applications/pymysql/requirements.txt b/contract-tests/images/applications/pymysql/requirements.txt index 2aca7a152..21e4cfea8 100644 --- a/contract-tests/images/applications/pymysql/requirements.txt +++ b/contract-tests/images/applications/pymysql/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 pymysql==1.1.1 diff --git a/contract-tests/images/applications/requests/requirements.txt b/contract-tests/images/applications/requests/requirements.txt index 85a32820a..b97170226 100644 --- a/contract-tests/images/applications/requests/requirements.txt +++ b/contract-tests/images/applications/requests/requirements.txt @@ -1,4 +1,4 @@ -opentelemetry-distro==0.54b0 -opentelemetry-exporter-otlp-proto-grpc==1.33.0 +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 requests~=2.0 diff --git a/contract-tests/images/mock-collector/pyproject.toml b/contract-tests/images/mock-collector/pyproject.toml index 9db408bee..42e13c868 100644 --- a/contract-tests/images/mock-collector/pyproject.toml +++ b/contract-tests/images/mock-collector/pyproject.toml @@ -11,8 +11,8 @@ requires-python = ">=3.9" dependencies = [ "grpcio ~= 1.66.0", - "opentelemetry-proto==1.33.0", - "opentelemetry-sdk==1.33.0", + "opentelemetry-proto==1.33.1", + "opentelemetry-sdk==1.33.1", "protobuf==5.26.1", "typing-extensions==4.12.2" ] diff --git a/contract-tests/images/mock-collector/requirements.txt b/contract-tests/images/mock-collector/requirements.txt index c77abc140..12e69148b 100644 --- a/contract-tests/images/mock-collector/requirements.txt +++ b/contract-tests/images/mock-collector/requirements.txt @@ -1,5 +1,5 @@ grpcio==1.66.2 -opentelemetry-proto==1.33.0 -opentelemetry-sdk==1.33.0 +opentelemetry-proto==1.33.1 +opentelemetry-sdk==1.33.1 protobuf==5.26.1 typing-extensions==4.12.2 diff --git a/contract-tests/tests/pyproject.toml b/contract-tests/tests/pyproject.toml index 3a7e57abb..5c2895fab 100644 --- a/contract-tests/tests/pyproject.toml +++ b/contract-tests/tests/pyproject.toml @@ -10,8 +10,8 @@ license = "Apache-2.0" requires-python = ">=3.9" dependencies = [ - "opentelemetry-proto==1.33.0", - "opentelemetry-sdk==1.33.0", + "opentelemetry-proto==1.33.1", + "opentelemetry-sdk==1.33.1", "testcontainers==3.7.1", "grpcio==1.66.2", "docker==7.1.0", diff --git a/lambda-layer/src/tests/requirements.txt b/lambda-layer/src/tests/requirements.txt index 3bca3e0e4..3ca32674a 100644 --- a/lambda-layer/src/tests/requirements.txt +++ b/lambda-layer/src/tests/requirements.txt @@ -1,3 +1,3 @@ # Dependencies used in tests only -opentelemetry-test-utils==0.46b0 -opentelemetry-instrumentation-aws-lambda==0.46b0 +opentelemetry-test-utils==0.54b1 +opentelemetry-instrumentation-aws-lambda==0.54b1 From 9f6a181bfa7dbc5598dff4f74d29abdb9b427e0c Mon Sep 17 00:00:00 2001 From: Michael He Date: Wed, 18 Jun 2025 03:35:10 +0000 Subject: [PATCH 06/15] use upstream's bedrock runtime extension --- .../distro/patches/_bedrock_patches.py | 219 +----------------- .../distro/patches/_botocore_patches.py | 9 +- .../distro/test_instrumentation_patch.py | 33 +-- .../applications/botocore/botocore_server.py | 28 +-- .../test/amazon/botocore/botocore_test.py | 41 +--- lambda-layer/src/tests/requirements.txt | 4 +- 6 files changed, 18 insertions(+), 316 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py index d8c241ede..549154771 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_bedrock_patches.py @@ -2,13 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 import abc import inspect -import io -import json import logging -import math -from typing import Any, Dict, Optional - -from botocore.response import StreamingBody +from typing import Dict, Optional from amazon.opentelemetry.distro._aws_attribute_keys import ( AWS_BEDROCK_AGENT_ID, @@ -17,16 +12,6 @@ AWS_BEDROCK_GUARDRAIL_ID, AWS_BEDROCK_KNOWLEDGE_BASE_ID, ) -from amazon.opentelemetry.distro._aws_span_processing_util import ( - GEN_AI_REQUEST_MAX_TOKENS, - GEN_AI_REQUEST_MODEL, - GEN_AI_REQUEST_TEMPERATURE, - GEN_AI_REQUEST_TOP_P, - GEN_AI_RESPONSE_FINISH_REASONS, - GEN_AI_SYSTEM, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, -) from opentelemetry.instrumentation.botocore.extensions.types import ( _AttributeMapT, _AwsSdkCallContext, @@ -245,205 +230,3 @@ def on_success(self, span: Span, result: _BotoResultT, instrumentor_context: _Bo AWS_BEDROCK_GUARDRAIL_ARN, guardrail_arn, ) - - -class _BedrockRuntimeExtension(_AwsSdkExtension): - """ - This class is an extension for - Amazon Bedrock Runtime. - """ - - def extract_attributes(self, attributes: _AttributeMapT): - attributes[GEN_AI_SYSTEM] = _AWS_BEDROCK_SYSTEM - - model_id = self._call_context.params.get(_MODEL_ID) - if model_id: - attributes[GEN_AI_REQUEST_MODEL] = model_id - - # Get the request body if it exists - body = self._call_context.params.get("body") - if body: - try: - request_body = json.loads(body) - - if "amazon.titan" in model_id: - self._extract_titan_attributes(attributes, request_body) - if "amazon.nova" in model_id: - self._extract_nova_attributes(attributes, request_body) - elif "anthropic.claude" in model_id: - self._extract_claude_attributes(attributes, request_body) - elif "meta.llama" in model_id: - self._extract_llama_attributes(attributes, request_body) - elif "cohere.command" in model_id: - self._extract_cohere_attributes(attributes, request_body) - elif "ai21.jamba" in model_id: - self._extract_ai21_attributes(attributes, request_body) - elif "mistral" in model_id: - self._extract_mistral_attributes(attributes, request_body) - - except json.JSONDecodeError: - _logger.debug("Error: Unable to parse the body as JSON") - - def _extract_titan_attributes(self, attributes, request_body): - config = request_body.get("textGenerationConfig", {}) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("topP")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("maxTokenCount")) - - def _extract_nova_attributes(self, attributes, request_body): - config = request_body.get("inferenceConfig", {}) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, config.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, config.get("top_p")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, config.get("max_new_tokens")) - - def _extract_claude_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_cohere_attributes(self, attributes, request_body): - prompt = request_body.get("message") - if prompt: - attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("p")) - - def _extract_ai21_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_llama_attributes(self, attributes, request_body): - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_gen_len")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - def _extract_mistral_attributes(self, attributes, request_body): - prompt = request_body.get("prompt") - if prompt: - attributes[GEN_AI_USAGE_INPUT_TOKENS] = math.ceil(len(prompt) / 6) - self._set_if_not_none(attributes, GEN_AI_REQUEST_MAX_TOKENS, request_body.get("max_tokens")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TEMPERATURE, request_body.get("temperature")) - self._set_if_not_none(attributes, GEN_AI_REQUEST_TOP_P, request_body.get("top_p")) - - @staticmethod - def _set_if_not_none(attributes, key, value): - if value is not None: - attributes[key] = value - - # pylint: disable=too-many-branches - def on_success(self, span: Span, result: Dict[str, Any], instrumentor_context: _BotocoreInstrumentorContext): - model_id = self._call_context.params.get(_MODEL_ID) - - if not model_id: - return - - if "body" in result and isinstance(result["body"], StreamingBody): - original_body = None - try: - original_body = result["body"] - body_content = original_body.read() - - # Use one stream for telemetry - stream = io.BytesIO(body_content) - telemetry_content = stream.read() - response_body = json.loads(telemetry_content.decode("utf-8")) - if "amazon.titan" in model_id: - self._handle_amazon_titan_response(span, response_body) - if "amazon.nova" in model_id: - self._handle_amazon_nova_response(span, response_body) - elif "anthropic.claude" in model_id: - self._handle_anthropic_claude_response(span, response_body) - elif "meta.llama" in model_id: - self._handle_meta_llama_response(span, response_body) - elif "cohere.command" in model_id: - self._handle_cohere_command_response(span, response_body) - elif "ai21.jamba" in model_id: - self._handle_ai21_jamba_response(span, response_body) - elif "mistral" in model_id: - self._handle_mistral_mistral_response(span, response_body) - # Replenish stream for downstream application use - new_stream = io.BytesIO(body_content) - result["body"] = StreamingBody(new_stream, len(body_content)) - - except json.JSONDecodeError: - _logger.debug("Error: Unable to parse the response body as JSON") - except Exception as e: # pylint: disable=broad-exception-caught, invalid-name - _logger.debug("Error processing response: %s", e) - finally: - if original_body is not None: - original_body.close() - - # pylint: disable=no-self-use - def _handle_amazon_titan_response(self, span: Span, response_body: Dict[str, Any]): - if "inputTextTokenCount" in response_body: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["inputTextTokenCount"]) - if "results" in response_body and response_body["results"]: - result = response_body["results"][0] - if "tokenCount" in result: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, result["tokenCount"]) - if "completionReason" in result: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [result["completionReason"]]) - - # pylint: disable=no-self-use - def _handle_amazon_nova_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "inputTokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["inputTokens"]) - if "outputTokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"]) - if "stopReason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stopReason"]]) - - # pylint: disable=no-self-use - def _handle_anthropic_claude_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "input_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["input_tokens"]) - if "output_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["output_tokens"]) - if "stop_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) - - # pylint: disable=no-self-use - def _handle_cohere_command_response(self, span: Span, response_body: Dict[str, Any]): - # Output tokens: Approximate from the response text - if "text" in response_body: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(response_body["text"]) / 6)) - if "finish_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["finish_reason"]]) - - # pylint: disable=no-self-use - def _handle_ai21_jamba_response(self, span: Span, response_body: Dict[str, Any]): - if "usage" in response_body: - usage = response_body["usage"] - if "prompt_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage["prompt_tokens"]) - if "completion_tokens" in usage: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage["completion_tokens"]) - if "choices" in response_body: - choices = response_body["choices"][0] - if "finish_reason" in choices: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [choices["finish_reason"]]) - - # pylint: disable=no-self-use - def _handle_meta_llama_response(self, span: Span, response_body: Dict[str, Any]): - if "prompt_token_count" in response_body: - span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, response_body["prompt_token_count"]) - if "generation_token_count" in response_body: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, response_body["generation_token_count"]) - if "stop_reason" in response_body: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [response_body["stop_reason"]]) - - # pylint: disable=no-self-use - def _handle_mistral_mistral_response(self, span: Span, response_body: Dict[str, Any]): - if "outputs" in response_body: - outputs = response_body["outputs"][0] - if "text" in outputs: - span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, math.ceil(len(outputs["text"]) / 6)) - if "stop_reason" in outputs: - span.set_attribute(GEN_AI_RESPONSE_FINISH_REASONS, [outputs["stop_reason"]]) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py index 3cea9146f..ffc81b348 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py @@ -19,7 +19,6 @@ _BedrockAgentExtension, _BedrockAgentRuntimeExtension, _BedrockExtension, - _BedrockRuntimeExtension, ) from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension @@ -196,17 +195,17 @@ def patch_on_success(self, span: Span, result: _BotoResultT, instrumentor_contex def _apply_botocore_bedrock_patch() -> None: - """Botocore instrumentation patch for Bedrock, Bedrock Agent, Bedrock Runtime and Bedrock Agent Runtime + """Botocore instrumentation patch for Bedrock, Bedrock Agent, and Bedrock Agent Runtime This patch adds an extension to the upstream's list of known extension for Bedrock. Extensions allow for custom logic for adding service-specific information to spans, such as attributes. - Specifically, we are adding logic to add the AWS_BEDROCK attributes referenced in _aws_attribute_keys, - GEN_AI_REQUEST_MODEL and GEN_AI_SYSTEM attributes referenced in _aws_span_processing_util. + Specifically, we are adding logic to add the AWS_BEDROCK attributes referenced in _aws_attribute_keys. + Note: Bedrock Runtime uses the upstream extension directly. """ _KNOWN_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension") _KNOWN_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension") _KNOWN_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension") - _KNOWN_EXTENSIONS["bedrock-runtime"] = _lazy_load(".", "_BedrockRuntimeExtension") + # bedrock-runtime is handled by upstream # The OpenTelemetry Authors code diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 7b62cfb49..2ef548a82 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -274,17 +274,6 @@ def _test_patched_botocore_instrumentation(self): output_prompt=cohere_output, ) - # BedrockRuntime - AI21 Jambda - self._test_patched_bedrock_runtime_invoke_model( - model_id="ai21.jamba-1-5-large-v1:0", - max_tokens=512, - temperature=0.5, - top_p=0.999, - finish_reason="end_turn", - input_tokens=23, - output_tokens=36, - ) - # BedrockRuntime - Mistral msg = "Hello World" mistral_input = f"[INST] {msg} [/INST]" @@ -429,7 +418,7 @@ def get_model_response_request(): "inferenceConfig": { "max_new_tokens": max_tokens, "temperature": temperature, - "top_p": top_p, + "topP": top_p, } } @@ -453,22 +442,6 @@ def get_model_response_request(): "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, } - if "ai21.jamba" in model_id: - request_body = { - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "choices": [{"finish_reason": finish_reason}], - "usage": { - "prompt_tokens": input_tokens, - "completion_tokens": output_tokens, - "total_tokens": (input_tokens + output_tokens), - }, - } - if "meta.llama" in model_id: request_body = { "max_gen_len": max_tokens, @@ -512,10 +485,10 @@ def get_model_response_request(): request_body, response_body = get_model_response_request() bedrock_runtime_attributes: Dict[str, str] = _do_extract_attributes_bedrock( - "bedrock-runtime", model_id=model_id, request_body=request_body + "bedrock-runtime", operation="InvokeModel", model_id=model_id, request_body=request_body ) bedrock_runtime_success_attributes: Dict[str, str] = _do_on_success_bedrock( - "bedrock-runtime", model_id=model_id, streaming_body=response_body + "bedrock-runtime", operation="InvokeModel", model_id=model_id, streaming_body=response_body ) bedrock_runtime_attributes.update(bedrock_runtime_success_attributes) diff --git a/contract-tests/images/applications/botocore/botocore_server.py b/contract-tests/images/applications/botocore/botocore_server.py index 6c315a4dc..80ecbc6fe 100644 --- a/contract-tests/images/applications/botocore/botocore_server.py +++ b/contract-tests/images/applications/botocore/botocore_server.py @@ -435,7 +435,7 @@ def get_model_request_response(path): "inferenceConfig": { "max_new_tokens": 800, "temperature": 0.9, - "top_p": 0.7, + "topP": 0.7, }, } @@ -496,32 +496,6 @@ def get_model_request_response(path): "text": "test-generation-text", } - if "ai21.jamba" in path: - model_id = "ai21.jamba-1-5-large-v1:0" - - request_body = { - "messages": [ - { - "role": "user", - "content": prompt, - }, - ], - "top_p": 0.8, - "temperature": 0.6, - "max_tokens": 512, - } - - response_body = { - "stop_reason": "end_turn", - "usage": { - "prompt_tokens": 21, - "completion_tokens": 24, - }, - "choices": [ - {"finish_reason": "stop"}, - ], - } - if "mistral" in path: model_id = "mistral.mistral-7b-instruct-v0:2" diff --git a/contract-tests/tests/test/amazon/botocore/botocore_test.py b/contract-tests/tests/test/amazon/botocore/botocore_test.py index ed04c9514..549ec3f50 100644 --- a/contract-tests/tests/test/amazon/botocore/botocore_test.py +++ b/contract-tests/tests/test/amazon/botocore/botocore_test.py @@ -440,7 +440,7 @@ def test_bedrock_runtime_invoke_model_amazon_titan(self): _GEN_AI_USAGE_INPUT_TOKENS: 15, _GEN_AI_USAGE_OUTPUT_TOKENS: 13, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="text_completion amazon.titan-text-premier-v1:0", ) def test_bedrock_runtime_invoke_model_amazon_nova(self): @@ -458,6 +458,7 @@ def test_bedrock_runtime_invoke_model_amazon_nova(self): cloudformation_primary_identifier="amazon.nova-pro-v1:0", request_specific_attributes={ _GEN_AI_REQUEST_MODEL: "amazon.nova-pro-v1:0", + _GEN_AI_SYSTEM: "aws.bedrock", _GEN_AI_REQUEST_MAX_TOKENS: 800, _GEN_AI_REQUEST_TEMPERATURE: 0.9, _GEN_AI_REQUEST_TOP_P: 0.7, @@ -467,7 +468,7 @@ def test_bedrock_runtime_invoke_model_amazon_nova(self): _GEN_AI_USAGE_INPUT_TOKENS: 432, _GEN_AI_USAGE_OUTPUT_TOKENS: 681, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat amazon.nova-pro-v1:0", ) def test_bedrock_runtime_invoke_model_anthropic_claude(self): @@ -495,7 +496,7 @@ def test_bedrock_runtime_invoke_model_anthropic_claude(self): _GEN_AI_USAGE_INPUT_TOKENS: 15, _GEN_AI_USAGE_OUTPUT_TOKENS: 13, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat anthropic.claude-v2:1", ) def test_bedrock_runtime_invoke_model_meta_llama(self): @@ -523,7 +524,7 @@ def test_bedrock_runtime_invoke_model_meta_llama(self): _GEN_AI_USAGE_INPUT_TOKENS: 31, _GEN_AI_USAGE_OUTPUT_TOKENS: 49, }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat meta.llama2-13b-chat-v1", ) def test_bedrock_runtime_invoke_model_cohere_command(self): @@ -553,35 +554,7 @@ def test_bedrock_runtime_invoke_model_cohere_command(self): ), _GEN_AI_USAGE_OUTPUT_TOKENS: math.ceil(len("test-generation-text") / 6), }, - span_name="Bedrock Runtime.InvokeModel", - ) - - def test_bedrock_runtime_invoke_model_ai21_jamba(self): - self.do_test_requests( - "bedrock/invokemodel/invoke-model/ai21.jamba-1-5-large-v1:0", - "GET", - 200, - 0, - 0, - rpc_service="Bedrock Runtime", - remote_service="AWS::BedrockRuntime", - remote_operation="InvokeModel", - remote_resource_type="AWS::Bedrock::Model", - remote_resource_identifier="ai21.jamba-1-5-large-v1:0", - cloudformation_primary_identifier="ai21.jamba-1-5-large-v1:0", - request_specific_attributes={ - _GEN_AI_REQUEST_MODEL: "ai21.jamba-1-5-large-v1:0", - _GEN_AI_SYSTEM: "aws.bedrock", - _GEN_AI_REQUEST_MAX_TOKENS: 512, - _GEN_AI_REQUEST_TEMPERATURE: 0.6, - _GEN_AI_REQUEST_TOP_P: 0.8, - }, - response_specific_attributes={ - _GEN_AI_RESPONSE_FINISH_REASONS: ["stop"], - _GEN_AI_USAGE_INPUT_TOKENS: 21, - _GEN_AI_USAGE_OUTPUT_TOKENS: 24, - }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat cohere.command-r-v1:0", ) def test_bedrock_runtime_invoke_model_mistral(self): @@ -611,7 +584,7 @@ def test_bedrock_runtime_invoke_model_mistral(self): ), _GEN_AI_USAGE_OUTPUT_TOKENS: math.ceil(len("test-output-text") / 6), }, - span_name="Bedrock Runtime.InvokeModel", + span_name="chat mistral.mistral-7b-instruct-v0:2", ) def test_bedrock_get_guardrail(self): diff --git a/lambda-layer/src/tests/requirements.txt b/lambda-layer/src/tests/requirements.txt index 3ca32674a..3bca3e0e4 100644 --- a/lambda-layer/src/tests/requirements.txt +++ b/lambda-layer/src/tests/requirements.txt @@ -1,3 +1,3 @@ # Dependencies used in tests only -opentelemetry-test-utils==0.54b1 -opentelemetry-instrumentation-aws-lambda==0.54b1 +opentelemetry-test-utils==0.46b0 +opentelemetry-instrumentation-aws-lambda==0.46b0 From d176d508307589c5be9727290fe33aa4c07e507d Mon Sep 17 00:00:00 2001 From: Michael He Date: Wed, 18 Jun 2025 04:50:58 +0000 Subject: [PATCH 07/15] remove pkg_resources and use alternative to align with upstream otel >= 1.28.0 --- .../src/amazon/opentelemetry/distro/_utils.py | 22 +++++++++++++++---- .../distro/test_aws_auth_session.py | 8 ++++--- .../distro/test_aws_opentelemetry_distro.py | 7 +++--- .../distro/test_instrumentation_patch.py | 14 +++++------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index 149f9ad29..d59d59287 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -3,9 +3,10 @@ import os import sys +from importlib.metadata import PackageNotFoundError, version from logging import Logger, getLogger -import pkg_resources +from packaging.requirements import Requirement _logger: Logger = getLogger(__name__) @@ -18,10 +19,23 @@ def is_installed(req: str) -> bool: if req in sys.modules and sys.modules[req] is not None: return True + return _is_installed(req) + + +def _is_installed(req): + req = Requirement(req) + try: - pkg_resources.get_distribution(req) - except Exception as exc: # pylint: disable=broad-except - _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) + dist_version = version(req.name) + except PackageNotFoundError: + return False + + if not req.specifier.filter(dist_version): + _logger.warning( + "instrumentation for package %s is available but version %s is installed. Skipping.", + req, + dist_version, + ) return False return True diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py index e0c62b89d..7d6479251 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_auth_session.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from importlib.metadata import PackageNotFoundError from unittest import TestCase from unittest.mock import patch @@ -19,11 +20,12 @@ class TestAwsAuthSession(TestCase): - @patch("pkg_resources.get_distribution", side_effect=ImportError("test error")) - @patch.dict("sys.modules", {"botocore": None}, clear=False) + @patch("amazon.opentelemetry.distro._utils.version") + @patch.dict("sys.modules", {"botocore": None}) @patch("requests.Session.request", return_value=requests.Response()) - def test_aws_auth_session_no_botocore(self, _, __): + def test_aws_auth_session_no_botocore(self, mock_request, mock_version): """Tests that aws_auth_session will not inject SigV4 Headers if botocore is not installed.""" + mock_version.side_effect = PackageNotFoundError("botocore") session = AwsAuthSession("us-east-1", "xray") actual_headers = {"test": "test"} diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py index b77e4fbf8..7368a04c8 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelemetry_distro.py @@ -1,13 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +from importlib.metadata import PackageNotFoundError, version from unittest import TestCase -from pkg_resources import DistributionNotFound, require - class TestAwsOpenTelemetryDistro(TestCase): def test_package_available(self): try: - require(["aws-opentelemetry-distro"]) - except DistributionNotFound: + version("aws-opentelemetry-distro") + except PackageNotFoundError: self.fail("aws-opentelemetry-distro not installed") diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 2ef548a82..da42c9d6b 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -3,13 +3,13 @@ import json import math import os +from importlib.metadata import PackageNotFoundError from io import BytesIO from typing import Any, Dict from unittest import TestCase from unittest.mock import MagicMock, patch import gevent.monkey -import pkg_resources from botocore.response import StreamingBody from amazon.opentelemetry.distro.patches._instrumentation_patch import ( @@ -38,7 +38,7 @@ _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" # Patch names -GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.pkg_resources.get_distribution" +GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.version" class TestInstrumentationPatch(TestCase): @@ -73,7 +73,7 @@ def test_instrumentation_patch(self): def _run_patch_behaviour_tests(self): # Test setup - self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "CorrectDistributionObject" + self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "1.0.0" # Test setup to not patch gevent os.environ[AWS_GEVENT_PATCH_MODULES] = "none" @@ -359,16 +359,12 @@ def _test_botocore_installed_flag(self): "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches" ) as mock_apply_patches: get_distribution_patch: patch = self.method_patches[GET_DISTRIBUTION_PATCH] - get_distribution_patch.side_effect = pkg_resources.DistributionNotFound - apply_instrumentation_patches() - mock_apply_patches.assert_not_called() - - get_distribution_patch.side_effect = pkg_resources.VersionConflict("botocore==1.0.0", "botocore==0.0.1") + get_distribution_patch.side_effect = PackageNotFoundError apply_instrumentation_patches() mock_apply_patches.assert_not_called() get_distribution_patch.side_effect = None - get_distribution_patch.return_value = "CorrectDistributionObject" + get_distribution_patch.return_value = "1.0.0" apply_instrumentation_patches() mock_apply_patches.assert_called() From b2ba88e85b1028d3b9989e1ce330cff046d2835a Mon Sep 17 00:00:00 2001 From: Michael He Date: Wed, 18 Jun 2025 18:48:05 +0000 Subject: [PATCH 08/15] add back debug logging --- .../src/amazon/opentelemetry/distro/_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index d59d59287..efd076a91 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -27,11 +27,12 @@ def _is_installed(req): try: dist_version = version(req.name) - except PackageNotFoundError: + except PackageNotFoundError as exc: + _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) return False if not req.specifier.filter(dist_version): - _logger.warning( + _logger.debug( "instrumentation for package %s is available but version %s is installed. Skipping.", req, dist_version, From deb4baaf599523ea16d8716c55e942b1bdf9f12d Mon Sep 17 00:00:00 2001 From: Michael He Date: Wed, 18 Jun 2025 19:09:13 +0000 Subject: [PATCH 09/15] remove bedrock invoke model unit tests since we are now using upstream extension --- .../distro/test_instrumentation_patch.py | 205 +----------------- 1 file changed, 1 insertion(+), 204 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index da42c9d6b..523f57d52 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -1,16 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import json -import math import os from importlib.metadata import PackageNotFoundError -from io import BytesIO from typing import Any, Dict from unittest import TestCase from unittest.mock import MagicMock, patch import gevent.monkey -from botocore.response import StreamingBody from amazon.opentelemetry.distro.patches._instrumentation_patch import ( AWS_GEVENT_PATCH_MODULES, @@ -213,84 +209,9 @@ def _test_patched_botocore_instrumentation(self): bedrock_agent_runtime_sucess_attributes: Dict[str, str] = _do_on_success_bedrock("bedrock-agent-runtime") self.assertEqual(len(bedrock_agent_runtime_sucess_attributes), 0) - # BedrockRuntime - Amazon Titan + # BedrockRuntime self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS) - self._test_patched_bedrock_runtime_invoke_model( - model_id="amazon.titan-embed-text-v1", - max_tokens=512, - temperature=0.9, - top_p=0.75, - finish_reason="FINISH", - input_tokens=123, - output_tokens=456, - ) - - self._test_patched_bedrock_runtime_invoke_model( - model_id="amazon.nova-pro-v1:0", - max_tokens=500, - temperature=0.9, - top_p=0.7, - finish_reason="FINISH", - input_tokens=123, - output_tokens=456, - ) - - # BedrockRuntime - Anthropic Claude - self._test_patched_bedrock_runtime_invoke_model( - model_id="anthropic.claude-v2:1", - max_tokens=512, - temperature=0.5, - top_p=0.999, - finish_reason="end_turn", - input_tokens=23, - output_tokens=36, - ) - - # BedrockRuntime - Meta LLama - self._test_patched_bedrock_runtime_invoke_model( - model_id="meta.llama2-13b-chat-v1", - max_tokens=512, - temperature=0.5, - top_p=0.9, - finish_reason="stop", - input_tokens=31, - output_tokens=36, - ) - - # BedrockRuntime - Cohere Command-r - cohere_input = "Hello, world" - cohere_output = "Goodbye, world" - - self._test_patched_bedrock_runtime_invoke_model( - model_id="cohere.command-r-v1:0", - max_tokens=512, - temperature=0.5, - top_p=0.75, - finish_reason="COMPLETE", - input_tokens=math.ceil(len(cohere_input) / 6), - output_tokens=math.ceil(len(cohere_output) / 6), - input_prompt=cohere_input, - output_prompt=cohere_output, - ) - - # BedrockRuntime - Mistral - msg = "Hello World" - mistral_input = f"[INST] {msg} [/INST]" - mistral_output = "Goodbye, World" - - self._test_patched_bedrock_runtime_invoke_model( - model_id="mistral.mistral-7b-instruct-v0:2", - max_tokens=512, - temperature=0.5, - top_p=0.9, - finish_reason="stop", - input_tokens=math.ceil(len(mistral_input) / 6), - output_tokens=math.ceil(len(mistral_output) / 6), - input_prompt=mistral_input, - output_prompt=mistral_output, - ) - # SecretsManager self.assertTrue("secretsmanager" in _KNOWN_EXTENSIONS) secretsmanager_attributes: Dict[str, str] = _do_extract_secretsmanager_attributes() @@ -374,130 +295,6 @@ def _test_patched_bedrock_instrumentation(self): self.assertEqual(len(bedrock_sucess_attributes), 1) self.assertEqual(bedrock_sucess_attributes["aws.bedrock.guardrail.id"], _BEDROCK_GUARDRAIL_ID) - def _test_patched_bedrock_runtime_invoke_model(self, **args): - model_id = args.get("model_id", None) - max_tokens = args.get("max_tokens", None) - temperature = args.get("temperature", None) - top_p = args.get("top_p", None) - finish_reason = args.get("finish_reason", None) - input_tokens = args.get("input_tokens", None) - output_tokens = args.get("output_tokens", None) - input_prompt = args.get("input_prompt", None) - output_prompt = args.get("output_prompt", None) - - def get_model_response_request(): - request_body = {} - response_body = {} - - if "amazon.titan" in model_id: - request_body = { - "textGenerationConfig": { - "maxTokenCount": max_tokens, - "temperature": temperature, - "topP": top_p, - } - } - - response_body = { - "inputTextTokenCount": input_tokens, - "results": [ - { - "tokenCount": output_tokens, - "outputText": "testing", - "completionReason": finish_reason, - } - ], - } - - if "amazon.nova" in model_id: - request_body = { - "inferenceConfig": { - "max_new_tokens": max_tokens, - "temperature": temperature, - "topP": top_p, - } - } - - response_body = { - "output": {"message": {"content": [{"text": ""}], "role": "assistant"}}, - "stopReason": finish_reason, - "usage": {"inputTokens": input_tokens, "outputTokens": output_tokens}, - } - - if "anthropic.claude" in model_id: - request_body = { - "anthropic_version": "bedrock-2023-05-31", - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "stop_reason": finish_reason, - "stop_sequence": None, - "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, - } - - if "meta.llama" in model_id: - request_body = { - "max_gen_len": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = { - "prompt_token_count": input_tokens, - "generation_token_count": output_tokens, - "stop_reason": finish_reason, - } - - if "cohere.command" in model_id: - request_body = { - "message": input_prompt, - "max_tokens": max_tokens, - "temperature": temperature, - "p": top_p, - } - - response_body = { - "text": output_prompt, - "finish_reason": finish_reason, - } - - if "mistral" in model_id: - request_body = { - "prompt": input_prompt, - "max_tokens": max_tokens, - "temperature": temperature, - "top_p": top_p, - } - - response_body = {"outputs": [{"text": output_prompt, "stop_reason": finish_reason}]} - - json_bytes = json.dumps(response_body).encode("utf-8") - - return json.dumps(request_body), StreamingBody(BytesIO(json_bytes), len(json_bytes)) - - request_body, response_body = get_model_response_request() - - bedrock_runtime_attributes: Dict[str, str] = _do_extract_attributes_bedrock( - "bedrock-runtime", operation="InvokeModel", model_id=model_id, request_body=request_body - ) - bedrock_runtime_success_attributes: Dict[str, str] = _do_on_success_bedrock( - "bedrock-runtime", operation="InvokeModel", model_id=model_id, streaming_body=response_body - ) - - bedrock_runtime_attributes.update(bedrock_runtime_success_attributes) - - self.assertEqual(bedrock_runtime_attributes["gen_ai.system"], _GEN_AI_SYSTEM) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.model"], model_id) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.max_tokens"], max_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.temperature"], temperature) - self.assertEqual(bedrock_runtime_attributes["gen_ai.request.top_p"], top_p) - self.assertEqual(bedrock_runtime_attributes["gen_ai.usage.input_tokens"], input_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.usage.output_tokens"], output_tokens) - self.assertEqual(bedrock_runtime_attributes["gen_ai.response.finish_reasons"], [finish_reason]) - def _test_patched_bedrock_agent_instrumentation(self): """For bedrock-agent service, both extract_attributes and on_success provides attributes, the attributes depend on the API being invoked.""" From 3ff2c18287c6f2788898be552c27cc8be237ec22 Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 02:53:20 +0000 Subject: [PATCH 10/15] use upstream gen ai semantic conventions values instead of defining in adot --- .../distro/_aws_metric_attribute_generator.py | 2 +- .../opentelemetry/distro/_aws_span_processing_util.py | 10 ---------- .../distro/test_aws_metric_attribute_generator.py | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py index ec5b693ed..173f8492b 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py @@ -35,7 +35,6 @@ ) from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute from amazon.opentelemetry.distro._aws_span_processing_util import ( - GEN_AI_REQUEST_MODEL, LOCAL_ROOT, MAX_KEYWORD_LENGTH, SQL_KEYWORD_PATTERN, @@ -60,6 +59,7 @@ from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import BoundedAttributes, ReadableSpan +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_REQUEST_MODEL from opentelemetry.semconv.trace import SpanAttributes # Pertinent OTEL attribute keys diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py index 21e19afa9..d2a039861 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py @@ -26,16 +26,6 @@ # Max keyword length supported by parsing into remote_operation from DB_STATEMENT MAX_KEYWORD_LENGTH = 27 -# TODO: Use Semantic Conventions once upgrade to 0.47b0 -GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model" -GEN_AI_SYSTEM: str = "gen_ai.system" -GEN_AI_REQUEST_MAX_TOKENS: str = "gen_ai.request.max_tokens" -GEN_AI_REQUEST_TEMPERATURE: str = "gen_ai.request.temperature" -GEN_AI_REQUEST_TOP_P: str = "gen_ai.request.top_p" -GEN_AI_RESPONSE_FINISH_REASONS: str = "gen_ai.response.finish_reasons" -GEN_AI_USAGE_INPUT_TOKENS: str = "gen_ai.usage.input_tokens" -GEN_AI_USAGE_OUTPUT_TOKENS: str = "gen_ai.usage.output_tokens" - # Get dialect keywords retrieved from dialect_keywords.json file. # Only meant to be invoked by SQL_KEYWORD_PATTERN and unit tests diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py index d122519cf..f99b0d154 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_metric_attribute_generator.py @@ -37,12 +37,12 @@ AWS_STEPFUNCTIONS_STATEMACHINE_ARN, ) from amazon.opentelemetry.distro._aws_metric_attribute_generator import _AwsMetricAttributeGenerator -from amazon.opentelemetry.distro._aws_span_processing_util import GEN_AI_REQUEST_MODEL from amazon.opentelemetry.distro.metric_attribute_generator import DEPENDENCY_METRIC, SERVICE_METRIC from opentelemetry.attributes import BoundedAttributes from opentelemetry.sdk.resources import _DEFAULT_RESOURCE, SERVICE_NAME from opentelemetry.sdk.trace import ReadableSpan, Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_REQUEST_MODEL from opentelemetry.semconv.trace import MessagingOperationValues, SpanAttributes from opentelemetry.trace import SpanContext, SpanKind from opentelemetry.util.types import Attributes From 78a9c19b6d9403b1c676a1b03245afdc7b4fd653 Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 02:58:19 +0000 Subject: [PATCH 11/15] update import metadata version patch in unit tests accordingly --- .../opentelemetry/distro/test_instrumentation_patch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index 523f57d52..fdc387ba4 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -34,7 +34,7 @@ _LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID" # Patch names -GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.version" +IMPORTLIB_METADATA_VERSION_PATCH: str = "amazon.opentelemetry.distro._utils.version" class TestInstrumentationPatch(TestCase): @@ -56,7 +56,7 @@ class TestInstrumentationPatch(TestCase): def test_instrumentation_patch(self): # Set up method patches used by all tests - self.method_patches[GET_DISTRIBUTION_PATCH] = patch(GET_DISTRIBUTION_PATCH).start() + self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH] = patch(IMPORTLIB_METADATA_VERSION_PATCH).start() # Run tests that validate patch behaviour before and after patching self._run_patch_behaviour_tests() @@ -69,7 +69,7 @@ def test_instrumentation_patch(self): def _run_patch_behaviour_tests(self): # Test setup - self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "1.0.0" + self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH].return_value = "1.0.0" # Test setup to not patch gevent os.environ[AWS_GEVENT_PATCH_MODULES] = "none" @@ -279,7 +279,7 @@ def _test_botocore_installed_flag(self): with patch( "amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches" ) as mock_apply_patches: - get_distribution_patch: patch = self.method_patches[GET_DISTRIBUTION_PATCH] + get_distribution_patch: patch = self.method_patches[IMPORTLIB_METADATA_VERSION_PATCH] get_distribution_patch.side_effect = PackageNotFoundError apply_instrumentation_patches() mock_apply_patches.assert_not_called() From 22ad0137bc7d88a5b4eb914f531f54660229c372 Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 03:50:17 +0000 Subject: [PATCH 12/15] condense is_installed function --- .../src/amazon/opentelemetry/distro/_utils.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index efd076a91..4fa079172 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import os -import sys from importlib.metadata import PackageNotFoundError, version from logging import Logger, getLogger @@ -15,14 +14,6 @@ def is_installed(req: str) -> bool: """Is the given required package installed?""" - - if req in sys.modules and sys.modules[req] is not None: - return True - - return _is_installed(req) - - -def _is_installed(req): req = Requirement(req) try: @@ -31,7 +22,7 @@ def _is_installed(req): _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) return False - if not req.specifier.filter(dist_version): + if not req.specifier.filter([dist_version]): _logger.debug( "instrumentation for package %s is available but version %s is installed. Skipping.", req, From 89aea117dc4531f9860c1ef423f1e571af733695 Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 17:40:10 +0000 Subject: [PATCH 13/15] remove otel dependencies from contract test sample apps --- contract-tests/images/applications/botocore/requirements.txt | 2 -- contract-tests/images/applications/django/requirements.txt | 2 -- .../images/applications/mysql-connector/requirements.txt | 2 -- contract-tests/images/applications/mysqlclient/requirements.txt | 2 -- contract-tests/images/applications/psycopg2/requirements.txt | 2 -- contract-tests/images/applications/pymysql/requirements.txt | 2 -- contract-tests/images/applications/requests/requirements.txt | 2 -- 7 files changed, 14 deletions(-) diff --git a/contract-tests/images/applications/botocore/requirements.txt b/contract-tests/images/applications/botocore/requirements.txt index 408eaffb2..61ddebf98 100644 --- a/contract-tests/images/applications/botocore/requirements.txt +++ b/contract-tests/images/applications/botocore/requirements.txt @@ -1,5 +1,3 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 botocore==1.34.143 boto3==1.34.143 diff --git a/contract-tests/images/applications/django/requirements.txt b/contract-tests/images/applications/django/requirements.txt index bb2b5f6d5..84dfdeabb 100644 --- a/contract-tests/images/applications/django/requirements.txt +++ b/contract-tests/images/applications/django/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 django==5.0.11 diff --git a/contract-tests/images/applications/mysql-connector/requirements.txt b/contract-tests/images/applications/mysql-connector/requirements.txt index bb25864af..f285dcb1f 100644 --- a/contract-tests/images/applications/mysql-connector/requirements.txt +++ b/contract-tests/images/applications/mysql-connector/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 mysql-connector-python~=9.1.0 diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt index 1293d78f1..933e606b4 100644 --- a/contract-tests/images/applications/mysqlclient/requirements.txt +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 mysqlclient==2.2.4 diff --git a/contract-tests/images/applications/psycopg2/requirements.txt b/contract-tests/images/applications/psycopg2/requirements.txt index 61f6b6780..8786aff35 100644 --- a/contract-tests/images/applications/psycopg2/requirements.txt +++ b/contract-tests/images/applications/psycopg2/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 psycopg2==2.9.9 diff --git a/contract-tests/images/applications/pymysql/requirements.txt b/contract-tests/images/applications/pymysql/requirements.txt index 21e4cfea8..8ba76defb 100644 --- a/contract-tests/images/applications/pymysql/requirements.txt +++ b/contract-tests/images/applications/pymysql/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 pymysql==1.1.1 diff --git a/contract-tests/images/applications/requests/requirements.txt b/contract-tests/images/applications/requests/requirements.txt index b97170226..700b31404 100644 --- a/contract-tests/images/applications/requests/requirements.txt +++ b/contract-tests/images/applications/requests/requirements.txt @@ -1,4 +1,2 @@ -opentelemetry-distro==0.54b1 -opentelemetry-exporter-otlp-proto-grpc==1.33.1 typing-extensions==4.12.2 requests~=2.0 From 80c32960e1dc18b8d079d7fca0cd3429c84afe9f Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 20:21:28 +0000 Subject: [PATCH 14/15] add unit tests for _utils.py for code coverage checks --- .../src/amazon/opentelemetry/distro/_utils.py | 2 +- .../amazon/opentelemetry/distro/test_utils.py | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index 4fa079172..fa5acf42c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -22,7 +22,7 @@ def is_installed(req: str) -> bool: _logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc) return False - if not req.specifier.filter([dist_version]): + if not list(req.specifier.filter([dist_version])): _logger.debug( "instrumentation for package %s is available but version %s is installed. Skipping.", req, diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py new file mode 100644 index 000000000..0839aec98 --- /dev/null +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_utils.py @@ -0,0 +1,96 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from importlib.metadata import PackageNotFoundError +from unittest import TestCase +from unittest.mock import patch + +from amazon.opentelemetry.distro._utils import AGENT_OBSERVABILITY_ENABLED, is_agent_observability_enabled, is_installed + + +class TestUtils(TestCase): + def setUp(self): + # Store original env var if it exists + self.original_env = os.environ.get(AGENT_OBSERVABILITY_ENABLED) + + def tearDown(self): + # Restore original env var + if self.original_env is not None: + os.environ[AGENT_OBSERVABILITY_ENABLED] = self.original_env + elif AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] + + def test_is_installed_package_not_found(self): + """Test is_installed returns False when package is not found""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Simulate package not found + mock_version.side_effect = PackageNotFoundError("test-package") + + result = is_installed("test-package>=1.0.0") + self.assertFalse(result) + + def test_is_installed(self): + """Test is_installed returns True when version matches the specifier""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Package is installed and version matches requirement + mock_version.return_value = "2.5.0" + + # Test with compatible version requirement + result = is_installed("test-package>=2.0.0") + self.assertTrue(result) + + # Test with exact version match + mock_version.return_value = "1.0.0" + result = is_installed("test-package==1.0.0") + self.assertTrue(result) + + # Test with version range + mock_version.return_value = "1.5.0" + result = is_installed("test-package>=1.0,<2.0") + self.assertTrue(result) + + def test_is_installed_version_mismatch(self): + """Test is_installed returns False when version doesn't match""" + with patch("amazon.opentelemetry.distro._utils.version") as mock_version: + # Package is installed but version doesn't match requirement + mock_version.return_value = "1.0.0" + + # Test with incompatible version requirement + result = is_installed("test-package>=2.0.0") + self.assertFalse(result) + + def test_is_agent_observability_enabled_various_values(self): + """Test is_agent_observability_enabled with various environment variable values""" + # Test with "True" (uppercase) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "True" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "TRUE" (all caps) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "TRUE" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "true" (lowercase) + os.environ[AGENT_OBSERVABILITY_ENABLED] = "true" + self.assertTrue(is_agent_observability_enabled()) + + # Test with "false" + os.environ[AGENT_OBSERVABILITY_ENABLED] = "false" + self.assertFalse(is_agent_observability_enabled()) + + # Test with "False" + os.environ[AGENT_OBSERVABILITY_ENABLED] = "False" + self.assertFalse(is_agent_observability_enabled()) + + # Test with arbitrary string + os.environ[AGENT_OBSERVABILITY_ENABLED] = "yes" + self.assertFalse(is_agent_observability_enabled()) + + # Test with empty string + os.environ[AGENT_OBSERVABILITY_ENABLED] = "" + self.assertFalse(is_agent_observability_enabled()) + + # Test when env var is not set + if AGENT_OBSERVABILITY_ENABLED in os.environ: + del os.environ[AGENT_OBSERVABILITY_ENABLED] + self.assertFalse(is_agent_observability_enabled()) From 5f18b40f3c56913cc7442996472303629a9d4e64 Mon Sep 17 00:00:00 2001 From: Michael He Date: Thu, 19 Jun 2025 20:41:33 +0000 Subject: [PATCH 15/15] add resource detector patch unit tests for code coverage threshold --- .../distro/test_instrumentation_patch.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py index fdc387ba4..8eff6f2e6 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py @@ -8,6 +8,8 @@ import gevent.monkey +import opentelemetry.sdk.extension.aws.resource.ec2 as ec2_resource +import opentelemetry.sdk.extension.aws.resource.eks as eks_resource from amazon.opentelemetry.distro.patches._instrumentation_patch import ( AWS_GEVENT_PATCH_MODULES, apply_instrumentation_patches, @@ -116,6 +118,8 @@ def _run_patch_mechanism_tests(self): """ self._test_botocore_installed_flag() self._reset_mocks() + self._test_resource_detector_patches() + self._reset_mocks() def _test_unpatched_botocore_instrumentation(self): # Kinesis @@ -352,6 +356,53 @@ def _test_patched_bedrock_agent_instrumentation(self): self.assertEqual(len(bedrock_agent_success_attributes), 1) self.assertEqual(bedrock_agent_success_attributes[attribute_tuple[0]], attribute_tuple[1]) + def _test_resource_detector_patches(self): + """Test that resource detector patches are applied and work correctly""" + # Test that the functions were patched + self.assertIsNotNone(ec2_resource._aws_http_request) + self.assertIsNotNone(eks_resource._aws_http_request) + + # Test EC2 patched function + with patch("amazon.opentelemetry.distro.patches._resource_detector_patches.urlopen") as mock_urlopen: + mock_response = MagicMock() + mock_response.read.return_value = b'{"test": "ec2-data"}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + result = ec2_resource._aws_http_request("GET", "/test/path", {"X-Test": "header"}) + self.assertEqual(result, '{"test": "ec2-data"}') + + # Verify the request was made correctly + args, kwargs = mock_urlopen.call_args + request = args[0] + self.assertEqual(request.full_url, "http://169.254.169.254/test/path") + self.assertEqual(request.headers, {"X-test": "header"}) + self.assertEqual(kwargs["timeout"], 5) + + # Test EKS patched function + with patch("amazon.opentelemetry.distro.patches._resource_detector_patches.urlopen") as mock_urlopen, patch( + "amazon.opentelemetry.distro.patches._resource_detector_patches.ssl.create_default_context" + ) as mock_ssl: + mock_response = MagicMock() + mock_response.read.return_value = b'{"test": "eks-data"}' + mock_urlopen.return_value.__enter__.return_value = mock_response + + mock_context = MagicMock() + mock_ssl.return_value = mock_context + + result = eks_resource._aws_http_request("GET", "/api/v1/test", "Bearer token123") + self.assertEqual(result, '{"test": "eks-data"}') + + # Verify the request was made correctly + args, kwargs = mock_urlopen.call_args + request = args[0] + self.assertEqual(request.full_url, "https://kubernetes.default.svc/api/v1/test") + self.assertEqual(request.headers, {"Authorization": "Bearer token123"}) + self.assertEqual(kwargs["timeout"], 5) + self.assertEqual(kwargs["context"], mock_context) + + # Verify SSL context was created with correct CA file + mock_ssl.assert_called_once_with(cafile="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + def _reset_mocks(self): for method_patch in self.method_patches.values(): method_patch.reset_mock()