From a340f675a022c113afe9a45e1344cefc263b7156 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 9 Jun 2026 04:45:21 +0000 Subject: [PATCH 01/15] chore: update OpenTelemetry dependencies to 1.42.1/0.63b1 --- aws-opentelemetry-distro/pyproject.toml | 110 +++++++++--------- .../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 +- tox.ini | 4 +- 6 files changed, 65 insertions(+), 65 deletions(-) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 683771ad0..2b8a2ac18 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -26,64 +26,64 @@ classifiers = [ ] dependencies = [ - "opentelemetry-api == 1.40.0", - "opentelemetry-sdk == 1.40.0", - "opentelemetry-exporter-otlp-proto-grpc == 1.40.0", - "opentelemetry-exporter-otlp-proto-http == 1.40.0", - "opentelemetry-propagator-b3 == 1.40.0", - "opentelemetry-propagator-jaeger == 1.40.0", - "opentelemetry-exporter-otlp-proto-common == 1.40.0", + "opentelemetry-api == 1.42.1", + "opentelemetry-sdk == 1.42.1", + "opentelemetry-exporter-otlp-proto-grpc == 1.42.1", + "opentelemetry-exporter-otlp-proto-http == 1.42.1", + "opentelemetry-propagator-b3 == 1.42.1", + "opentelemetry-propagator-jaeger == 1.42.1", + "opentelemetry-exporter-otlp-proto-common == 1.42.1", "opentelemetry-sdk-extension-aws == 2.1.0", "opentelemetry-propagator-aws-xray == 1.0.2", - "opentelemetry-distro == 0.61b0", - "opentelemetry-processor-baggage == 0.61b0", - "opentelemetry-propagator-ot-trace == 0.61b0", - "opentelemetry-instrumentation == 0.61b0", - "opentelemetry-instrumentation-aws-lambda == 0.61b0", - "opentelemetry-instrumentation-aio-pika == 0.61b0", - "opentelemetry-instrumentation-aiohttp-client == 0.61b0", - "opentelemetry-instrumentation-aiokafka == 0.61b0", - "opentelemetry-instrumentation-aiopg == 0.61b0", - "opentelemetry-instrumentation-asgi == 0.61b0", - "opentelemetry-instrumentation-asyncpg == 0.61b0", - "opentelemetry-instrumentation-boto == 0.61b0", - "opentelemetry-instrumentation-boto3sqs == 0.61b0", - "opentelemetry-instrumentation-botocore == 0.61b0", - "opentelemetry-instrumentation-celery == 0.61b0", - "opentelemetry-instrumentation-confluent-kafka == 0.61b0", - "opentelemetry-instrumentation-dbapi == 0.61b0", - "opentelemetry-instrumentation-django == 0.61b0", - "opentelemetry-instrumentation-elasticsearch == 0.61b0", - "opentelemetry-instrumentation-falcon == 0.61b0", - "opentelemetry-instrumentation-fastapi == 0.61b0", - "opentelemetry-instrumentation-flask == 0.61b0", - "opentelemetry-instrumentation-grpc == 0.61b0", - "opentelemetry-instrumentation-httpx == 0.61b0", - "opentelemetry-instrumentation-jinja2 == 0.61b0", - "opentelemetry-instrumentation-kafka-python == 0.61b0", - "opentelemetry-instrumentation-logging == 0.61b0", - "opentelemetry-instrumentation-mysql == 0.61b0", - "opentelemetry-instrumentation-mysqlclient == 0.61b0", - "opentelemetry-instrumentation-pika == 0.61b0", - "opentelemetry-instrumentation-psycopg2 == 0.61b0", - "opentelemetry-instrumentation-pymemcache == 0.61b0", - "opentelemetry-instrumentation-pymongo == 0.61b0", - "opentelemetry-instrumentation-pymysql == 0.61b0", - "opentelemetry-instrumentation-pyramid == 0.61b0", - "opentelemetry-instrumentation-redis == 0.61b0", - "opentelemetry-instrumentation-remoulade == 0.61b0", - "opentelemetry-instrumentation-requests == 0.61b0", - "opentelemetry-instrumentation-sqlalchemy == 0.61b0", - "opentelemetry-instrumentation-sqlite3 == 0.61b0", - "opentelemetry-instrumentation-starlette == 0.61b0", - "opentelemetry-instrumentation-system-metrics == 0.61b0", + "opentelemetry-distro == 0.63b1", + "opentelemetry-processor-baggage == 0.63b1", + "opentelemetry-propagator-ot-trace == 0.63b1", + "opentelemetry-instrumentation == 0.63b1", + "opentelemetry-instrumentation-aws-lambda == 0.63b1", + "opentelemetry-instrumentation-aio-pika == 0.63b1", + "opentelemetry-instrumentation-aiohttp-client == 0.63b1", + "opentelemetry-instrumentation-aiokafka == 0.63b1", + "opentelemetry-instrumentation-aiopg == 0.63b1", + "opentelemetry-instrumentation-asgi == 0.63b1", + "opentelemetry-instrumentation-asyncpg == 0.63b1", + "opentelemetry-instrumentation-boto == 0.63b1", + "opentelemetry-instrumentation-boto3sqs == 0.63b1", + "opentelemetry-instrumentation-botocore == 0.63b1", + "opentelemetry-instrumentation-celery == 0.63b1", + "opentelemetry-instrumentation-confluent-kafka == 0.63b1", + "opentelemetry-instrumentation-dbapi == 0.63b1", + "opentelemetry-instrumentation-django == 0.63b1", + "opentelemetry-instrumentation-elasticsearch == 0.63b1", + "opentelemetry-instrumentation-falcon == 0.63b1", + "opentelemetry-instrumentation-fastapi == 0.63b1", + "opentelemetry-instrumentation-flask == 0.63b1", + "opentelemetry-instrumentation-grpc == 0.63b1", + "opentelemetry-instrumentation-httpx == 0.63b1", + "opentelemetry-instrumentation-jinja2 == 0.63b1", + "opentelemetry-instrumentation-kafka-python == 0.63b1", + "opentelemetry-instrumentation-logging == 0.63b1", + "opentelemetry-instrumentation-mysql == 0.63b1", + "opentelemetry-instrumentation-mysqlclient == 0.63b1", + "opentelemetry-instrumentation-pika == 0.63b1", + "opentelemetry-instrumentation-psycopg2 == 0.63b1", + "opentelemetry-instrumentation-pymemcache == 0.63b1", + "opentelemetry-instrumentation-pymongo == 0.63b1", + "opentelemetry-instrumentation-pymysql == 0.63b1", + "opentelemetry-instrumentation-pyramid == 0.63b1", + "opentelemetry-instrumentation-redis == 0.63b1", + "opentelemetry-instrumentation-remoulade == 0.63b1", + "opentelemetry-instrumentation-requests == 0.63b1", + "opentelemetry-instrumentation-sqlalchemy == 0.63b1", + "opentelemetry-instrumentation-sqlite3 == 0.63b1", + "opentelemetry-instrumentation-starlette == 0.63b1", + "opentelemetry-instrumentation-system-metrics == 0.63b1", "opentelemetry-instrumentation-threading == 0.61b0", - "opentelemetry-instrumentation-tornado == 0.61b0", - "opentelemetry-instrumentation-tortoiseorm == 0.61b0", - "opentelemetry-instrumentation-urllib == 0.61b0", - "opentelemetry-instrumentation-urllib3 == 0.61b0", - "opentelemetry-instrumentation-wsgi == 0.61b0", - "opentelemetry-instrumentation-cassandra == 0.61b0", + "opentelemetry-instrumentation-tornado == 0.63b1", + "opentelemetry-instrumentation-tortoiseorm == 0.63b1", + "opentelemetry-instrumentation-urllib == 0.63b1", + "opentelemetry-instrumentation-urllib3 == 0.63b1", + "opentelemetry-instrumentation-wsgi == 0.63b1", + "opentelemetry-instrumentation-cassandra == 0.63b1", "opentelemetry-instrumentation-openai-agents-v2 == 0.1.0", "cachetools == 6.2.4", "urllib3 >= 2.7.0; python_version >= '3.10'", diff --git a/contract-tests/images/mock-collector/pyproject.toml b/contract-tests/images/mock-collector/pyproject.toml index 4cfde5712..90f8b44a3 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.76.0", - "opentelemetry-proto == 1.40.0", - "opentelemetry-sdk == 1.40.0", + "opentelemetry-proto == 1.42.1", + "opentelemetry-sdk == 1.42.1", "protobuf==6.33.5", "typing-extensions==4.15.0" ] diff --git a/contract-tests/images/mock-collector/requirements.txt b/contract-tests/images/mock-collector/requirements.txt index 1c7b1a640..a78fe344e 100644 --- a/contract-tests/images/mock-collector/requirements.txt +++ b/contract-tests/images/mock-collector/requirements.txt @@ -1,5 +1,5 @@ grpcio==1.76.0 -opentelemetry-proto==1.40.0 -opentelemetry-sdk==1.40.0 +opentelemetry-proto==1.42.1 +opentelemetry-sdk==1.42.1 protobuf==6.33.5 typing-extensions==4.15.0 diff --git a/contract-tests/tests/pyproject.toml b/contract-tests/tests/pyproject.toml index 6f062e306..52da2e705 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.40.0", - "opentelemetry-sdk == 1.40.0", + "opentelemetry-proto == 1.42.1", + "opentelemetry-sdk == 1.42.1", "testcontainers==3.7.1", "grpcio==1.76.0", "docker==7.1.0", diff --git a/lambda-layer/src/tests/requirements.txt b/lambda-layer/src/tests/requirements.txt index 08b79885c..075a8b001 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.61b0 -opentelemetry-instrumentation-aws-lambda==0.61b0 +opentelemetry-test-utils==0.63b1 +opentelemetry-instrumentation-aws-lambda==0.63b1 diff --git a/tox.ini b/tox.ini index d517226d9..dfedde76d 100644 --- a/tox.ini +++ b/tox.ini @@ -29,8 +29,8 @@ deps = test: httpx setenv = - CORE_REPO=git+https://github.com/open-telemetry/opentelemetry-python.git@release/v1.40.x-0.61bx - CONTRIB_REPO=git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@release/v1.40.x-0.61bx + CORE_REPO=git+https://github.com/open-telemetry/opentelemetry-python.git@release/v1.42.x-0.63bx + CONTRIB_REPO=git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@release/v1.42.x-0.63bx COVERAGE_FILE={toxinidir}/.coverage COVERAGE_RUN=coverage run --parallel --source={envsitepackagesdir}/amazon --rcfile={toxinidir}/.coveragerc ; Enable coverage collection in subprocesses spawned during tests. From 58718abf87a5d6fe531c77c26c11d66650aed15e Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 15 Jun 2026 16:09:27 -0700 Subject: [PATCH 02/15] chore(deps): drop opentelemetry-instrumentation-boto The legacy boto2 instrumentation was removed from opentelemetry-python-contrib in 0.62b0 (open-telemetry/opentelemetry-python-contrib#4303). Pinning it at 0.63b1 breaks pip resolution. --- aws-opentelemetry-distro/pyproject.toml | 1 - .../opentelemetry/distro/code_correlation/internal/3rd.txt | 1 - scripts/update_dependencies.py | 1 - 3 files changed, 3 deletions(-) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 771e433eb..880324012 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -46,7 +46,6 @@ dependencies = [ "opentelemetry-instrumentation-aiopg == 0.63b1", "opentelemetry-instrumentation-asgi == 0.63b1", "opentelemetry-instrumentation-asyncpg == 0.63b1", - "opentelemetry-instrumentation-boto == 0.63b1", "opentelemetry-instrumentation-boto3sqs == 0.63b1", "opentelemetry-instrumentation-botocore == 0.63b1", "opentelemetry-instrumentation-celery == 0.63b1", diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt index 51d3e94b2..59528329f 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/code_correlation/internal/3rd.txt @@ -3596,7 +3596,6 @@ autoevals keyrings-codeartifact lpips ldapdomaindump -opentelemetry-instrumentation-boto suds-py3 appengine-python-standard litellm-proxy-extras diff --git a/scripts/update_dependencies.py b/scripts/update_dependencies.py index 6f2d8ba9e..eecd590f9 100644 --- a/scripts/update_dependencies.py +++ b/scripts/update_dependencies.py @@ -32,7 +32,6 @@ "opentelemetry-instrumentation-aiopg", "opentelemetry-instrumentation-asgi", "opentelemetry-instrumentation-asyncpg", - "opentelemetry-instrumentation-boto", "opentelemetry-instrumentation-boto3sqs", "opentelemetry-instrumentation-botocore", "opentelemetry-instrumentation-celery", From f692018e3ce6a7e2b67a9ca1a5f66ddaa4f41fcd Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 15 Jun 2026 16:25:09 -0700 Subject: [PATCH 03/15] chore(deps): drop Python 3.9 support opentelemetry-python 1.42.0 and opentelemetry-python-contrib 0.63b0 dropped Python 3.9 support, so ADOT cannot install on it either. - Bump requires-python to >=3.10 across distro and contract tests - Remove 3.9 from tox envlist and CI test matrices (pr-build, run-instrumentation-tests) - Drop py39 e2e jobs (default + asg + arm64); add py310 platform/arch coverage - Delete .coveragerc-py39 and the 3.9 branch in set_up action - Narrow DI bytecode-injection engine to Python 3.10-3.11 (rename helper, bump version checks, refresh comments) - Pin pytest>=9 in dev-requirements (was pinned to <10 for 3.9) --- .coveragerc-py39 | 35 ------------------- .github/actions/set_up/action.yml | 4 +-- .../application-signals-e2e-test.yml | 23 ++++-------- .github/workflows/pr-build.yml | 2 +- .../workflows/run-instrumentation-tests.yml | 2 +- README.md | 2 +- aws-opentelemetry-distro/pyproject.toml | 7 ++-- .../src/amazon/opentelemetry/distro/_utils.py | 2 +- .../_bytecode_injection_engine.py | 20 +++++------ .../_instrumentation_engine.py | 2 +- .../debugger/instrumentation_manager.py | 8 ++--- .../internal/test_packages_resolver.py | 14 ++++---- .../test_bytecode_injection_engine.py | 2 +- .../test_aws_opentelementry_configurator.py | 2 +- .../distro/test_aws_opentelemetry_distro.py | 2 +- .../applications/botocore/pyproject.toml | 2 +- .../images/applications/django/pyproject.toml | 2 +- .../mysql-connector/pyproject.toml | 2 +- .../applications/mysqlclient/pyproject.toml | 2 +- .../applications/psycopg2/pyproject.toml | 2 +- .../applications/pymysql/pyproject.toml | 2 +- .../applications/requests/pyproject.toml | 2 +- .../images/mock-collector/pyproject.toml | 2 +- contract-tests/tests/pyproject.toml | 2 +- dev-requirements.txt | 5 ++- tox.ini | 4 +-- 26 files changed, 52 insertions(+), 102 deletions(-) delete mode 100644 .coveragerc-py39 diff --git a/.coveragerc-py39 b/.coveragerc-py39 deleted file mode 100644 index 9a54d53ac..000000000 --- a/.coveragerc-py39 +++ /dev/null @@ -1,35 +0,0 @@ -[run] -; This flag writes per-process coverage data files so subprocess coverage -; can be combined with the main test process by pytest-cov. Works with COVERAGE_PROCESS_START in -; tox.ini env variable -; see: https://coverage.readthedocs.io/en/6.5.0/cmd.html#combining-data-files-coverage-combine -parallel = true - -[paths] -; Normalize installed package paths back to source tree so coverage data -; from different tox envs can be combined into a single report -source = - aws-opentelemetry-distro/src/amazon - .tox/3.*/lib/python*/site-packages/amazon - */site-packages/amazon - -[report] -include_namespace_packages = true -fail_under = 95.00 -precision = 2 -; Exclude framework instrumentation not installed on 3.9, -; test files that share the amazon namespace, and pip build artifacts -; so only source code is reported -omit = - */instrumentation/crewai/* - */instrumentation/llama_index/* - */instrumentation/langchain/* - */instrumentation/mcp/* - */instrumentation/openai_agents/* - */tests/* - */pip-build-env-*/* - ; Dynamic Instrumentation line-breakpoint engines are version-exclusive (bytecode for - ; 3.9-3.11, sys.monitoring for 3.12+). Excluded from the coverage gate; each is unit-tested - ; on its native version and both are validated by the DI contract tests. - */distro/debugger/instrumentation_engine/_bytecode_injection_engine.py - */distro/debugger/instrumentation_engine/_sys_monitoring_engine.py diff --git a/.github/actions/set_up/action.yml b/.github/actions/set_up/action.yml index 0580407e2..18ae802e9 100644 --- a/.github/actions/set_up/action.yml +++ b/.github/actions/set_up/action.yml @@ -55,9 +55,7 @@ runs: shell: bash run: | pip install coverage - if [ "${{ inputs.python_version }}" = "3.9" ]; then - RCFILE=.coveragerc-py39 - elif [ "${{ inputs.python_version }}" = "3.14" ]; then + if [ "${{ inputs.python_version }}" = "3.14" ]; then RCFILE=.coveragerc-py314 else RCFILE=.coveragerc diff --git a/.github/workflows/application-signals-e2e-test.yml b/.github/workflows/application-signals-e2e-test.yml index fce880f91..0d1a85b5e 100644 --- a/.github/workflows/application-signals-e2e-test.yml +++ b/.github/workflows/application-signals-e2e-test.yml @@ -46,17 +46,6 @@ jobs: # DEFAULT SETTING: {Python Version}, EC2, AMD64, AL2 # - default-py39-amd64: - needs: [ upload-main-build ] - uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-ec2-default-test.yml@main - secrets: inherit - with: - aws-region: us-east-1 - caller-workflow-name: 'main-build' - python-version: '3.9' - cpu-architecture: 'x86_64' - staging-wheel-name: ${{ inputs.staging-wheel-name }} - default-py310-amd64: needs: [ upload-main-build ] uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-ec2-default-test.yml@main @@ -186,17 +175,17 @@ jobs: # # PACKAGED DISTRIBUTION PLATFORM COVERAGE - # DEFAULT SETTING: Python 3.9, {Platform}, AMD64, AL2 + # DEFAULT SETTING: Python 3.10, {Platform}, AMD64, AL2 # - asg-py39-amd64: + asg-py310-amd64: needs: [ upload-main-build ] uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-ec2-asg-test.yml@main secrets: inherit with: aws-region: us-east-1 caller-workflow-name: 'main-build' - python-version: '3.9' + python-version: '3.10' staging-wheel-name: ${{ inputs.staging-wheel-name }} # @@ -225,17 +214,17 @@ jobs: # # CPU ARCHITECTURE COVERAGE - # DEFAULT SETTING: Python 3.9, EC2, {CPU Architecture}, AL2 + # DEFAULT SETTING: Python 3.10, EC2, {CPU Architecture}, AL2 # - default-py39-arm64: + default-py310-arm64: needs: [ upload-main-build ] uses: aws-observability/aws-application-signals-test-framework/.github/workflows/python-ec2-default-test.yml@main secrets: inherit with: aws-region: us-east-1 caller-workflow-name: 'main-build' - python-version: '3.9' + python-version: '3.10' cpu-architecture: 'arm64' staging-wheel-name: ${{ inputs.staging-wheel-name }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index ec0098592..ef25f6fb3 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -127,7 +127,7 @@ jobs: strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout Repo @ SHA - ${{ github.sha }} uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #5.0.0 diff --git a/.github/workflows/run-instrumentation-tests.yml b/.github/workflows/run-instrumentation-tests.yml index 6b06e3d00..82c9eb28c 100644 --- a/.github/workflows/run-instrumentation-tests.yml +++ b/.github/workflows/run-instrumentation-tests.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] tox-env: - test-aws-opentelemetry-distro-langchain - test-aws-opentelemetry-distro-crewai diff --git a/README.md b/README.md index 51ca80e5e..a34d4b890 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This project is licensed under the Apache-2.0 License. ## Notices ### Python Version Support -This project ensures compatibility with the following supported Python versions: 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 +This project ensures compatibility with the following supported Python versions: 3.10, 3.11, 3.12, 3.13, 3.14 ### Note on Amazon CloudWatch Application Signals [Amazon CloudWatch Application Signals](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Application-Monitoring-Sections.html) components are designed to seamlessly work with all library instrumentations offered by [OpenTelemetry Python auto-instrumentation](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/README.md). While upstream OpenTelemetry Python instrumentations are in beta, Application Signals components are stable, production ready and have also been tested for popular libraries/frameworks such as [Django, Boto3, and others](https://github.com/aws-observability/aws-otel-python-instrumentation/tree/main/contract-tests/images/applications). We will prioritize backward compatibility for Application Signals components, striving to ensure that they remain functional even in the face of potential breaking changes introduced by OpenTelemetry upstream libraries. Please [raise an issue](https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/CONTRIBUTING.md#reporting-bugsfeature-requests) if you notice Application Signals doesn't work for a particular OpenTelemetry supported library. diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 880324012..597ea10ce 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "AWS OpenTelemetry Python Distro" readme = "README.rst" license = {text = "Apache-2.0"} -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name = "Amazon Web Services" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -92,9 +91,9 @@ dependencies = [ # (debugger/_data_models.py). Required on all supported Python versions, hence a core dependency. "python-dateutil >= 2.7.0", # Used by Dynamic Instrumentation's bytecode-injection engine for line-level instrumentation on - # Python 3.9-3.11 (3.12+ uses the built-in sys.monitoring instead, so the marker excludes it there). + # Python 3.10-3.11 (3.12+ uses the built-in sys.monitoring instead, so the marker excludes it there). # A core dependency so `pip install aws-opentelemetry-distro` gives a fully working feature with no extras. - "bytecode; python_version >= '3.9' and python_version < '3.12'", + "bytecode; python_version >= '3.10' and python_version < '3.12'", ] [project.optional-dependencies] diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py index aa54ef117..b2e7b0ea2 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_utils.py @@ -52,7 +52,7 @@ def should_add_application_signals_dimensions() -> bool: IS_BOTOCORE_INSTALLED: bool = is_installed("botocore") # The 'bytecode' package is an optional dependency (the 'debugger' extra), installed only on -# Python 3.9-3.11. Dynamic Instrumentation's bytecode-injection engine guards on this flag. +# Python 3.10-3.11. Dynamic Instrumentation's bytecode-injection engine guards on this flag. IS_BYTECODE_INSTALLED: bool = is_installed("bytecode") diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_bytecode_injection_engine.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_bytecode_injection_engine.py index 54950667a..877290b75 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_bytecode_injection_engine.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_bytecode_injection_engine.py @@ -5,7 +5,7 @@ Bytecode injection engine for live debugger. This module handles the injection of breakpoint calls into Python functions -using bytecode injection for Python 3.9-3.11. +using bytecode injection for Python 3.10-3.11. """ import inspect @@ -69,7 +69,7 @@ class InjectionState: class BytecodeInjectionEngine(InstrumentationEngine): """ - Bytecode injection engine for line-level debugging on Python 3.9-3.11. + Bytecode injection engine for line-level debugging on Python 3.10-3.11. Safety guarantees: - Never crashes the application (comprehensive exception handling) @@ -93,7 +93,7 @@ def __init__(self): if not IS_BYTECODE_INSTALLED: logger.warning( "bytecode library not available. " - "Debugger will not function on Python 3.9-3.11. " + "Debugger will not function on Python 3.10-3.11. " "Install with: pip install bytecode" ) @@ -121,8 +121,8 @@ def initialize(self, hit_count_callback=None) -> None: @staticmethod def supports_runtime() -> bool: - """Check if bytecode injection is supported (Python 3.9-3.11).""" - return (3, 9) <= sys.version_info < (3, 12) and IS_BYTECODE_INSTALLED + """Check if bytecode injection is supported (Python 3.10-3.11).""" + return (3, 10) <= sys.version_info < (3, 12) and IS_BYTECODE_INSTALLED def enable_breakpoints_for_function( self, @@ -494,8 +494,8 @@ def _create_breakpoint_instructions(self, function_key: str, line_number: int) - try: if sys.version_info >= (3, 11): return self._create_breakpoint_instructions_py311(function_key, line_number) - if sys.version_info >= (3, 9): - return self._create_breakpoint_instructions_py39_py310(function_key, line_number) + if sys.version_info >= (3, 10): + return self._create_breakpoint_instructions_py310(function_key, line_number) logger.error("Unsupported Python version: %s", sys.version_info) return None except Exception as exc: # pylint: disable=broad-exception-caught @@ -537,14 +537,14 @@ def _create_breakpoint_instructions_py311(function_key: str, line_number: int) - ] @staticmethod - def _create_breakpoint_instructions_py39_py310(function_key: str, line_number: int) -> list: + def _create_breakpoint_instructions_py310(function_key: str, line_number: int) -> list: """ - Create Python 3.9/3.10-specific breakpoint instructions. + Create Python 3.10-specific breakpoint instructions. Generates bytecode that calls: _breakpoint_handler(function_key, line_number, locals()) - Python 3.9/3.10 calling convention: + Python 3.10 calling convention: - LOAD_GLOBAL (load _breakpoint_handler) - LOAD_CONST (load arguments) - LOAD_GLOBAL + CALL_FUNCTION (call locals()) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_instrumentation_engine.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_instrumentation_engine.py index b26622b12..beb8cab71 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_instrumentation_engine.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_engine/_instrumentation_engine.py @@ -6,7 +6,7 @@ This module provides a common interface for different instrumentation mechanisms: - SysMonitoringEngine (Python 3.12+) -- BytecodeInjectionEngine (Python 3.9-3.11) +- BytecodeInjectionEngine (Python 3.10-3.11) """ from abc import ABC, abstractmethod diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_manager.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_manager.py index 940db97b5..d6654cbed 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_manager.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/debugger/instrumentation_manager.py @@ -127,8 +127,8 @@ def _select_engine(self) -> Optional[InstrumentationEngine]: # pragma: no cover except Exception as exception: # pylint: disable=broad-exception-caught logger.error("Failed to initialize SysMonitoringEngine: %s", exception, exc_info=True) - # Python 3.9-3.11 - Use BytecodeInjectionEngine - elif sys.version_info >= (3, 9): + # Python 3.10-3.11 - Use BytecodeInjectionEngine + elif sys.version_info >= (3, 10): try: # pylint: disable=import-outside-toplevel from amazon.opentelemetry.distro.debugger.instrumentation_engine._bytecode_injection_engine import ( @@ -146,11 +146,11 @@ def _select_engine(self) -> Optional[InstrumentationEngine]: # pragma: no cover except Exception as exception: # pylint: disable=broad-exception-caught logger.error("Failed to initialize BytecodeInjectionEngine: %s", exception, exc_info=True) - # Python < 3.9 - Not supported + # Python < 3.10 - Not supported else: logger.warning( "Python %d.%d is not supported for line breakpoints. " - "Supported versions: 3.9-3.12+. Function wrapping will still work, but line breakpoints will not.", + "Supported versions: 3.10-3.12+. Function wrapping will still work, but line breakpoints will not.", sys.version_info.major, sys.version_info.minor, ) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py index 4b897eb44..ae3dbbf27 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/code_correlation/internal/test_packages_resolver.py @@ -218,7 +218,7 @@ def test_determine_effective_root_package_with_init(self): with patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.exists", return_value=True): relative_path = Path("mypackage/submodule.py") - parent_path = Path("/usr/lib/python3.9/site-packages") + parent_path = Path("/usr/lib/python3.10/site-packages") result = _determine_effective_root(relative_path, parent_path) self.assertEqual(result, "mypackage") @@ -227,7 +227,7 @@ def test_determine_effective_root_no_init(self): """Test determining root for module without __init__.py.""" with patch("pathlib.Path.is_dir", return_value=False): relative_path = Path("module/file.py") - parent_path = Path("/usr/lib/python3.9/site-packages") + parent_path = Path("/usr/lib/python3.10/site-packages") result = _determine_effective_root(relative_path, parent_path) self.assertEqual(result, "module/file.py") @@ -237,7 +237,7 @@ def test_determine_effective_root_directory_no_init(self): with patch("pathlib.Path.is_dir", return_value=True), patch("pathlib.Path.exists", return_value=False): relative_path = Path("namespace/subpackage.py") - parent_path = Path("/usr/lib/python3.9/site-packages") + parent_path = Path("/usr/lib/python3.10/site-packages") result = _determine_effective_root(relative_path, parent_path) self.assertEqual(result, "namespace/subpackage.py") @@ -289,7 +289,7 @@ class TestExtractRootModuleName(TestCase): @patch("amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._PURELIB_PATH") def test_extract_root_module_name_purelib(self, mock_purelib): """Test extracting root module name from purelib path.""" - mock_purelib.return_value = Path("/usr/lib/python3.9/site-packages") + mock_purelib.return_value = Path("/usr/lib/python3.10/site-packages") with patch.object(Path, "resolve") as mock_resolve, patch.object( Path, "relative_to" @@ -297,11 +297,11 @@ def test_extract_root_module_name_purelib(self, mock_purelib): "amazon.opentelemetry.distro.code_correlation.internal.packages_resolver._determine_effective_root" ) as mock_determine: - mock_resolve.return_value = Path("/usr/lib/python3.9/site-packages/mypackage/module.py") + mock_resolve.return_value = Path("/usr/lib/python3.10/site-packages/mypackage/module.py") mock_relative_to.return_value = Path("mypackage/module.py") mock_determine.return_value = "mypackage" - file_path = Path("/usr/lib/python3.9/site-packages/mypackage/module.py") + file_path = Path("/usr/lib/python3.10/site-packages/mypackage/module.py") result = _extract_root_module_name(file_path) self.assertEqual(result, "mypackage") @@ -611,7 +611,7 @@ def test_is_user_code_stdlib(self, mock_is_third_party, mock_is_stdlib): mock_is_stdlib.return_value = True mock_is_third_party.return_value = False - result = is_user_code("/usr/lib/python3.9/os.py") + result = is_user_code("/usr/lib/python3.10/os.py") self.assertFalse(result) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/debugger/instrumentation_engine/test_bytecode_injection_engine.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/debugger/instrumentation_engine/test_bytecode_injection_engine.py index a679c1f1e..68292b182 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/debugger/instrumentation_engine/test_bytecode_injection_engine.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/debugger/instrumentation_engine/test_bytecode_injection_engine.py @@ -12,7 +12,7 @@ from amazon.opentelemetry.distro.debugger.instrumentation_engine import _bytecode_injection_engine -@unittest.skipIf(not (3, 9) <= sys.version_info < (3, 12), "BytecodeInjectionEngine requires Python 3.9-3.11") +@unittest.skipIf(not (3, 10) <= sys.version_info < (3, 12), "BytecodeInjectionEngine requires Python 3.10-3.11") class TestBytecodeInjectionEngine(InstrumentationEngineTestBase): """Tests for BytecodeInjectionEngine.""" 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 418cf11e7..f8d0674fb 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 @@ -129,7 +129,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - # Explicitly shut down meter provider to avoid I/O errors on Python 3.9 with gevent + # Explicitly shut down meter provider to avoid I/O errors with gevent # This ensures ConsoleMetricExporter is properly closed before Python cleanup try: meter_provider = get_meter_provider() 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 ab67f5d76..800cb81f0 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 @@ -774,7 +774,7 @@ def test_parsing_handles_environment_markers(self): f"{self.MODULE_PATH}._get_version" ) as mock_version: mock_requires.return_value = [ - 'opentelemetry-api == 1.40.0 ; python_version >= "3.9"', + 'opentelemetry-api == 1.40.0 ; python_version >= "3.10"', "opentelemetry-sdk == 1.40.0", ] mock_version.side_effect = lambda pkg: "1.33.1" diff --git a/contract-tests/images/applications/botocore/pyproject.toml b/contract-tests/images/applications/botocore/pyproject.toml index 5c530ff96..8e5175745 100644 --- a/contract-tests/images/applications/botocore/pyproject.toml +++ b/contract-tests/images/applications/botocore/pyproject.toml @@ -3,4 +3,4 @@ name = "botocore-server" description = "Simple server that relies on botocore library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/django/pyproject.toml b/contract-tests/images/applications/django/pyproject.toml index 8f7685c33..9cfbe6b2a 100644 --- a/contract-tests/images/applications/django/pyproject.toml +++ b/contract-tests/images/applications/django/pyproject.toml @@ -3,4 +3,4 @@ name = "django-server" description = "Simple server that relies on django library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/mysql-connector/pyproject.toml b/contract-tests/images/applications/mysql-connector/pyproject.toml index f19ac95e8..fe67448c7 100644 --- a/contract-tests/images/applications/mysql-connector/pyproject.toml +++ b/contract-tests/images/applications/mysql-connector/pyproject.toml @@ -3,4 +3,4 @@ name = "mysql-connector-server" description = "Simple server that relies on mysql-connector library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/mysqlclient/pyproject.toml b/contract-tests/images/applications/mysqlclient/pyproject.toml index b932e6c71..4c67d94f8 100644 --- a/contract-tests/images/applications/mysqlclient/pyproject.toml +++ b/contract-tests/images/applications/mysqlclient/pyproject.toml @@ -3,4 +3,4 @@ name = "mysqlclient-server" description = "Simple server that relies on mysqlclient library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/psycopg2/pyproject.toml b/contract-tests/images/applications/psycopg2/pyproject.toml index 3aa07335a..741cc8cfc 100644 --- a/contract-tests/images/applications/psycopg2/pyproject.toml +++ b/contract-tests/images/applications/psycopg2/pyproject.toml @@ -3,4 +3,4 @@ name = "psycopg2-server" description = "Simple server that relies on psycopg2 library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/pymysql/pyproject.toml b/contract-tests/images/applications/pymysql/pyproject.toml index 1ca5fcc2f..0a2b87c52 100644 --- a/contract-tests/images/applications/pymysql/pyproject.toml +++ b/contract-tests/images/applications/pymysql/pyproject.toml @@ -3,4 +3,4 @@ name = "pymysql-server" description = "Simple server that relies on pymysql library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/applications/requests/pyproject.toml b/contract-tests/images/applications/requests/pyproject.toml index 9894c46bf..461d60dd7 100644 --- a/contract-tests/images/applications/requests/pyproject.toml +++ b/contract-tests/images/applications/requests/pyproject.toml @@ -3,4 +3,4 @@ name = "requests-server" description = "Simple server that relies on requests library" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" diff --git a/contract-tests/images/mock-collector/pyproject.toml b/contract-tests/images/mock-collector/pyproject.toml index 90f8b44a3..937fb0317 100644 --- a/contract-tests/images/mock-collector/pyproject.toml +++ b/contract-tests/images/mock-collector/pyproject.toml @@ -7,7 +7,7 @@ name = "mock-collector" description = "Mock Collector used by contract tests for AWS OTEL Python Instrumentation" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "grpcio ~= 1.76.0", diff --git a/contract-tests/tests/pyproject.toml b/contract-tests/tests/pyproject.toml index 52da2e705..ca8881556 100644 --- a/contract-tests/tests/pyproject.toml +++ b/contract-tests/tests/pyproject.toml @@ -7,7 +7,7 @@ name = "contract-tests" description = "Contract tests for AWS OTEL Python Instrumentation" version = "1.0.0" license = "Apache-2.0" -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "opentelemetry-proto == 1.42.1", diff --git a/dev-requirements.txt b/dev-requirements.txt index cddbb23ce..69ed3b762 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,8 +4,7 @@ isort==5.12.0 black==24.3.0 httpretty==1.1.4 mypy==0.931 -# TODO: pin to pytest>=9 once Python 3.9 is dropped from the test matrix. -pytest>=7.1.3,<10 +pytest>=9 readme-renderer==42.0 bleach==4.1.0 # transient dependency for readme-renderer protobuf>=5.29.5 @@ -17,7 +16,7 @@ pyyaml==6.0.3 flaky==3.7.0 botocore~=1.0 # Needed so lint can resolve `import bytecode` in the DI bytecode-injection engine. It is a core -# runtime dependency (see pyproject.toml) installed on Python 3.9-3.11, but the lint env does not +# runtime dependency (see pyproject.toml) installed on Python 3.10-3.11, but the lint env does not # install the distro package itself, so it must be listed here too. bytecode>=0.13.0 flask>=2.0.0 diff --git a/tox.ini b/tox.ini index dfedde76d..3ceb3104e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ skipsdist = True skip_missing_interpreters = True envlist = ; aws-opentelemetry-distro - 3.{9,10,11,12,13,14}-test-aws-opentelemetry-distro + 3.{10,11,12,13,14}-test-aws-opentelemetry-distro 3.{10,11,12,13,14}-test-aws-opentelemetry-distro-langchain-{oldest,latest} ; crewai does not support Python 3.14 yet 3.{10,11,12,13}-test-aws-opentelemetry-distro-crewai-{oldest,latest} @@ -42,7 +42,7 @@ changedir = commands_pre = ; Install without -e to test the actual installation - 3.{9,10,11,12,13,14}: python -m pip install -U pip setuptools wheel + 3.{10,11,12,13,14}: python -m pip install -U pip setuptools wheel ; Install common packages for all the tests. These are not needed in all the ; cases but it saves a lot of boilerplate in this file. test: pip install botocore From ebe0c025684225c20e618c499edef76fc0fa332f Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 15 Jun 2026 16:52:09 -0700 Subject: [PATCH 04/15] fix(deps): bump opentelemetry-instrumentation-threading to 0.63b1 Threading was the lone instrumentation pinned at 0.61b0, transitively requiring opentelemetry-instrumentation==0.61b0, which conflicted with the ==0.63b1 pin coming from every other 0.63b1 instrumentation. Caused all CI build/test jobs to fail at pip install with ResolutionImpossible. --- aws-opentelemetry-distro/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/pyproject.toml b/aws-opentelemetry-distro/pyproject.toml index 597ea10ce..e70c1ea8c 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -75,7 +75,7 @@ dependencies = [ "opentelemetry-instrumentation-sqlite3 == 0.63b1", "opentelemetry-instrumentation-starlette == 0.63b1", "opentelemetry-instrumentation-system-metrics == 0.63b1", - "opentelemetry-instrumentation-threading == 0.61b0", + "opentelemetry-instrumentation-threading == 0.63b1", "opentelemetry-instrumentation-tornado == 0.63b1", "opentelemetry-instrumentation-tortoiseorm == 0.63b1", "opentelemetry-instrumentation-urllib == 0.63b1", From 7255e74d2f6ba0e7ca21f175ed47fb8fc6ee46fa Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 15 Jun 2026 16:52:09 -0700 Subject: [PATCH 05/15] chore(deps): add Python 3.14 classifier --- 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 e70c1ea8c..c6d66769f 100644 --- a/aws-opentelemetry-distro/pyproject.toml +++ b/aws-opentelemetry-distro/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ From 345792a53a90e8b90550e8cef6b590ef63332228 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 15 Jun 2026 20:37:26 -0700 Subject: [PATCH 06/15] fix(deps): switch importlib_metadata import to stdlib importlib.metadata OTel core 1.42.0 dropped the third-party `importlib-metadata` dependency in favor of the stdlib `importlib.metadata` since the project now requires Python >= 3.10 (open-telemetry/opentelemetry-python#3234). With OTel upgraded but `importlib-metadata` no longer transitively installed, the ADOT configurator's import fails with ModuleNotFoundError at test collection time on every supported Python version. --- .../opentelemetry/distro/aws_opentelemetry_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index 38561802a..f4526b5e4 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -12,7 +12,7 @@ from typing import ClassVar, Dict, List, NamedTuple, Optional, Type, Union import yaml -from importlib_metadata import version +from importlib.metadata import version from typing_extensions import override from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE, AWS_SERVICE_TYPE From 8ed1b0350775c75f84d41756f0d84049f7150e76 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 09:35:45 -0700 Subject: [PATCH 07/15] fix(botocore): adapt patches to ExtensionRegistry API in contrib 0.63b1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream contrib 0.63b1 refactored the botocore extension API: - _KNOWN_EXTENSIONS split into _BOTOCORE_EXTENSIONS + _AIOBOTOCORE_EXTENSIONS (to support the new aiobotocore instrumentation added in 0.62b0) - _find_extension removed; lookup now goes through an ExtensionRegistry instance attached to the instrumentor as self.extension_registry - _safe_invoke moved from opentelemetry.instrumentation.botocore to opentelemetry.instrumentation.botocore.utils Update ADOT's _botocore_patches.py to: - import _BOTOCORE_EXTENSIONS instead of _KNOWN_EXTENSIONS - import _safe_invoke from the new utils module - replace _find_extension() with self.extension_registry.get_extension() - replace _get_tracer/_get_meter/_get_metrics/_get_logger calls with the registry-method equivalents (and drop the redundant _get_meter — metrics are now fetched directly without a meter intermediary) Tests in test_instrumentation_patch.py patch the new ExtensionRegistry.get_extension/get_tracer/get_logger/get_metrics methods on the registry class instead of the module-level lookups that no longer exist. --- .../distro/patches/_botocore_patches.py | 34 +++--- .../patches/test_instrumentation_patch.py | 102 ++++++++++-------- 2 files changed, 76 insertions(+), 60 deletions(-) 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 c1b2d0b9f..3985c20d2 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 @@ -26,9 +26,8 @@ BotocoreInstrumentor, _apply_response_attributes, _determine_call_context, - _safe_invoke, ) -from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, _find_extension +from opentelemetry.instrumentation.botocore.extensions import _BOTOCORE_EXTENSIONS from opentelemetry.instrumentation.botocore.extensions.dynamodb import _DynamoDbExtension from opentelemetry.instrumentation.botocore.extensions.lmbd import _LambdaExtension from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension @@ -38,7 +37,7 @@ _BotocoreInstrumentorContext, _BotoResultT, ) -from opentelemetry.instrumentation.botocore.utils import get_server_attributes +from opentelemetry.instrumentation.botocore.utils import _safe_invoke, get_server_attributes from opentelemetry.instrumentation.utils import is_instrumentation_enabled, suppress_http_instrumentation from opentelemetry.propagate import get_global_textmap from opentelemetry.semconv._incubating.attributes.aws_attributes import ( @@ -135,7 +134,7 @@ def _apply_botocore_kinesis_patch() -> None: the `aws.kinesis.stream.name` attribute, to be used to generate RemoteTarget and achieve parity with the Java instrumentation. """ - _KNOWN_EXTENSIONS["kinesis"] = _lazy_load(".", "_KinesisExtension") + _BOTOCORE_EXTENSIONS["kinesis"] = _lazy_load(".", "_KinesisExtension") def _apply_botocore_s3_patch() -> None: @@ -147,7 +146,7 @@ def _apply_botocore_s3_patch() -> None: Callout that AWS_S3_BUCKET is in the AWS Semantic Conventions, and is simply not implemented in Python instrumentation. """ - _KNOWN_EXTENSIONS["s3"] = _lazy_load(".", "_S3Extension") + _BOTOCORE_EXTENSIONS["s3"] = _lazy_load(".", "_S3Extension") def _apply_botocore_sqs_patch() -> None: @@ -190,11 +189,11 @@ def _apply_botocore_bedrock_patch() -> None: 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-agentcore"] = _lazy_load(".._bedrock_agentcore_patches", "_BedrockAgentCoreExtension") - _KNOWN_EXTENSIONS["bedrock-agentcore-control"] = _lazy_load( + _BOTOCORE_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension") + _BOTOCORE_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension") + _BOTOCORE_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension") + _BOTOCORE_EXTENSIONS["bedrock-agentcore"] = _lazy_load(".._bedrock_agentcore_patches", "_BedrockAgentCoreExtension") + _BOTOCORE_EXTENSIONS["bedrock-agentcore-control"] = _lazy_load( ".._bedrock_agentcore_patches", "_BedrockAgentCoreExtension" ) @@ -230,13 +229,13 @@ def patched_api_call(self, original_func, instance, args, kwargs): - aws.auth.account.access_key - aws.auth.region - Note: Current implementation duplicates upstream code in v1.33.x-0.54bx. Future improvements should: + Note: Current implementation duplicates upstream code in v1.42.x-0.63bx. Future improvements should: 1. Propose refactoring upstream _patched_api_call into smaller components 2. Apply targeted patches to these components to reduce code duplication Reference: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/ - release/v1.33.x-0.54bx/instrumentation/opentelemetry-instrumentation-botocore/src/ - opentelemetry/instrumentation/botocore/__init__.py#L263 + release/v1.42.x-0.63bx/instrumentation/opentelemetry-instrumentation-botocore/src/ + opentelemetry/instrumentation/botocore/__init__.py """ if not is_instrumentation_enabled(): return original_func(*args, **kwargs) @@ -245,7 +244,7 @@ def patched_api_call(self, original_func, instance, args, kwargs): if call_context is None: return original_func(*args, **kwargs) - extension = _find_extension(call_context) + extension = self.extension_registry.get_extension(call_context) if not extension.should_trace_service_call(): return original_func(*args, **kwargs) @@ -268,11 +267,10 @@ def patched_api_call(self, original_func, instance, args, kwargs): _safe_invoke(extension.extract_attributes, attributes) end_span_on_exit = extension.should_end_span_on_exit() - tracer = self._get_tracer(extension) - meter = self._get_meter(extension) - metrics = self._get_metrics(extension, meter) + tracer = self.extension_registry.get_tracer(extension) + metrics = self.extension_registry.get_metrics(extension) instrumentor_ctx = _BotocoreInstrumentorContext( - logger=self._get_logger(extension), + logger=self.extension_registry.get_logger(extension), metrics=metrics, ) with tracer.start_as_current_span( diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 46798dbbc..da229ad54 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -34,7 +34,7 @@ GEN_AI_RUNTIME_ID, ) from opentelemetry.instrumentation.botocore import BotocoreInstrumentor -from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, bedrock_utils +from opentelemetry.instrumentation.botocore.extensions import _BOTOCORE_EXTENSIONS, bedrock_utils from opentelemetry.propagate import get_global_textmap from opentelemetry.semconv._incubating.attributes.aws_attributes import ( AWS_BEDROCK_GUARDRAIL_ID, @@ -178,55 +178,55 @@ def _run_patch_mechanism_tests(self): def _test_unpatched_botocore_instrumentation(self): # Kinesis - self.assertFalse("kinesis" in _KNOWN_EXTENSIONS, "Upstream has added a Kinesis extension") + self.assertFalse("kinesis" in _BOTOCORE_EXTENSIONS, "Upstream has added a Kinesis extension") # S3 - self.assertFalse("s3" in _KNOWN_EXTENSIONS, "Upstream has added a S3 extension") + self.assertFalse("s3" in _BOTOCORE_EXTENSIONS, "Upstream has added a S3 extension") # SQS - self.assertTrue("sqs" in _KNOWN_EXTENSIONS, "Upstream has removed the SQS extension") + self.assertTrue("sqs" in _BOTOCORE_EXTENSIONS, "Upstream has removed the SQS extension") # Bedrock - self.assertFalse("bedrock" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock extension") + self.assertFalse("bedrock" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock extension") # Bedrock Agent - self.assertFalse("bedrock-agent" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock Agent extension") + self.assertFalse("bedrock-agent" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock Agent extension") # Bedrock Agent Runtime self.assertFalse( - "bedrock-agent-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock Agent Runtime extension" + "bedrock-agent-runtime" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock Agent Runtime extension" ) # Bedrock AgentCore - self.assertFalse("bedrock-agentcore" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock AgentCore extension") + self.assertFalse("bedrock-agentcore" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock AgentCore extension") # Bedrock AgentCore Control self.assertFalse( - "bedrock-agentcore-control" in _KNOWN_EXTENSIONS, "Upstream has added a Bedrock AgentCore Control extension" + "bedrock-agentcore-control" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock AgentCore Control extension" ) # BedrockRuntime - self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS, "Upstream has added a bedrock-runtime extension") + self.assertTrue("bedrock-runtime" in _BOTOCORE_EXTENSIONS, "Upstream has added a bedrock-runtime extension") # SecretsManager - self.assertTrue("secretsmanager" in _KNOWN_EXTENSIONS, "Upstream has removed the SecretsManager extension") + self.assertTrue("secretsmanager" in _BOTOCORE_EXTENSIONS, "Upstream has removed the SecretsManager extension") # SNS - self.assertTrue("sns" in _KNOWN_EXTENSIONS, "Upstream has removed the SNS extension") + self.assertTrue("sns" in _BOTOCORE_EXTENSIONS, "Upstream has removed the SNS extension") # StepFunctions - self.assertTrue("stepfunctions" in _KNOWN_EXTENSIONS, "Upstream has removed the StepFunctions extension") + self.assertTrue("stepfunctions" in _BOTOCORE_EXTENSIONS, "Upstream has removed the StepFunctions extension") # Lambda - self.assertTrue("lambda" in _KNOWN_EXTENSIONS, "Upstream has removed the Lambda extension") + self.assertTrue("lambda" in _BOTOCORE_EXTENSIONS, "Upstream has removed the Lambda extension") # DynamoDB - self.assertTrue("dynamodb" in _KNOWN_EXTENSIONS, "Upstream has removed a DynamoDB extension") + self.assertTrue("dynamodb" in _BOTOCORE_EXTENSIONS, "Upstream has removed a DynamoDB extension") # pylint: disable=too-many-statements, too-many-locals def _test_patched_botocore_instrumentation(self): # Kinesis - self.assertTrue("kinesis" in _KNOWN_EXTENSIONS) + self.assertTrue("kinesis" in _BOTOCORE_EXTENSIONS) kinesis_attributes: Dict[str, str] = _do_extract_kinesis_attributes() self.assertTrue(AWS_KINESIS_STREAM_NAME in kinesis_attributes) self.assertEqual(kinesis_attributes[AWS_KINESIS_STREAM_NAME], _STREAM_NAME) @@ -234,13 +234,13 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(kinesis_attributes[AWS_KINESIS_STREAM_ARN], _STREAM_ARN) # S3 - self.assertTrue("s3" in _KNOWN_EXTENSIONS) + self.assertTrue("s3" in _BOTOCORE_EXTENSIONS) s3_attributes: Dict[str, str] = _do_extract_s3_attributes() self.assertTrue(AWS_S3_BUCKET in s3_attributes) self.assertEqual(s3_attributes[AWS_S3_BUCKET], _BUCKET_NAME) # SQS - self.assertTrue("sqs" in _KNOWN_EXTENSIONS) + self.assertTrue("sqs" in _BOTOCORE_EXTENSIONS) sqs_attributes: Dict[str, str] = _do_extract_sqs_attributes() self.assertTrue(AWS_SQS_QUEUE_URL in sqs_attributes) self.assertEqual(sqs_attributes[AWS_SQS_QUEUE_URL], _QUEUE_URL) @@ -264,7 +264,7 @@ def _test_patched_botocore_instrumentation(self): self._test_patched_from_converse_with_malformed_response() # Bedrock Agent Runtime - self.assertTrue("bedrock-agent-runtime" in _KNOWN_EXTENSIONS) + self.assertTrue("bedrock-agent-runtime" in _BOTOCORE_EXTENSIONS) bedrock_agent_runtime_attributes: Dict[str, str] = _do_extract_attributes_bedrock("bedrock-agent-runtime") self.assertEqual(len(bedrock_agent_runtime_attributes), 2) self.assertEqual(bedrock_agent_runtime_attributes[AWS_BEDROCK_AGENT_ID], _BEDROCK_AGENT_ID) @@ -273,8 +273,8 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(len(bedrock_agent_runtime_sucess_attributes), 0) # Bedrock AgentCore - self.assertTrue("bedrock-agentcore" in _KNOWN_EXTENSIONS) - self.assertTrue("bedrock-agentcore-control" in _KNOWN_EXTENSIONS) + self.assertTrue("bedrock-agentcore" in _BOTOCORE_EXTENSIONS) + self.assertTrue("bedrock-agentcore-control" in _BOTOCORE_EXTENSIONS) _do_extract_bedrock_agentcore_attributes, _do_on_success_bedrock_agentcore = _do_bedrock_agentcore_tests() bedrock_agentcore_attributes: Dict[str, str] = _do_extract_bedrock_agentcore_attributes() @@ -314,10 +314,10 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(name_success_attrs[AWS_AUTH_CREDENTIAL_PROVIDER], _AGENTCORE_CREDENTIAL_PROVIDER_NAME) # BedrockRuntime - self.assertTrue("bedrock-runtime" in _KNOWN_EXTENSIONS) + self.assertTrue("bedrock-runtime" in _BOTOCORE_EXTENSIONS) # SecretsManager - self.assertTrue("secretsmanager" in _KNOWN_EXTENSIONS) + self.assertTrue("secretsmanager" in _BOTOCORE_EXTENSIONS) secretsmanager_attributes: Dict[str, str] = _do_extract_secretsmanager_attributes() self.assertTrue(AWS_SECRETSMANAGER_SECRET_ARN in secretsmanager_attributes) self.assertEqual(secretsmanager_attributes[AWS_SECRETSMANAGER_SECRET_ARN], _SECRET_ARN) @@ -326,13 +326,13 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(secretsmanager_success_attributes[AWS_SECRETSMANAGER_SECRET_ARN], _SECRET_ARN) # SNS - self.assertTrue("sns" in _KNOWN_EXTENSIONS) + self.assertTrue("sns" in _BOTOCORE_EXTENSIONS) sns_attributes: Dict[str, str] = _do_extract_sns_attributes() self.assertTrue(AWS_SNS_TOPIC_ARN in sns_attributes) self.assertEqual(sns_attributes[AWS_SNS_TOPIC_ARN], _TOPIC_ARN) # StepFunctions - self.assertTrue("stepfunctions" in _KNOWN_EXTENSIONS) + self.assertTrue("stepfunctions" in _BOTOCORE_EXTENSIONS) stepfunctions_attributes: Dict[str, str] = _do_extract_stepfunctions_attributes() self.assertTrue(AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN in stepfunctions_attributes) self.assertEqual(stepfunctions_attributes[AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN], _STATE_MACHINE_ARN) @@ -340,7 +340,7 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(stepfunctions_attributes[AWS_STEP_FUNCTIONS_ACTIVITY_ARN], _ACTIVITY_ARN) # Lambda - self.assertTrue("lambda" in _KNOWN_EXTENSIONS) + self.assertTrue("lambda" in _BOTOCORE_EXTENSIONS) lambda_attributes: Dict[str, str] = _do_extract_lambda_attributes() self.assertTrue(AWS_LAMBDA_FUNCTION_NAME in lambda_attributes) self.assertEqual(lambda_attributes[AWS_LAMBDA_FUNCTION_NAME], _LAMBDA_FUNCTION_NAME) @@ -348,7 +348,7 @@ def _test_patched_botocore_instrumentation(self): self.assertEqual(lambda_attributes[AWS_LAMBDA_RESOURCE_MAPPING_ID], _LAMBDA_SOURCE_MAPPING_ID) # DynamoDB - self.assertTrue("dynamodb" in _KNOWN_EXTENSIONS) + self.assertTrue("dynamodb" in _BOTOCORE_EXTENSIONS) dynamodb_success_attributes: Dict[str, str] = _do_on_success_dynamodb() self.assertTrue(AWS_DYNAMODB_TABLE_ARN in dynamodb_success_attributes) self.assertEqual(dynamodb_success_attributes[AWS_DYNAMODB_TABLE_ARN], _TABLE_ARN) @@ -388,16 +388,22 @@ def mock_start_span(*args, **kwargs): with patch( "opentelemetry.instrumentation.botocore._determine_call_context", return_value=mock_call_context - ), patch("opentelemetry.instrumentation.botocore._find_extension", return_value=mock_extension), patch( + ), patch( + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_extension", + return_value=mock_extension, + ), patch( "opentelemetry.instrumentation.botocore.is_instrumentation_enabled", return_value=True ), patch( "amazon.opentelemetry.distro.patches._botocore_patches.get_server_attributes", return_value={} ), patch( - "opentelemetry.instrumentation.botocore.get_tracer", return_value=mock_tracer + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_tracer", + return_value=mock_tracer, ), patch( - "opentelemetry.instrumentation.botocore.get_logger", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_logger", + return_value=MagicMock(), ), patch( - "opentelemetry.instrumentation.botocore.get_meter", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_metrics", + return_value={}, ): instrumentor = BotocoreInstrumentor() instrumentor.instrument() @@ -436,16 +442,22 @@ def mock_start_span(*args, **kwargs): with patch( "opentelemetry.instrumentation.botocore._determine_call_context", return_value=mock_call_context - ), patch("opentelemetry.instrumentation.botocore._find_extension", return_value=mock_extension), patch( + ), patch( + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_extension", + return_value=mock_extension, + ), patch( "opentelemetry.instrumentation.botocore.is_instrumentation_enabled", return_value=True ), patch( "amazon.opentelemetry.distro.patches._botocore_patches.get_server_attributes", return_value={} ), patch( - "opentelemetry.instrumentation.botocore.get_tracer", return_value=mock_tracer + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_tracer", + return_value=mock_tracer, ), patch( - "opentelemetry.instrumentation.botocore.get_logger", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_logger", + return_value=MagicMock(), ), patch( - "opentelemetry.instrumentation.botocore.get_meter", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_metrics", + return_value={}, ): instrumentor = BotocoreInstrumentor() instrumentor.instrument() @@ -484,16 +496,22 @@ def mock_start_span(*args, **kwargs): with patch( "opentelemetry.instrumentation.botocore._determine_call_context", return_value=mock_call_context - ), patch("opentelemetry.instrumentation.botocore._find_extension", return_value=mock_extension), patch( + ), patch( + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_extension", + return_value=mock_extension, + ), patch( "opentelemetry.instrumentation.botocore.is_instrumentation_enabled", return_value=True ), patch( "amazon.opentelemetry.distro.patches._botocore_patches.get_server_attributes", return_value={} ), patch( - "opentelemetry.instrumentation.botocore.get_tracer", return_value=mock_tracer + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_tracer", + return_value=mock_tracer, ), patch( - "opentelemetry.instrumentation.botocore.get_logger", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_logger", + return_value=MagicMock(), ), patch( - "opentelemetry.instrumentation.botocore.get_meter", return_value=MagicMock() + "opentelemetry.instrumentation.botocore.extensions.registry.ExtensionRegistry.get_metrics", + return_value={}, ): instrumentor = BotocoreInstrumentor() instrumentor.instrument() @@ -745,7 +763,7 @@ def _test_process_anthropic_claude_chunk( 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.""" - self.assertTrue("bedrock-agent" in _KNOWN_EXTENSIONS) + self.assertTrue("bedrock-agent" in _BOTOCORE_EXTENSIONS) operation_to_expected_attribute = { "CreateAgentActionGroup": (AWS_BEDROCK_AGENT_ID, _BEDROCK_AGENT_ID), "CreateAgentAlias": (AWS_BEDROCK_AGENT_ID, _BEDROCK_AGENT_ID), @@ -1286,7 +1304,7 @@ def _do_extract_attributes(service_name: str, params: Dict[str, Any], operation: if operation: mock_call_context.operation = operation attributes: Dict[str, str] = {} - sqs_extension = _KNOWN_EXTENSIONS[service_name]()(mock_call_context) + sqs_extension = _BOTOCORE_EXTENSIONS[service_name]()(mock_call_context) sqs_extension.extract_attributes(attributes) return attributes @@ -1316,7 +1334,7 @@ def set_side_effect(set_key, set_value): if params: mock_call_context.params = params - extension = _KNOWN_EXTENSIONS[service_name]()(mock_call_context) + extension = _BOTOCORE_EXTENSIONS[service_name]()(mock_call_context) extension.on_success(span_mock, result, mock_instrumentor_context) return span_attributes From 40f647803152bdab75c7d2b8da938bf0acf4c47a Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 09:37:30 -0700 Subject: [PATCH 08/15] chore(starlette): drop ASGI suppress_http_instrumentation patch Contrib 0.62b0 merged opentelemetry-python-contrib#4375 which makes the ASGI middleware respect the suppress_http_instrumentation context natively to skip server span creation when HTTP instrumentation is suppressed. The ADOT patch carried a TODO pointing to that PR and is now upstream behavior. Remove the patched __call__ method and the matching test. The exclude_receive_span/exclude_send_span patch above is kept since its tracking issue (#3725) is still open. --- .../distro/patches/_starlette_patches.py | 12 ---- .../distro/patches/test_starlette_patches.py | 65 ------------------- 2 files changed, 77 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_starlette_patches.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_starlette_patches.py index bf6c5f58d..ac35d8b2c 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_starlette_patches.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_starlette_patches.py @@ -16,7 +16,6 @@ def _apply_starlette_instrumentation_patches() -> None: try: # pylint: disable=import-outside-toplevel from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware - from opentelemetry.instrumentation.utils import is_http_instrumentation_enabled # pylint: disable=line-too-long # Patch to exclude http receive/send ASGI event spans from Bedrock AgentCore, @@ -37,17 +36,6 @@ def patched_init(self, app, **kwargs): OpenTelemetryMiddleware.__init__ = patched_init - # TODO: Remove this patch once upstream PR is merged: - # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4375 - original_call = OpenTelemetryMiddleware.__call__ - - async def patched_call(self, scope, receive, send): - if not is_http_instrumentation_enabled(): - return await self.app(scope, receive, send) - return await original_call(self, scope, receive, send) - - OpenTelemetryMiddleware.__call__ = patched_call - _logger.debug("Successfully patched Starlette ASGI middleware") except Exception as exc: # pylint: disable=broad-except _logger.warning("Failed to apply Starlette instrumentation patches: %s", exc) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py index d90a31457..3b1aaf3d4 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py @@ -1,6 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -import asyncio from unittest import TestCase from unittest.mock import MagicMock, patch @@ -8,11 +7,6 @@ _apply_starlette_code_attributes_patch, _apply_starlette_instrumentation_patches, ) -from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware -from opentelemetry.instrumentation.utils import suppress_http_instrumentation -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter class TestStarlettePatch(TestCase): @@ -82,65 +76,6 @@ def test_starlette_patch_handles_import_error(self, mock_logger): args = mock_logger.warning.call_args[0] self.assertIn("Failed to apply Starlette instrumentation patches", args[0]) - def test_asgi_suppress_http_instrumentation(self): - original_call = OpenTelemetryMiddleware.__call__ - - async def simple_asgi(scope, receive, send): - await send({"type": "http.response.start", "status": 200, "headers": []}) - await send({"type": "http.response.body", "body": b""}) - - scope = { - "type": "http", - "method": "GET", - "path": "/", - "query_string": b"", - "headers": [], - "server": ("127.0.0.1", 80), - "scheme": "http", - "http_version": "1.0", - "client": ("127.0.0.1", 32767), - } - - for case, apply_patch, expect_spans in [ - ("unpatched", False, True), - ("patched", True, False), - ]: - with self.subTest(case=case): - OpenTelemetryMiddleware.__call__ = original_call - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - - if apply_patch: - with patch.dict("os.environ", {"AGENT_OBSERVABILITY_ENABLED": "true"}, clear=False): - _apply_starlette_instrumentation_patches() - - async def run(): - app = OpenTelemetryMiddleware(simple_asgi, tracer_provider=provider) - messages = [{"type": "http.request", "body": b""}] - - async def receive(): - return messages.pop(0) - - sent = [] - - async def send(msg): - sent.append(msg) - - with suppress_http_instrumentation(): - await app(scope, receive, send) - - asyncio.run(run()) - spans = exporter.get_finished_spans() - - if expect_spans: - self.assertGreater(len(spans), 0) - else: - self.assertEqual(len(spans), 0) - - OpenTelemetryMiddleware.__call__ = original_call - - class TestStarletteCodeAttributesPatch(TestCase): """Test the Starlette code attributes instrumentation patches using real Route class.""" From 3b9bb0d21bb2c6d479f09e7266253513b327845b Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 09:40:29 -0700 Subject: [PATCH 09/15] test(starlette): keep suppress_http_instrumentation test as upstream regression check Restore the test removed in 40f6478. Behavior under suppress_http_instrumentation is now provided natively by contrib 0.62b0+ so the unpatched case expects no spans (the only flip vs. the original). --- .../distro/patches/test_starlette_patches.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py index 3b1aaf3d4..8181637c6 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py @@ -1,5 +1,6 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import asyncio from unittest import TestCase from unittest.mock import MagicMock, patch @@ -7,6 +8,11 @@ _apply_starlette_code_attributes_patch, _apply_starlette_instrumentation_patches, ) +from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.utils import suppress_http_instrumentation +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter class TestStarlettePatch(TestCase): @@ -76,6 +82,65 @@ def test_starlette_patch_handles_import_error(self, mock_logger): args = mock_logger.warning.call_args[0] self.assertIn("Failed to apply Starlette instrumentation patches", args[0]) + def test_asgi_suppress_http_instrumentation(self): + original_call = OpenTelemetryMiddleware.__call__ + + async def simple_asgi(scope, receive, send): + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b""}) + + scope = { + "type": "http", + "method": "GET", + "path": "/", + "query_string": b"", + "headers": [], + "server": ("127.0.0.1", 80), + "scheme": "http", + "http_version": "1.0", + "client": ("127.0.0.1", 32767), + } + + for case, apply_patch, expect_spans in [ + ("unpatched", False, False), + ("patched", True, False), + ]: + with self.subTest(case=case): + OpenTelemetryMiddleware.__call__ = original_call + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + if apply_patch: + with patch.dict("os.environ", {"AGENT_OBSERVABILITY_ENABLED": "true"}, clear=False): + _apply_starlette_instrumentation_patches() + + async def run(): + app = OpenTelemetryMiddleware(simple_asgi, tracer_provider=provider) + messages = [{"type": "http.request", "body": b""}] + + async def receive(): + return messages.pop(0) + + sent = [] + + async def send(msg): + sent.append(msg) + + with suppress_http_instrumentation(): + await app(scope, receive, send) + + asyncio.run(run()) + spans = exporter.get_finished_spans() + + if expect_spans: + self.assertGreater(len(spans), 0) + else: + self.assertEqual(len(spans), 0) + + OpenTelemetryMiddleware.__call__ = original_call + + class TestStarletteCodeAttributesPatch(TestCase): """Test the Starlette code attributes instrumentation patches using real Route class.""" From 807e92b07a48613c50c11d6ba614b6b57918d233 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 10:48:27 -0700 Subject: [PATCH 10/15] test: adapt tests to upstream 0.63b1 API changes Three small test-only fixes for behavior changes in the upgraded opentelemetry libs: - BaggageSpanProcessor renamed its internal _baggage_key_predicate attribute to _predicates (now a list of callables). Replace direct attr access with any(p(key) for p in ._predicates). - opentelemetry.attributes._VALID_ANY_VALUE_TYPES now references collections.abc.{Mapping,Sequence} instead of typing.{Mapping,Sequence}. - Black reformatting on the long multi-arg assertFalse() call in test_instrumentation_patch.py (lint check fix). --- .../distro/patches/test_instrumentation_patch.py | 3 ++- .../exporter/test_otlp_attribute_vs_body_types.py | 6 +++--- .../distro/test_aws_opentelementry_configurator.py | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index da229ad54..78a2e7aed 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -202,7 +202,8 @@ def _test_unpatched_botocore_instrumentation(self): # Bedrock AgentCore Control self.assertFalse( - "bedrock-agentcore-control" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock AgentCore Control extension" + "bedrock-agentcore-control" in _BOTOCORE_EXTENSIONS, + "Upstream has added a Bedrock AgentCore Control extension", ) # BedrockRuntime diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/serviceevents/exporter/test_otlp_attribute_vs_body_types.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/serviceevents/exporter/test_otlp_attribute_vs_body_types.py index 781d3bfdc..8b4c133b2 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/serviceevents/exporter/test_otlp_attribute_vs_body_types.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/serviceevents/exporter/test_otlp_attribute_vs_body_types.py @@ -367,13 +367,13 @@ def test_valid_attr_types_are_primitives_only(self): def test_valid_anyvalue_types_include_mapping(self): """_VALID_ANY_VALUE_TYPES includes Mapping (dict) — Body can hold dicts.""" - import typing + import collections.abc import opentelemetry.attributes as attrs valid = attrs._VALID_ANY_VALUE_TYPES - self.assertIn(typing.Mapping, valid) - self.assertIn(typing.Sequence, valid) + self.assertIn(collections.abc.Mapping, valid) + self.assertIn(collections.abc.Sequence, valid) self.assertIn(str, valid) self.assertIn(int, valid) 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 f8d0674fb..cfd9cd947 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 @@ -629,7 +629,7 @@ def test_baggage_span_processor_session_id_xray_endpoint(self): processors = tracer_provider._active_span_processor._span_processors baggage_processors = [p for p in processors if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = baggage_processors[0]._baggage_key_predicate + predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) self.assertTrue(predicate("session.id")) self.assertFalse(predicate("user.id")) @@ -647,7 +647,7 @@ def test_baggage_span_processor_custom_keys(self): processors = tracer_provider._active_span_processor._span_processors baggage_processors = [p for p in processors if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = baggage_processors[0]._baggage_key_predicate + predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) self.assertTrue(predicate("user.id")) self.assertTrue(predicate("request.id")) self.assertTrue(predicate("session.id")) @@ -667,7 +667,7 @@ def test_baggage_span_processor_rejects_keys_without_custom_config(self): added = [c.args[0] for c in mock_tracer_provider.add_span_processor.call_args_list] baggage_processors = [p for p in added if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = baggage_processors[0]._baggage_key_predicate + predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) self.assertFalse(predicate("any.key")) os.environ.pop("AGENT_OBSERVABILITY_ENABLED", None) @@ -684,7 +684,7 @@ def test_session_id_always_added_when_agent_observability_enabled(self): processors = tracer_provider._active_span_processor._span_processors baggage_processors = [p for p in processors if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - self.assertTrue(baggage_processors[0]._baggage_key_predicate("session.id")) + self.assertTrue(any(p("session.id") for p in baggage_processors[0]._predicates)) # With custom baggage keys, session.id should still be present alongside them os.environ["OTEL_BAGGAGE_SPAN_ATTRIBUTE_KEYS"] = "custom.key" @@ -692,7 +692,7 @@ def test_session_id_always_added_when_agent_observability_enabled(self): _customize_span_processors(tracer_provider2, Resource.get_empty(), MagicMock()) processors2 = tracer_provider2._active_span_processor._span_processors baggage_processors2 = [p for p in processors2 if p.__class__.__name__ == "BaggageSpanProcessor"] - predicate2 = baggage_processors2[0]._baggage_key_predicate + predicate2 = (lambda key: any(p(key) for p in baggage_processors2[0]._predicates)) self.assertTrue(predicate2("session.id")) self.assertTrue(predicate2("custom.key")) From 3ce07b12a8e8f4aa9ea5a8483d33a61292eb4f7a Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 11:00:56 -0700 Subject: [PATCH 11/15] style: apply black/isort/flake8 formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Black collapsed the redundant parens around four lambda assignments in test_baggage_span_processor_* — add noqa: E731 since the helper is purely local and the inline form is clearest in context. Isort moves the now-stdlib importlib.metadata import out of the third-party block. Black also re-wraps the multi-arg assertFalse in test_instrumentation_patch.py. --- .../distro/aws_opentelemetry_configurator.py | 2 +- .../distro/patches/test_instrumentation_patch.py | 4 +++- .../distro/test_aws_opentelementry_configurator.py | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py index f4526b5e4..c4f24d409 100644 --- a/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py +++ b/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py @@ -7,12 +7,12 @@ import logging import os import re +from importlib.metadata import version from logging import Logger, getLogger from pathlib import Path from typing import ClassVar, Dict, List, NamedTuple, Optional, Type, Union import yaml -from importlib.metadata import version from typing_extensions import override from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE, AWS_SERVICE_TYPE diff --git a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py index 78a2e7aed..49bf411a5 100644 --- a/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py +++ b/aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py @@ -198,7 +198,9 @@ def _test_unpatched_botocore_instrumentation(self): ) # Bedrock AgentCore - self.assertFalse("bedrock-agentcore" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock AgentCore extension") + self.assertFalse( + "bedrock-agentcore" in _BOTOCORE_EXTENSIONS, "Upstream has added a Bedrock AgentCore extension" + ) # Bedrock AgentCore Control self.assertFalse( 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 cfd9cd947..b094bf65b 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 @@ -629,7 +629,7 @@ def test_baggage_span_processor_session_id_xray_endpoint(self): processors = tracer_provider._active_span_processor._span_processors baggage_processors = [p for p in processors if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) + predicate = lambda key: any(p(key) for p in baggage_processors[0]._predicates) # noqa: E731 self.assertTrue(predicate("session.id")) self.assertFalse(predicate("user.id")) @@ -647,7 +647,7 @@ def test_baggage_span_processor_custom_keys(self): processors = tracer_provider._active_span_processor._span_processors baggage_processors = [p for p in processors if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) + predicate = lambda key: any(p(key) for p in baggage_processors[0]._predicates) # noqa: E731 self.assertTrue(predicate("user.id")) self.assertTrue(predicate("request.id")) self.assertTrue(predicate("session.id")) @@ -667,7 +667,7 @@ def test_baggage_span_processor_rejects_keys_without_custom_config(self): added = [c.args[0] for c in mock_tracer_provider.add_span_processor.call_args_list] baggage_processors = [p for p in added if p.__class__.__name__ == "BaggageSpanProcessor"] self.assertEqual(len(baggage_processors), 1) - predicate = (lambda key: any(p(key) for p in baggage_processors[0]._predicates)) + predicate = lambda key: any(p(key) for p in baggage_processors[0]._predicates) # noqa: E731 self.assertFalse(predicate("any.key")) os.environ.pop("AGENT_OBSERVABILITY_ENABLED", None) @@ -692,7 +692,7 @@ def test_session_id_always_added_when_agent_observability_enabled(self): _customize_span_processors(tracer_provider2, Resource.get_empty(), MagicMock()) processors2 = tracer_provider2._active_span_processor._span_processors baggage_processors2 = [p for p in processors2 if p.__class__.__name__ == "BaggageSpanProcessor"] - predicate2 = (lambda key: any(p(key) for p in baggage_processors2[0]._predicates)) + predicate2 = lambda key: any(p(key) for p in baggage_processors2[0]._predicates) # noqa: E731 self.assertTrue(predicate2("session.id")) self.assertTrue(predicate2("custom.key")) From 1c9cb7f9311a38823150c251de1e7327eaeaffe9 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 13:09:34 -0700 Subject: [PATCH 12/15] fix(contract-tests): bump mysqlclient to 2.2.8 so db.name is populated mysqlclient 2.2.7 (PyMySQL/mysqlclient#752, 2025-01) added Python-level self.db / self.database attributes in Connection.__init__ for OTel instrumentation to read. The contract test was pinned at 2.2.4 where those attributes didn't exist, so dbapi's getattr(conn, "db") returned None and the upstream dbapi instrumentation's empty-string default landed on the span. The 0.62b0 dbapi semantic-convention migration (open-telemetry/opentelemetry-python-contrib#4109) added an `if value:` filter in _set_db_name, so the empty string is now dropped entirely and the test's `db.name == ""` assertion fails with "key not found". Bump the contract-test pin to 2.2.8 (current latest) so conn.db returns the actual database name, and update the assertion to expect DATABASE_NAME instead of the empty placeholder. Verified locally against MySQL 8: mysqlclient 2.2.4 -> conn.db is None, mysqlclient 2.2.7+ -> conn.db == "testdb". --- contract-tests/images/applications/mysqlclient/requirements.txt | 2 +- .../tests/test/amazon/mysqlclient/mysqlclient_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contract-tests/images/applications/mysqlclient/requirements.txt b/contract-tests/images/applications/mysqlclient/requirements.txt index 53d9a31ba..ab4e24373 100644 --- a/contract-tests/images/applications/mysqlclient/requirements.txt +++ b/contract-tests/images/applications/mysqlclient/requirements.txt @@ -1,2 +1,2 @@ typing-extensions==4.15.0 -mysqlclient==2.2.4 +mysqlclient==2.2.8 diff --git a/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py index b745c7de8..7a6a62fc2 100644 --- a/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py +++ b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py @@ -94,7 +94,7 @@ def _assert_semantic_conventions_attributes(self, attributes_list: List[KeyValue attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) self.assertTrue(attributes_dict.get("db.statement").string_value.startswith(command)) self._assert_str_attribute(attributes_dict, "db.system", self.get_remote_service()) - self._assert_str_attribute(attributes_dict, "db.name", "") + self._assert_str_attribute(attributes_dict, "db.name", DATABASE_NAME) self.assertTrue("net.peer.name" not in attributes_dict) self._assert_int_attribute(attributes_dict, "net.peer.port", self.get_database_port()) self.assertTrue("server.address" not in attributes_dict) From aaa9d8041d0e532de228a477ac8c9487cf670b50 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 13:50:57 -0700 Subject: [PATCH 13/15] test(mysqlclient): drop overrides; mysqlclient 2.2.8 spans now match base With mysqlclient 2.2.4, conn.host/conn.user/conn.db were not exposed as Python attributes, so OTel's dbapi instrumentation produced skinny spans missing net.peer.name / db.name / db user. The test override worked around those gaps with negated assertions. mysqlclient 2.2.7 (PyMySQL/mysqlclient#752) added Python-level Connection attributes, so spans now carry net.peer.name and db.name. ADOT's _get_db_connection sees an address and emits the full RemoteResourceType="DB::Connection" + RemoteResourceIdentifier set, matching what the base DatabaseContractTestBase already asserts. Drop the override methods entirely and let the base assertions run. --- .../amazon/mysqlclient/mysqlclient_test.py | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py index 7a6a62fc2..ab3193a33 100644 --- a/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py +++ b/contract-tests/tests/test/amazon/mysqlclient/mysqlclient_test.py @@ -1,7 +1,5 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List - from testcontainers.mysql import MySqlContainer from typing_extensions import override @@ -11,19 +9,8 @@ DATABASE_NAME, DATABASE_PASSWORD, DATABASE_USER, - SPAN_KIND_LOCAL_ROOT, DatabaseContractTestBase, ) -from amazon.utils.application_signals_constants import ( - AWS_LOCAL_OPERATION, - AWS_LOCAL_SERVICE, - AWS_REMOTE_OPERATION, - AWS_REMOTE_RESOURCE_IDENTIFIER, - AWS_REMOTE_RESOURCE_TYPE, - AWS_REMOTE_SERVICE, - AWS_SPAN_KIND, -) -from opentelemetry.proto.common.v1.common_pb2 import AnyValue, KeyValue class MysqlClientTest(DatabaseContractTestBase): @@ -68,35 +55,3 @@ def test_create_database_succeeds(self) -> None: def test_fault(self) -> None: self.assert_fault() - - # This adapter is not currently fully supported by OTEL - # GitHub issue: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1319 - # TODO: Once the adapter is supported, we could remove _assert_aws_attributes and - # _assert_semantic_conventions_attributes methods from this class. - @override - def _assert_aws_attributes( - self, attributes_list: List[KeyValue], expected_span_kind: str = SPAN_KIND_LOCAL_ROOT, **kwargs - ) -> None: - attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) - self._assert_str_attribute(attributes_dict, AWS_LOCAL_SERVICE, self.get_application_otel_service_name()) - # InternalOperation as OTEL does not instrument the basic server we are using, so the client span is a local - # root. - self._assert_str_attribute(attributes_dict, AWS_LOCAL_OPERATION, "InternalOperation") - self._assert_str_attribute(attributes_dict, AWS_REMOTE_SERVICE, self.get_remote_service()) - self._assert_str_attribute(attributes_dict, AWS_REMOTE_OPERATION, kwargs.get("sql_command")) - self.assertTrue(AWS_REMOTE_RESOURCE_TYPE not in attributes_dict) - self.assertTrue(AWS_REMOTE_RESOURCE_IDENTIFIER not in attributes_dict) - # See comment above AWS_LOCAL_OPERATION - self._assert_str_attribute(attributes_dict, AWS_SPAN_KIND, expected_span_kind) - - @override - def _assert_semantic_conventions_attributes(self, attributes_list: List[KeyValue], command: str) -> None: - attributes_dict: Dict[str, AnyValue] = self._get_attributes_dict(attributes_list) - self.assertTrue(attributes_dict.get("db.statement").string_value.startswith(command)) - self._assert_str_attribute(attributes_dict, "db.system", self.get_remote_service()) - self._assert_str_attribute(attributes_dict, "db.name", DATABASE_NAME) - self.assertTrue("net.peer.name" not in attributes_dict) - self._assert_int_attribute(attributes_dict, "net.peer.port", self.get_database_port()) - self.assertTrue("server.address" not in attributes_dict) - self.assertTrue("server.port" not in attributes_dict) - self.assertTrue("db.operation" not in attributes_dict) From d6cb052ed9de4f1107ee6606d21c49d08ce155b4 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 15:50:54 -0700 Subject: [PATCH 14/15] test(debugger): align DI hit-limit tests with SDK MaxHits semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK's increment_hit_count uses `hit_count > config.max_hits` for the disable check (instrumentation_manager.py:631), so MaxHits=N emits N snapshots and disables on the (N+1)th hit. The DI Flask, FastAPI, and Django hit-limit contract tests expected N-1 snapshots, claiming the check was `>=` in their docstrings and inline comments — neither matched the SDK. The tests were passing on main by accident: BatchLogRecordProcessor's default schedule_delay_millis was 5000ms, so the snapshot emitted on the 3rd hit was still sitting in the batch buffer when the test's 2-second sleep elapsed and read the collector. core 1.41.0 tightened the default to 1000ms (open-telemetry/opentelemetry-python#4998), shrinking the window enough that the 3rd snapshot now lands before the test reads. Fix the tests (and the docstrings on the application's target functions) to match SDK behavior: MaxHits=3 -> 3 snapshots emitted, breakpoint disables on the 4th hit. Send a 4th request to actually probe the disabled state, and widen the disabled-state observation window to 5s so any spurious 4th snapshot has plenty of time to flush before we assert the breakpoint stayed disabled. --- .../di-fastapi/di_fastapi_server.py | 4 +-- .../applications/di-flask/di_flask_server.py | 4 +-- .../tests/test/amazon/di/django_test.py | 19 +++++++------ .../tests/test/amazon/di/fastapi_test.py | 28 ++++++++++--------- .../tests/test/amazon/di/flask_test.py | 28 ++++++++++--------- 5 files changed, 44 insertions(+), 39 deletions(-) diff --git a/contract-tests/images/applications/di-fastapi/di_fastapi_server.py b/contract-tests/images/applications/di-fastapi/di_fastapi_server.py index e73580d10..91ab7e0ea 100644 --- a/contract-tests/images/applications/di-fastapi/di_fastapi_server.py +++ b/contract-tests/images/applications/di-fastapi/di_fastapi_server.py @@ -62,8 +62,8 @@ def calculate_sum(a, b): def limited_function(x): """Target function with hit limit (MaxHits=3). - MaxHits=3 means: allow 2 snapshots, disable at 3rd hit. - (The check is hit_count >= max_hits, so hit 1&2 pass, hit 3 is blocked.) + MaxHits=3 means: allow 3 snapshots, disable on the 4th hit. + (The check is hit_count > max_hits, so hits 1-3 pass, hit 4 is blocked.) """ return x * 10 diff --git a/contract-tests/images/applications/di-flask/di_flask_server.py b/contract-tests/images/applications/di-flask/di_flask_server.py index f3f3bcb54..b61896169 100644 --- a/contract-tests/images/applications/di-flask/di_flask_server.py +++ b/contract-tests/images/applications/di-flask/di_flask_server.py @@ -55,8 +55,8 @@ def calculate_sum(a, b): def limited_function(x): """Target function with hit limit (MaxHits=3). - MaxHits=3 means: allow 2 snapshots, disable at 3rd hit. - (The check is hit_count >= max_hits, so hit 1&2 pass, hit 3 is blocked.) + MaxHits=3 means: allow 3 snapshots, disable on the 4th hit. + (The check is hit_count > max_hits, so hits 1-3 pass, hit 4 is blocked.) """ return x * 10 diff --git a/contract-tests/tests/test/amazon/di/django_test.py b/contract-tests/tests/test/amazon/di/django_test.py index f2803da9b..03a644317 100644 --- a/contract-tests/tests/test/amazon/di/django_test.py +++ b/contract-tests/tests/test/amazon/di/django_test.py @@ -273,33 +273,34 @@ def get_application_wait_pattern(self) -> str: return "Ready" def test_breakpoint_generates_snapshots_up_to_limit(self) -> None: - """BREAKPOINT generates snapshots up to (max_hits - 1).""" + """BREAKPOINT generates a snapshot for each hit up to and including max_hits.""" + self.send_request("GET", "limited") self.send_request("GET", "limited") self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) limited_logs = self.logs_for_method(logs, "limited_function") - self.assertEqual(len(limited_logs), 2, "Expected exactly 2 snapshots (MaxHits=3 allows 2)") + self.assertEqual(len(limited_logs), 3, "Expected exactly 3 snapshots (MaxHits=3 allows 3)") def test_breakpoint_disabled_after_hit_limit(self) -> None: """BREAKPOINT stops generating snapshots after hit limit is reached.""" self.send_request("GET", "limited") self.send_request("GET", "limited") + self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) initial_count = len(self.logs_for_method(logs, "limited_function")) - self.assertEqual(initial_count, 2) + self.assertEqual(initial_count, 3) self.send_request("GET", "limited") - self.send_request("GET", "limited") - time.sleep(2) + time.sleep(5) final_logs = self._peek_snapshots() final_count = len(self.logs_for_method(final_logs, "limited_function")) self.assertEqual( final_count, - 2, - f"Expected 2 snapshots (MaxHits=3), but got {final_count}. " + 3, + f"Expected 3 snapshots (MaxHits=3), but got {final_count}. " "BREAKPOINT should be disabled after hitting limit.", ) diff --git a/contract-tests/tests/test/amazon/di/fastapi_test.py b/contract-tests/tests/test/amazon/di/fastapi_test.py index dbc80296e..f6628b38b 100644 --- a/contract-tests/tests/test/amazon/di/fastapi_test.py +++ b/contract-tests/tests/test/amazon/di/fastapi_test.py @@ -278,7 +278,8 @@ class DIFastAPIHitLimitTest(DITestInfrastructure): """Test BREAKPOINT hit limit behavior. BREAKPOINTs have a max_hits limit. With MaxHits=3, the check is - hit_count >= max_hits, so hits 1 and 2 generate snapshots, hit 3 is blocked. + hit_count > max_hits, so hits 1, 2, and 3 generate snapshots and the + breakpoint is disabled on hit 4. """ __test__ = True @@ -293,36 +294,37 @@ def get_application_wait_pattern(self) -> str: return "Ready" def test_breakpoint_generates_snapshots_up_to_limit(self) -> None: - """BREAKPOINT generates snapshots up to (max_hits - 1).""" - # limited_function has MaxHits=3, so 2 snapshots should be generated + """BREAKPOINT generates a snapshot for each hit up to and including max_hits.""" + # limited_function has MaxHits=3, so 3 snapshots should be generated + self.send_request("GET", "limited") self.send_request("GET", "limited") self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) limited_logs = self.logs_for_method(logs, "limited_function") - self.assertEqual(len(limited_logs), 2, "Expected exactly 2 snapshots (MaxHits=3 allows 2)") + self.assertEqual(len(limited_logs), 3, "Expected exactly 3 snapshots (MaxHits=3 allows 3)") def test_breakpoint_disabled_after_hit_limit(self) -> None: """BREAKPOINT stops generating snapshots after hit limit is reached.""" - # First 2 calls generate snapshots (hits 1 and 2) + # First 3 calls generate snapshots (hits 1, 2, and 3) + self.send_request("GET", "limited") self.send_request("GET", "limited") self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) initial_count = len(self.logs_for_method(logs, "limited_function")) - self.assertEqual(initial_count, 2) + self.assertEqual(initial_count, 3) - # 3rd and 4th calls hit the limit -- no new snapshots - self.send_request("GET", "limited") + # 4th call hits the limit -- no new snapshot self.send_request("GET", "limited") - time.sleep(2) + time.sleep(5) final_logs = self._peek_snapshots() final_count = len(self.logs_for_method(final_logs, "limited_function")) self.assertEqual( final_count, - 2, - f"Expected 2 snapshots (MaxHits=3), but got {final_count}. " + 3, + f"Expected 3 snapshots (MaxHits=3), but got {final_count}. " "BREAKPOINT should be disabled after hitting limit.", ) diff --git a/contract-tests/tests/test/amazon/di/flask_test.py b/contract-tests/tests/test/amazon/di/flask_test.py index bc55377f3..9ac4be6cb 100644 --- a/contract-tests/tests/test/amazon/di/flask_test.py +++ b/contract-tests/tests/test/amazon/di/flask_test.py @@ -270,7 +270,8 @@ class DIFlaskHitLimitTest(DITestInfrastructure): """Test BREAKPOINT hit limit behavior. BREAKPOINTs have a max_hits limit. With MaxHits=3, the check is - hit_count >= max_hits, so hits 1 and 2 generate snapshots, hit 3 is blocked. + hit_count > max_hits, so hits 1, 2, and 3 generate snapshots and the + breakpoint is disabled on hit 4. """ __test__ = True @@ -285,36 +286,37 @@ def get_application_wait_pattern(self) -> str: return "Ready" def test_breakpoint_generates_snapshots_up_to_limit(self) -> None: - """BREAKPOINT generates snapshots up to (max_hits - 1).""" - # limited_function has MaxHits=3, so 2 snapshots should be generated + """BREAKPOINT generates a snapshot for each hit up to and including max_hits.""" + # limited_function has MaxHits=3, so 3 snapshots should be generated + self.send_request("GET", "limited") self.send_request("GET", "limited") self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) limited_logs = self.logs_for_method(logs, "limited_function") - self.assertEqual(len(limited_logs), 2, "Expected exactly 2 snapshots (MaxHits=3 allows 2)") + self.assertEqual(len(limited_logs), 3, "Expected exactly 3 snapshots (MaxHits=3 allows 3)") def test_breakpoint_disabled_after_hit_limit(self) -> None: """BREAKPOINT stops generating snapshots after hit limit is reached.""" - # First 2 calls generate snapshots (hits 1 and 2) + # First 3 calls generate snapshots (hits 1, 2, and 3) + self.send_request("GET", "limited") self.send_request("GET", "limited") self.send_request("GET", "limited") - logs = self.wait_for_snapshots(min_count=2) + logs = self.wait_for_snapshots(min_count=3) initial_count = len(self.logs_for_method(logs, "limited_function")) - self.assertEqual(initial_count, 2) + self.assertEqual(initial_count, 3) - # 3rd and 4th calls hit the limit -- no new snapshots - self.send_request("GET", "limited") + # 4th call hits the limit -- no new snapshot self.send_request("GET", "limited") - time.sleep(2) + time.sleep(5) final_logs = self._peek_snapshots() final_count = len(self.logs_for_method(final_logs, "limited_function")) self.assertEqual( final_count, - 2, - f"Expected 2 snapshots (MaxHits=3), but got {final_count}. " + 3, + f"Expected 3 snapshots (MaxHits=3), but got {final_count}. " "BREAKPOINT should be disabled after hitting limit.", ) From 5cf93b2ff8e8cab98f67534d5e7d1f14be7be818 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 16 Jun 2026 17:31:55 -0700 Subject: [PATCH 15/15] docs: add changelog entry for #762 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02ae299c2..ecba70407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t ## Unreleased +- Nightly dependency update: OpenTelemetry 1.42.1/0.63b1 + ([#762](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/762)) - feat(agent-observability): add `AWS_AGENTIC_INSTRUMENTATION` (`auto`/`enabled`/`disabled`, case-insensitive) as an escape hatch over auto-detection when `AGENT_OBSERVABILITY_ENABLED=true`; the switch only governs AWS native instrumentors and never disables third-party ones ([#769](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/769)) - fix(otlp-aws-exporter): avoid `RecursionError` when `pip_system_certs` replaces `ssl.SSLContext` (truststore injection) by rebinding stale `botocore`/`urllib3` SSL context references and caching credentials in `AwsAuthSession`