diff --git a/.flake8 b/.flake8 index 8b2144c00f..5683cfc1f3 100644 --- a/.flake8 +++ b/.flake8 @@ -12,6 +12,10 @@ ignore = # allow whitespace before ':' (https://github.com/psf/black#slices) E203 + # conflicts with black + E701 + E704 + exclude = .bzr .git diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 2597b185d0..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -labels: bug ---- - -**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main. - -**Steps to reproduce** -Describe exactly how to reproduce the error. Include a code sample if applicable. - -**What is the expected behavior?** -What did you expect to see? - -**What is the actual behavior?** -What did you see instead? - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..6718481a76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,66 @@ +--- +name: Bug Report +description: Create a report to help us improve +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please make sure to fill out the entire form below, providing as much context as you can in order to help us triage and track down your bug as quickly as possible. + + Before filing a bug, please be sure you have searched through [existing bugs](https://github.com/open-telemetry/opentelemetry-python-contrib/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abug) to see if your bug is already addressed. + + - type: textarea + id: environment + attributes: + label: Describe your environment + description: | + Please describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main. + value: | + OS: (e.g, Ubuntu) + Python version: (e.g., Python 3.8.10) + Package version: (e.g., 0.46.0) + + - type: textarea + attributes: + label: What happened? + description: Please provide as much detail as you reasonably can. + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: Provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if possible and the needed steps to reproduce the problem. + validations: + required: true + + - type: textarea + attributes: + label: Expected Result + description: What did you expect to see? + validations: + required: true + + - type: textarea + attributes: + label: Actual Result + description: What did you see instead? + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. + placeholder: Any additional information... + + - type: dropdown + id: contribute + attributes: + label: Would you like to implement a fix? + description: For guidance on how to get started, refer to the [contribution guide](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md). + options: + - "No" + - "Yes" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..f6acad9c9b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: Slack + url: https://cloud-native.slack.com/archives/C01PD4HUVBL + about: Or the `#otel-python` channel in the CNCF Slack instance. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 973549ab2d..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project -labels: feature-request ---- - -Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenTelemetry client libraries](https://github.com/open-telemetry/). If so, please [open an issue on opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification/issues/new) first. - -**Is your feature request related to a problem?** -If so, provide a concise description of the problem. - -**Describe the solution you'd like** -What do you want to happen instead? What is the expected behavior? - -**Describe alternatives you've considered** -Which alternative solutions or features have you considered? - -**Additional context** -Add any other context about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000000..270409a495 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,50 @@ +--- +name: Feature Request +description: Suggest an idea for this project +labels: [feature-request] +body: + - type: markdown + attributes: + value: | + Before opening a feature request against this repo, consider whether the feature should/could be implemented in the [other OpenTelemetry client libraries](https://github.com/open-telemetry/). If so, please [open an issue on opentelemetry-specification](https://github.com/open-telemetry/opentelemetry-specification/issues/new) first. + - type: textarea + id: related-problem + attributes: + label: What problem do you want to solve? + description: Is your feature request related to a problem? If so, provide a concise description of the problem. + placeholder: Describe the problem and include relevant issue IDs + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: What do you want to happen instead? What is the expected behavior? + placeholder: I'd like to ... + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: Which alternative solutions or features have you considered? + placeholder: Some potential solutions + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Add any other context about the feature request here. + placeholder: Some related requests in other projects or upstream spec proposals. + validations: + required: false + - type: dropdown + id: contribute + attributes: + label: Would you like to implement a fix? + description: | + For guidance on how to get started, refer to the [contribution guide](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md). + options: + - "No" + - "Yes" diff --git a/.github/component_owners.yml b/.github/component_owners.yml index ab14a41aec..efd15a6775 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -73,3 +73,6 @@ components: instrumentation/opentelemetry-instrumentation-psycopg: - federicobond + + processor/opentelemetry-processor-baggage: + - codeboten diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f308c5757b..26789093f2 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -16,7 +16,7 @@ jobs: exit 1 fi - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # history is needed to run git cherry-pick below fetch-depth: 0 @@ -40,4 +40,4 @@ jobs: gh pr create --title "[$GITHUB_REF_NAME] $title" \ --body "Clean cherry-pick of #$NUMBER to the \`$GITHUB_REF_NAME\` branch." \ --head $branch \ - --base $GITHUB_REF_NAME \ No newline at end of file + --base $GITHUB_REF_NAME diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 5238e01c4b..491ddd27fa 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -18,7 +18,7 @@ jobs: && github.actor != 'opentelemetrybot' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Check for CHANGELOG changes run: | @@ -33,4 +33,4 @@ jobs: echo "No CHANGELOG was modified." echo "Please add a CHANGELOG entry, or add the \"Skip Changelog\" label if not required." false - fi \ No newline at end of file + fi diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b979a3121b..8ef01d21cb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,16 +20,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: python - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 \ No newline at end of file + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/instrumentations_0.yml b/.github/workflows/instrumentations_0.yml index 5505fce008..fb8df0dcbb 100644 --- a/.github/workflows/instrumentations_0.yml +++ b/.github/workflows/instrumentations_0.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: a1253585f66d63e7c05a19f070f3bfe0ab6460c1 + CORE_REPO_SHA: 141a6a2e473ef7f0ec4915dfb71e3c0fa595283e jobs: instrumentations-0: @@ -16,13 +16,14 @@ jobs: py39: 3.9 py310: "3.10" py311: "3.11" + py312: "3.12" pypy3: pypy-3.8 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [py38, py39, py310, py311, pypy3] + python-version: [py38, py39, py310, py311, py312, pypy3] package: # Do not add more instrumentations here, add them in instrumentations_1.yml. # The reason for this separation of instrumentations into more than one YAML file is @@ -80,6 +81,12 @@ jobs: package: "sklearn" - python-version: py311 package: "sklearn" + - python-version: py312 + package: "sklearn" + - python-version: py312 + package: "boto" + - python-version: py312 + package: "kafka-python" - python-version: pypy3 package: "aiopg" - python-version: pypy3 @@ -104,16 +111,16 @@ jobs: package: "grpc" steps: - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ env[matrix.python-version] }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env[matrix.python-version] }} - name: Install tox run: pip install tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: | .tox diff --git a/.github/workflows/instrumentations_1.yml b/.github/workflows/instrumentations_1.yml index 0640c056a6..7783105cd1 100644 --- a/.github/workflows/instrumentations_1.yml +++ b/.github/workflows/instrumentations_1.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: a1253585f66d63e7c05a19f070f3bfe0ab6460c1 + CORE_REPO_SHA: 141a6a2e473ef7f0ec4915dfb71e3c0fa595283e jobs: instrumentations-1: @@ -16,43 +16,42 @@ jobs: py39: 3.9 py310: "3.10" py311: "3.11" + py312: "3.12" pypy3: pypy-3.8 RUN_MATRIX_COMBINATION: ${{ matrix.python-version }}-${{ matrix.package }}-${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: - python-version: [py38, py39, py310, py311, pypy3] + python-version: [py38, py39, py310, py311, py312, pypy3] package: - "urllib" - - "urllib3v" + - "urllib3" - "wsgi" - "distro" - "richconsole" - "psycopg" - "prometheus-remote-write" - - "sdkextension-aws" + - "sdk-extension-aws" - "propagator-aws-xray" - "propagator-ot-trace" - "resource-detector-container" os: [ubuntu-20.04] exclude: - - python-version: py311 - package: "prometheus-remote-write" - python-version: pypy3 package: "prometheus-remote-write" steps: - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python ${{ env[matrix.python-version] }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env[matrix.python-version] }} - name: Install tox run: pip install tox - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: | .tox diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..debecc6ff8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,121 @@ +name: Lint tests + +on: + push: + branches-ignore: + - 'release/*' + pull_request: +env: + CORE_REPO_SHA: 141a6a2e473ef7f0ec4915dfb71e3c0fa595283e + +jobs: + lint-3_12: + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + package: + - "distro" + - "exporter-prometheus-remote-write" + - "exporter-richconsole" + - "instrumentation-aio-pika" + - "instrumentation-aiohttp-client" + - "instrumentation-aiohttp-server" + - "instrumentation-aiopg" + - "instrumentation-asgi" + - "instrumentation-asyncio" + - "instrumentation-asyncpg" + - "instrumentation-aws-lambda" + - "instrumentation-boto" + - "instrumentation-boto3sqs" + - "instrumentation-botocore" + - "instrumentation-cassandra" + - "instrumentation-celery" + - "instrumentation-confluent-kafka" + - "instrumentation-dbapi" + - "instrumentation-django" + - "instrumentation-elasticsearch" + - "instrumentation-falcon" + - "instrumentation-fastapi" + - "instrumentation-flask" + - "instrumentation-grpc" + - "instrumentation-httpx" + - "instrumentation-jinja2" + - "instrumentation-kafka-python" + - "instrumentation-logging" + - "instrumentation-mysql" + - "instrumentation-mysqlclient" + - "instrumentation-psycopg" + - "instrumentation-psycopg2" + - "instrumentation-pymemcache" + - "instrumentation-pymongo" + - "instrumentation-pymysql" + - "instrumentation-pyramid" + - "instrumentation-redis" + - "instrumentation-remoulade" + - "instrumentation-requests" + - "instrumentation-sio-pika" + - "instrumentation-sqlalchemy" + - "instrumentation-sqlite3" + - "instrumentation-starlette" + - "instrumentation-system-metrics" + - "instrumentation-threading" + - "instrumentation-tornado" + - "instrumentation-tortoiseorm" + - "instrumentation-urllib" + - "instrumentation-urllib3" + - "instrumentation-wsgi" + - "opentelemetry-instrumentation" + - "processor-baggage" + - "propagator-aws-xray" + - "propagator-ot-trace" + - "resource-detector-container" + - "sdk-extension-aws" + os: [ubuntu-20.04] + runs-on: ubuntu-20.04 + steps: + - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install tox + run: pip install tox + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v4 + with: + path: | + .tox + ~/.cache/pip + key: v7-build-tox-cache-${{ matrix.package }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} + - name: run tox + run: tox -e lint-${{ matrix.package }} + + lint-3_8: + strategy: + fail-fast: false # ensures the entire test matrix is run, even if one permutation fails + matrix: + package: + - "instrumentation-sklearn" + os: [ubuntu-20.04] + runs-on: ubuntu-20.04 + steps: + - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: 3.8 + - name: Install tox + run: pip install tox + - name: Cache tox environment + # Preserves .tox directory between runs for faster installs + uses: actions/cache@v4 + with: + path: | + .tox + ~/.cache/pip + key: v7-build-tox-cache-${{ matrix.package }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} + - name: run tox + run: tox -e lint-${{ matrix.package }} diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 49b9c89560..7c854d436d 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -6,7 +6,7 @@ jobs: prepare-patch-release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then @@ -50,7 +50,7 @@ jobs: run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install tox @@ -79,4 +79,4 @@ jobs: gh pr create --title "[$GITHUB_REF_NAME] $message" \ --body "$message." \ --head $branch \ - --base $GITHUB_REF_NAME \ No newline at end of file + --base $GITHUB_REF_NAME diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml index a4caf86ebe..6818772acf 100644 --- a/.github/workflows/prepare-release-branch.yml +++ b/.github/workflows/prepare-release-branch.yml @@ -10,7 +10,7 @@ jobs: prereqs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Verify prerequisites env: @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest needs: prereqs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Create release branch env: @@ -77,7 +77,7 @@ jobs: run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install tox @@ -112,7 +112,7 @@ jobs: runs-on: ubuntu-latest needs: prereqs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set environment variables env: @@ -161,7 +161,7 @@ jobs: run: .github/scripts/update-version.sh $STABLE_NEXT_VERSION $UNSTABLE_NEXT_VERSION - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install tox @@ -192,4 +192,4 @@ jobs: gh pr create --title "$message" \ --body "$body" \ --head $branch \ - --base main \ No newline at end of file + --base main diff --git a/.github/workflows/publish-a-package-from-tag.yml b/.github/workflows/publish-a-package-from-tag.yml index 2c07bf5d7a..a64f5fcf15 100644 --- a/.github/workflows/publish-a-package-from-tag.yml +++ b/.github/workflows/publish-a-package-from-tag.yml @@ -10,8 +10,8 @@ jobs: name: Publish package from tag runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' - name: Log tag that triggered publish workflow diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6c267003a..ec544b43c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: exit 1 fi - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set environment variables run: | @@ -56,15 +56,15 @@ jobs: # check out main branch to verify there won't be problems with merging the change log # at the end of this workflow - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: main # back to the release branch - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # next few steps publish to pypi - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v5 with: python-version: '3.8' @@ -81,12 +81,14 @@ jobs: # rejected by pypi (e.g "3 - Beta"). This would cause a failure during the # middle of the package upload causing the action to fail, and certain packages # might have already been updated, this would be bad. - - name: Publish to TestPyPI - env: - TWINE_USERNAME: '__token__' - TWINE_PASSWORD: ${{ secrets.test_pypi_token }} - run: | - twine upload --repository testpypi --skip-existing --verbose dist/* + # EDIT: 5/31/2024 - TestPypi now requires a verified email. Commenting out as a temporary measure + # until we found TestPypi credentials. + # - name: Publish to TestPyPI + # env: + # TWINE_USERNAME: '__token__' + # TWINE_PASSWORD: ${{ secrets.test_pypi_token }} + # run: | + # twine upload --repository testpypi --skip-existing --verbose dist/* - name: Publish to PyPI env: @@ -127,7 +129,7 @@ jobs: --discussion-category announcements \ v$UNSTABLE_VERSION - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # the step below is creating a pull request against main ref: main diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000000..68d12b805f --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,19 @@ +name: Shellcheck + +on: + push: + branches-ignore: + - 'release/*' + pull_request: + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install shellcheck + run: sudo apt update && sudo apt install --assume-yes shellcheck + + - name: Run shellcheck + run: find . -name \*.sh | xargs shellcheck --severity=warning diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bc748551b..ee66efac64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,31 +6,28 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: a1253585f66d63e7c05a19f070f3bfe0ab6460c1 + CORE_REPO_SHA: 141a6a2e473ef7f0ec4915dfb71e3c0fa595283e jobs: misc: strategy: fail-fast: false matrix: - tox-environment: [ "docker-tests", "spellcheck", "lint", "docs", "generate" ] + tox-environment: [ "docker-tests", "spellcheck", "docs", "generate" ] name: ${{ matrix.tox-environment }} runs-on: ubuntu-20.04 steps: - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install tox run: pip install tox - - name: Install libsnappy-dev - if: ${{ matrix.tox-environment == 'lint' }} - run: sudo apt-get install -y libsnappy-dev - name: Cache tox environment # Preserves .tox directory between runs for faster installs - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: | .tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..b01b7ce4d7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.3.0 + hooks: + - id: black + language_version: python3.12 + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/pycqa/flake8 + rev: '6.1.0' + hooks: + - id: flake8 diff --git a/.pylintrc b/.pylintrc index 5ea4385ea0..39ea7b5d35 100644 --- a/.pylintrc +++ b/.pylintrc @@ -45,6 +45,9 @@ suggestion-mode=yes # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no +# Run python dependant checks considering the baseline version +py-version=3.8 + [MESSAGES CONTROL] @@ -81,6 +84,7 @@ disable=missing-docstring, missing-module-docstring, # temp-pylint-upgrade import-error, # needed as a workaround as reported here: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/290 cyclic-import, + not-context-manager # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -491,4 +495,4 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "Exception". -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/.readthedocs.yml b/.readthedocs.yml index 3dcf0e5cf6..2a3c920b45 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,9 +6,10 @@ sphinx: configuration: docs/conf.py build: - image: latest + os: "ubuntu-22.04" + tools: + python: "3.8" python: - version: 3.8 install: - requirements: docs-requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 023425035d..b4da8ef772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,129 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-instrumentation-aws-lambda` Bugfix: AWS Lambda event source key incorrect for SNS in instrumentation library. + ([#2612](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2612)) +- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. + ([#2630](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2630)) + +### Added + +- `opentelemetry-instrumentation-pyramid` Record exceptions raised when serving a request + ([#2622](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2622)) +- `opentelemetry-sdk-extension-aws` Add AwsXrayLambdaPropagator + ([#2573](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2573)) +- `opentelemetry-instrumentation-confluent-kafka` Add support for version 2.4.0 of confluent_kafka + ([#2616](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2616)) +- `opentelemetry-instrumentation-confluent-kafka` Add support for produce purge + ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) + +### Breaking changes + +- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware + ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) + +### Fixed + +- `opentelemetry-instrumentation-httpx` Ensure httpx.get or httpx.request like methods are instrumented + ([#2538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538)) +- Add Python 3.12 support + ([#2572](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2572)) +- `opentelemetry-instrumentation-aiohttp-server`, `opentelemetry-instrumentation-httpx` Ensure consistently use of suppress_instrumentation utils + ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) +- Reference symbols from generated semantic conventions + ([#2611](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2611)) + +## Version 1.25.0/0.46b0 (2024-05-31) + +### Breaking changes + +- Add return statement to Confluent kafka Producer poll() and flush() calls when instrumented by ConfluentKafkaInstrumentor().instrument_producer() ([#2527](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2527)) +- Rename `type` attribute to `asgi.event.type` in `opentelemetry-instrumentation-asgi` + ([#2300](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2300)) +- Rename AwsLambdaInstrumentor span attributes `faas.id` to `cloud.resource_id`, `faas.execution` to `faas.invocation_id` + ([#2372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2372)) +- Drop support for instrumenting elasticsearch client < 6 + ([#2422](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2422)) +- `opentelemetry-instrumentation-wsgi` Add `http.method` to `span.name` + ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) +- `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` + ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) +- Record repeated HTTP headers in lists, rather than a comma separate strings for ASGI based web frameworks + ([#2361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2361)) +- ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` for internal spans +- ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546)) + +### Added + +- `opentelemetry-sdk-extension-aws` Register AWS resource detectors under the + `opentelemetry_resource_detector` entry point + ([#2382](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2382)) +- `opentelemetry-instrumentation-wsgi` Implement new semantic convention opt-in with stable http semantic conventions + ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) +- `opentelemetry-instrumentation-flask` Implement new semantic convention opt-in with stable http semantic conventions + ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) +- `opentelemetry-instrumentation-threading` Initial release for threading + ([#2253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2253)) +- `opentelemetry-instrumentation-pika` Instrumentation for `channel.consume()` (supported + only for global, non channel specific instrumentation) + ([#2397](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2397)) +- `opentelemetry-processor-baggage` Initial release + ([#2436](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2436)) +- `opentelemetry-processor-baggage` Add baggage key predicate + ([#2535](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2535)) + ### Fixed -- Align gRPC span status codes to OTEL specification ([#1756](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1756)) + +- `opentelemetry-instrumentation-dbapi` Fix compatibility with Psycopg3 to extract libpq build version + ([#2500](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2500)) +- `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object + ([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363)) +- `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource + ([#2161](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2161)) +- `opentelemetry-instrumentation-aws-lambda` Fix exception handling for events with requestContext + ([#2418](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2418)) +- Use sqlalchemy version in sqlalchemy commenter instead of opentelemetry library version + ([#2404](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2404)) +- `opentelemetry-instrumentation-asyncio` Check for cancelledException in the future + ([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461)) +- Remove SDK dependency from opentelemetry-instrumentation-grpc + ([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474)) +- `opentelemetry-instrumentation-elasticsearch` Improved support for version 8 + ([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420)) +- `opentelemetry-instrumentation-elasticsearch` Disabling instrumentation with native OTel support enabled + ([#2524](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2524)) +- `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine + ([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521)) +- `opentelemetry-instrumentation-requests` Fix wrong time unit for duration histogram + ([#2553](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2553)) +- `opentelemetry-util-http` Preserve brackets around literal IPv6 hosts ([#2552](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2552)) +- `opentelemetry-util-redis` Fix net peer attribute for unix socket connection ([#2493](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2493)) + +## Version 1.24.0/0.45b0 (2024-03-28) + +### Added + +- `opentelemetry-instrumentation-psycopg` Async Instrumentation for psycopg 3.x + ([#2146](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2146)) + +### Fixed +- `opentelemetry-instrumentation-celery` Allow Celery instrumentation to be installed multiple times + ([#2342](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2342)) +- Align gRPC span status codes to OTEL specification + ([#1756](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1756)) +- `opentelemetry-instrumentation-flask` Add importlib metadata default for deprecation warning flask version + ([#2297](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2297)) +- Ensure all http.server.duration metrics have the same description + ([#2151](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2298)) +- Fix regression in httpx `request.url` not being of type `httpx.URL` after `0.44b0` + ([#2359](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2359)) +- Avoid losing repeated HTTP headers + ([#2266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2266)) +- `opentelemetry-instrumentation-elasticsearch` Don't send bulk request body as db statement + ([#2355](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2355)) +- AwsLambdaInstrumentor sets `cloud.account.id` span attribute + ([#2367](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2367)) + ## Version 1.23.0/0.44b0 (2024-02-23) @@ -33,6 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version 1.22.0/0.43b0 (2023-12-14) ### Added + - `opentelemetry-instrumentation-asyncio` Add support for asyncio ([#1919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1943)) - `opentelemetry-instrumentation` Added Otel semantic convention opt-in mechanism @@ -54,7 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959)) - `opentelemetry-resource-detector-azure` Added dependency for Cloud Resource ID attribute ([#2072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2072)) - + ## Version 1.21.0/0.42b0 (2023-11-01) ### Added @@ -1470,4 +1592,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) - AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c76634db7..f5a4913440 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,30 @@ on how to become a [**Member**](https://github.com/open-telemetry/community/blob [**Approver**](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) and [**Maintainer**](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer). -## Find a Buddy and get Started Quickly! +Before you can contribute, you will need to sign the [Contributor License Agreement](https://docs.linuxfoundation.org/lfx/easycla/contributors). + +Please also read the [OpenTelemetry Contributor Guide](https://github.com/open-telemetry/community/blob/main/CONTRIBUTING.md). + +## Index + +* [Find a Buddy and get Started Quickly](#find-a-buddy-and-get-started-quickly) +* [Development](#development) + * [Troubleshooting](#troubleshooting) + * [Benchmarks](#benchmarks) +* [Pull requests](#pull-requests) + * [How to Send Pull Requests](#how-to-send-pull-requests) + * [How to Receive Comments](#how-to-receive-comments) + * [How to Get PRs Reviewed](#how-to-get-prs-reviewed) + * [How to Get PRs Merged](#how-to-get-prs-merged) +* [Design Choices](#design-choices) + * [Focus on Capabilities, Not Structure Compliance](#focus-on-capabilities-not-structure-compliance) +* [Running Tests Locally](#running-tests-locally) + * [Testing against a different Core repo branch/commit](#testing-against-a-different-core-repo-branchcommit) +* [Style Guide](#style-guide) +* [Guideline for instrumentations](#guideline-for-instrumentations) +* [Expectations from contributors](#expectations-from-contributors) + +## Find a Buddy and get Started Quickly If you are looking for someone to help you find a starting point and be a resource for your first contribution, join our Slack and find a buddy! @@ -31,19 +54,19 @@ This project uses [tox](https://tox.readthedocs.io) to automate some aspects of development, including testing against multiple Python versions. To install `tox`, run: -```console +```sh $ pip install tox ``` You can run `tox` with the following arguments: -- `tox` to run all existing tox commands, including unit tests for all packages +* `tox` to run all existing tox commands, including unit tests for all packages under multiple Python versions -- `tox -e docs` to regenerate the API docs -- `tox -e py311-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific +* `tox -e docs` to regenerate the API docs +* `tox -e py312-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific Python version -- `tox -e spellcheck` to run a spellcheck on all the code -- `tox -e lint` to run lint checks on all code +* `tox -e spellcheck` to run a spellcheck on all the code +* `tox -e lint-some-package` to run lint checks on `some-package` `black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually. An easier way to do so is: @@ -51,13 +74,25 @@ An easier way to do so is: 1. Run `.tox/lint/bin/black .` 2. Run `.tox/lint/bin/isort .` +Or you can call formatting and linting in one command by [pre-commit](https://pre-commit.com/): + +```console +$ pre-commit +``` + +You can also configure it to run lint tools automatically before committing with: + +```console +$ pre-commit install +``` + See [`tox.ini`](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/tox.ini) for more detail on available tox commands. ### Troubleshooting -- Some packages may require additional system wide dependencies to be installed. For example, you may need to install `libpq-dev` to run the postgresql client libraries instrumentation tests. or `libsnappy-dev` to run the prometheus exporter tests. If you encounter a build error, please check the installation instructions for the package you are trying to run tests for. +> Some packages may require additional system wide dependencies to be installed. For example, you may need to install `libpq-dev` to run the postgresql client libraries instrumentation tests. or `libsnappy-dev` to run the prometheus exporter tests. If you encounter a build error, please check the installation instructions for the package you are trying to run tests for. ### Benchmarks @@ -95,6 +130,7 @@ To create a new PR, fork the project in GitHub and clone the upstream repo: ```sh $ git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git +$ cd opentelemetry-python-contrib ``` Add your fork as an origin: @@ -126,6 +162,7 @@ Open a pull request against the main `opentelemetry-python-contrib` repo. * If the PR is not ready for review, please put `[WIP]` in the title, tag it as `work-in-progress`, or mark it as [`draft`](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +* Make sure tests and lint are passing locally before requesting a review. * Make sure CLA is signed and CI is clear. ### How to Get PRs Reviewed @@ -142,6 +179,7 @@ If you are not getting reviews, please contact the respective owners directly. ### How to Get PRs Merged A PR is considered to be **ready to merge** when: + * It has received two approvals from [Approvers](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver) / [Maintainers](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer) (at different companies). @@ -180,14 +218,26 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp 2. Make sure you have `tox` installed. `pip install tox`. 3. Run `tox` without any arguments to run tests for all the packages. Read more about [tox](https://tox.readthedocs.io/en/latest/). +Some tests can be slow due to pre-steps that do dependencies installs. To help with that, you can run tox a first time, and after that run the tests using previous installed dependencies in toxdir as following: + +1. First time run (e.g., opentelemetry-instrumentation-aiopg) +```console +tox -e py312-test-instrumentation-aiopg +``` +2. Run tests again without pre-steps: +```console +.tox/py312-test-instrumentation-aiopg/bin/pytest instrumentation/opentelemetry-instrumentation-aiopg +``` + ### Testing against a different Core repo branch/commit Some of the tox targets install packages from the [OpenTelemetry Python Core Repository](https://github.com/open-telemetry/opentelemetry-python) via pip. The version of the packages installed defaults to the main branch in that repository when tox is run locally. It is possible to install packages tagged with a specific git commit hash by setting an environment variable before running tox as per the following example: +```sh CORE_REPO_SHA=c49ad57bfe35cfc69bfa863d74058ca9bec55fc3 tox +``` -The continuation integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/test.yml#L9). - +The continuous integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/.github/workflows/test.yml#L9). ## Style Guide @@ -203,27 +253,46 @@ Below is a checklist of things to be mindful of when implementing a new instrume - Follow semantic conventions - The instrumentation should follow the semantic conventions defined [here](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/semantic-conventions.md) -- Extends from [BaseInstrumentor](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py#L26) +- Extends from [BaseInstrumentor](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/2518a4ac07cb62ad6587dd8f6cbb5f8663a7e179/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py#L35) - Supports auto-instrumentation - - Add an entry point (ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/f045c43affff6ff1af8fa2f7514a4fdaca97dacf/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml#L44) + - Add an entry point (ex. ) - Run `python scripts/generate_instrumentation_bootstrap.py` after adding a new instrumentation package. - Functionality that is common amongst other instrumentation and can be abstracted [here](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation) - Request/response [hooks](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/408) for http instrumentations - `suppress_instrumentation` functionality - - ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/3ec77360cb20482b08b30312a6bedc8b946e3fa1/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py#L111 + - ex. - Suppress propagation functionality - https://github.com/open-telemetry/opentelemetry-python-contrib/issues/344 for more context - `exclude_urls` functionality - - ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/0fcb60d2ad139f78a52edd85b1cc4e32f2e962d0/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py#L91 + - ex. - `url_filter` functionality - - ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/0fcb60d2ad139f78a52edd85b1cc4e32f2e962d0/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py#L235 + - ex. - `is_recording()` optimization on non-sampled spans - - ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py#L133 + - ex. - Appropriate error handling - - ex. https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py#L146 - + - ex. ## Expectations from contributors OpenTelemetry is an open source community, and as such, greatly encourages contributions from anyone interested in the project. With that being said, there is a certain level of expectation from contributors even after a pull request is merged, specifically pertaining to instrumentations. The OpenTelemetry Python community expects contributors to maintain a level of support and interest in the instrumentations they contribute. This is to ensure that the instrumentation does not become stale and still functions the way the original contributor intended. Some instrumentations also pertain to libraries that the current members of the community are not so familiar with, so it is necessary to rely on the expertise of the original contributing parties. +## Updating supported Python versions + +### Bumping the Python baseline + +When updating the minimum supported Python version remember to: + +- Remove the version in `pyproject.toml` trove classifiers +- Remove the version from `tox.ini` +- Search for `sys.version_info` usage and remove code for unsupported versions +- Bump `py-version` in `.pylintrc` for Python version dependent checks + +### Adding support for a new Python release + +When adding support for a new Python release remember to: + +- Add the version in `tox.ini` +- Add the version in `pyproject.toml` trove classifiers +- Update github workflows accordingly; lint and benchmarks use the latest supported version +- Update `.pre-commit-config.yaml` +- Update tox examples in the documentation diff --git a/LICENSE b/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE.Apache b/LICENSE.Apache index bff56b5431..261eeb9e9f 100644 --- a/LICENSE.Apache +++ b/LICENSE.Apache @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,8 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2016 Datadog, Inc. + Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/README.md b/README.md index 0a3d37ab3f..6c019239ae 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,19 @@ --- -## OpenTelemetry Python Contrib +# OpenTelemetry Python Contrib The Python auto-instrumentation libraries for [OpenTelemetry](https://opentelemetry.io/) (per [OTEP 0001](https://github.com/open-telemetry/oteps/blob/main/text/0001-telemetry-without-manual-instrumentation.md)) -### Installation +## Index + +* [Installation](#installation) +* [Releasing](#releasing) + * [Releasing a package as `1.0` stable](#releasing-a-package-as-10-stable) +* [Contributing](#contributing) +* [Thanks to all the people who already contributed](#thanks-to-all-the-people-who-already-contributed) + +## Installation This repository includes installable packages for each instrumented library. Libraries that produce telemetry data should only depend on `opentelemetry-api`, and defer the choice of the SDK to the application developer. Applications may @@ -79,6 +87,7 @@ To resolve this, members of the community are encouraged to commit to becoming a ### Releasing a package as `1.0` stable To release a package as `1.0` stable, the package: + - SHOULD have a CODEOWNER. To become one, submit an issue and explain why you meet the responsibilities found in [CODEOWNERS](.github/CODEOWNERS). - MUST have unit tests that cover all supported versions of the instrumented library. - e.g. Instrumentation packages might use different techniques to instrument different major versions of python packages @@ -86,11 +95,15 @@ To release a package as `1.0` stable, the package: - e.g. If an instrumentation package uses flags, a token as context, or parameters that are not typical of the `BaseInstrumentor` class, these are documented - After the release of `1.0`, a CODEOWNER may no longer feel like they have the bandwidth to meet the responsibilities of maintaining the package. That's not a problem at all, life happens! However, if that is the case, we ask that the CODEOWNER please raise an issue indicating that they would like to be removed as a CODEOWNER so that they don't get pinged on future PRs. Ultimately, we hope to use that issue to find a new CODEOWNER. +## Semantic Convention status of instrumentations + +In our efforts to maintain optimal user experience and prevent breaking changes for transitioning into stable semantic conventions, OpenTelemetry Python is adopting the [semantic convention migration plan](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/migration-guide.md) for several instrumentations. Currently this plan is only being adopted for HTTP-related instrumentations, but will eventually cover all types. Please refer to the `semconv status` column of the [instrumentation README](instrumentation/README.md) of the current status of instrumentations' semantic conventions. The possible values are `experimental`, `stable` and `migration` referring to [status](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/document-status.md#lifecycle-status) of that particular semantic convention. `Migration` refers to an instrumentation that currently supports the migration plan. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) -We meet weekly on Thursday, and the time of the meeting alternates between 9AM PT and 4PM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) for specific dates and for the Zoom link. +We meet weekly on Thursday at 9AM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=c_2bf73e3b6b530da4babd444e72b76a6ad893a5c3f43cf40467abc7a9a897f977%40group.calendar.google.com) for specific dates and for the Zoom link. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1CIMGoIOZ-c3-igzbd6_Pnxx1SjAkjwqoYSUWxPY8XIs/edit). For edit access, get in touch on [GitHub Discussions](https://github.com/open-telemetry/opentelemetry-python/discussions). @@ -98,14 +111,20 @@ Approvers ([@open-telemetry/python-approvers](https://github.com/orgs/open-telem - [Aaron Abbott](https://github.com/aabmass), Google - [Jeremy Voss](https://github.com/jeremydvoss), Microsoft +- [Owais Lone](https://github.com/owais), Splunk +- [Pablo Collins](https://github.com/pmcollins), Splunk +- [Riccardo Magliocchetti](https://github.com/xrmx), Elastic - [Sanket Mehta](https://github.com/sanketmehta28), Cisco +- [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io +- [Tammy Baylis](https://github.com/tammy-baylis-swi), SolarWinds Emeritus Approvers: +- [Ashutosh Goel](https://github.com/ashu658), Cisco - [Héctor Hernández](https://github.com/hectorhdzg), Microsoft -- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google +- [Nikolay Sokolik](https://github.com/oxeye-nikolay), Oxeye +- [Nikolay Sokolik](https://github.com/nikosokolik), Oxeye - [Nathaniel Ruiz Nowell](https://github.com/NathanielRN), AWS -- [Ashutosh Goel](https://github.com/ashu658), Cisco *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#approver).* @@ -119,21 +138,12 @@ Emeritus Maintainers: - [Alex Boten](https://github.com/codeboten), Lightstep - [Owais Lone](https://github.com/owais), Splunk -- [Srikanth Chekuri](https://github.com/srikanthccv), signoz.io +- [Yusuke Tsutsumi](https://github.com/toumorokoshi), Google *Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/main/community-membership.md#maintainer).* -## Running Tests Locally - -1. Go to your Contrib repo directory. `cd ~/git/opentelemetry-python-contrib`. -2. Create a virtual env in your Contrib repo directory. `python3 -m venv my_test_venv`. -3. Activate your virtual env. `source my_test_venv/bin/activate`. -4. Make sure you have `tox` installed. `pip install tox`. -5. Run tests for a package. (e.g. `tox -e test-instrumentation-flask`.) - -### Thanks to all the people who already contributed! +### Thanks to all the people who already contributed - diff --git a/RELEASING.md b/RELEASING.md index 1547cec224..256674d1b8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -77,15 +77,17 @@ * If for some reason the action failed, see [Publish failed](#publish-failed) below * Move stable tag * Run the following (TODO automate): + ```bash git tag -d stable git tag stable git push --delete origin tagname git push origin stable ``` + * This will ensure the docs are pointing at the stable release. * To validate this worked, ensure the stable build has run successfully: - https://readthedocs.org/projects/opentelemetry-python/builds/. + . If the build has not run automatically, it can be manually trigger via the readthedocs interface. ## Troubleshooting @@ -98,4 +100,4 @@ If for some reason the action failed, do it manually: - Build distributions with `./scripts/build.sh` - Delete distributions we don't want to push (e.g. `testutil`) - Push to PyPI as `twine upload --skip-existing --verbose dist/*` -- Double check PyPI! \ No newline at end of file +- Double check PyPI! diff --git a/_template/pyproject.toml b/_template/pyproject.toml index 55607cc9b1..514b537f42 100644 --- a/_template/pyproject.toml +++ b/_template/pyproject.toml @@ -26,17 +26,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", ] -[project.optional-dependencies] -test = [ - # add any test dependencies here - "", -] - [project.entry-points.opentelemetry_instrumentor] # REPLACE ME: the entrypoint for the instrumentor e.g # sqlalchemy = "opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor" diff --git a/_template/version.py b/_template/version.py index 2b23bc4994..b6955b0eca 100644 --- a/_template/version.py +++ b/_template/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/dev-requirements.txt b/dev-requirements.txt index fffb4c445d..3289650ac8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,19 +1,21 @@ pylint==3.0.2 flake8==6.1.0 isort==5.12.0 -black==22.3.0 +black==24.3.0 httpretty==1.1.4 mypy==0.931 sphinx==7.1.2 sphinx-rtd-theme==2.0.0rc4 sphinx-autodoc-typehints==1.25.2 -pytest==7.1.3 +pytest==7.4.4 pytest-cov==4.1.0 readme-renderer==42.0 bleach==4.1.0 # transient dependency for readme-renderer protobuf~=3.13 markupsafe>=2.0.1 codespell==2.1.0 -requests==2.31.0 +requests==2.32.3 ruamel.yaml==0.17.21 flaky==3.7.0 +pre-commit==3.7.0; python_version >= '3.9' +pre-commit==3.5.0; python_version < '3.9' diff --git a/docs-requirements.txt b/docs-requirements.txt index aff449fcf8..34ce74ca7a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -27,8 +27,8 @@ botocore~=1.0 boto3~=1.0 cassandra-driver~=3.25 celery>=4.0 -confluent-kafka>= 1.8.2,<= 2.3.0 -elasticsearch>=2.0,<9.0 +confluent-kafka>= 1.8.2,<= 2.4.0 +elasticsearch>=6.0,<9.0 flask~=2.0 falcon~=2.0 grpcio~=1.27 @@ -38,8 +38,8 @@ mysqlclient~=2.1.1 psutil>=5 psycopg~=3.1.17 pika>=0.12.0 -pymongo~=3.1 -PyMySQL~=0.9.3 +pymongo~=4.6.3 +PyMySQL~=1.1.1 pyramid>=1.7 redis>=2.6 remoulade>=0.50 diff --git a/docs/index.rst b/docs/index.rst index 5203c377e4..e2bf32e12e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ installed separately via pip: pip install opentelemetry-exporter-{exporter} pip install opentelemetry-instrumentation-{instrumentation} - pip install opentelemetry-sdk-extension-{sdkextension} + pip install opentelemetry-sdk-extension-{sdk-extension} A complete list of packages can be found at the `Contrib repo instrumentation `_ diff --git a/docs/instrumentation/threading/threading.rst b/docs/instrumentation/threading/threading.rst new file mode 100644 index 0000000000..06bca89a49 --- /dev/null +++ b/docs/instrumentation/threading/threading.rst @@ -0,0 +1,7 @@ +OpenTelemetry Threading Instrumentation +======================================= + +.. automodule:: opentelemetry.instrumentation.threading + :members: + :undoc-members: + :show-inheritance: diff --git a/eachdist.ini b/eachdist.ini index 8d6bdbb6be..7f170e4947 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -16,7 +16,7 @@ sortfirst= ext/* [stable] -version=1.24.0.dev +version=1.26.0.dev packages= opentelemetry-sdk @@ -34,7 +34,7 @@ packages= opentelemetry-api [prerelease] -version=0.45b0.dev +version=0.47b0.dev packages= all diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh index 3cde0bd1ac..a07181add3 100755 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/proto/generate-proto-py.sh @@ -1,11 +1,11 @@ #!/bin/bash +set -e PROM_VERSION=v2.39.0 PROTO_VERSION=v1.3.2 # SRC_DIR is from protoc perspective. ie its the destination for our checkouts/clones SRC_DIR=opentelemetry/exporter/prometheus_remote_write/gen/ -DST_DIR=../src/opentelemetry/exporter/prometheus_remote_write/gen/ #TODO: # Check that black & protoc are installed properly diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml index a6dea9a9de..4218e13472 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml @@ -22,6 +22,8 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "protobuf ~= 4.21", @@ -31,9 +33,6 @@ dependencies = [ "python-snappy ~= 0.6", ] -[project.optional-dependencies] -test = [] - [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-prometheus-remote-write" diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py index 3efcb36536..f8724074f6 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/remote_pb2.py @@ -34,13 +34,13 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"Z\006prompb" _WRITEREQUEST.fields_by_name["timeseries"]._options = None - _WRITEREQUEST.fields_by_name[ - "timeseries" - ]._serialized_options = b"\310\336\037\000" + _WRITEREQUEST.fields_by_name["timeseries"]._serialized_options = ( + b"\310\336\037\000" + ) _WRITEREQUEST.fields_by_name["metadata"]._options = None - _WRITEREQUEST.fields_by_name[ - "metadata" - ]._serialized_options = b"\310\336\037\000" + _WRITEREQUEST.fields_by_name["metadata"]._serialized_options = ( + b"\310\336\037\000" + ) _WRITEREQUEST._serialized_start = 216 _WRITEREQUEST._serialized_end = 338 _READREQUEST._serialized_start = 341 diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py index fbfa2123ad..30cf2e38cc 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/types_pb2.py @@ -31,31 +31,31 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b"Z\006prompb" _EXEMPLAR.fields_by_name["labels"]._options = None - _EXEMPLAR.fields_by_name[ - "labels" - ]._serialized_options = b"\310\336\037\000" + _EXEMPLAR.fields_by_name["labels"]._serialized_options = ( + b"\310\336\037\000" + ) _TIMESERIES.fields_by_name["labels"]._options = None - _TIMESERIES.fields_by_name[ - "labels" - ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["labels"]._serialized_options = ( + b"\310\336\037\000" + ) _TIMESERIES.fields_by_name["samples"]._options = None - _TIMESERIES.fields_by_name[ - "samples" - ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["samples"]._serialized_options = ( + b"\310\336\037\000" + ) _TIMESERIES.fields_by_name["exemplars"]._options = None - _TIMESERIES.fields_by_name[ - "exemplars" - ]._serialized_options = b"\310\336\037\000" + _TIMESERIES.fields_by_name["exemplars"]._serialized_options = ( + b"\310\336\037\000" + ) _LABELS.fields_by_name["labels"]._options = None _LABELS.fields_by_name["labels"]._serialized_options = b"\310\336\037\000" _CHUNKEDSERIES.fields_by_name["labels"]._options = None - _CHUNKEDSERIES.fields_by_name[ - "labels" - ]._serialized_options = b"\310\336\037\000" + _CHUNKEDSERIES.fields_by_name["labels"]._serialized_options = ( + b"\310\336\037\000" + ) _CHUNKEDSERIES.fields_by_name["chunks"]._options = None - _CHUNKEDSERIES.fields_by_name[ - "chunks" - ]._serialized_options = b"\310\336\037\000" + _CHUNKEDSERIES.fields_by_name["chunks"]._serialized_options = ( + b"\310\336\037\000" + ) _METRICMETADATA._serialized_start = 152 _METRICMETADATA._serialized_end = 400 _METRICMETADATA_METRICTYPE._serialized_start = 279 diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py index 2b23bc4994..b6955b0eca 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt new file mode 100644 index 0000000000..b41245cd1f --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +cramjam==2.8.1 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +protobuf==4.25.3 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-snappy==0.7.1 +requests==2.32.3 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e exporter/opentelemetry-exporter-prometheus-remote-write diff --git a/exporter/opentelemetry-exporter-richconsole/pyproject.toml b/exporter/opentelemetry-exporter-richconsole/pyproject.toml index c8a509f643..745a3bc25d 100644 --- a/exporter/opentelemetry-exporter-richconsole/pyproject.toml +++ b/exporter/opentelemetry-exporter-richconsole/pyproject.toml @@ -22,17 +22,15 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "rich>=10.0.0", ] -[project.optional-dependencies] -test = [] - [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-richconsole" diff --git a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py index 2b23bc4994..b6955b0eca 100644 --- a/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py +++ b/exporter/opentelemetry-exporter-richconsole/src/opentelemetry/exporter/richconsole/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt new file mode 100644 index 0000000000..af50fa87e8 --- /dev/null +++ b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +flaky==3.7.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +markdown-it-py==3.0.0 +mdurl==0.1.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +Pygments==2.17.2 +pytest==7.4.4 +pytest-benchmark==4.0.0 +rich==13.7.1 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e exporter/opentelemetry-exporter-richconsole diff --git a/gen-requirements.txt b/gen-requirements.txt index 0f96f12a56..b2d5c4f695 100644 --- a/gen-requirements.txt +++ b/gen-requirements.txt @@ -1,6 +1,6 @@ -c dev-requirements.txt astor==0.8.1 -jinja2~=2.7 +jinja2==3.1.4 markupsafe==2.0.1 isort black diff --git a/instrumentation/README.md b/instrumentation/README.md index 0cce7e5de7..682334db1a 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -1,50 +1,51 @@ -| Instrumentation | Supported Packages | Metrics support | -| --------------- | ------------------ | --------------- | -| [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No -| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No -| [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | No -| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No -| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | No -| [opentelemetry-instrumentation-asyncio](./opentelemetry-instrumentation-asyncio) | asyncio | No -| [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No -| [opentelemetry-instrumentation-aws-lambda](./opentelemetry-instrumentation-aws-lambda) | aws_lambda | No -| [opentelemetry-instrumentation-boto](./opentelemetry-instrumentation-boto) | boto~=2.0 | No -| [opentelemetry-instrumentation-boto3sqs](./opentelemetry-instrumentation-boto3sqs) | boto3 ~= 1.0 | No -| [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No -| [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No -| [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No -| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.3.0 | No -| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No -| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes -| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No -| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes -| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes -| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes -| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No -| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No -| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No -| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No -| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No -| [opentelemetry-instrumentation-mysql](./opentelemetry-instrumentation-mysql) | mysql-connector-python ~= 8.0 | No -| [opentelemetry-instrumentation-mysqlclient](./opentelemetry-instrumentation-mysqlclient) | mysqlclient < 3 | No -| [opentelemetry-instrumentation-pika](./opentelemetry-instrumentation-pika) | pika >= 0.12.0 | No -| [opentelemetry-instrumentation-psycopg](./opentelemetry-instrumentation-psycopg) | psycopg >= 3.1.0 | No -| [opentelemetry-instrumentation-psycopg2](./opentelemetry-instrumentation-psycopg2) | psycopg2 >= 2.7.3.1 | No -| [opentelemetry-instrumentation-pymemcache](./opentelemetry-instrumentation-pymemcache) | pymemcache >= 1.3.5, < 5 | No -| [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No -| [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No -| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes -| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No -| [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No -| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes -| [opentelemetry-instrumentation-sklearn](./opentelemetry-instrumentation-sklearn) | scikit-learn ~= 0.24.0 | No -| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy | Yes -| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No -| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette ~= 0.13.0 | Yes -| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No -| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes -| [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No -| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes -| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes -| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes \ No newline at end of file +| Instrumentation | Supported Packages | Metrics support | Semconv status | +| --------------- | ------------------ | --------------- | -------------- | +| [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No | experimental +| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No | experimental +| [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | No | experimental +| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No | experimental +| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | No | experimental +| [opentelemetry-instrumentation-asyncio](./opentelemetry-instrumentation-asyncio) | asyncio | No | experimental +| [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No | experimental +| [opentelemetry-instrumentation-aws-lambda](./opentelemetry-instrumentation-aws-lambda) | aws_lambda | No | experimental +| [opentelemetry-instrumentation-boto](./opentelemetry-instrumentation-boto) | boto~=2.0 | No | experimental +| [opentelemetry-instrumentation-boto3sqs](./opentelemetry-instrumentation-boto3sqs) | boto3 ~= 1.0 | No | experimental +| [opentelemetry-instrumentation-botocore](./opentelemetry-instrumentation-botocore) | botocore ~= 1.0 | No | experimental +| [opentelemetry-instrumentation-cassandra](./opentelemetry-instrumentation-cassandra) | cassandra-driver ~= 3.25,scylla-driver ~= 3.25 | No | experimental +| [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No | experimental +| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.4.0 | No | experimental +| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental +| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental +| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental +| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental +| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental +| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration +| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental +| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | experimental +| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | experimental +| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0 | No | experimental +| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | experimental +| [opentelemetry-instrumentation-mysql](./opentelemetry-instrumentation-mysql) | mysql-connector-python ~= 8.0 | No | experimental +| [opentelemetry-instrumentation-mysqlclient](./opentelemetry-instrumentation-mysqlclient) | mysqlclient < 3 | No | experimental +| [opentelemetry-instrumentation-pika](./opentelemetry-instrumentation-pika) | pika >= 0.12.0 | No | experimental +| [opentelemetry-instrumentation-psycopg](./opentelemetry-instrumentation-psycopg) | psycopg >= 3.1.0 | No | experimental +| [opentelemetry-instrumentation-psycopg2](./opentelemetry-instrumentation-psycopg2) | psycopg2 >= 2.7.3.1 | No | experimental +| [opentelemetry-instrumentation-pymemcache](./opentelemetry-instrumentation-pymemcache) | pymemcache >= 1.3.5, < 5 | No | experimental +| [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No | experimental +| [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No | experimental +| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | experimental +| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | experimental +| [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No | experimental +| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration +| [opentelemetry-instrumentation-sklearn](./opentelemetry-instrumentation-sklearn) | scikit-learn ~= 0.24.0 | No | experimental +| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy | Yes | experimental +| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No | experimental +| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette ~= 0.13.0 | Yes | experimental +| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No | experimental +| [opentelemetry-instrumentation-threading](./opentelemetry-instrumentation-threading) | threading | No | experimental +| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes | experimental +| [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No | experimental +| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes | experimental +| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes | experimental +| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | migration \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/LICENSE b/instrumentation/opentelemetry-instrumentation-aio-pika/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/README.rst b/instrumentation/opentelemetry-instrumentation-aio-pika/README.rst index aa0f1a3f5c..bb5e46cf2f 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/README.rst +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/README.rst @@ -18,6 +18,6 @@ Installation References ---------- -* `OpenTelemetry Aio-pika instrumentation `_ +* `OpenTelemetry Aio-pika instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml index f328eab452..3907320999 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/pyproject.toml @@ -22,10 +22,11 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.5", - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -33,12 +34,6 @@ dependencies = [ instruments = [ "aio_pika >= 7.2.0, < 10.0.0", ] -test = [ - "opentelemetry-instrumentation-aio-pika[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "pytest", - "wrapt >= 1.0.0, < 2.0.0", -] [project.entry-points.opentelemetry_instrumentor] aio-pika = "opentelemetry.instrumentation.aio_pika:AioPikaInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/span_builder.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/span_builder.py index b73afa62b3..c62b1ea9bf 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/span_builder.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/span_builder.py @@ -47,8 +47,13 @@ def set_destination(self, destination: str): self._attributes[SpanAttributes.MESSAGING_DESTINATION] = destination def set_channel(self, channel: AbstractChannel): - connection = channel.connection - if getattr(connection, "connection", None): + if hasattr(channel, "_connection"): + # aio_rmq 9.1 and above removed the connection attribute from the abstract listings + connection = channel._connection + else: + # aio_rmq 9.0.5 and below + connection = channel.connection + if hasattr(connection, "connection"): # aio_rmq 7 url = connection.connection.url else: @@ -57,7 +62,7 @@ def set_channel(self, channel: AbstractChannel): self._attributes.update( { SpanAttributes.NET_PEER_NAME: url.host, - SpanAttributes.NET_PEER_PORT: url.port, + SpanAttributes.NET_PEER_PORT: url.port or 5672, } ) diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt new file mode 100644 index 0000000000..3744af85b1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt @@ -0,0 +1,21 @@ +aio-pika==7.2.0 +aiormq==6.2.3 +asgiref==3.7.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==24.0 +pamqp==3.1.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-aio-pika diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt new file mode 100644 index 0000000000..bb35e2e2ba --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt @@ -0,0 +1,21 @@ +aio-pika==8.3.0 +aiormq==6.6.4 +asgiref==3.7.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==24.0 +pamqp==3.2.1 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-aio-pika diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt new file mode 100644 index 0000000000..3b17c80c25 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt @@ -0,0 +1,21 @@ +aio-pika==9.0.5 +aiormq==6.7.1 +asgiref==3.7.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==24.0 +pamqp==3.2.1 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-aio-pika diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt new file mode 100644 index 0000000000..4ea66f8dd6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt @@ -0,0 +1,21 @@ +aio-pika==9.4.1 +aiormq==6.8.0 +asgiref==3.7.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==24.0 +pamqp==3.3.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-aio-pika diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py b/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py index 41cd11d5a6..90a029531d 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py @@ -75,9 +75,7 @@ def _test_publish(self, exchange_type: Type[Exchange]): with mock.patch.object( PublishDecorator, "_get_publish_span" ) as mock_get_publish_span: - with mock.patch.object( - Exchange, "publish", return_value=asyncio.sleep(0) - ) as mock_publish: + with mock.patch.object(Exchange, "publish") as mock_publish: decorated_publish = PublishDecorator( self.tracer, exchange ).decorate(mock_publish) @@ -101,9 +99,7 @@ def _test_publish_works_with_not_recording_span(self, exchange_type): mocked_not_recording_span = MagicMock() mocked_not_recording_span.is_recording.return_value = False mock_get_publish_span.return_value = mocked_not_recording_span - with mock.patch.object( - Exchange, "publish", return_value=asyncio.sleep(0) - ) as mock_publish: + with mock.patch.object(Exchange, "publish") as mock_publish: with mock.patch( "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" ) as mock_inject: @@ -158,9 +154,7 @@ def _test_publish(self, exchange_type: Type[Exchange]): with mock.patch.object( PublishDecorator, "_get_publish_span" ) as mock_get_publish_span: - with mock.patch.object( - Exchange, "publish", return_value=asyncio.sleep(0) - ) as mock_publish: + with mock.patch.object(Exchange, "publish") as mock_publish: decorated_publish = PublishDecorator( self.tracer, exchange ).decorate(mock_publish) @@ -184,9 +178,7 @@ def _test_publish_works_with_not_recording_span(self, exchange_type): mocked_not_recording_span = MagicMock() mocked_not_recording_span.is_recording.return_value = False mock_get_publish_span.return_value = mocked_not_recording_span - with mock.patch.object( - Exchange, "publish", return_value=asyncio.sleep(0) - ) as mock_publish: + with mock.patch.object(Exchange, "publish") as mock_publish: with mock.patch( "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" ) as mock_inject: diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml index 9aac42647a..f8b4750872 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,10 +36,6 @@ dependencies = [ instruments = [ "aiohttp ~= 3.0", ] -test = [ - "opentelemetry-instrumentation-aiohttp-client[instruments]", - "http-server-mock" -] [project.entry-points.opentelemetry_instrumentor] aiohttp-client = "opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index f7a124c7b8..deef26c62f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt new file mode 100644 index 0000000000..f597361598 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt @@ -0,0 +1,35 @@ +aiohttp==3.9.4 +aiosignal==1.3.1 +asgiref==3.7.2 +async-timeout==4.0.3 +blinker==1.7.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +Deprecated==1.2.14 +Flask==3.0.2 +frozenlist==1.4.1 +http_server_mock==1.7 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +multidict==6.0.5 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +Werkzeug==3.0.3 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-aiohttp-client diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 2fa97f40b0..b6fe1eb57a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -14,7 +14,6 @@ import asyncio import contextlib -import sys import typing import unittest import urllib.parse @@ -117,10 +116,6 @@ def test_status_codes(self): ) url = f"http://{host}:{port}/test-path?query=param#foobar" - # if python version is < 3.8, then the url will be - if sys.version_info[1] < 8: - url = f"http://{host}:{port}/test-path#foobar" - self.assert_spans( [ ( diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml index 566f9dbb35..cd5516c8f6 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml @@ -21,13 +21,14 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11" + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,11 +36,6 @@ dependencies = [ instruments = [ "aiohttp ~= 3.0", ] -test = [ - "opentelemetry-instrumentation-aiohttp-server[instruments]", - "pytest-asyncio", - "pytest-aiohttp", -] [project.entry-points.opentelemetry_instrumentor] aiohttp-server = "opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py index 914f005b2a..2e519ac1c5 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -19,12 +19,14 @@ from aiohttp import web from multidict import CIMultiDictProxy -from opentelemetry import context, metrics, trace -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry import metrics, trace from opentelemetry.instrumentation.aiohttp_server.package import _instruments from opentelemetry.instrumentation.aiohttp_server.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.instrumentation.utils import ( + http_status_to_status_code, + is_http_instrumentation_enabled, +) from opentelemetry.propagate import extract from opentelemetry.propagators.textmap import Getter from opentelemetry.semconv.metrics import MetricInstruments @@ -191,10 +193,8 @@ def keys(self, carrier: Dict) -> List: @web.middleware async def middleware(request, handler): """Middleware for aiohttp implementing tracing logic""" - if ( - context.get_value("suppress_instrumentation") - or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) - or _excluded_urls.url_disabled(request.url.path) + if not is_http_instrumentation_enabled() or _excluded_urls.url_disabled( + request.url.path ): return await handler(request) @@ -207,7 +207,7 @@ async def middleware(request, handler): duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration of the inbound HTTP request", + description="Duration of HTTP client requests.", ) active_requests_counter = meter.create_up_down_counter( diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt new file mode 100644 index 0000000000..fe7582a2bb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt @@ -0,0 +1,25 @@ +aiohttp==3.9.4 +aiosignal==1.3.1 +asgiref==3.7.2 +async-timeout==4.0.3 +Deprecated==1.2.14 +frozenlist==1.4.1 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-aiohttp==1.0.5 +pytest-asyncio==0.23.5 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-aiohttp-server diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py index b5e8ec468f..e9dfb11389 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py @@ -23,6 +23,7 @@ from opentelemetry.instrumentation.aiohttp_server import ( AioHttpServerInstrumentor, ) +from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.globals_test import reset_trace_globals from opentelemetry.test.test_base import TestBase @@ -64,16 +65,25 @@ async def default_handler(request, status=200): return aiohttp.web.Response(status=status) +@pytest.fixture(name="suppress") +def fixture_suppress(): + return False + + @pytest_asyncio.fixture(name="server_fixture") -async def fixture_server_fixture(tracer, aiohttp_server): +async def fixture_server_fixture(tracer, aiohttp_server, suppress): _, memory_exporter = tracer AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() app.add_routes([aiohttp.web.get("/test-path", default_handler)]) + if suppress: + with suppress_http_instrumentation(): + server = await aiohttp_server(app) + else: + server = await aiohttp_server(app) - server = await aiohttp_server(app) yield server, app memory_exporter.clear() @@ -128,3 +138,18 @@ async def test_status_code_instrumentation( f"http://{server.host}:{server.port}{url}" == span.attributes[SpanAttributes.HTTP_URL] ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("suppress", [True]) +async def test_suppress_instrumentation( + tracer, server_fixture, aiohttp_client +): + _, memory_exporter = tracer + server, _ = server_fixture + assert len(memory_exporter.get_finished_spans()) == 0 + + client = await aiohttp_client(server) + await client.get("/test-path") + + assert len(memory_exporter.get_finished_spans()) == 0 diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE b/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-aiopg/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml index ebb88ee0ba..e9e7f7f613 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aiopg/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -34,11 +35,6 @@ dependencies = [ instruments = [ "aiopg >= 0.13.0, < 2.0.0", ] -test = [ - "opentelemetry-instrumentation-aiopg[instruments]", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] aiopg = "opentelemetry.instrumentation.aiopg:AiopgInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt new file mode 100644 index 0000000000..033cee4492 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt @@ -0,0 +1,20 @@ +aiopg==1.4.0 +asgiref==3.7.2 +async-timeout==4.0.3 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +install==1.3.5 +packaging==24.0 +pluggy==1.5.0 +psycopg2-binary==2.9.9 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-aiopg diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py index c6d00a51fe..c497ae4564 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -17,6 +17,7 @@ from unittest.mock import MagicMock import aiopg +import psycopg2 import opentelemetry.instrumentation.aiopg from opentelemetry import trace as trace_api @@ -76,7 +77,7 @@ def test_instrumentor_connect(self): cnx = async_call(aiopg.connect(database="test")) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" - cursor.execute(query) + async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @@ -127,7 +128,7 @@ def test_instrumentor_create_pool(self): cnx = async_call(pool.acquire()) cursor = async_call(cnx.cursor()) query = "SELECT * FROM test" - cursor.execute(query) + async_call(cursor.execute(query)) spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) @@ -384,7 +385,9 @@ def test_span_failed(self): span.attributes[SpanAttributes.DB_STATEMENT], "Test query" ) self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) - self.assertEqual(span.status.description, "Exception: Test Exception") + self.assertEqual( + span.status.description, "ProgrammingError: Test Exception" + ) def test_executemany(self): db_integration = AiopgIntegration(self.tracer, "testcomponent") @@ -570,17 +573,17 @@ class MockCursor: # pylint: disable=unused-argument, no-self-use async def execute(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument, no-self-use async def executemany(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") # pylint: disable=unused-argument, no-self-use async def callproc(self, query, params=None, throw_exception=False): if throw_exception: - raise Exception("Test Exception") + raise psycopg2.ProgrammingError("Test Exception") def close(self): pass diff --git a/instrumentation/opentelemetry-instrumentation-asgi/LICENSE b/instrumentation/opentelemetry-instrumentation-asgi/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-asgi/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml index d642a07e48..d733700a15 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-asgi/pyproject.toml @@ -22,23 +22,20 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "asgiref ~= 3.0", "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "asgiref ~= 3.0", ] -test = [ - "opentelemetry-instrumentation-asgi[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asgi" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index dba7414cb1..f24160fcb6 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -81,15 +81,15 @@ async def hello(): .. code-block:: python - def server_request_hook(span: Span, scope: dict): + def server_request_hook(span: Span, scope: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") - def client_request_hook(span: Span, scope: dict): + def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") - def client_response_hook(span: Span, message: dict): + def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") @@ -129,10 +129,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -163,10 +163,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** @@ -193,13 +193,19 @@ def client_response_hook(span: Span, message: dict): import typing import urllib +from collections import defaultdict from functools import wraps from timeit import default_timer -from typing import Any, Awaitable, Callable, Tuple, cast +from typing import Any, Awaitable, Callable, DefaultDict, Tuple from asgiref.compatibility import guarantee_single_callable from opentelemetry import context, trace +from opentelemetry.instrumentation.asgi.types import ( + ClientRequestHook, + ClientResponseHook, + ServerRequestHook, +) from opentelemetry.instrumentation.asgi.version import __version__ # noqa from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, @@ -212,7 +218,7 @@ def client_response_hook(span: Span, message: dict): from opentelemetry.propagators.textmap import Getter, Setter from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import Span, set_span_in_context +from opentelemetry.trace import set_span_in_context from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, @@ -227,10 +233,6 @@ def client_response_hook(span: Span, message: dict): remove_url_credentials, ) -_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]] - class ASGIGetter(Getter[dict]): def get( @@ -339,19 +341,20 @@ def collect_custom_headers_attributes( sanitize: SanitizeValue, header_regexes: list[str], normalize_names: Callable[[str], str], -) -> dict[str, str]: +) -> dict[str, list[str]]: """ Returns custom HTTP request or response headers to be added into SERVER span as span attributes. Refer specifications: - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers """ - # Decode headers before processing. - headers: dict[str, str] = { - _key.decode("utf8"): _value.decode("utf8") - for (_key, _value) in scope_or_response_message.get("headers") - or cast("list[tuple[bytes, bytes]]", []) - } + headers: DefaultDict[str, list[str]] = defaultdict(list) + raw_headers = scope_or_response_message.get("headers") + if raw_headers: + for key, value in raw_headers: + # Decode headers before processing. + headers[key.decode()].append(value.decode()) + return sanitize.sanitize_header_values( headers, header_regexes, @@ -448,12 +451,14 @@ class OpenTelemetryMiddleware: Optional: Defaults to get_default_span_details. server_request_hook: Optional callback which is called with the server span and ASGI scope object for every incoming request. - client_request_hook: Optional callback which is called with the internal span and an ASGI - scope which is sent as a dictionary for when the method receive is called. - client_response_hook: Optional callback which is called with the internal span and an ASGI - event which is sent as a dictionary for when the method send is called. + client_request_hook: Optional callback which is called with the internal span, and ASGI + scope and event which are sent as dictionaries for when the method receive is called. + client_response_hook: Optional callback which is called with the internal span, and ASGI + scope and event which are sent as dictionaries for when the method send is called. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. + meter_provider: The optional meter provider to use. If omitted + the current globally configured one is used. """ # pylint: disable=too-many-branches @@ -462,22 +467,27 @@ def __init__( app, excluded_urls=None, default_span_details=None, - server_request_hook: _ServerRequestHookT = None, - client_request_hook: _ClientRequestHookT = None, - client_response_hook: _ClientResponseHookT = None, + server_request_hook: ServerRequestHook = None, + client_request_hook: ClientRequestHook = None, + client_response_hook: ClientResponseHook = None, tracer_provider=None, meter_provider=None, + tracer=None, meter=None, http_capture_headers_server_request: list[str] | None = None, http_capture_headers_server_response: list[str] | None = None, http_capture_headers_sanitize_fields: list[str] | None = None, ): self.app = guarantee_single_callable(app) - self.tracer = trace.get_tracer( - __name__, - __version__, - tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + self.tracer = ( + trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) + if tracer is None + else tracer ) self.meter = ( get_meter( @@ -492,7 +502,7 @@ def __init__( self.duration_histogram = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration of the inbound HTTP request", + description="Duration of HTTP client requests.", ) self.server_response_size_histogram = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, @@ -660,13 +670,15 @@ async def otel_receive(): with self.tracer.start_as_current_span( " ".join((server_span_name, scope["type"], "receive")) ) as receive_span: - if callable(self.client_request_hook): - self.client_request_hook(receive_span, scope) message = await receive() + if callable(self.client_request_hook): + self.client_request_hook(receive_span, scope, message) if receive_span.is_recording(): if message["type"] == "websocket.receive": set_status_code(receive_span, 200) - receive_span.set_attribute("type", message["type"]) + receive_span.set_attribute( + "asgi.event.type", message["type"] + ) return message return otel_receive @@ -683,13 +695,13 @@ async def otel_send(message: dict[str, Any]): " ".join((server_span_name, scope["type"], "send")) ) as send_span: if callable(self.client_response_hook): - self.client_response_hook(send_span, message) + self.client_response_hook(send_span, scope, message) if send_span.is_recording(): if message["type"] == "http.response.start": status_code = message["status"] - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = status_code + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( + status_code + ) set_status_code(server_span, status_code) set_status_code(send_span, status_code) @@ -697,7 +709,7 @@ async def otel_send(message: dict[str, Any]): elif message["type"] == "websocket.send": set_status_code(server_span, 200) set_status_code(send_span, 200) - send_span.set_attribute("type", message["type"]) + send_span.set_attribute("asgi.event.type", message["type"]) if ( server_span.is_recording() and server_span.kind == trace.SpanKind.SERVER diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/types.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/types.py new file mode 100644 index 0000000000..bc0c11afc9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/types.py @@ -0,0 +1,49 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Callable, Dict, Optional + +from opentelemetry.trace import Span + +_Scope = Dict[str, Any] +_Message = Dict[str, Any] + +ServerRequestHook = Optional[Callable[[Span, _Scope], None]] +""" +Incoming request callback type. + +Args: + - Server span + - ASGI scope as a mapping +""" + +ClientRequestHook = Optional[Callable[[Span, _Scope, _Message], None]] +""" +Receive callback type. + +Args: + - Internal span + - ASGI scope as a mapping + - ASGI event as a mapping +""" + +ClientResponseHook = Optional[Callable[[Span, _Scope, _Message], None]] +""" +Send callback type. + +Args: + - Internal span + - ASGI scope as a mapping + - ASGI event as a mapping +""" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt new file mode 100644 index 0000000000..9411be3b90 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-asgi diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py index c50839f72f..5394d62ff0 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -40,6 +40,24 @@ async def http_app_with_custom_headers(scope, receive, send): await send({"type": "http.response.body", "body": b"*"}) +async def http_app_with_repeat_headers(scope, receive, send): + message = await receive() + assert scope["type"] == "http" + if message.get("type") == "http.request": + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"Content-Type", b"text/plain"), + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-1", b"test-header-value-2"), + ], + } + ) + await send({"type": "http.response.body", "body": b"*"}) + + async def websocket_app_with_custom_headers(scope, receive, send): assert scope["type"] == "websocket" while True: @@ -121,6 +139,26 @@ def test_http_custom_request_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) + def test_http_repeat_request_headers_in_span_attributes(self): + self.scope["headers"].extend( + [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-1", b"test-header-value-2"), + ] + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + "test-header-value-2", + ), + } + span = next(span for span in span_list if span.kind == SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_http_custom_request_headers_not_in_span_attributes(self): self.scope["headers"].extend( [ @@ -176,6 +214,25 @@ def test_http_custom_response_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) + def test_http_repeat_response_headers_in_span_attributes(self): + self.app = otel_asgi.OpenTelemetryMiddleware( + http_app_with_repeat_headers, + tracer_provider=self.tracer_provider, + **self.constructor_params, + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + "test-header-value-2", + ), + } + span = next(span for span in span_list if span.kind == SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_http_custom_response_headers_not_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 0b2a844d96..d2fe6bc52b 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -268,20 +268,20 @@ def validate_outputs(self, outputs, error=None, modifiers=None): { "name": "GET / http receive", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.request"}, + "attributes": {"asgi.event.type": "http.request"}, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { SpanAttributes.HTTP_STATUS_CODE: 200, - "type": "http.response.start", + "asgi.event.type": "http.response.start", }, }, { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.response.body"}, + "attributes": {"asgi.event.type": "http.response.body"}, }, { "name": "GET /", @@ -309,6 +309,10 @@ def validate_outputs(self, outputs, error=None, modifiers=None): self.assertEqual(span.name, expected["name"]) self.assertEqual(span.kind, expected["kind"]) self.assertDictEqual(dict(span.attributes), expected["attributes"]) + self.assertEqual( + span.instrumentation_scope.name, + "opentelemetry.instrumentation.asgi", + ) def test_basic_asgi_call(self): """Test that spans are emitted as expected.""" @@ -358,7 +362,7 @@ def add_more_body_spans(expected: list): more_body_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.response.body"}, + "attributes": {"asgi.event.type": "http.response.body"}, } extra_spans = [more_body_span] * 3 expected[2:2] = extra_spans @@ -396,12 +400,12 @@ def add_body_and_trailer_span(expected: list): body_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.response.body"}, + "attributes": {"asgi.event.type": "http.response.body"}, } trailer_span = { "name": "GET / http send", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "http.response.trailers"}, + "attributes": {"asgi.event.type": "http.response.trailers"}, } expected[2:2] = [body_span] expected[4:4] = [trailer_span] * 2 @@ -582,18 +586,18 @@ def test_websocket(self): { "name": "/ websocket receive", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "websocket.connect"}, + "attributes": {"asgi.event.type": "websocket.connect"}, }, { "name": "/ websocket send", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "websocket.accept"}, + "attributes": {"asgi.event.type": "websocket.accept"}, }, { "name": "/ websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { - "type": "websocket.receive", + "asgi.event.type": "websocket.receive", SpanAttributes.HTTP_STATUS_CODE: 200, }, }, @@ -601,14 +605,14 @@ def test_websocket(self): "name": "/ websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { - "type": "websocket.send", + "asgi.event.type": "websocket.send", SpanAttributes.HTTP_STATUS_CODE: 200, }, }, { "name": "/ websocket receive", "kind": trace_api.SpanKind.INTERNAL, - "attributes": {"type": "websocket.disconnect"}, + "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { "name": "/", @@ -683,10 +687,10 @@ def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") - def client_request_hook(recieve_span, request): - recieve_span.update_name("name from client request hook") + def client_request_hook(receive_span, scope, message): + receive_span.update_name("name from client request hook") - def client_response_hook(send_span, response): + def client_response_hook(send_span, scope, message): send_span.set_attribute("attr-from-hook", "value") def update_expected_hook_results(expected): @@ -728,6 +732,10 @@ def test_asgi_metrics(self): self.assertTrue(len(resource_metric.scope_metrics) != 0) for scope_metric in resource_metric.scope_metrics: self.assertTrue(len(scope_metric.metrics) != 0) + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.asgi", + ) for metric in scope_metric.metrics: self.assertIn(metric.name, _expected_metric_names) data_points = list(metric.data.data_points) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE b/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml index 358fbefeb4..2a53899ecd 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml @@ -22,23 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.14", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-test-utils == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-test-utils == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-instrumentation-asyncio[instruments]", - "pytest", - "wrapt >= 1.0.0, < 2.0.0", - "pytest-asyncio", -] [project.entry-points.opentelemetry_instrumentor] asyncio = "opentelemetry.instrumentation.asyncio:AsyncioInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py index 68e3d0839f..fc1b535270 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -116,21 +116,11 @@ class AsyncioInstrumentor(BaseInstrumentor): "run_coroutine_threadsafe", ] - def __init__(self): - super().__init__() - self.process_duration_histogram = None - self.process_created_counter = None - - self._tracer = None - self._meter = None - self._coros_name_to_trace: set = set() - self._to_thread_name_to_trace: set = set() - self._future_active_enabled: bool = False - def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): + # pylint: disable=attribute-defined-outside-init self._tracer = get_tracer( __name__, __version__, kwargs.get("tracer_provider") ) @@ -271,6 +261,8 @@ def trace_item(self, coro_or_future): return coro_or_future async def trace_coroutine(self, coro): + if not hasattr(coro, "__name__"): + return coro start = default_timer() attr = { "type": "coroutine", @@ -307,13 +299,17 @@ def trace_future(self, future): ) def callback(f): - exception = f.exception() attr = { "type": "future", + "state": ( + "cancelled" + if f.cancelled() + else determine_state(f.exception()) + ), } - state = determine_state(exception) - attr["state"] = state - self.record_process(start, attr, span, exception) + self.record_process( + start, attr, span, None if f.cancelled() else f.exception() + ) future.add_done_callback(callback) return future diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt new file mode 100644 index 0000000000..4943fcc851 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-asyncio==0.23.5 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-asyncio diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py new file mode 100644 index 0000000000..9ce3fc4b33 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_anext.py @@ -0,0 +1,56 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + + +class TestAsyncioAnext(TestBase): + @patch.dict( + "os.environ", + {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"}, + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + # Asyncio anext() does not have __name__ attribute, which is used to determine if the coroutine should be traced. + # This test is to ensure that the instrumentation does not break when the coroutine does not have __name__ attribute. + def test_asyncio_anext(self): + async def main(): + async def async_gen(): + for it in range(2): + yield it + + async_gen_instance = async_gen() + agen = anext(async_gen_instance) + await asyncio.create_task(agen) + + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py new file mode 100644 index 0000000000..f8f4e5f230 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py @@ -0,0 +1,60 @@ +import asyncio +from unittest.mock import patch + +from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + + +class TestTraceFuture(TestBase): + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"} + ) + def setUp(self): + super().setUp() + self._tracer = get_tracer( + __name__, + ) + self.instrumentor = AsyncioInstrumentor() + self.instrumentor.instrument() + + def tearDown(self): + super().tearDown() + self.instrumentor.uninstrument() + + def test_trace_future_cancelled(self): + async def future_cancelled(): + with self._tracer.start_as_current_span("root"): + future = asyncio.Future() + future = self.instrumentor.trace_future(future) + future.cancel() + + try: + asyncio.run(future_cancelled()) + except asyncio.CancelledError as exc: + self.assertEqual(isinstance(exc, asyncio.CancelledError), True) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + self.assertEqual(spans[0].name, "root") + self.assertEqual(spans[1].name, "asyncio future") + + metrics = ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ) + self.assertEqual(len(metrics), 2) + + self.assertEqual(metrics[0].name, "asyncio.process.duration") + self.assertEqual( + metrics[0].data.data_points[0].attributes["state"], "cancelled" + ) + + self.assertEqual(metrics[1].name, "asyncio.process.created") + self.assertEqual( + metrics[1].data.data_points[0].attributes["state"], "cancelled" + ) diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/LICENSE b/instrumentation/opentelemetry-instrumentation-asyncpg/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml index bd9e26bbe4..f5d4ffd1f6 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "asyncpg >= 0.12.0", ] -test = [ - "opentelemetry-instrumentation-asyncpg[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] asyncpg = "opentelemetry.instrumentation.asyncpg:AsyncPGInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index 11c579f96a..798a5dc00b 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -77,14 +77,14 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict: if isinstance(addr, tuple): span_attributes[SpanAttributes.NET_PEER_NAME] = addr[0] span_attributes[SpanAttributes.NET_PEER_PORT] = addr[1] - span_attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.IP_TCP.value + span_attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.IP_TCP.value + ) elif isinstance(addr, str): span_attributes[SpanAttributes.NET_PEER_NAME] = addr - span_attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.OTHER.value + span_attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.OTHER.value + ) if query is not None: span_attributes[SpanAttributes.DB_STATEMENT] = query diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt new file mode 100644 index 0000000000..2ef86b3d94 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +async-timeout==4.0.3 +asyncpg==0.29.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-asyncpg diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE b/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml index 6ab6e23ffc..cbed1edb9e 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml @@ -19,18 +19,19 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.45b0.dev", -] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aws-lambda" diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py index 130622f8c8..4acf4dea90 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py @@ -306,9 +306,11 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches disable_aws_context_propagation, ) - span_kind = None try: - if lambda_event["Records"][0]["eventSource"] in { + event_source = lambda_event["Records"][0].get( + "eventSource" + ) or lambda_event["Records"][0].get("EventSource") + if event_source in { "aws:sqs", "aws:s3", "aws:sns", @@ -340,21 +342,32 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches if span.is_recording(): lambda_context = args[1] # NOTE: The specs mention an exception here, allowing the - # `ResourceAttributes.FAAS_ID` attribute to be set as a span + # `SpanAttributes.CLOUD_RESOURCE_ID` attribute to be set as a span # attribute instead of a resource attribute. # # See more: - # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#example + # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#resource-detector span.set_attribute( - ResourceAttributes.FAAS_ID, + SpanAttributes.CLOUD_RESOURCE_ID, lambda_context.invoked_function_arn, ) span.set_attribute( - SpanAttributes.FAAS_EXECUTION, + SpanAttributes.FAAS_INVOCATION_ID, lambda_context.aws_request_id, ) + # NOTE: `cloud.account.id` can be parsed from the ARN as the fifth item when splitting on `:` + # + # See more: + # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#all-triggers + account_id = lambda_context.invoked_function_arn.split(":")[4] + span.set_attribute( + ResourceAttributes.CLOUD_ACCOUNT_ID, + account_id, + ) + exception = None + result = None try: result = call_wrapped(*args, **kwargs) except Exception as exc: # pylint: disable=W0703 diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt new file mode 100644 index 0000000000..b5168dc7fe --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e propagator/opentelemetry-propagator-aws-xray +-e instrumentation/opentelemetry-instrumentation-aws-lambda diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py index a878d0f06a..539c896a0b 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -22,4 +22,5 @@ def rest_api_handler(event, context): def handler_exc(event, context): + # pylint: disable=broad-exception-raised raise Exception("500 internal server error") diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py index 2fa4aafee5..ecce9ea12c 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py @@ -17,13 +17,6 @@ from typing import Any, Callable, Dict from unittest import mock -from tests.mocks.api_gateway_http_api_event import ( - MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT, -) -from tests.mocks.api_gateway_proxy_event import ( - MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT, -) - from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.instrumentation.aws_lambda import ( _HANDLER, @@ -45,6 +38,11 @@ TraceContextTextMapPropagator, ) +from .mocks.api_gateway_http_api_event import ( + MOCK_LAMBDA_API_GATEWAY_HTTP_API_EVENT, +) +from .mocks.api_gateway_proxy_event import MOCK_LAMBDA_API_GATEWAY_PROXY_EVENT + class MockLambdaContext: def __init__(self, aws_request_id, invoked_function_arn): @@ -54,7 +52,7 @@ def __init__(self, aws_request_id, invoked_function_arn): MOCK_LAMBDA_CONTEXT = MockLambdaContext( aws_request_id="mock_aws_request_id", - invoked_function_arn="arn://mock-lambda-function-arn", + invoked_function_arn="arn:aws:lambda:us-east-1:123456:function:myfunction:myalias", ) MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C @@ -145,8 +143,13 @@ def test_active_tracing(self): self.assertSpanHasAttributes( span, { - ResourceAttributes.FAAS_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, - SpanAttributes.FAAS_EXECUTION: MOCK_LAMBDA_CONTEXT.aws_request_id, + SpanAttributes.CLOUD_RESOURCE_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, + SpanAttributes.FAAS_INVOCATION_ID: MOCK_LAMBDA_CONTEXT.aws_request_id, + ResourceAttributes.CLOUD_ACCOUNT_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn.split( + ":" + )[ + 4 + ], }, ) @@ -346,12 +349,43 @@ def test_lambda_handles_multiple_consumers(self): mock_execute_lambda({"Records": [{"eventSource": "aws:sqs"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:s3"}]}) - mock_execute_lambda({"Records": [{"eventSource": "aws:sns"}]}) + mock_execute_lambda({"Records": [{"EventSource": "aws:sns"}]}) mock_execute_lambda({"Records": [{"eventSource": "aws:dynamodb"}]}) spans = self.memory_exporter.get_finished_spans() assert spans + assert len(spans) == 4 + + for span in spans: + assert span.kind == SpanKind.CONSUMER + + test_env_patch.stop() + + def test_lambda_handles_invalid_event_source(self): + test_env_patch = mock.patch.dict( + "os.environ", + { + **os.environ, + # NOT Active Tracing + _X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED, + # NOT using the X-Ray Propagator + OTEL_PROPAGATORS: "tracecontext", + }, + ) + test_env_patch.start() + + AwsLambdaInstrumentor().instrument() + + mock_execute_lambda({"Records": [{"eventSource": "invalid_source"}]}) + + spans = self.memory_exporter.get_finished_spans() + + assert spans + assert len(spans) == 1 + assert ( + spans[0].kind == SpanKind.SERVER + ) # Default to SERVER for unknown sources test_env_patch.stop() @@ -431,6 +465,31 @@ def test_lambda_handles_handler_exception(self): exc_env_patch.stop() + def test_lambda_handles_handler_exception_with_api_gateway_proxy_event( + self, + ): + exc_env_patch = mock.patch.dict( + "os.environ", + {_HANDLER: "tests.mocks.lambda_function.handler_exc"}, + ) + exc_env_patch.start() + AwsLambdaInstrumentor().instrument() + # instrumentor re-raises the exception + with self.assertRaises(Exception): + mock_execute_lambda( + {"requestContext": {"http": {"method": "GET"}}} + ) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + + exc_env_patch.stop() + def test_uninstrument(self): AwsLambdaInstrumentor().instrument() diff --git a/instrumentation/opentelemetry-instrumentation-boto/LICENSE b/instrumentation/opentelemetry-instrumentation-boto/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-boto/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml b/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml index 307a96e5aa..8ddd1f3884 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-boto/pyproject.toml @@ -25,20 +25,14 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "boto~=2.0", ] -test = [ - "opentelemetry-instrumentation-boto[instruments]", - "markupsafe==2.0.1", - "moto~=2.0", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] boto = "opentelemetry.instrumentation.boto:BotoInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt new file mode 100644 index 0000000000..54c8bb0558 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt @@ -0,0 +1,39 @@ +asgiref==3.7.2 +boto==2.49.0 +boto3==1.34.44 +botocore==1.34.44 +certifi==2024.2.2 +cffi==1.16.0 +charset-normalizer==3.3.2 +cryptography==42.0.5 +Deprecated==1.2.14 +docker==7.0.0 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +Jinja2==3.1.4 +jmespath==1.0.1 +MarkupSafe==2.1.5 +moto==2.3.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pycparser==2.21 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.32.3 +responses==0.25.0 +s3transfer==0.10.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==1.26.19 +Werkzeug==2.3.8 +wrapt==1.16.0 +xmltodict==0.13.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-boto diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/LICENSE b/instrumentation/opentelemetry-instrumentation-boto3sqs/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml b/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml index 89e1e112d8..e8a2196483 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -34,10 +35,6 @@ dependencies = [ instruments = [ "boto3 ~= 1.0", ] -test = [ - "opentelemetry-instrumentation-boto3sqs[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] boto3 = "opentelemetry.instrumentation.boto3sqs:Boto3SQSInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py index ee7f4a59a6..c0231f81e4 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py @@ -31,7 +31,7 @@ import logging from typing import Any, Collection, Dict, Generator, List, Mapping, Optional -import boto3 +import boto3.session import botocore.client from wrapt import wrap_function_wrapper @@ -382,7 +382,7 @@ def client_wrapper(wrapped, instance, args, kwargs): self._decorate_sqs(type(retval)) return retval - wrap_function_wrapper(boto3, "client", client_wrapper) + wrap_function_wrapper(boto3.session.Session, "client", client_wrapper) def _decorate_sqs(self, sqs_class: type) -> None: """ @@ -433,7 +433,7 @@ def _instrument(self, **kwargs: Dict[str, Any]) -> None: self._decorate_sqs(client_cls) def _uninstrument(self, **kwargs: Dict[str, Any]) -> None: - unwrap(boto3, "client") + unwrap(boto3.session.Session, "client") for client_cls in botocore.client.BaseClient.__subclasses__(): self._un_decorate_sqs(client_cls) diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt new file mode 100644 index 0000000000..54fcf790d7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +boto3==1.34.44 +botocore==1.34.44 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +jmespath==1.0.1 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +s3transfer==0.10.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==1.26.19 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-boto3sqs diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/tests/test_boto3sqs_instrumentation.py b/instrumentation/opentelemetry-instrumentation-boto3sqs/tests/test_boto3sqs_instrumentation.py index a6ca0e062b..7f7f00cf0a 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/tests/test_boto3sqs_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/tests/test_boto3sqs_instrumentation.py @@ -20,7 +20,7 @@ import boto3 from botocore.awsrequest import AWSResponse -from wrapt import BoundFunctionWrapper, FunctionWrapper +from wrapt import BoundFunctionWrapper from opentelemetry.instrumentation.boto3sqs import ( Boto3SQSGetter, @@ -37,8 +37,17 @@ from opentelemetry.trace.span import Span, format_span_id, format_trace_id -def _make_sqs_client(): - return boto3.client( +def _make_sqs_client(*, session=False): + return (boto3.Session() if session else boto3).client( + "sqs", + region_name="us-east-1", + aws_access_key_id="dummy", + aws_secret_access_key="dummy", + ) + + +def _make_sqs_resource(*, session=False): + return (boto3.Session() if session else boto3).resource( "sqs", region_name="us-east-1", aws_access_key_id="dummy", @@ -48,7 +57,6 @@ def _make_sqs_client(): class TestBoto3SQSInstrumentor(TestCase): def _assert_instrumented(self, client): - self.assertIsInstance(boto3.client, FunctionWrapper) self.assertIsInstance(client.send_message, BoundFunctionWrapper) self.assertIsInstance(client.send_message_batch, BoundFunctionWrapper) self.assertIsInstance(client.receive_message, BoundFunctionWrapper) @@ -57,6 +65,17 @@ def _assert_instrumented(self, client): client.delete_message_batch, BoundFunctionWrapper ) + def _assert_uninstrumented(self, client): + self.assertNotIsInstance(client.send_message, BoundFunctionWrapper) + self.assertNotIsInstance( + client.send_message_batch, BoundFunctionWrapper + ) + self.assertNotIsInstance(client.receive_message, BoundFunctionWrapper) + self.assertNotIsInstance(client.delete_message, BoundFunctionWrapper) + self.assertNotIsInstance( + client.delete_message_batch, BoundFunctionWrapper + ) + @staticmethod @contextmanager def _active_instrumentor(): @@ -67,19 +86,48 @@ def _active_instrumentor(): Boto3SQSInstrumentor().uninstrument() def test_instrument_api_before_client_init(self) -> None: - with self._active_instrumentor(): - client = _make_sqs_client() - self._assert_instrumented(client) + for session in (False, True): + with self._active_instrumentor(): + client = _make_sqs_client(session=session) + self._assert_instrumented(client) + self._assert_uninstrumented(client) def test_instrument_api_after_client_init(self) -> None: - client = _make_sqs_client() - with self._active_instrumentor(): - self._assert_instrumented(client) + for session in (False, True): + client = _make_sqs_client(session=session) + with self._active_instrumentor(): + self._assert_instrumented(client) + self._assert_uninstrumented(client) def test_instrument_multiple_clients(self): - with self._active_instrumentor(): - self._assert_instrumented(_make_sqs_client()) - self._assert_instrumented(_make_sqs_client()) + for session in (False, True): + with self._active_instrumentor(): + self._assert_instrumented(_make_sqs_client(session=session)) + self._assert_instrumented(_make_sqs_client(session=session)) + + def test_instrument_api_before_resource_init(self) -> None: + for session in (False, True): + with self._active_instrumentor(): + sqs = _make_sqs_resource(session=session) + self._assert_instrumented(sqs.meta.client) + self._assert_uninstrumented(sqs.meta.client) + + def test_instrument_api_after_resource_init(self) -> None: + for session in (False, True): + sqs = _make_sqs_resource(session=session) + with self._active_instrumentor(): + self._assert_instrumented(sqs.meta.client) + self._assert_uninstrumented(sqs.meta.client) + + def test_instrument_multiple_resources(self): + for session in (False, True): + with self._active_instrumentor(): + self._assert_instrumented( + _make_sqs_resource(session=session).meta.client + ) + self._assert_instrumented( + _make_sqs_resource(session=session).meta.client + ) class TestBoto3SQSGetter(TestCase): diff --git a/instrumentation/opentelemetry-instrumentation-botocore/LICENSE b/instrumentation/opentelemetry-instrumentation-botocore/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-botocore/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml index cd8add1aa7..6c92273301 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "opentelemetry-propagator-aws-xray == 1.0.1", ] @@ -34,13 +35,6 @@ dependencies = [ instruments = [ "botocore ~= 1.0", ] -test = [ - "opentelemetry-instrumentation-botocore[instruments]", - "markupsafe==2.0.1", - "botocore ~= 1.0, < 1.31.81", - "moto[all] ~= 2.2.6", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] botocore = "opentelemetry.instrumentation.botocore:BotocoreInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py index 36b973e318..0481b248aa 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py @@ -51,7 +51,7 @@ request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request -this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None +this function signature is: def response_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None for example: diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py index 299a37ab6c..57fb8b6794 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/lmbd.py @@ -62,9 +62,9 @@ def extract_attributes( cls, call_context: _AwsSdkCallContext, attributes: _AttributeMapT ): attributes[SpanAttributes.FAAS_INVOKED_PROVIDER] = "aws" - attributes[ - SpanAttributes.FAAS_INVOKED_NAME - ] = cls._parse_function_name(call_context) + attributes[SpanAttributes.FAAS_INVOKED_NAME] = ( + cls._parse_function_name(call_context) + ) attributes[SpanAttributes.FAAS_INVOKED_REGION] = call_context.region @classmethod diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sns.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sns.py index aa55ae697f..9c3df3a2bc 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sns.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sns.py @@ -76,9 +76,9 @@ def extract_attributes( destination_name, is_phone_number = cls._extract_destination_name( call_context ) - attributes[ - SpanAttributes.MESSAGING_DESTINATION_KIND - ] = MessagingDestinationKindValues.TOPIC.value + attributes[SpanAttributes.MESSAGING_DESTINATION_KIND] = ( + MessagingDestinationKindValues.TOPIC.value + ) attributes[SpanAttributes.MESSAGING_DESTINATION] = destination_name # TODO: Use SpanAttributes.MESSAGING_DESTINATION_NAME when opentelemetry-semantic-conventions 0.42b0 is released diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py index 777108cbb5..194e47b57f 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/sqs.py @@ -35,9 +35,9 @@ def extract_attributes(self, attributes: _AttributeMapT): attributes[SpanAttributes.MESSAGING_SYSTEM] = "aws.sqs" attributes[SpanAttributes.MESSAGING_URL] = queue_url try: - attributes[ - SpanAttributes.MESSAGING_DESTINATION - ] = queue_url.split("/")[-1] + attributes[SpanAttributes.MESSAGING_DESTINATION] = ( + queue_url.split("/")[-1] + ) except IndexError: _logger.error( "Could not extract messaging destination from '%s'", diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt new file mode 100644 index 0000000000..f9fe9abe8a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt @@ -0,0 +1,40 @@ +asgiref==3.7.2 +aws-xray-sdk==2.12.1 +boto3==1.28.80 +botocore==1.31.80 +certifi==2024.2.2 +cffi==1.16.0 +charset-normalizer==3.3.2 +cryptography==42.0.5 +Deprecated==1.2.14 +docker==7.0.0 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +Jinja2==3.1.4 +jmespath==1.0.1 +MarkupSafe==2.1.5 +moto==5.0.9 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pycparser==2.21 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.32.3 +responses==0.25.0 +s3transfer==0.7.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==1.26.19 +Werkzeug==3.0.3 +wrapt==1.16.0 +xmltodict==0.13.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e propagator/opentelemetry-propagator-aws-xray +-e instrumentation/opentelemetry-instrumentation-botocore diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py index 1b7f5bb0cb..2240baff3a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_dynamodb.py @@ -16,7 +16,7 @@ from unittest import mock import botocore.session -from moto import mock_dynamodb2 # pylint: disable=import-error +from moto import mock_aws # pylint: disable=import-error from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.instrumentation.botocore.extensions.dynamodb import ( @@ -184,7 +184,7 @@ def assert_extension_item_col_metrics(self, operation: str): ) self.assert_item_col_metrics(span) - @mock_dynamodb2 + @mock_aws def test_batch_get_item(self): table_name1 = "test_table1" table_name2 = "test_table2" @@ -203,7 +203,7 @@ def test_batch_get_item(self): self.assert_table_names(span, table_name1, table_name2) self.assert_consumed_capacity(span, table_name1, table_name2) - @mock_dynamodb2 + @mock_aws def test_batch_write_item(self): table_name1 = "test_table1" table_name2 = "test_table2" @@ -224,7 +224,7 @@ def test_batch_write_item(self): self.assert_consumed_capacity(span, table_name1, table_name2) self.assert_item_col_metrics(span) - @mock_dynamodb2 + @mock_aws def test_create_table(self): local_sec_idx = { "IndexName": "local_sec_idx", @@ -268,7 +268,7 @@ def test_create_table(self): ) self.assert_provisioned_read_cap(span, 42) - @mock_dynamodb2 + @mock_aws def test_delete_item(self): self._create_prepared_table() @@ -297,7 +297,7 @@ def test_delete_item_consumed_capacity(self): def test_delete_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("DeleteItem") - @mock_dynamodb2 + @mock_aws def test_delete_table(self): self._create_prepared_table() @@ -306,7 +306,7 @@ def test_delete_table(self): span = self.assert_span("DeleteTable") self.assert_table_names(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_describe_table(self): self._create_prepared_table() @@ -315,26 +315,42 @@ def test_describe_table(self): span = self.assert_span("DescribeTable") self.assert_table_names(span, self.default_table_name) - @mock_dynamodb2 - def test_get_item(self): + @mock_aws + def test_get_item_expression(self): self._create_prepared_table() self.client.get_item( TableName=self.default_table_name, Key={"id": {"S": "1"}}, ConsistentRead=True, - AttributesToGet=["id"], - ProjectionExpression="1,2", + ProjectionExpression="PE", + ReturnConsumedCapacity="TOTAL", + ) + + span = self.assert_span("GetItem") + self.assert_table_names(span, self.default_table_name) + self.assert_consistent_read(span, True) + self.assert_consumed_capacity(span, self.default_table_name) + + @mock_aws + def test_get_item_non_expression(self): + self._create_prepared_table() + + self.client.get_item( + TableName=self.default_table_name, + Key={"id": {"S": "1"}}, + ConsistentRead=True, + ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) span = self.assert_span("GetItem") self.assert_table_names(span, self.default_table_name) self.assert_consistent_read(span, True) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_list_tables(self): self._create_table(TableName="my_table") self._create_prepared_table() @@ -351,7 +367,7 @@ def test_list_tables(self): ) self.assertEqual(5, span.attributes[SpanAttributes.AWS_DYNAMODB_LIMIT]) - @mock_dynamodb2 + @mock_aws def test_put_item(self): table = "test_table" self._create_prepared_table(TableName=table) @@ -372,7 +388,7 @@ def test_put_item(self): def test_put_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("PutItem") - @mock_dynamodb2 + @mock_aws def test_query(self): self._create_prepared_table() @@ -390,7 +406,7 @@ def test_query(self): } }, ScanIndexForward=True, - ProjectionExpression="1,2", + ProjectionExpression="PE", ReturnConsumedCapacity="TOTAL", ) @@ -403,11 +419,11 @@ def test_query(self): self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_scan(self): self._create_prepared_table() @@ -419,7 +435,7 @@ def test_scan(self): Select="ALL_ATTRIBUTES", TotalSegments=17, Segment=21, - ProjectionExpression="1,2", + ProjectionExpression="PE", ConsistentRead=True, ReturnConsumedCapacity="TOTAL", ) @@ -440,11 +456,11 @@ def test_scan(self): self.assert_consistent_read(span, True) self.assert_index_name(span, "lsi") self.assert_limit(span, 42) - self.assert_projection(span, "1,2") + self.assert_projection(span, "PE") self.assert_select(span, "ALL_ATTRIBUTES") self.assert_consumed_capacity(span, self.default_table_name) - @mock_dynamodb2 + @mock_aws def test_update_item(self): self._create_prepared_table() @@ -465,7 +481,7 @@ def test_update_item(self): def test_update_item_item_collection_metrics(self): self.assert_extension_item_col_metrics("UpdateItem") - @mock_dynamodb2 + @mock_aws def test_update_table(self): self._create_prepared_table() diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index bb6d283399..62357a3336 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -12,19 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -from unittest.mock import Mock, patch +from unittest.mock import ANY, Mock, patch import botocore.session from botocore.exceptions import ParamValidationError -from moto import ( # pylint: disable=import-error - mock_ec2, - mock_kinesis, - mock_kms, - mock_s3, - mock_sqs, - mock_sts, - mock_xray, -) +from moto import mock_aws # pylint: disable=import-error from opentelemetry import trace as trace_api from opentelemetry.instrumentation.botocore import BotocoreInstrumentor @@ -39,7 +31,7 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace.span import format_span_id, format_trace_id -_REQUEST_ID_REGEX_MATCH = r"[A-Z0-9]{52}" +_REQUEST_ID_REGEX_MATCH = r"[A-Za-z0-9]{52}" # pylint:disable=too-many-public-methods @@ -102,7 +94,7 @@ def assert_span( self.assertEqual(f"{service}.{operation}", span.name) return span - @mock_ec2 + @mock_aws def test_traced_client(self): ec2 = self._make_client("ec2") @@ -111,7 +103,7 @@ def test_traced_client(self): request_id = "fdcdcab1-ae5c-489e-9c33-4637c5dda355" self.assert_span("EC2", "DescribeInstances", request_id=request_id) - @mock_ec2 + @mock_aws def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() @@ -126,7 +118,7 @@ def test_not_recording(self): self.assertFalse(mock_span.set_attribute.called) self.assertFalse(mock_span.set_status.called) - @mock_s3 + @mock_aws def test_exception(self): s3 = self._make_client("s3") @@ -149,14 +141,14 @@ def test_exception(self): self.assertIn(SpanAttributes.EXCEPTION_TYPE, event.attributes) self.assertIn(SpanAttributes.EXCEPTION_MESSAGE, event.attributes) - @mock_s3 + @mock_aws def test_s3_client(self): s3 = self._make_client("s3") s3.list_buckets() self.assert_span("S3", "ListBuckets") - @mock_s3 + @mock_aws def test_s3_put(self): s3 = self._make_client("s3") @@ -174,7 +166,7 @@ def test_s3_put(self): s3.get_object(Bucket="mybucket", Key="foo") self.assert_span("S3", "GetObject", request_id=_REQUEST_ID_REGEX_MATCH) - @mock_sqs + @mock_aws def test_sqs_client(self): sqs = self._make_client("sqs") @@ -184,7 +176,7 @@ def test_sqs_client(self): "SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH ) - @mock_sqs + @mock_aws def test_sqs_send_message(self): sqs = self._make_client("sqs") test_queue_name = "test_queue_name" @@ -205,14 +197,14 @@ def test_sqs_send_message(self): attributes={"aws.queue_url": queue_url}, ) - @mock_kinesis + @mock_aws def test_kinesis_client(self): kinesis = self._make_client("kinesis") kinesis.list_streams() self.assert_span("Kinesis", "ListStreams") - @mock_kinesis + @mock_aws def test_unpatch(self): kinesis = self._make_client("kinesis") @@ -221,7 +213,7 @@ def test_unpatch(self): kinesis.list_streams() self.assertEqual(0, len(self.memory_exporter.get_finished_spans())) - @mock_ec2 + @mock_aws def test_uninstrument_does_not_inject_headers(self): headers = {} @@ -240,7 +232,7 @@ def intercept_headers(**kwargs): self.assertNotIn(TRACE_HEADER_KEY, headers) - @mock_sqs + @mock_aws def test_double_patch(self): sqs = self._make_client("sqs") @@ -252,19 +244,19 @@ def test_double_patch(self): "SQS", "ListQueues", request_id=_REQUEST_ID_REGEX_MATCH ) - @mock_kms + @mock_aws def test_kms_client(self): kms = self._make_client("kms") kms.list_keys(Limit=21) span = self.assert_only_span() + expected = self._default_span_attributes("KMS", "ListKeys") + expected["aws.request_id"] = ANY # check for exact attribute set to make sure not to leak any kms secrets - self.assertEqual( - self._default_span_attributes("KMS", "ListKeys"), span.attributes - ) + self.assertEqual(expected, dict(span.attributes)) - @mock_sts + @mock_aws def test_sts_client(self): sts = self._make_client("sts") @@ -272,11 +264,11 @@ def test_sts_client(self): span = self.assert_only_span() expected = self._default_span_attributes("STS", "GetCallerIdentity") - expected["aws.request_id"] = "c6104cbe-af31-11e0-8154-cbc7ccf896c7" + expected["aws.request_id"] = ANY # check for exact attribute set to make sure not to leak any sts secrets - self.assertEqual(expected, span.attributes) + self.assertEqual(expected, dict(span.attributes)) - @mock_ec2 + @mock_aws def test_propagator_injects_into_request(self): headers = {} previous_propagator = get_global_textmap() @@ -316,7 +308,7 @@ def check_headers(**kwargs): finally: set_global_textmap(previous_propagator) - @mock_ec2 + @mock_aws def test_override_xray_propagator_injects_into_request(self): headers = {} @@ -335,7 +327,7 @@ def check_headers(**kwargs): self.assertNotIn(MockTextMapPropagator.TRACE_ID_KEY, headers) self.assertNotIn(MockTextMapPropagator.SPAN_ID_KEY, headers) - @mock_xray + @mock_aws def test_suppress_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_instrumentation(): @@ -343,7 +335,7 @@ def test_suppress_instrumentation_xray_client(self): xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(0, len(self.get_finished_spans())) - @mock_xray + @mock_aws def test_suppress_http_instrumentation_xray_client(self): xray_client = self._make_client("xray") with suppress_http_instrumentation(): @@ -351,7 +343,7 @@ def test_suppress_http_instrumentation_xray_client(self): xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) self.assertEqual(2, len(self.get_finished_spans())) - @mock_s3 + @mock_aws def test_request_hook(self): request_hook_service_attribute_name = "request_hook.service_name" request_hook_operation_attribute_name = "request_hook.operation_name" @@ -386,7 +378,7 @@ def request_hook(span, service_name, operation_name, api_params): }, ) - @mock_s3 + @mock_aws def test_response_hook(self): response_hook_service_attribute_name = "request_hook.service_name" response_hook_operation_attribute_name = "response_hook.operation_name" diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py index 7388323100..098edfc896 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_lambda.py @@ -19,7 +19,7 @@ from unittest import mock import botocore.session -from moto import mock_iam, mock_lambda # pylint: disable=import-error +from moto import mock_aws # pylint: disable=import-error from pytest import mark from opentelemetry.instrumentation.botocore import BotocoreInstrumentor @@ -96,12 +96,12 @@ def _create_extension(operation: str) -> _LambdaExtension: mock_call_context = mock.MagicMock(operation=operation, params={}) return _LambdaExtension(mock_call_context) - @mock_lambda + @mock_aws def test_list_functions(self): self.client.list_functions() self.assert_span("ListFunctions") - @mock_iam + @mock_aws def _create_role_and_get_arn(self) -> str: return self.iam_client.create_role( RoleName="my-role", @@ -131,7 +131,7 @@ def _create_lambda_function(self, function_name: str, function_code: str): sys.platform == "win32", reason="requires docker and Github CI Windows does not have docker installed by default", ) - @mock_lambda + @mock_aws def test_invoke(self): previous_propagator = get_global_textmap() try: diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py index e2b4c55732..5d6b94f145 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py @@ -18,7 +18,7 @@ import botocore.session from botocore.awsrequest import AWSResponse -from moto import mock_sns +from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv.trace import ( @@ -91,11 +91,11 @@ def assert_injected_span(self, message_attrs: Dict[str, Any], span: Span): self.assertEqual(span_context.trace_id, int(trace_parent[1], 16)) self.assertEqual(span_context.span_id, int(trace_parent[2], 16)) - @mock_sns + @mock_aws def test_publish_to_topic_arn(self): self._test_publish_to_arn("TopicArn") - @mock_sns + @mock_aws def test_publish_to_target_arn(self): self._test_publish_to_arn("TargetArn") @@ -125,7 +125,7 @@ def _test_publish_to_arn(self, arg_name: str): span.attributes["messaging.destination.name"], ) - @mock_sns + @mock_aws def test_publish_to_phone_number(self): phone_number = "+10000000000" self.client.publish( @@ -138,7 +138,7 @@ def test_publish_to_phone_number(self): phone_number, span.attributes[SpanAttributes.MESSAGING_DESTINATION] ) - @mock_sns + @mock_aws def test_publish_injects_span(self): message_attrs = {} topic_arn = self._create_topic() diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py index 6bcffd9274..cdf39e4ece 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sqs.py @@ -1,5 +1,5 @@ import botocore.session -from moto import mock_sqs +from moto import mock_aws from opentelemetry.instrumentation.botocore import BotocoreInstrumentor from opentelemetry.semconv.trace import SpanAttributes @@ -22,7 +22,7 @@ def tearDown(self): super().tearDown() BotocoreInstrumentor().uninstrument() - @mock_sqs + @mock_aws def test_sqs_messaging_send_message(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -51,7 +51,7 @@ def test_sqs_messaging_send_message(self): response["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_send_message_batch(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -85,7 +85,7 @@ def test_sqs_messaging_send_message_batch(self): response["Successful"][0]["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_receive_message(self): create_queue_result = self.client.create_queue( QueueName="test_queue_name" @@ -116,7 +116,7 @@ def test_sqs_messaging_receive_message(self): message_result["Messages"][0]["MessageId"], ) - @mock_sqs + @mock_aws def test_sqs_messaging_failed_operation(self): with self.assertRaises(Exception): self.client.send_message( diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/LICENSE b/instrumentation/opentelemetry-instrumentation-cassandra/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-cassandra/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-cassandra/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/pyproject.toml b/instrumentation/opentelemetry-instrumentation-cassandra/pyproject.toml index 9088332425..17596cee98 100644 --- a/instrumentation/opentelemetry-instrumentation-cassandra/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-cassandra/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,10 +36,6 @@ instruments = [ "cassandra-driver ~= 3.25", "scylla-driver ~= 3.25", ] -test = [ - "opentelemetry-instrumentation-cassandra[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] cassandra = "opentelemetry.instrumentation.cassandra:CassandraInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra/version.py b/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra/version.py +++ b/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry/instrumentation/cassandra/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt new file mode 100644 index 0000000000..f0d811982b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +cassandra-driver==3.29.0 +click==8.1.7 +Deprecated==1.2.14 +geomet==0.2.1.post1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +PyYAML==6.0.1 +scylla-driver==3.26.6 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-cassandra diff --git a/instrumentation/opentelemetry-instrumentation-celery/LICENSE b/instrumentation/opentelemetry-instrumentation-celery/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-celery/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml b/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml index ea866d4b4b..72808bbda7 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-celery/pyproject.toml @@ -22,22 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "celery >= 4.0, < 6.0", ] -test = [ - "opentelemetry-instrumentation-celery[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "pytest", -] [project.entry-points.opentelemetry_instrumentor] celery = "opentelemetry.instrumentation.celery:CeleryInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py index 94cac68b70..10ccca1270 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/__init__.py @@ -113,10 +113,8 @@ def keys(self, carrier): class CeleryInstrumentor(BaseInstrumentor): - def __init__(self): - super().__init__() - self.metrics = None - self.task_id_to_start_time = {} + metrics = None + task_id_to_start_time = {} def instrumentation_dependencies(self) -> Collection[str]: return _instruments diff --git a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py +++ b/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry/instrumentation/celery/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt new file mode 100644 index 0000000000..3159fefaf8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt @@ -0,0 +1,30 @@ +amqp==5.2.0 +asgiref==3.7.2 +backports.zoneinfo==0.2.1 +billiard==4.2.0 +celery==5.3.6 +click==8.1.7 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.3.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +kombu==5.3.5 +packaging==24.0 +pluggy==1.5.0 +prompt-toolkit==3.0.43 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +tzdata==2024.1 +vine==5.1.0 +wcwidth==0.2.13 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-celery diff --git a/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt new file mode 100644 index 0000000000..6012f89379 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt @@ -0,0 +1,29 @@ +amqp==5.2.0 +asgiref==3.7.2 +billiard==4.2.0 +celery==5.3.6 +click==8.1.7 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.3.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +kombu==5.3.5 +packaging==24.0 +pluggy==1.5.0 +prompt-toolkit==3.0.43 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +tzdata==2024.1 +vine==5.1.0 +wcwidth==0.2.13 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-celery diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_duplicate.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_duplicate.py new file mode 100644 index 0000000000..ab1f7804cf --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_duplicate.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from opentelemetry.instrumentation.celery import CeleryInstrumentor + + +class TestUtils(unittest.TestCase): + def test_duplicate_instrumentaion(self): + first = CeleryInstrumentor() + first.instrument() + second = CeleryInstrumentor() + second.instrument() + CeleryInstrumentor().uninstrument() + self.assertIsNotNone(first.metrics) + self.assertIsNotNone(second.metrics) + self.assertEqual(first.task_id_to_start_time, {}) + self.assertEqual(second.task_id_to_start_time, {}) diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_metrics.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_metrics.py index 01c9487ea8..f83759317b 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_metrics.py @@ -1,7 +1,10 @@ import threading import time +from platform import python_implementation from timeit import default_timer +from pytest import mark + from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.test.test_base import TestBase @@ -62,6 +65,9 @@ def test_basic_metric(self): est_value_delta=200, ) + @mark.skipif( + python_implementation() == "PyPy", reason="Fails randomly in pypy" + ) def test_metric_uninstrument(self): CeleryInstrumentor().instrument() diff --git a/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py b/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py index ed4dbb5b1d..3ac6a5a70c 100644 --- a/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py +++ b/instrumentation/opentelemetry-instrumentation-celery/tests/test_tasks.py @@ -125,8 +125,9 @@ def test_task_raises(self): self.assertIn(SpanAttributes.EXCEPTION_STACKTRACE, event.attributes) - self.assertEqual( - event.attributes[SpanAttributes.EXCEPTION_TYPE], "CustomError" + # TODO: use plain assertEqual after 1.25 is released (https://github.com/open-telemetry/opentelemetry-python/pull/3837) + self.assertIn( + "CustomError", event.attributes[SpanAttributes.EXCEPTION_TYPE] ) self.assertEqual( diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/LICENSE b/instrumentation/opentelemetry-instrumentation-confluent-kafka/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml index 4ded0836b2..94a2497b36 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/pyproject.toml @@ -19,18 +19,20 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ + "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-api ~= 1.12", "wrapt >= 1.0.0, < 2.0.0", ] [project.optional-dependencies] instruments = [ - "confluent-kafka >= 1.8.2, <= 2.3.0", -] -test = [ - "opentelemetry-instrumentation-confluent-kafka[instruments]", + "confluent-kafka >= 1.8.2, <= 2.4.0", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py index c869d03dd9..45d45ccb63 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/__init__.py @@ -151,10 +151,13 @@ def __init__(self, producer: Producer, tracer: Tracer): self._tracer = tracer def flush(self, timeout=-1): - self._producer.flush(timeout) + return self._producer.flush(timeout) def poll(self, timeout=-1): - self._producer.poll(timeout) + return self._producer.poll(timeout) + + def purge(self, in_queue=True, in_flight=True, blocking=True): + self._producer.purge(in_queue, in_flight, blocking) def produce( self, topic, value=None, *args, **kwargs diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py index bbc900c1dd..6ebddd30ac 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/package.py @@ -13,4 +13,4 @@ # limitations under the License. -_instruments = ("confluent-kafka >= 1.8.2, <= 2.3.0",) +_instruments = ("confluent-kafka >= 1.8.2, <= 2.4.0",) diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry/instrumentation/confluent_kafka/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt new file mode 100644 index 0000000000..87387ded81 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +confluent-kafka==2.4.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-confluent-kafka diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py index 21d5bd6f83..205de27733 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/test_instrumentation.py @@ -31,7 +31,7 @@ ) from opentelemetry.test.test_base import TestBase -from .utils import MockConsumer, MockedMessage +from .utils import MockConsumer, MockedMessage, MockedProducer class TestConfluentKafka(TestBase): @@ -246,3 +246,35 @@ def _compare_spans(self, spans, expected_spans): self.assertEqual( expected_attribute_value, span.attributes[attribute_key] ) + + def test_producer_poll(self) -> None: + instrumentation = ConfluentKafkaInstrumentor() + message_queue = [] + + producer = MockedProducer( + message_queue, + { + "bootstrap.servers": "localhost:29092", + }, + ) + + producer = instrumentation.instrument_producer(producer) + producer.produce(topic="topic-1", key="key-1", value="value-1") + msg = producer.poll() + self.assertIsNotNone(msg) + + def test_producer_flush(self) -> None: + instrumentation = ConfluentKafkaInstrumentor() + message_queue = [] + + producer = MockedProducer( + message_queue, + { + "bootstrap.servers": "localhost:29092", + }, + ) + + producer = instrumentation.instrument_producer(producer) + producer.produce(topic="topic-1", key="key-1", value="value-1") + msg = producer.flush() + self.assertIsNotNone(msg) diff --git a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/utils.py b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/utils.py index 798daaeff4..92e11798f6 100644 --- a/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/utils.py +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests/utils.py @@ -1,4 +1,6 @@ -from confluent_kafka import Consumer +from typing import Optional + +from confluent_kafka import Consumer, Producer class MockConsumer(Consumer): @@ -20,11 +22,21 @@ def poll(self, timeout=None): class MockedMessage: - def __init__(self, topic: str, partition: int, offset: int, headers): + def __init__( + self, + topic: str, + partition: int, + offset: int, + headers, + key: Optional[str] = None, + value: Optional[str] = None, + ): self._topic = topic self._partition = partition self._offset = offset self._headers = headers + self._key = key + self._value = value def topic(self): return self._topic @@ -37,3 +49,35 @@ def offset(self): def headers(self): return self._headers + + def key(self): + return self._key + + def value(self): + return self._value + + +class MockedProducer(Producer): + def __init__(self, queue, config): + self._queue = queue + super().__init__(config) + + def produce( + self, *args, **kwargs + ): # pylint: disable=keyword-arg-before-vararg + self._queue.append( + MockedMessage( + topic=kwargs.get("topic"), + partition=0, + offset=0, + headers=[], + key=kwargs.get("key"), + value=kwargs.get("value"), + ) + ) + + def poll(self, *args, **kwargs): + return len(self._queue) + + def flush(self, *args, **kwargs): + return len(self._queue) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE b/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-dbapi/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml index 3032c12da6..6a81203f01 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-dbapi/pyproject.toml @@ -22,19 +22,17 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.45b0.dev", -] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-dbapi" diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index b0acbed185..0857d2989b 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -427,12 +427,19 @@ def traced_execution( if args and self._commenter_enabled: try: args_list = list(args) + if hasattr(self._connect_module, "__libpq_version__"): + libpq_version = self._connect_module.__libpq_version__ + else: + libpq_version = ( + self._connect_module.pq.__build_version__ + ) + commenter_data = { # Psycopg2/framework information "db_driver": f"psycopg2:{self._connect_module.__version__.split(' ')[0]}", "dbapi_threadsafety": self._connect_module.threadsafety, "dbapi_level": self._connect_module.apilevel, - "libpq_version": self._connect_module.__libpq_version__, + "libpq_version": libpq_version, "driver_paramstyle": self._connect_module.paramstyle, } if self._commenter_options.get( diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py index 6340c1adfb..db4e3a0022 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt new file mode 100644 index 0000000000..d35a55f831 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt @@ -0,0 +1,15 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 0d19ce8373..eb2d628a3a 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -275,6 +275,32 @@ def test_executemany_comment(self): r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", ) + def test_compatible_build_version_psycopg_psycopg2_libpq(self): + connect_module = mock.MagicMock() + connect_module.__version__ = mock.MagicMock() + connect_module.pq = mock.MagicMock() + connect_module.pq.__build_version__ = 123 + connect_module.apilevel = 123 + connect_module.threadsafety = 123 + connect_module.paramstyle = "test" + + db_integration = dbapi.DatabaseApiIntegration( + "testname", + "testcomponent", + enable_commenter=True, + commenter_options={"db_driver": False, "dbapi_level": False}, + connect_module=connect_module, + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + cursor = mock_connection.cursor() + cursor.executemany("Select 1;") + self.assertRegex( + cursor.query, + r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;", + ) + def test_executemany_flask_integration_comment(self): connect_module = mock.MagicMock() connect_module.__version__ = mock.MagicMock() @@ -393,11 +419,13 @@ def __init__(self) -> None: # pylint: disable=unused-argument, no-self-use def execute(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") # pylint: disable=unused-argument, no-self-use def executemany(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") self.query = query self.params = params @@ -405,4 +433,5 @@ def executemany(self, query, params=None, throw_exception=False): # pylint: disable=unused-argument, no-self-use def callproc(self, query, params=None, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-django/LICENSE b/instrumentation/opentelemetry-instrumentation-django/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-django/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-django/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-django/pyproject.toml b/instrumentation/opentelemetry-instrumentation-django/pyproject.toml index 50cc227e44..e1f67c283d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-django/pyproject.toml @@ -22,26 +22,23 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-wsgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-wsgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] asgi = [ - "opentelemetry-instrumentation-asgi == 0.45b0.dev", + "opentelemetry-instrumentation-asgi == 0.47b0.dev", ] instruments = [ "django >= 1.10", ] -test = [ - "opentelemetry-instrumentation-django[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] django = "opentelemetry.instrumentation.django:DjangoInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 583f1adeb6..37ac760283 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -322,7 +322,7 @@ def _instrument(self, **kwargs): _DjangoMiddleware._duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration of the inbound http request", + description="Duration of HTTP client requests.", ) _DjangoMiddleware._active_request_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index bc677a81cf..1b747fd2c0 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -163,9 +163,9 @@ class _DjangoMiddleware(MiddlewareMixin): _active_request_counter = None _otel_request_hook: Callable[[Span, HttpRequest], None] = None - _otel_response_hook: Callable[ - [Span, HttpRequest, HttpResponse], None - ] = None + _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = ( + None + ) @staticmethod def _get_span_name(request): @@ -229,9 +229,9 @@ def process_request(self, request): ) duration_attrs = _parse_duration_attrs(attributes) - request.META[ - self._environ_active_request_attr_key - ] = active_requests_count_attrs + request.META[self._environ_active_request_attr_key] = ( + active_requests_count_attrs + ) request.META[self._environ_duration_attr_key] = duration_attrs self._active_request_counter.add(1, active_requests_count_attrs) if span.is_recording(): @@ -336,9 +336,9 @@ def process_response(self, request, response): self._environ_duration_attr_key, None ) if duration_attrs: - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = response.status_code + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( + response.status_code + ) request_start_time = request.META.pop(self._environ_timer_key, None) if activation and span: diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py index 30492a8be5..ef53d5dc38 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py @@ -83,20 +83,26 @@ def __call__(self, execute: Type[T], sql, params, many, context) -> T: sql = _add_sql_comment( sql, # Information about the controller. - controller=resolver_match.view_name - if resolver_match and with_controller - else None, + controller=( + resolver_match.view_name + if resolver_match and with_controller + else None + ), # route is the pattern that matched a request with a controller i.e. the regex # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route # getattr() because the attribute doesn't exist in Django < 2.2. - route=getattr(resolver_match, "route", None) - if resolver_match and with_route - else None, + route=( + getattr(resolver_match, "route", None) + if resolver_match and with_route + else None + ), # app_name is the application namespace for the URL pattern that matches the URL. # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name - app_name=(resolver_match.app_name or None) - if resolver_match and with_app_name - else None, + app_name=( + (resolver_match.app_name or None) + if resolver_match and with_app_name + else None + ), # Framework centric information. framework=f"django:{_django_version}" if with_framework else None, # Information about the database and driver. diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt new file mode 100644 index 0000000000..a6162e7c00 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +Django==2.2.28 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +pytz==2024.1 +sqlparse==0.5.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-django diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt new file mode 100644 index 0000000000..58f5d5c88b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +Django==3.2.25 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +pytz==2024.1 +sqlparse==0.5.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e instrumentation/opentelemetry-instrumentation-asgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-django diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt new file mode 100644 index 0000000000..ffc43f4023 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +backports.zoneinfo==0.2.1 +Deprecated==1.2.14 +Django==4.2.11 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +sqlparse==0.5.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e instrumentation/opentelemetry-instrumentation-asgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-django diff --git a/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt new file mode 100644 index 0000000000..92ebaa83e4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +Django==4.2.11 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +sqlparse==0.5.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e instrumentation/opentelemetry-instrumentation-asgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-django diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index d7bb1e544f..63af1e6b86 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -155,9 +155,11 @@ def test_templated_route_get(self): self.assertEqual( span.name, - "GET ^route/(?P[0-9]{4})/template/$" - if DJANGO_2_2 - else "GET", + ( + "GET ^route/(?P[0-9]{4})/template/$" + if DJANGO_2_2 + else "GET" + ), ) self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.UNSET) @@ -390,7 +392,7 @@ def response_hook(span, request, response): self.assertIsInstance(response_hook_args[2], HttpResponse) self.assertEqual(response_hook_args[2], response) - async def test_trace_parent(self): + def test_trace_parent(self): id_generator = RandomIdGenerator() trace_id = format_trace_id(id_generator.generate_trace_id()) span_id = format_span_id(id_generator.generate_span_id()) @@ -398,7 +400,7 @@ async def test_trace_parent(self): Client().get( "/span_name/1234/", - traceparent=traceparent_value, + HTTP_TRACEPARENT=traceparent_value, ) span = self.memory_exporter.get_finished_spans()[0] diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 452a7c0fdd..6310664100 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -35,12 +35,12 @@ def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" - response[ - "my-custom-regex-header-1" - ] = "my-custom-regex-value-1,my-custom-regex-value-2" - response[ - "my-custom-regex-header-2" - ] = "my-custom-regex-value-3,my-custom-regex-value-4" + response["my-custom-regex-header-1"] = ( + "my-custom-regex-value-1,my-custom-regex-value-2" + ) + response["my-custom-regex-header-2"] = ( + "my-custom-regex-value-3,my-custom-regex-value-4" + ) response["my-secret-header"] = "my-secret-value" return response diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE b/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml b/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml index c716769f5c..d6e8408738 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/pyproject.toml @@ -22,22 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] [project.optional-dependencies] instruments = [ - "elasticsearch >= 2.0", -] -test = [ - "opentelemetry-instrumentation-elasticsearch[instruments]", - "elasticsearch-dsl >= 2.0", - "opentelemetry-test-utils == 0.45b0.dev", + "elasticsearch >= 6.0", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index dd72a5235e..f8d7920e20 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -16,6 +16,15 @@ This library allows tracing HTTP elasticsearch made by the `elasticsearch `_ library. +.. warning:: + The elasticsearch package got native OpenTelemetry support since version + `8.13 `_. + To avoid duplicated tracing this instrumentation disables itself if it finds an elasticsearch client + that has OpenTelemetry support enabled. + + Please be aware that the two libraries may use a different semantic convention, see + `elasticsearch documentation `_. + Usage ----- @@ -54,7 +63,7 @@ def response_hook(span: Span, response: dict) for example: -.. code: python +.. code-block: python from opentelemetry.instrumentation.elasticsearch import ElasticsearchInstrumentor import elasticsearch @@ -81,6 +90,7 @@ def response_hook(span, response): """ import re +import warnings from logging import getLogger from os import environ from typing import Collection @@ -94,7 +104,7 @@ def response_hook(span, response): from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind, get_tracer +from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer from .utils import sanitize_body @@ -103,6 +113,7 @@ def response_hook(span, response): es_transport_split = elasticsearch.VERSION[0] > 7 if es_transport_split: import elastic_transport + from elastic_transport._models import DefaultType logger = getLogger(__name__) @@ -173,7 +184,12 @@ def _instrument(self, **kwargs): def _uninstrument(self, **kwargs): # pylint: disable=no-member - unwrap(elasticsearch.Transport, "perform_request") + transport_class = ( + elastic_transport.Transport + if es_transport_split + else elasticsearch.Transport + ) + unwrap(transport_class, "perform_request") _regex_doc_url = re.compile(r"/_doc/([^/]+)") @@ -182,6 +198,7 @@ def _uninstrument(self, **kwargs): _regex_search_url = re.compile(r"/([^/]+)/_search[/]?") +# pylint: disable=too-many-statements def _wrap_perform_request( tracer, span_name_prefix, @@ -190,6 +207,16 @@ def _wrap_perform_request( ): # pylint: disable=R0912,R0914 def wrapper(wrapped, _, args, kwargs): + # if wrapped elasticsearch has native OTel instrumentation just call the wrapped function + otel_span = kwargs.get("otel_span") + if otel_span and otel_span.otel_span: + warnings.warn( + "Instrumentation disabled, relying on elasticsearch native OTel support, see " + "https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/elasticsearch/elasticsearch.html", + Warning, + ) + return wrapped(*args, **kwargs) + method = url = None try: method, url, *_ = args @@ -234,7 +261,27 @@ def wrapper(wrapped, _, args, kwargs): kind=SpanKind.CLIENT, ) as span: if callable(request_hook): - request_hook(span, method, url, kwargs) + # elasticsearch 8 changed the parameters quite a bit + if es_transport_split: + + def normalize_kwargs(k, v): + if isinstance(v, DefaultType): + v = str(v) + elif isinstance(v, elastic_transport.HttpHeaders): + v = dict(v) + elif isinstance( + v, elastic_transport.OpenTelemetrySpan + ): + # the transport Span is always a dummy one + v = None + return (k, v) + + hook_kwargs = dict( + normalize_kwargs(k, v) for k, v in kwargs.items() + ) + else: + hook_kwargs = kwargs + request_hook(span, method, url, hook_kwargs) if span.is_recording(): attributes = { @@ -245,9 +292,11 @@ def wrapper(wrapped, _, args, kwargs): if method: attributes["elasticsearch.method"] = method if body: - attributes[SpanAttributes.DB_STATEMENT] = sanitize_body( - body - ) + # Don't set db.statement for bulk requests, as it can be very large + if isinstance(body, dict): + attributes[SpanAttributes.DB_STATEMENT] = ( + sanitize_body(body) + ) if params: attributes["elasticsearch.params"] = str(params) if doc_id: @@ -258,16 +307,41 @@ def wrapper(wrapped, _, args, kwargs): span.set_attribute(key, value) rv = wrapped(*args, **kwargs) - if isinstance(rv, dict) and span.is_recording(): + + body = rv.body if es_transport_split else rv + if isinstance(body, dict) and span.is_recording(): for member in _ATTRIBUTES_FROM_RESULT: - if member in rv: + if member in body: span.set_attribute( f"elasticsearch.{member}", - str(rv[member]), + str(body[member]), + ) + + # since the transport split the raising of exceptions that set the error status + # are called after this code so need to set error status manually + if es_transport_split and span.is_recording(): + if not (method == "HEAD" and rv.meta.status == 404) and ( + not 200 <= rv.meta.status < 299 + ): + exception = elasticsearch.exceptions.HTTP_EXCEPTIONS.get( + rv.meta.status, elasticsearch.exceptions.ApiError + ) + message = str(body) + if isinstance(body, dict): + error = body.get("error", message) + if isinstance(error, dict) and "type" in error: + error = error["type"] + message = error + + span.set_status( + Status( + status_code=StatusCode.ERROR, + description=f"{exception.__name__}: {message}", ) + ) if callable(response_hook): - response_hook(span, rv) + response_hook(span, body) return rv return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py index 5b0fb7e6ea..bae644a70b 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/package.py @@ -13,4 +13,4 @@ # limitations under the License. -_instruments = ("elasticsearch >= 2.0",) +_instruments = ("elasticsearch >= 6.0",) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt new file mode 100644 index 0000000000..e6d9bb6f9d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +elasticsearch==6.8.2 +elasticsearch-dsl==6.4.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==1.26.19 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-elasticsearch diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt new file mode 100644 index 0000000000..12e3a1c229 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +elasticsearch==7.17.9 +elasticsearch-dsl==7.4.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==1.26.19 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-elasticsearch diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt new file mode 100644 index 0000000000..f34d67d9c8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +elasticsearch==8.13.1 +elasticsearch-dsl==8.13.1 +elastic-transport==8.13.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-elasticsearch diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py deleted file mode 100644 index 008a95d671..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es2.py +++ /dev/null @@ -1,33 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=no-name-in-module - DocType, - String, -) - - -class Article(DocType): - title = String(analyzer="snowball", fields={"raw": String()}) - body = String(analyzer="snowball") - - class Meta: - index = "test-index" - - -dsl_create_statement = { - "mappings": { - "article": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "string"}}, - "type": "string", - }, - "body": {"analyzer": "snowball", "type": "string"}, - } - } - }, - "settings": {"analysis": {}}, -} -dsl_index_result = (1, {}, '{"created": true}') -dsl_index_span_name = "Elasticsearch/test-index/article/2" -dsl_index_url = "/test-index/article/2" -dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py deleted file mode 100644 index cf32d98863..0000000000 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es5.py +++ /dev/null @@ -1,33 +0,0 @@ -from elasticsearch_dsl import ( # pylint: disable=no-name-in-module - DocType, - Keyword, - Text, -) - - -class Article(DocType): - title = Text(analyzer="snowball", fields={"raw": Keyword()}) - body = Text(analyzer="snowball") - - class Meta: - index = "test-index" - - -dsl_create_statement = { - "mappings": { - "article": { - "properties": { - "title": { - "analyzer": "snowball", - "fields": {"raw": {"type": "keyword"}}, - "type": "text", - }, - "body": {"analyzer": "snowball", "type": "text"}, - } - } - }, -} -dsl_index_result = (1, {}, '{"created": true}') -dsl_index_span_name = "Elasticsearch/test-index/article/2" -dsl_index_url = "/test-index/article/2" -dsl_search_method = "GET" diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py index b27d291ba3..8169eb25c4 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es6.py @@ -31,3 +31,9 @@ class Index: dsl_index_span_name = "Elasticsearch/test-index/doc/2" dsl_index_url = "/test-index/doc/2" dsl_search_method = "GET" + +perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" + + +def mock_response(body: str, status_code: int = 200): + return (status_code, {}, body) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py index a2d37a54a9..377173f7ac 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es7.py @@ -26,6 +26,12 @@ class Index: } } dsl_index_result = (1, {}, '{"result": "created"}') -dsl_index_span_name = "Elasticsearch/test-index/_doc/2" +dsl_index_span_name = "Elasticsearch/test-index/_doc/:id" dsl_index_url = "/test-index/_doc/2" dsl_search_method = "POST" + +perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" + + +def mock_response(body: str, status_code: int = 200): + return (status_code, {}, body) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py index 04ed2efda2..a450be68ec 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/helpers_es8.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from elastic_transport import ApiResponseMeta, HttpHeaders +from elastic_transport._node import NodeApiResponse from elasticsearch_dsl import Document, Keyword, Text @@ -36,6 +38,23 @@ class Index: } } dsl_index_result = (1, {}, '{"result": "created"}') -dsl_index_span_name = "Elasticsearch/test-index/_doc/2" +dsl_index_span_name = "Elasticsearch/test-index/_doc/:id" dsl_index_url = "/test-index/_doc/2" dsl_search_method = "POST" + +perform_request_mock_path = ( + "elastic_transport._node._http_urllib3.Urllib3HttpNode.perform_request" +) + + +def mock_response(body: str, status_code: int = 200): + return NodeApiResponse( + ApiResponseMeta( + status=status_code, + headers=HttpHeaders({}), + duration=100, + http_version="1.1", + node="node", + ), + body.encode(), + ) diff --git a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py index 0c84cf5cd6..b7e24d87c9 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/tests/test_elasticsearch.py @@ -23,6 +23,7 @@ import elasticsearch.exceptions from elasticsearch import Elasticsearch from elasticsearch_dsl import Search +from pytest import mark import opentelemetry.instrumentation.elasticsearch from opentelemetry import trace @@ -36,7 +37,7 @@ from . import sanitization_queries # pylint: disable=no-name-in-module -major_version = elasticsearch.VERSION[0] +major_version, minor_version = elasticsearch.VERSION[:2] if major_version == 8: from . import helpers_es8 as helpers # pylint: disable=no-name-in-module @@ -44,16 +45,34 @@ from . import helpers_es7 as helpers # pylint: disable=no-name-in-module elif major_version == 6: from . import helpers_es6 as helpers # pylint: disable=no-name-in-module -elif major_version == 5: - from . import helpers_es5 as helpers # pylint: disable=no-name-in-module -else: - from . import helpers_es2 as helpers # pylint: disable=no-name-in-module Article = helpers.Article +# pylint: disable=too-many-public-methods -@mock.patch( - "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request" + +def normalize_arguments(doc_type, body=None): + if major_version < 7: + return ( + {"body": body, "doc_type": doc_type} + if body + else {"doc_type": doc_type} + ) + return {"document": body} if body else {} + + +def get_elasticsearch_client(*args, **kwargs): + client = Elasticsearch(*args, **kwargs) + if major_version == 8: + client._verified_elasticsearch = True + elif major_version == 7: + client.transport._verified_elasticsearch = True + return client + + +@mock.patch(helpers.perform_request_mock_path) +@mock.patch.dict( + os.environ, {"OTEL_PYTHON_INSTRUMENTATION_ELASTICSEARCH_ENABLED": "false"} ) class TestElasticsearchIntegration(TestBase): search_attributes = { @@ -81,17 +100,20 @@ def tearDown(self): ElasticsearchInstrumentor().uninstrument() def test_instrumentor(self, request_mock): - request_mock.return_value = (1, {}, {}) + request_mock.return_value = helpers.mock_response("{}") - es = Elasticsearch() - es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"}) + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.index( + index="sw", + id=1, + **normalize_arguments(body={"name": "adam"}, doc_type="_doc"), + ) spans_list = self.get_finished_spans() self.assertEqual(len(spans_list), 1) span = spans_list[0] # Check version and name in span's instrumentation info - # self.assertEqualSpanInstrumentationInfo(span, opentelemetry.instrumentation.elasticsearch) self.assertEqualSpanInstrumentationInfo( span, opentelemetry.instrumentation.elasticsearch ) @@ -99,20 +121,24 @@ def test_instrumentor(self, request_mock): # check that no spans are generated after uninstrument ElasticsearchInstrumentor().uninstrument() - es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"}) + es.index( + index="sw", + id=1, + **normalize_arguments(body={"name": "adam"}, doc_type="_doc"), + ) spans_list = self.get_finished_spans() self.assertEqual(len(spans_list), 1) def test_span_not_recording(self, request_mock): - request_mock.return_value = (1, {}, {}) + request_mock.return_value = (1, {}, "{}") mock_tracer = mock.Mock() mock_span = mock.Mock() mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: tracer.return_value = mock_tracer - Elasticsearch() + get_elasticsearch_client(hosts=["http://localhost:9200"]) self.assertFalse(mock_span.is_recording()) self.assertTrue(mock_span.is_recording.called) self.assertFalse(mock_span.set_attribute.called) @@ -124,7 +150,7 @@ def test_prefix_arg(self, request_mock): prefix = "prefix-from-env" ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor(span_name_prefix=prefix).instrument() - request_mock.return_value = (1, {}, {}) + request_mock.return_value = helpers.mock_response("{}") self._test_prefix(prefix) def test_prefix_env(self, request_mock): @@ -133,13 +159,17 @@ def test_prefix_env(self, request_mock): os.environ[env_var] = prefix ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor().instrument() - request_mock.return_value = (1, {}, {}) + request_mock.return_value = helpers.mock_response("{}") del os.environ[env_var] self._test_prefix(prefix) def _test_prefix(self, prefix): - es = Elasticsearch() - es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"}) + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.index( + index="sw", + id=1, + **normalize_arguments(body={"name": "adam"}, doc_type="_doc"), + ) spans_list = self.get_finished_spans() self.assertEqual(len(spans_list), 1) @@ -147,13 +177,13 @@ def _test_prefix(self, prefix): self.assertTrue(span.name.startswith(prefix)) def test_result_values(self, request_mock): - request_mock.return_value = ( - 1, - {}, - '{"found": false, "timed_out": true, "took": 7}', + request_mock.return_value = helpers.mock_response( + '{"found": false, "timed_out": true, "took": 7}' + ) + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.get( + index="test-index", **normalize_arguments(doc_type="_doc"), id=1 ) - es = Elasticsearch() - es.get(index="test-index", doc_type="_doc", id=1) spans = self.get_finished_spans() @@ -172,15 +202,28 @@ def test_trace_error_unknown(self, request_mock): def test_trace_error_not_found(self, request_mock): msg = "record not found" - exc = elasticsearch.exceptions.NotFoundError(404, msg) - request_mock.return_value = (1, {}, {}) - request_mock.side_effect = exc + if major_version == 8: + error = {"error": msg} + response = helpers.mock_response( + json.dumps(error), status_code=404 + ) + request_mock.return_value = response + exc = elasticsearch.exceptions.NotFoundError( + msg, meta=response.meta, body=None + ) + else: + exc = elasticsearch.exceptions.NotFoundError(404, msg) + request_mock.side_effect = exc self._test_trace_error(StatusCode.ERROR, exc) def _test_trace_error(self, code, exc): - es = Elasticsearch() + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) try: - es.get(index="test-index", doc_type="_doc", id=1) + es.get( + index="test-index", + **normalize_arguments(doc_type="_doc"), + id=1, + ) except Exception: # pylint: disable=broad-except pass @@ -189,15 +232,20 @@ def _test_trace_error(self, code, exc): span = spans[0] self.assertFalse(span.status.is_ok) self.assertEqual(span.status.status_code, code) + message = getattr(exc, "message", str(exc)) self.assertEqual( - span.status.description, f"{type(exc).__name__}: {exc}" + span.status.description, f"{type(exc).__name__}: {message}" ) def test_parent(self, request_mock): - request_mock.return_value = (1, {}, {}) - es = Elasticsearch() + request_mock.return_value = helpers.mock_response("{}") + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) with self.tracer.start_as_current_span("parent"): - es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"}) + es.index( + index="sw", + **normalize_arguments(doc_type="_doc", body={"name": "adam"}), + id=1, + ) spans = self.get_finished_spans() self.assertEqual(len(spans), 2) @@ -208,8 +256,8 @@ def test_parent(self, request_mock): self.assertEqual(child.parent.span_id, parent.context.span_id) def test_multithread(self, request_mock): - request_mock.return_value = (1, {}, {}) - es = Elasticsearch() + request_mock.return_value = helpers.mock_response("{}") + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) ev = threading.Event() # 1. Start tracing from thread-1; make thread-2 wait @@ -217,13 +265,21 @@ def test_multithread(self, request_mock): # 3. Check the spans got different parents, and are in the expected order. def target1(parent_span): with trace.use_span(parent_span): - es.get(index="test-index", doc_type="_doc", id=1) + es.get( + index="test-index", + **normalize_arguments(doc_type="_doc"), + id=1, + ) ev.set() ev.wait() def target2(): ev.wait() - es.get(index="test-index", doc_type="_doc", id=2) + es.get( + index="test-index", + **normalize_arguments(doc_type="_doc"), + id=2, + ) ev.set() with self.tracer.start_as_current_span("parent") as span: @@ -247,9 +303,11 @@ def target2(): self.assertIsNone(s3.parent) def test_dsl_search(self, request_mock): - request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') + request_mock.return_value = helpers.mock_response( + '{"hits": {"hits": []}}' + ) - client = Elasticsearch() + client = get_elasticsearch_client(hosts=["http://localhost:9200"]) search = Search(using=client, index="test-index").filter( "term", author="testing" ) @@ -265,8 +323,10 @@ def test_dsl_search(self, request_mock): ) def test_dsl_search_sanitized(self, request_mock): - request_mock.return_value = (1, {}, '{"hits": {"hits": []}}') - client = Elasticsearch() + request_mock.return_value = helpers.mock_response( + '{"hits": {"hits": []}}' + ) + client = get_elasticsearch_client(hosts=["http://localhost:9200"]) search = Search(using=client, index="test-index").filter( "term", author="testing" ) @@ -282,8 +342,11 @@ def test_dsl_search_sanitized(self, request_mock): ) def test_dsl_create(self, request_mock): - request_mock.return_value = (1, {}, {}) - client = Elasticsearch() + request_mock.side_effect = [ + helpers.mock_response("{}", status_code=404), + helpers.mock_response("{}"), + ] + client = get_elasticsearch_client(hosts=["http://localhost:9200"]) Article.init(using=client) spans = self.get_finished_spans() @@ -309,8 +372,11 @@ def test_dsl_create(self, request_mock): ) def test_dsl_create_sanitized(self, request_mock): - request_mock.return_value = (1, {}, {}) - client = Elasticsearch() + request_mock.side_effect = [ + helpers.mock_response("{}", status_code=404), + helpers.mock_response("{}"), + ] + client = get_elasticsearch_client(hosts=["http://localhost:9200"]) Article.init(using=client) spans = self.get_finished_spans() @@ -325,9 +391,11 @@ def test_dsl_create_sanitized(self, request_mock): ) def test_dsl_index(self, request_mock): - request_mock.return_value = helpers.dsl_index_result + request_mock.return_value = helpers.mock_response( + helpers.dsl_index_result[2] + ) - client = Elasticsearch() + client = get_elasticsearch_client(hosts=["http://localhost:9200"]) article = Article( meta={"id": 2}, title="About searching", @@ -371,16 +439,19 @@ def request_hook(span, method, url, kwargs): ElasticsearchInstrumentor().uninstrument() ElasticsearchInstrumentor().instrument(request_hook=request_hook) - request_mock.return_value = ( - 1, - {}, - '{"found": false, "timed_out": true, "took": 7}', + request_mock.return_value = helpers.mock_response( + '{"found": false, "timed_out": true, "took": 7}' ) - es = Elasticsearch() + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) index = "test-index" doc_id = 1 - kwargs = {"params": {"test": True}} - es.get(index=index, doc_type="_doc", id=doc_id, **kwargs) + kwargs = {"params": {"refresh": True, "realtime": True}} + es.get( + index=index, + id=doc_id, + **normalize_arguments(doc_type="_doc"), + **kwargs, + ) spans = self.get_finished_spans() @@ -388,13 +459,37 @@ def request_hook(span, method, url, kwargs): self.assertEqual( "GET", spans[0].attributes[request_hook_method_attribute] ) + expected_url = f"/{index}/_doc/{doc_id}" + if major_version == 8: + expected_url += "?realtime=true&refresh=true" self.assertEqual( - f"/{index}/_doc/{doc_id}", + expected_url, spans[0].attributes[request_hook_url_attribute], ) + + if major_version == 8: + expected_kwargs = { + "body": None, + "request_timeout": "", + "max_retries": "", + "retry_on_status": "", + "retry_on_timeout": "", + "client_meta": "", + "headers": { + "accept": "application/vnd.elasticsearch+json; compatible-with=8" + }, + "otel_span": None, + } + elif major_version == 7: + expected_kwargs = { + **kwargs, + "headers": {"accept": "application/json"}, + } + else: + expected_kwargs = {**kwargs} self.assertEqual( - json.dumps(kwargs), - spans[0].attributes[request_hook_kwargs_attribute], + expected_kwargs, + json.loads(spans[0].attributes[request_hook_kwargs_attribute]), ) def test_response_hook(self, request_mock): @@ -433,13 +528,13 @@ def response_hook(span, response): }, } - request_mock.return_value = ( - 1, - {}, - json.dumps(response_payload), + request_mock.return_value = helpers.mock_response( + json.dumps(response_payload) + ) + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.get( + index="test-index", **normalize_arguments(doc_type="_doc"), id=1 ) - es = Elasticsearch() - es.get(index="test-index", doc_type="_doc", id=1) spans = self.get_finished_spans() @@ -455,13 +550,11 @@ def test_no_op_tracer_provider(self, request_mock): tracer_provider=trace.NoOpTracerProvider() ) response_payload = '{"found": false, "timed_out": true, "took": 7}' - request_mock.return_value = ( - 1, - {}, - response_payload, + request_mock.return_value = helpers.mock_response(response_payload) + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + res = es.get( + index="test-index", **normalize_arguments(doc_type="_doc"), id=1 ) - es = Elasticsearch() - res = es.get(index="test-index", doc_type="_doc", id=1) self.assertEqual( res.get("found"), json.loads(response_payload).get("found") ) @@ -486,3 +579,62 @@ def test_body_sanitization(self, _): sanitize_body(json.dumps(sanitization_queries.interval_query)), str(sanitization_queries.interval_query_sanitized), ) + + def test_bulk(self, request_mock): + request_mock.return_value = helpers.mock_response("{}") + + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.bulk( + body=[ + { + "_op_type": "index", + "_index": "sw", + "_doc_type": "_doc", + "_id": 1, + "doc": {"name": "adam"}, + }, + { + "_op_type": "index", + "_index": "sw", + "_doc_type": "_doc", + "_id": 1, + "doc": {"name": "adam"}, + }, + ] + ) + + spans_list = self.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.elasticsearch + ) + + @mark.skipif( + (major_version, minor_version) < (8, 13), + reason="Native OTel since elasticsearch 8.13", + ) + @mock.patch.dict( + os.environ, + {"OTEL_PYTHON_INSTRUMENTATION_ELASTICSEARCH_ENABLED": "true"}, + ) + def test_instrumentation_is_disabled_if_native_support_enabled( + self, request_mock + ): + request_mock.return_value = helpers.mock_response("{}") + + es = get_elasticsearch_client(hosts=["http://localhost:9200"]) + es.index( + index="sw", + id=1, + **normalize_arguments(body={"name": "adam"}, doc_type="_doc"), + ) + + spans_list = self.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check that name in span's instrumentation info is not from this instrumentation + self.assertEqual(span.instrumentation_info.name, "elasticsearch-api") diff --git a/instrumentation/opentelemetry-instrumentation-falcon/LICENSE b/instrumentation/opentelemetry-instrumentation-falcon/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-falcon/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml b/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml index 1e4b5e5254..5e0bcdad19 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-falcon/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-wsgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-wsgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "packaging >= 20.0", ] @@ -36,10 +37,6 @@ dependencies = [ instruments = [ "falcon >= 1.4.1, < 3.1.2", ] -test = [ - "opentelemetry-instrumentation-falcon[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] falcon = "opentelemetry.instrumentation.falcon:FalconInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index d6cf8249a4..79c9a0cf0f 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -268,7 +268,7 @@ def __init__(self, *args, **kwargs): self.duration_histogram = self._otel_meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration of the inbound HTTP request", + description="Duration of HTTP client requests.", ) self.active_requests_counter = self._otel_meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, @@ -382,9 +382,9 @@ def _start_response(status, response_headers, *args, **kwargs): raise finally: if span.is_recording(): - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( + span.attributes.get(SpanAttributes.HTTP_STATUS_CODE) + ) duration = max(round((default_timer() - start) * 1000), 0) self.duration_histogram.record(duration, duration_attrs) self.active_requests_counter.add(-1, active_requests_count_attrs) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt new file mode 100644 index 0000000000..f17ada63f4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +falcon==1.4.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-mimeparse==1.6.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-falcon diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt new file mode 100644 index 0000000000..68b1aba13d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +falcon==2.0.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-falcon diff --git a/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt new file mode 100644 index 0000000000..4b4f8e7c0d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +falcon==3.1.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-falcon diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 2245dbfd80..bf7f1d4f49 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -16,21 +16,21 @@ from unittest.mock import Mock, patch import pytest -from falcon import __version__ as _falcon_verison +from falcon import __version__ as _falcon_version from falcon import testing from packaging import version as package_version from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + _server_active_requests_count_attrs_old, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.falcon import FalconInstrumentor from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) -from opentelemetry.instrumentation.wsgi import ( - _active_requests_count_attrs, - _duration_attrs, -) from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, @@ -53,8 +53,8 @@ "http.server.duration", ] _recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": _duration_attrs, + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, } @@ -125,7 +125,7 @@ def _test_method(self, method): SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_HOST: "falconframework.org", SpanAttributes.HTTP_TARGET: "/", - SpanAttributes.NET_PEER_PORT: "65133", + SpanAttributes.NET_PEER_PORT: 65133, SpanAttributes.HTTP_FLAVOR: "1.1", "falcon.resource": "HelloWorldResource", SpanAttributes.HTTP_STATUS_CODE: 201, @@ -156,7 +156,7 @@ def test_404(self): SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_HOST: "falconframework.org", SpanAttributes.HTTP_TARGET: "/", - SpanAttributes.NET_PEER_PORT: "65133", + SpanAttributes.NET_PEER_PORT: 65133, SpanAttributes.HTTP_FLAVOR: "1.1", SpanAttributes.HTTP_STATUS_CODE: 404, }, @@ -193,7 +193,7 @@ def test_500(self): SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_HOST: "falconframework.org", SpanAttributes.HTTP_TARGET: "/", - SpanAttributes.NET_PEER_PORT: "65133", + SpanAttributes.NET_PEER_PORT: 65133, SpanAttributes.HTTP_FLAVOR: "1.1", SpanAttributes.HTTP_STATUS_CODE: 500, }, @@ -226,7 +226,7 @@ def test_url_template(self): SpanAttributes.NET_HOST_PORT: 80, SpanAttributes.HTTP_HOST: "falconframework.org", SpanAttributes.HTTP_TARGET: "/", - SpanAttributes.NET_PEER_PORT: "65133", + SpanAttributes.NET_PEER_PORT: 65133, SpanAttributes.HTTP_FLAVOR: "1.1", "falcon.resource": "UserResource", SpanAttributes.HTTP_STATUS_CODE: 200, @@ -336,6 +336,7 @@ def test_falcon_metric_values(self): "http.flavor": "1.1", "http.server_name": "falconframework.org", "net.host.port": 80, + "net.host.name": "falconframework.org", "http.status_code": 404, } expected_requests_count_attributes = { @@ -344,6 +345,8 @@ def test_falcon_metric_values(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "falconframework.org", + "net.host.name": "falconframework.org", + "net.host.port": 80, } start = default_timer() self.client().simulate_get("/hello/756") @@ -523,7 +526,7 @@ def test_custom_request_header_not_added_in_internal_span(self): self.assertNotIn(key, span.attributes) @pytest.mark.skipif( - condition=package_version.parse(_falcon_verison) + condition=package_version.parse(_falcon_version) < package_version.parse("2.0.0"), reason="falcon<2 does not implement custom response headers", ) @@ -558,7 +561,7 @@ def test_custom_response_header_added_in_server_span(self): self.assertNotIn(key, span.attributes) @pytest.mark.skipif( - condition=package_version.parse(_falcon_verison) + condition=package_version.parse(_falcon_version) < package_version.parse("2.0.0"), reason="falcon<2 does not implement custom response headers", ) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/LICENSE b/instrumentation/opentelemetry-instrumentation-fastapi/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-fastapi/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml index 4df6d3760c..7bae75494e 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml @@ -22,25 +22,20 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-asgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-asgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "fastapi ~= 0.58", ] -test = [ - "opentelemetry-instrumentation-fastapi[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "requests ~= 2.23", # needed for testclient - "httpx ~= 0.22", # needed for testclient -] [project.entry-points.opentelemetry_instrumentor] fastapi = "opentelemetry.instrumentation.fastapi:FastAPIInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 10b73c7a5b..4c673d214a 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -59,20 +59,20 @@ async def foobar(): right after a span is created for a request and right before the span is finished for the response. - The server request hook is passed a server span and ASGI scope object for every incoming request. -- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. -- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. +- The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called. +- The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called. .. code-block:: python - def server_request_hook(span: Span, scope: dict): + def server_request_hook(span: Span, scope: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") - def client_request_hook(span: Span, scope: dict): + def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") - def client_response_hook(span: Span, message: dict): + def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") @@ -115,7 +115,7 @@ def client_response_hook(span: Span, message: dict): single item list containing all the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -146,10 +146,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** @@ -172,28 +172,28 @@ def client_response_hook(span: Span, message: dict): --- """ import logging -import typing from typing import Collection import fastapi from starlette.routing import Match from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.asgi.types import ( + ClientRequestHook, + ClientResponseHook, + ServerRequestHook, +) from opentelemetry.instrumentation.fastapi.package import _instruments from opentelemetry.instrumentation.fastapi.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import Span +from opentelemetry.trace import get_tracer from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls _excluded_urls_from_env = get_excluded_urls("FASTAPI") _logger = logging.getLogger(__name__) -_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]] - class FastAPIInstrumentor(BaseInstrumentor): """An instrumentor for FastAPI @@ -206,9 +206,9 @@ class FastAPIInstrumentor(BaseInstrumentor): @staticmethod def instrument_app( app: fastapi.FastAPI, - server_request_hook: _ServerRequestHookT = None, - client_request_hook: _ClientRequestHookT = None, - client_response_hook: _ClientResponseHookT = None, + server_request_hook: ServerRequestHook = None, + client_request_hook: ClientRequestHook = None, + client_response_hook: ClientResponseHook = None, tracer_provider=None, meter_provider=None, excluded_urls=None, @@ -222,6 +222,12 @@ def instrument_app( excluded_urls = _excluded_urls_from_env else: excluded_urls = parse_excluded_urls(excluded_urls) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter = get_meter( __name__, __version__, @@ -236,7 +242,8 @@ def instrument_app( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, - tracer_provider=tracer_provider, + # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation + tracer=tracer, meter=meter, ) app._is_instrumented_by_opentelemetry = True @@ -292,13 +299,19 @@ class _InstrumentedFastAPI(fastapi.FastAPI): _tracer_provider = None _meter_provider = None _excluded_urls = None - _server_request_hook: _ServerRequestHookT = None - _client_request_hook: _ClientRequestHookT = None - _client_response_hook: _ClientResponseHookT = None + _server_request_hook: ServerRequestHook = None + _client_request_hook: ClientRequestHook = None + _client_response_hook: ClientResponseHook = None _instrumented_fastapi_apps = set() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + tracer = get_tracer( + __name__, + __version__, + _InstrumentedFastAPI._tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter = get_meter( __name__, __version__, @@ -312,7 +325,8 @@ def __init__(self, *args, **kwargs): server_request_hook=_InstrumentedFastAPI._server_request_hook, client_request_hook=_InstrumentedFastAPI._client_request_hook, client_response_hook=_InstrumentedFastAPI._client_response_hook, - tracer_provider=_InstrumentedFastAPI._tracer_provider, + # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation + tracer=tracer, meter=meter, ) self._is_instrumented_by_opentelemetry = True diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt new file mode 100644 index 0000000000..2116980b3f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt @@ -0,0 +1,33 @@ +annotated-types==0.6.0 +anyio==4.3.0 +asgiref==3.7.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +exceptiongroup==1.2.0 +fastapi==0.109.2 +h11==0.14.0 +httpcore==1.0.4 +httpx==0.27.0 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pydantic==2.6.2 +pydantic_core==2.16.3 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +sniffio==1.3.0 +starlette==0.36.3 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-asgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-fastapi diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 4269dfa2e4..26d9e743a8 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -11,9 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import unittest +from collections.abc import Mapping from timeit import default_timer +from typing import Tuple from unittest.mock import patch import fastapi @@ -117,6 +118,10 @@ def test_instrument_app_with_instrument(self): self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /foobar", span.name) + self.assertEqual( + span.instrumentation_scope.name, + "opentelemetry.instrumentation.fastapi", + ) def test_uninstrument_app(self): self._client.get("/foobar") @@ -197,6 +202,10 @@ def test_fastapi_metrics(self): for resource_metric in metrics_list.resource_metrics: self.assertTrue(len(resource_metric.scope_metrics) == 1) for scope_metric in resource_metric.scope_metrics: + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.fastapi", + ) self.assertTrue(len(scope_metric.metrics) == 3) for metric in scope_metric.metrics: self.assertIn(metric.name, _expected_metric_names) @@ -246,7 +255,7 @@ def test_basic_metric_success(self): dict(point.attributes), ) self.assertEqual(point.count, 1) - self.assertAlmostEqual(duration, point.sum, delta=30) + self.assertAlmostEqual(duration, point.sum, delta=40) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, @@ -271,7 +280,7 @@ def test_basic_post_request_metric_success(self): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) if metric.name == "http.server.duration": - self.assertAlmostEqual(duration, point.sum, delta=30) + self.assertAlmostEqual(duration, point.sum, delta=40) elif metric.name == "http.server.response.size": self.assertEqual(response_size, point.sum) elif metric.name == "http.server.request.size": @@ -279,7 +288,7 @@ def test_basic_post_request_metric_success(self): if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) - def test_metric_uninstruemnt_app(self): + def test_metric_uninstrument_app(self): self._client.get("/foobar") self._instrumentor.uninstrument_app(self._app) self._client.get("/foobar") @@ -342,23 +351,23 @@ def server_request_hook(self, span, scope): if self._server_request_hook is not None: self._server_request_hook(span, scope) - def client_request_hook(self, receive_span, request): + def client_request_hook(self, receive_span, scope, message): if self._client_request_hook is not None: - self._client_request_hook(receive_span, request) + self._client_request_hook(receive_span, scope, message) - def client_response_hook(self, send_span, response): + def client_response_hook(self, send_span, scope, message): if self._client_response_hook is not None: - self._client_response_hook(send_span, response) + self._client_response_hook(send_span, scope, message) def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") - def client_request_hook(receive_span, request): + def client_request_hook(receive_span, scope, message): receive_span.update_name("name from client hook") receive_span.set_attribute("attr-from-request-hook", "set") - def client_response_hook(send_span, response): + def client_response_hook(send_span, scope, message): send_span.update_name("name from response hook") send_span.set_attribute("attr-from-response-hook", "value") @@ -549,6 +558,24 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) +class MultiMapping(Mapping): + + def __init__(self, *items: Tuple[str, str]): + self._items = items + + def __len__(self): + return len(self._items) + + def __getitem__(self, __key): + raise NotImplementedError("use .items() instead") + + def __iter__(self): + raise NotImplementedError("use .items() instead") + + def items(self): + return self._items + + @patch.dict( "os.environ", { @@ -575,13 +602,15 @@ def _create_app(): @app.get("/foobar") async def _(): - headers = { - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", - "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", - "My-Secret-Header": "My Secret Value", - } + headers = MultiMapping( + ("custom-test-header-1", "test-header-value-1"), + ("custom-test-header-2", "test-header-value-2"), + ("my-custom-regex-header-1", "my-custom-regex-value-1"), + ("my-custom-regex-header-1", "my-custom-regex-value-2"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-3"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-4"), + ("My-Secret-Header", "My Secret Value"), + ) content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) @@ -657,10 +686,12 @@ def test_http_custom_response_headers_in_span_attributes(self): "test-header-value-2", ), "http.response.header.my_custom_regex_header_1": ( - "my-custom-regex-value-1,my-custom-regex-value-2", + "my-custom-regex-value-1", + "my-custom-regex-value-2", ), "http.response.header.my_custom_regex_header_2": ( - "my-custom-regex-value-3,my-custom-regex-value-4", + "my-custom-regex-value-3", + "my-custom-regex-value-4", ), "http.response.header.my_secret_header": ("[REDACTED]",), } diff --git a/instrumentation/opentelemetry-instrumentation-flask/LICENSE b/instrumentation/opentelemetry-instrumentation-flask/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-flask/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml b/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml index ca26f4c860..1b769d2957 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-flask/pyproject.toml @@ -22,25 +22,22 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-wsgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-wsgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "packaging >= 21.0", + "importlib-metadata >= 4.0", ] [project.optional-dependencies] instruments = [ "flask >= 1.0", ] -test = [ - "opentelemetry-instrumentation-flask[instruments]", - "markupsafe==2.1.2", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] flask = "opentelemetry.instrumentation.flask:FlaskInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py index 5e81cc5abe..34e9b5ea50 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py @@ -245,22 +245,21 @@ def response_hook(span: Span, status: str, response_headers: List): from typing import Collection import flask +import importlib_metadata as metadata from packaging import version as package_version import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import context, trace +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, +) from opentelemetry.instrumentation.flask.package import _instruments from opentelemetry.instrumentation.flask.version import __version__ - -try: - flask_version = flask.__version__ -except AttributeError: - try: - from importlib import metadata - except ImportError: - import importlib_metadata as metadata - flask_version = metadata.version("flask") - from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, @@ -268,8 +267,15 @@ def response_hook(span: Span, status: str, response_headers: List): from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.metrics import get_meter from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls +from opentelemetry.util.http import ( + get_excluded_urls, + parse_excluded_urls, + sanitize_method, +) _logger = getLogger(__name__) @@ -281,6 +287,8 @@ def response_hook(span: Span, status: str, response_headers: List): _excluded_urls_from_env = get_excluded_urls("FLASK") +flask_version = metadata.version("flask") + if package_version.parse(flask_version) >= package_version.parse("2.2.0"): def _request_ctx_ref() -> weakref.ReferenceType: @@ -293,8 +301,13 @@ def _request_ctx_ref() -> weakref.ReferenceType: def get_default_span_name(): + method = sanitize_method( + flask.request.environ.get("REQUEST_METHOD", "").strip() + ) + if method == "_OTHER": + method = "HTTP" try: - span_name = flask.request.url_rule.rule + span_name = f"{method} {flask.request.url_rule.rule}" except AttributeError: span_name = otel_wsgi.get_default_span_name(flask.request.environ) return span_name @@ -303,9 +316,11 @@ def get_default_span_name(): def _rewrapped_app( wsgi_app, active_requests_counter, - duration_histogram, + duration_histogram_old=None, response_hook=None, excluded_urls=None, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, + duration_histogram_new=None, ): def _wrapped_app(wrapped_app_environ, start_response): # We want to measure the time for route matching, etc. @@ -314,11 +329,16 @@ def _wrapped_app(wrapped_app_environ, start_response): # we better avoid it. wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns() start = default_timer() - attributes = otel_wsgi.collect_request_attributes(wrapped_app_environ) + attributes = otel_wsgi.collect_request_attributes( + wrapped_app_environ, sem_conv_opt_in_mode + ) active_requests_count_attrs = ( - otel_wsgi._parse_active_request_count_attrs(attributes) + otel_wsgi._parse_active_request_count_attrs( + attributes, + sem_conv_opt_in_mode, + ) ) - duration_attrs = otel_wsgi._parse_duration_attrs(attributes) + active_requests_counter.add(1, active_requests_count_attrs) def _start_response(status, response_headers, *args, **kwargs): @@ -337,13 +357,12 @@ def _start_response(status, response_headers, *args, **kwargs): if span: otel_wsgi.add_response_attributes( - span, status, response_headers + span, + status, + response_headers, + attributes, + sem_conv_opt_in_mode, ) - status_code = otel_wsgi._parse_status_code(status) - if status_code is not None: - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = status_code if ( span.is_recording() and span.kind == trace.SpanKind.SERVER @@ -364,8 +383,21 @@ def _start_response(status, response_headers, *args, **kwargs): return start_response(status, response_headers, *args, **kwargs) result = wsgi_app(wrapped_app_environ, _start_response) - duration = max(round((default_timer() - start) * 1000), 0) - duration_histogram.record(duration, duration_attrs) + duration_s = default_timer() - start + if duration_histogram_old: + duration_attrs_old = otel_wsgi._parse_duration_attrs( + attributes, _HTTPStabilityMode.DEFAULT + ) + duration_histogram_old.record( + max(round(duration_s * 1000), 0), duration_attrs_old + ) + if duration_histogram_new: + duration_attrs_new = otel_wsgi._parse_duration_attrs( + attributes, _HTTPStabilityMode.HTTP + ) + duration_histogram_new.record( + max(duration_s, 0), duration_attrs_new + ) active_requests_counter.add(-1, active_requests_count_attrs) return result @@ -378,6 +410,7 @@ def _wrapped_before_request( excluded_urls=None, enable_commenter=True, commenter_options=None, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, ): def _before_request(): if excluded_urls and excluded_urls.url_disabled(flask.request.url): @@ -386,7 +419,8 @@ def _before_request(): span_name = get_default_span_name() attributes = otel_wsgi.collect_request_attributes( - flask_request_environ + flask_request_environ, + sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) if flask.request.url_rule: # For 404 that result from no route found, etc, we @@ -497,6 +531,7 @@ class _InstrumentedFlask(flask.Flask): _enable_commenter = True _commenter_options = None _meter_provider = None + _sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -510,11 +545,20 @@ def __init__(self, *args, **kwargs): _InstrumentedFlask._meter_provider, schema_url="https://opentelemetry.io/schemas/1.11.0", ) - duration_histogram = meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="measures the duration of the inbound HTTP request", - ) + duration_histogram_old = None + if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode): + duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + duration_histogram_new = None + if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode): + duration_histogram_new = meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + unit="s", + description="measures the duration of the inbound HTTP request", + ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", @@ -524,9 +568,11 @@ def __init__(self, *args, **kwargs): self.wsgi_app = _rewrapped_app( self.wsgi_app, active_requests_counter, - duration_histogram, + duration_histogram_old, _InstrumentedFlask._response_hook, excluded_urls=_InstrumentedFlask._excluded_urls, + sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode, + duration_histogram_new=duration_histogram_new, ) tracer = trace.get_tracer( @@ -542,6 +588,7 @@ def __init__(self, *args, **kwargs): excluded_urls=_InstrumentedFlask._excluded_urls, enable_commenter=_InstrumentedFlask._enable_commenter, commenter_options=_InstrumentedFlask._commenter_options, + sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode, ) self._before_request = _before_request self.before_request(_before_request) @@ -585,11 +632,19 @@ def _instrument(self, **kwargs): _InstrumentedFlask._commenter_options = commenter_options meter_provider = kwargs.get("meter_provider") _InstrumentedFlask._meter_provider = meter_provider + + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + + _InstrumentedFlask._sem_conv_opt_in_mode = sem_conv_opt_in_mode + flask.Flask = _InstrumentedFlask def _uninstrument(self, **kwargs): flask.Flask = self._original_flask + # pylint: disable=too-many-locals @staticmethod def instrument_app( app, @@ -605,6 +660,11 @@ def instrument_app( app._is_instrumented_by_opentelemetry = False if not app._is_instrumented_by_opentelemetry: + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) excluded_urls = ( parse_excluded_urls(excluded_urls) if excluded_urls is not None @@ -614,33 +674,44 @@ def instrument_app( __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", - ) - duration_histogram = meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="measures the duration of the inbound HTTP request", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) + duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + duration_histogram_new = meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + unit="s", + description="measures the duration of the inbound HTTP request", + ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, - unit="requests", - description="measures the number of concurrent HTTP requests that are currently in-flight", + unit="{request}", + description="Number of active HTTP server requests.", ) app._original_wsgi_app = app.wsgi_app app.wsgi_app = _rewrapped_app( app.wsgi_app, active_requests_counter, - duration_histogram, - response_hook, + duration_histogram_old, + response_hook=response_hook, excluded_urls=excluded_urls, + sem_conv_opt_in_mode=sem_conv_opt_in_mode, + duration_histogram_new=duration_histogram_new, ) tracer = trace.get_tracer( __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) _before_request = _wrapped_before_request( @@ -648,9 +719,10 @@ def instrument_app( tracer, excluded_urls=excluded_urls, enable_commenter=enable_commenter, - commenter_options=commenter_options - if commenter_options - else {}, + commenter_options=( + commenter_options if commenter_options else {} + ), + sem_conv_opt_in_mode=sem_conv_opt_in_mode, ) app._before_request = _before_request app.before_request(_before_request) diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py index d83adbede0..150ca0ca9e 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py @@ -16,3 +16,5 @@ _instruments = ("flask >= 1.0",) _supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt new file mode 100644 index 0000000000..fad2f5e2b0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +click==8.1.7 +Deprecated==1.2.14 +Flask==2.1.3 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +Werkzeug==2.3.8 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-flask diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt new file mode 100644 index 0000000000..919ee6d431 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +click==8.1.7 +Deprecated==1.2.14 +Flask==2.2.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +Werkzeug==2.3.8 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-flask diff --git a/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt new file mode 100644 index 0000000000..16d91d2058 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt @@ -0,0 +1,24 @@ +asgiref==3.7.2 +blinker==1.7.0 +click==8.1.7 +Deprecated==1.2.14 +Flask==3.0.2 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +Werkzeug==3.0.3 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-flask diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py index 6117521bb9..307ac3ccf0 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py @@ -76,18 +76,26 @@ def _custom_response_headers(): resp = flask.Response("test response") resp.headers["content-type"] = "text/plain; charset=utf-8" resp.headers["content-length"] = "13" - resp.headers[ - "my-custom-header" - ] = "my-custom-value-1,my-custom-header-2" - resp.headers[ - "my-custom-regex-header-1" - ] = "my-custom-regex-value-1,my-custom-regex-value-2" - resp.headers[ - "My-Custom-Regex-Header-2" - ] = "my-custom-regex-value-3,my-custom-regex-value-4" + resp.headers["my-custom-header"] = ( + "my-custom-value-1,my-custom-header-2" + ) + resp.headers["my-custom-regex-header-1"] = ( + "my-custom-regex-value-1,my-custom-regex-value-2" + ) + resp.headers["My-Custom-Regex-Header-2"] = ( + "my-custom-regex-value-3,my-custom-regex-value-4" + ) resp.headers["my-secret-header"] = "my-secret-value" return resp + @staticmethod + def _repeat_custom_response_headers(): + headers = { + "content-type": "text/plain; charset=utf-8", + "my-custom-header": ["my-custom-value-1", "my-custom-header-2"], + } + return flask.Response("test response", headers=headers) + def _common_initialization(self): def excluded_endpoint(): return "excluded" @@ -106,6 +114,9 @@ def excluded2_endpoint(): self.app.route("/test_custom_response_headers")( self._custom_response_headers ) + self.app.route("/test_repeat_custom_response_headers")( + self._repeat_custom_response_headers + ) # pylint: disable=attribute-defined-outside-init self.client = Client(self.app, Response) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_copy_context.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_copy_context.py index 96268de5e7..7a57d01c43 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_copy_context.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_copy_context.py @@ -44,5 +44,5 @@ def test_copycontext(self): resp = client.get("/copy_context", headers={"x-req": "a-header"}) self.assertEqual(200, resp.status_code) - self.assertEqual("/copy_context", resp.json["span_name"]) + self.assertEqual("GET /copy_context", resp.json["span_name"]) self.assertEqual("a-header", resp.json["request_header"]) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 3bd2f74ba8..f50d3245a0 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -12,28 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines from timeit import default_timer from unittest.mock import Mock, patch from flask import Flask, request from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.propagators import ( TraceResponsePropagator, get_global_response_propagator, set_global_response_propagator, ) -from opentelemetry.instrumentation.wsgi import ( - OpenTelemetryMiddleware, - _active_requests_count_attrs, - _duration_attrs, -) +from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import ( @@ -54,6 +60,7 @@ def expected_attributes(override_attributes): SpanAttributes.HTTP_SERVER_NAME: "localhost", SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.NET_HOST_NAME: "localhost", SpanAttributes.HTTP_HOST: "localhost", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_FLAVOR: "1.1", @@ -64,26 +71,71 @@ def expected_attributes(override_attributes): return default_attributes -_expected_metric_names = [ +def expected_attributes_new(override_attributes): + default_attributes = { + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.SERVER_PORT: 80, + SpanAttributes.SERVER_ADDRESS: "localhost", + SpanAttributes.URL_PATH: "/hello/123", + SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + } + for key, val in override_attributes.items(): + default_attributes[key] = val + return default_attributes + + +_expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", ] -_recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": _duration_attrs, +_expected_metric_names_new = [ + "http.server.active_requests", + "http.server.request.duration", +] +_recommended_metrics_attrs_old = { + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, +} +_recommended_metrics_attrs_new = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, +} +_server_active_requests_count_attrs_both = ( + _server_active_requests_count_attrs_old +) +_server_active_requests_count_attrs_both.extend( + _server_active_requests_count_attrs_new +) +_recommended_metrics_attrs_both = { + "http.server.active_requests": _server_active_requests_count_attrs_both, + "http.server.duration": _server_duration_attrs_old, + "http.server.request.duration": _server_duration_attrs_new, } +# pylint: disable=too-many-public-methods class TestProgrammatic(InstrumentationTest, WsgiTestBase): def setUp(self): super().setUp() + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" + self.env_patch = patch.dict( "os.environ", { - "OTEL_PYTHON_FLASK_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg" + "OTEL_PYTHON_FLASK_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) + _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() self.exclude_patch = patch( @@ -169,7 +221,45 @@ def test_simple(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/") + self.assertEqual(span_list[0].name, "GET /hello/") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_simple_new_semconv(self): + expected_attrs = expected_attributes_new( + { + SpanAttributes.HTTP_ROUTE: "/hello/", + SpanAttributes.URL_SCHEME: "http", + } + ) + self.client.get("/hello/123") + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "GET /hello/") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_simple_both_semconv(self): + expected_attrs = expected_attributes( + { + SpanAttributes.HTTP_TARGET: "/hello/123", + SpanAttributes.HTTP_ROUTE: "/hello/", + } + ) + expected_attrs.update( + expected_attributes_new( + { + SpanAttributes.HTTP_ROUTE: "/hello/", + SpanAttributes.URL_SCHEME: "http", + } + ) + ) + self.client.get("/hello/123") + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) @@ -219,6 +309,53 @@ def test_404(self): self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + def test_404_new_semconv(self): + expected_attrs = expected_attributes_new( + { + SpanAttributes.HTTP_REQUEST_METHOD: "POST", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 404, + SpanAttributes.URL_PATH: "/bye", + SpanAttributes.URL_SCHEME: "http", + } + ) + + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "POST /bye") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_404_both_semconv(self): + expected_attrs = expected_attributes( + { + SpanAttributes.HTTP_METHOD: "POST", + SpanAttributes.HTTP_TARGET: "/bye", + SpanAttributes.HTTP_STATUS_CODE: 404, + } + ) + expected_attrs.update( + expected_attributes_new( + { + SpanAttributes.HTTP_REQUEST_METHOD: "POST", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 404, + SpanAttributes.URL_PATH: "/bye", + SpanAttributes.URL_SCHEME: "http", + } + ) + ) + + resp = self.client.post("/bye") + self.assertEqual(404, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "POST /bye") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + def test_internal_error(self): expected_attrs = expected_attributes( { @@ -232,7 +369,53 @@ def test_internal_error(self): resp.close() span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) - self.assertEqual(span_list[0].name, "/hello/") + self.assertEqual(span_list[0].name, "GET /hello/") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_internal_error_new_semconv(self): + expected_attrs = expected_attributes_new( + { + SpanAttributes.URL_PATH: "/hello/500", + SpanAttributes.HTTP_ROUTE: "/hello/", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 500, + ERROR_TYPE: "500", + SpanAttributes.URL_SCHEME: "http", + } + ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "GET /hello/") + self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) + self.assertEqual(span_list[0].attributes, expected_attrs) + + def test_internal_error_both_semconv(self): + expected_attrs = expected_attributes( + { + SpanAttributes.HTTP_TARGET: "/hello/500", + SpanAttributes.HTTP_ROUTE: "/hello/", + SpanAttributes.HTTP_STATUS_CODE: 500, + } + ) + expected_attrs.update( + expected_attributes_new( + { + SpanAttributes.URL_PATH: "/hello/500", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 500, + ERROR_TYPE: "500", + SpanAttributes.URL_SCHEME: "http", + } + ) + ) + resp = self.client.get("/hello/500") + self.assertEqual(500, resp.status_code) + resp.close() + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + self.assertEqual(span_list[0].name, "GET /hello/") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) @@ -290,7 +473,41 @@ def test_flask_metrics(self): for scope_metric in resource_metric.scope_metrics: self.assertTrue(len(scope_metric.metrics) != 0) for metric in scope_metric.metrics: - self.assertIn(metric.name, _expected_metric_names) + self.assertIn(metric.name, _expected_metric_names_old) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + self.assertAlmostEqual( + duration, point.sum, delta=10 + ) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_old[metric.name], + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_flask_metrics_new_semconv(self): + start = default_timer() + self.client.get("/hello/123") + self.client.get("/hello/321") + self.client.get("/hello/756") + duration = max(round((default_timer() - start) * 1000), 0) + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: @@ -304,7 +521,8 @@ def test_flask_metrics(self): number_data_point_seen = True for attr in point.attributes: self.assertIn( - attr, _recommended_attrs[metric.name] + attr, + _recommended_metrics_attrs_new[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) @@ -358,6 +576,7 @@ def test_basic_metric_success(self): "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 200, + "net.host.name": "localhost", } expected_requests_count_attributes = { "http.method": "GET", @@ -365,6 +584,25 @@ def test_basic_metric_success(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", + "net.host.name": "localhost", + "net.host.port": 80, + } + self._assert_basic_metric( + expected_duration_attributes, + expected_requests_count_attributes, + ) + + def test_basic_metric_success_new_semconv(self): + self.client.get("/hello/756") + expected_duration_attributes = { + "http.request.method": "GET", + "url.scheme": "http", + "network.protocol.version": "1.1", + "http.response.status_code": 200, + } + expected_requests_count_attributes = { + "http.request.method": "GET", + "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, @@ -374,20 +612,40 @@ def test_basic_metric_success(self): def test_basic_metric_nonstandard_http_method_success(self): self.client.open("/hello/756", method="NONSTANDARD") expected_duration_attributes = { - "http.method": "UNKNOWN", + "http.method": "_OTHER", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 405, + "net.host.name": "localhost", } expected_requests_count_attributes = { - "http.method": "UNKNOWN", + "http.method": "_OTHER", "http.host": "localhost", "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", + "net.host.name": "localhost", + "net.host.port": 80, + } + self._assert_basic_metric( + expected_duration_attributes, + expected_requests_count_attributes, + ) + + def test_basic_metric_nonstandard_http_method_success_new_semconv(self): + self.client.open("/hello/756", method="NONSTANDARD") + expected_duration_attributes = { + "http.request.method": "_OTHER", + "url.scheme": "http", + "network.protocol.version": "1.1", + "http.response.status_code": 405, + } + expected_requests_count_attributes = { + "http.request.method": "_OTHER", + "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, @@ -400,23 +658,19 @@ def test_basic_metric_nonstandard_http_method_success(self): OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS: "1", }, ) - def test_basic_metric_nonstandard_http_method_allowed_success(self): + def test_basic_metric_nonstandard_http_method_allowed_success_new_semconv( + self, + ): self.client.open("/hello/756", method="NONSTANDARD") expected_duration_attributes = { - "http.method": "NONSTANDARD", - "http.host": "localhost", - "http.scheme": "http", - "http.flavor": "1.1", - "http.server_name": "localhost", - "net.host.port": 80, - "http.status_code": 405, + "http.request.method": "NONSTANDARD", + "url.scheme": "http", + "network.protocol.version": "1.1", + "http.response.status_code": 405, } expected_requests_count_attributes = { - "http.method": "NONSTANDARD", - "http.host": "localhost", - "http.scheme": "http", - "http.flavor": "1.1", - "http.server_name": "localhost", + "http.request.method": "NONSTANDARD", + "url.scheme": "http", } self._assert_basic_metric( expected_duration_attributes, @@ -671,6 +925,22 @@ def test_custom_request_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) + def test_repeat_custom_request_header_added_in_server_span(self): + headers = [ + ("Custom-Test-Header-1", "Test Value 1"), + ("Custom-Test-Header-1", "Test Value 2"), + ] + resp = self.client.get("/hello/123", headers=headers) + self.assertEqual(200, resp.status_code) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.request.header.custom_test_header_1": ( + "Test Value 1, Test Value 2", + ), + } + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): @@ -724,6 +994,21 @@ def test_custom_response_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) + def test_repeat_custom_response_header_added_in_server_span(self): + resp = self.client.get("/test_repeat_custom_response_headers") + self.assertEqual(resp.status_code, 200) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.response.header.content_type": ( + "text/plain; charset=utf-8", + ), + "http.response.header.my_custom_header": ( + "my-custom-value-1,my-custom-header-2", + ), + } + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_response_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): diff --git a/instrumentation/opentelemetry-instrumentation-grpc/LICENSE b/instrumentation/opentelemetry-instrumentation-grpc/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-grpc/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml index 8373feaa40..d43c5ed173 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-grpc/pyproject.toml @@ -22,12 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-sdk ~= 1.12", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,12 +35,6 @@ dependencies = [ instruments = [ "grpcio ~= 1.27", ] -test = [ - "opentelemetry-instrumentation-grpc[instruments]", - "opentelemetry-sdk ~= 1.12", - "opentelemetry-test-utils == 0.45b0.dev", - "protobuf ~= 3.13", -] [project.entry-points.opentelemetry_instrumentor] grpc_client = "opentelemetry.instrumentation.grpc:GrpcInstrumentorClient" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py index 8fc992be73..9c8cc5cdf3 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_client.py @@ -14,10 +14,9 @@ import functools import logging -from collections import OrderedDict import grpc -from grpc.aio import ClientCallDetails +from grpc.aio import ClientCallDetails, Metadata from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, @@ -55,20 +54,19 @@ def callback(call): class _BaseAioClientInterceptor(OpenTelemetryClientInterceptor): @staticmethod - def propagate_trace_in_details(client_call_details): + def propagate_trace_in_details(client_call_details: ClientCallDetails): metadata = client_call_details.metadata if not metadata: - mutable_metadata = OrderedDict() + mutable_metadata = Metadata() else: - mutable_metadata = OrderedDict(metadata) + mutable_metadata = Metadata(*tuple(metadata)) inject(mutable_metadata, setter=_carrier_setter) - metadata = tuple(mutable_metadata.items()) return ClientCallDetails( client_call_details.method, client_call_details.timeout, - metadata, + mutable_metadata, client_call_details.credentials, client_call_details.wait_for_ready, ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt new file mode 100644 index 0000000000..d30aa5c5e9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +grpcio==1.62.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +protobuf==3.20.3 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-grpc diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py index 9658df1587..6c0b8eac21 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/_aio_client.py @@ -21,7 +21,7 @@ async def simple_method(stub, error=False): request = Request( client_id=CLIENT_ID, request_data="error" if error else "data" ) - return await stub.SimpleMethod(request) + return await stub.SimpleMethod(request, metadata=(("key", "value"),)) async def client_streaming_method(stub, error=False): @@ -41,7 +41,7 @@ def server_streaming_method(stub, error=False): client_id=CLIENT_ID, request_data="error" if error else "data" ) - return stub.ServerStreamingMethod(request) + return stub.ServerStreamingMethod(request, metadata=(("key", "value"),)) def bidirectional_streaming_method(stub, error=False): @@ -53,4 +53,6 @@ def request_messages(): ) yield request - return stub.BidirectionalStreamingMethod(request_messages()) + return stub.BidirectionalStreamingMethod( + request_messages(), metadata=(("key", "value"),) + ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py index 69222b37a4..67e7d0a625 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/_client.py @@ -21,14 +21,14 @@ def simple_method(stub, error=False): request = Request( client_id=CLIENT_ID, request_data="error" if error else "data" ) - stub.SimpleMethod(request) + stub.SimpleMethod(request, metadata=(("key", "value"),)) def simple_method_future(stub, error=False): request = Request( client_id=CLIENT_ID, request_data="error" if error else "data" ) - return stub.SimpleMethod.future(request) + return stub.SimpleMethod.future(request, metadata=(("key", "value"),)) def client_streaming_method(stub, error=False): @@ -40,14 +40,18 @@ def request_messages(): ) yield request - stub.ClientStreamingMethod(request_messages()) + stub.ClientStreamingMethod( + request_messages(), metadata=(("key", "value"),) + ) def server_streaming_method(stub, error=False): request = Request( client_id=CLIENT_ID, request_data="error" if error else "data" ) - response_iterator = stub.ServerStreamingMethod(request) + response_iterator = stub.ServerStreamingMethod( + request, metadata=(("key", "value"),) + ) list(response_iterator) @@ -59,6 +63,8 @@ def request_messages(): ) yield request - response_iterator = stub.BidirectionalStreamingMethod(request_messages()) + response_iterator = stub.BidirectionalStreamingMethod( + request_messages(), metadata=(("key", "value"),) + ) list(response_iterator) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py index ad3dcf3fe7..f00b5f82e7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2.py @@ -142,7 +142,7 @@ (_message.Message,), { "DESCRIPTOR": _REQUEST, - "__module__": "test_server_pb2" + "__module__": "test_server_pb2", # @@protoc_insertion_point(class_scope:Request) }, ) @@ -153,7 +153,7 @@ (_message.Message,), { "DESCRIPTOR": _RESPONSE, - "__module__": "test_server_pb2" + "__module__": "test_server_pb2", # @@protoc_insertion_point(class_scope:Response) }, ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py index 6b1006b8a3..7ae1649149 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py @@ -11,24 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -try: - from unittest import IsolatedAsyncioTestCase -except ImportError: - # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use - # simplifies the following tests. Without it, the amount of test code - # increases significantly, with most of the additional code handling - # the asyncio set up. - from unittest import TestCase - - class IsolatedAsyncioTestCase(TestCase): - def run(self, result=None): - self.skipTest( - "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" - ) - +from unittest import IsolatedAsyncioTestCase import grpc -import pytest import opentelemetry.instrumentation.grpc from opentelemetry import trace @@ -65,7 +50,6 @@ async def intercept_unary_unary( return await continuation(client_call_details, request) -@pytest.mark.asyncio class TestAioClientInterceptor(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() @@ -305,11 +289,10 @@ async def test_client_interceptor_trace_context_propagation(self): await simple_method(stub) metadata = recording_interceptor.recorded_details.metadata - assert len(metadata) == 2 - assert metadata[0][0] == "mock-traceid" - assert metadata[0][1] == "0" - assert metadata[1][0] == "mock-spanid" - assert metadata[1][1] == "0" + assert len(metadata) == 3 + assert metadata.get_all("key") == ["value"] + assert metadata.get_all("mock-traceid") == ["0"] + assert metadata.get_all("mock-spanid") == ["0"] finally: set_global_textmap(previous_propagator) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py index b8c408c6cf..2bd68fd492 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_filter.py @@ -11,27 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -try: - from unittest import IsolatedAsyncioTestCase -except ImportError: - # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use - # simplifies the following tests. Without it, the amount of test code - # increases significantly, with most of the additional code handling - # the asyncio set up. - from unittest import TestCase - - class IsolatedAsyncioTestCase(TestCase): - def run(self, result=None): - self.skipTest( - "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" - ) - - import os -from unittest import mock +from unittest import IsolatedAsyncioTestCase, mock import grpc -import pytest from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorClient, @@ -50,7 +33,6 @@ def run(self, result=None): from .protobuf import test_server_pb2_grpc # pylint: disable=no-name-in-module -@pytest.mark.asyncio class TestAioClientInterceptorFiltered(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py index fe906b26c1..40c2334ae7 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor_hooks.py @@ -11,24 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -try: - from unittest import IsolatedAsyncioTestCase -except ImportError: - # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use - # simplifies the following tests. Without it, the amount of test code - # increases significantly, with most of the additional code handling - # the asyncio set up. - from unittest import TestCase - - class IsolatedAsyncioTestCase(TestCase): - def run(self, result=None): - self.skipTest( - "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" - ) - +from unittest import IsolatedAsyncioTestCase import grpc -import pytest from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorClient from opentelemetry.test.test_base import TestBase @@ -47,14 +32,13 @@ def response_hook(span, response): def request_hook_with_exception(_span, _request): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised -@pytest.mark.asyncio class TestAioClientInterceptorWithHooks(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py index 7b31b085de..050f6f8d13 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py @@ -12,26 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio - -try: - from unittest import IsolatedAsyncioTestCase -except ImportError: - # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use - # simplifies the following tests. Without it, the amount of test code - # increases significantly, with most of the additional code handling - # the asyncio set up. - from unittest import TestCase - - class IsolatedAsyncioTestCase(TestCase): - def run(self, result=None): - self.skipTest( - "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" - ) - +from unittest import IsolatedAsyncioTestCase import grpc import grpc.aio -import pytest import opentelemetry.instrumentation.grpc from opentelemetry import trace @@ -97,7 +81,6 @@ async def run_with_test_server( return resp -@pytest.mark.asyncio class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): async def test_instrumentor(self): """Check that automatic instrumentation configures the interceptor""" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py index 837d9c7618..34b755ced8 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor_filter.py @@ -11,25 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -try: - from unittest import IsolatedAsyncioTestCase -except ImportError: - # unittest.IsolatedAsyncioTestCase was introduced in Python 3.8. It's use - # simplifies the following tests. Without it, the amount of test code - # increases significantly, with most of the additional code handling - # the asyncio set up. - from unittest import TestCase - - class IsolatedAsyncioTestCase(TestCase): - def run(self, result=None): - self.skipTest( - "This test requires Python 3.8 for unittest.IsolatedAsyncioTestCase" - ) - +from unittest import IsolatedAsyncioTestCase import grpc import grpc.aio -import pytest from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( @@ -68,7 +53,6 @@ async def run_with_test_server( return resp -@pytest.mark.asyncio class TestOpenTelemetryAioServerInterceptor(TestBase, IsolatedAsyncioTestCase): async def test_instrumentor(self): """Check that automatic instrumentation configures the interceptor""" diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py index 2436aca40c..38759352b3 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py @@ -14,9 +14,6 @@ # pylint:disable=cyclic-import import grpc -from tests.protobuf import ( # pylint: disable=no-name-in-module - test_server_pb2_grpc, -) import opentelemetry.instrumentation.grpc from opentelemetry import trace @@ -41,6 +38,7 @@ simple_method_future, ) from ._server import create_test_server +from .protobuf import test_server_pb2_grpc from .protobuf.test_server_pb2 import Request diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py index 9a9aefad59..b6ae975dff 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py @@ -17,9 +17,6 @@ from unittest import mock import grpc -from tests.protobuf import ( # pylint: disable=no-name-in-module - test_server_pb2_grpc, -) import opentelemetry.instrumentation.grpc from opentelemetry import trace @@ -44,6 +41,7 @@ simple_method_future, ) from ._server import create_test_server +from .protobuf import test_server_pb2_grpc from .protobuf.test_server_pb2 import Request diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py index ca649f7bb1..ac65c76c34 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_hooks.py @@ -13,9 +13,6 @@ # limitations under the License. import grpc -from tests.protobuf import ( # pylint: disable=no-name-in-module - test_server_pb2_grpc, -) from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient @@ -23,6 +20,7 @@ from ._client import simple_method from ._server import create_test_server +from .protobuf import test_server_pb2_grpc # User defined interceptor. Is used in the tests along with the opentelemetry client interceptor. @@ -75,11 +73,11 @@ def response_hook(span, response): def request_hook_with_exception(_span, _request): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised def response_hook_with_exception(_span, _response): - raise Exception() + raise Exception() # pylint: disable=broad-exception-raised class TestHooks(TestBase): diff --git a/instrumentation/opentelemetry-instrumentation-httpx/LICENSE b/instrumentation/opentelemetry-instrumentation-httpx/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-httpx/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml index f13fa816bc..de890755dc 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml @@ -22,23 +22,19 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "httpx >= 0.18.0", ] -test = [ - "opentelemetry-instrumentation-httpx[instruments]", - "opentelemetry-sdk ~= 1.12", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] httpx = "opentelemetry.instrumentation.httpx:HTTPXClientInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py index 53542e7ef3..5404b2f025 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py @@ -196,11 +196,13 @@ async def async_response_hook(span, request, response): import httpx -from opentelemetry import context from opentelemetry.instrumentation.httpx.package import _instruments from opentelemetry.instrumentation.httpx.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.instrumentation.utils import ( + http_status_to_status_code, + is_http_instrumentation_enabled, +) from opentelemetry.propagate import inject from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind, TracerProvider, get_tracer @@ -270,7 +272,7 @@ def _extract_parameters(args, kwargs): # In httpx >= 0.20.0, handle_request receives a Request object request: httpx.Request = args[0] method = request.method.encode() - url = remove_url_credentials(str(request.url)) + url = httpx.URL(remove_url_credentials(str(request.url))) headers = request.headers stream = request.stream extensions = request.extensions @@ -347,7 +349,7 @@ def handle_request( httpx.Response, ]: """Add request info to span.""" - if context.get_value("suppress_instrumentation"): + if not is_http_instrumentation_enabled(): return self._transport.handle_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters( @@ -433,14 +435,12 @@ async def __aexit__( ) -> None: await self._transport.__aexit__(exc_type, exc_value, traceback) - async def handle_async_request( - self, *args, **kwargs - ) -> typing.Union[ + async def handle_async_request(self, *args, **kwargs) -> typing.Union[ typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict], httpx.Response, ]: """Add request info to span.""" - if context.get_value("suppress_instrumentation"): + if not is_http_instrumentation_enabled(): return await self._transport.handle_async_request(*args, **kwargs) method, url, headers, stream, extensions = _extract_parameters( @@ -566,11 +566,13 @@ def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") _InstrumentedClient._tracer_provider = tracer_provider _InstrumentedAsyncClient._tracer_provider = tracer_provider - httpx.Client = _InstrumentedClient + # Intentionally using a private attribute here, see: + # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538#discussion_r1610603719 + httpx.Client = httpx._api.Client = _InstrumentedClient httpx.AsyncClient = _InstrumentedAsyncClient def _uninstrument(self, **kwargs): - httpx.Client = self._original_client + httpx.Client = httpx._api.Client = self._original_client httpx.AsyncClient = self._original_async_client _InstrumentedClient._tracer_provider = None _InstrumentedClient._request_hook = None diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt new file mode 100644 index 0000000000..ca3a0908fa --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt @@ -0,0 +1,26 @@ +anyio==3.7.1 +asgiref==3.7.2 +certifi==2024.2.2 +Deprecated==1.2.14 +exceptiongroup==1.2.0 +h11==0.12.0 +httpcore==0.13.7 +httpx==0.18.2 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +respx==0.17.1 +rfc3986==1.5.0 +sniffio==1.3.1 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-httpx diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt new file mode 100644 index 0000000000..d3476cea4b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt @@ -0,0 +1,25 @@ +anyio==4.3.0 +asgiref==3.7.2 +certifi==2024.2.2 +Deprecated==1.2.14 +exceptiongroup==1.2.0 +h11==0.14.0 +httpcore==1.0.4 +httpx==0.27.0 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +respx==0.20.2 +sniffio==1.3.1 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-httpx diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py index b8d7fbb6b6..06ad963ab0 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py @@ -21,12 +21,13 @@ import respx import opentelemetry.instrumentation.httpx -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.httpx import ( AsyncOpenTelemetryTransport, HTTPXClientInstrumentor, SyncOpenTelemetryTransport, ) +from opentelemetry.instrumentation.utils import suppress_http_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv.trace import SpanAttributes @@ -51,12 +52,18 @@ HTTP_RESPONSE_BODY = "http.response.body" +def _is_url_tuple(request: "RequestInfo"): + """Determine if request url format is for httpx versions < 0.20.0.""" + return isinstance(request[1], tuple) and len(request[1]) == 4 + + def _async_call(coro: typing.Coroutine) -> asyncio.Task: loop = asyncio.get_event_loop() return loop.run_until_complete(coro) def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"): + assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) span.set_attribute( HTTP_RESPONSE_BODY, b"".join(response[2]), @@ -66,6 +73,7 @@ def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"): async def _async_response_hook( span: "Span", request: "RequestInfo", response: "ResponseInfo" ): + assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) span.set_attribute( HTTP_RESPONSE_BODY, b"".join([part async for part in response[2]]), @@ -73,11 +81,13 @@ async def _async_response_hook( def _request_hook(span: "Span", request: "RequestInfo"): + assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) url = httpx.URL(request[1]) span.update_name("GET" + str(url)) async def _async_request_hook(span: "Span", request: "RequestInfo"): + assert _is_url_tuple(request) or isinstance(request.url, httpx.URL) url = httpx.URL(request[1]) span.update_name("GET" + str(url)) @@ -182,14 +192,9 @@ def test_not_foundbasic(self): ) def test_suppress_instrumentation(self): - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: + with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.text, "Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) @@ -503,15 +508,10 @@ def test_not_recording(self): def test_suppress_instrumentation_new_client(self): HTTPXClientInstrumentor().instrument() - token = context.attach( - context.set_value("suppress_instrumentation", True) - ) - try: + with suppress_http_instrumentation(): client = self.create_client() result = self.perform_request(self.URL, client=client) self.assertEqual(result.text, "Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) HTTPXClientInstrumentor().uninstrument() @@ -523,12 +523,36 @@ def test_instrument_client(self): self.assertEqual(result.text, "Hello!") self.assert_span(num_spans=1) + def test_instrumentation_without_client(self): + + HTTPXClientInstrumentor().instrument() + results = [ + httpx.get(self.URL), + httpx.request("GET", self.URL), + ] + with httpx.stream("GET", self.URL) as stream: + stream.read() + results.append(stream) + + spans = self.assert_span(num_spans=len(results)) + for idx, res in enumerate(results): + with self.subTest(idx=idx, res=res): + self.assertEqual(res.text, "Hello!") + self.assertEqual( + spans[idx].attributes[SpanAttributes.HTTP_URL], + self.URL, + ) + + HTTPXClientInstrumentor().uninstrument() + def test_uninstrument(self): HTTPXClientInstrumentor().instrument() HTTPXClientInstrumentor().uninstrument() client = self.create_client() result = self.perform_request(self.URL, client=client) + result_no_client = httpx.get(self.URL) self.assertEqual(result.text, "Hello!") + self.assertEqual(result_no_client.text, "Hello!") self.assert_span(num_spans=0) def test_uninstrument_client(self): diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE b/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-jinja2/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml b/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml index c3648a2922..78417289df 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-jinja2/pyproject.toml @@ -22,10 +22,11 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -33,11 +34,6 @@ dependencies = [ instruments = [ "jinja2 >= 2.7, < 4.0", ] -test = [ - "opentelemetry-instrumentation-jinja2[instruments]", - "markupsafe==2.0.1", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] jinja2 = "opentelemetry.instrumentation.jinja2:Jinja2Instrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt new file mode 100644 index 0000000000..e547f9bd20 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +Jinja2==3.1.4 +MarkupSafe==2.0.1 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-jinja2 diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/LICENSE b/instrumentation/opentelemetry-instrumentation-kafka-python/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml b/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml index b303a61a5c..bd06b90f06 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/pyproject.toml @@ -25,19 +25,14 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.5", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "kafka-python >= 2.0", ] -test = [ - "opentelemetry-instrumentation-kafka-python[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "wrapt >= 1.0.0, < 2.0.0", -] [project.entry-points.opentelemetry_instrumentor] kafka = "opentelemetry.instrumentation.kafka:KafkaInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt new file mode 100644 index 0000000000..2f7007f872 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +kafka-python==2.0.2 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-kafka-python diff --git a/instrumentation/opentelemetry-instrumentation-logging/LICENSE b/instrumentation/opentelemetry-instrumentation-logging/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-logging/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml b/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml index 518fdb0403..658d4eac3c 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-logging/pyproject.toml @@ -22,17 +22,15 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] logging = "opentelemetry.instrumentation.logging:LoggingInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py b/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py index 6340c1adfb..db4e3a0022 100644 --- a/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py +++ b/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt new file mode 100644 index 0000000000..b7fcdc3dee --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt @@ -0,0 +1,15 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-logging diff --git a/instrumentation/opentelemetry-instrumentation-mysql/LICENSE b/instrumentation/opentelemetry-instrumentation-mysql/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-mysql/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml b/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml index 7f7b703266..58982a6781 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-mysql/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "mysql-connector-python ~= 8.0", ] -test = [ - "opentelemetry-instrumentation-mysql[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] mysql = "opentelemetry.instrumentation.mysql:MySQLInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt new file mode 100644 index 0000000000..95cd6ab35f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +mysql-connector-python==8.3.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-mysql diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/LICENSE b/instrumentation/opentelemetry-instrumentation-mysqlclient/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/pyproject.toml b/instrumentation/opentelemetry-instrumentation-mysqlclient/pyproject.toml index de8c31a1f3..c36f0e1c55 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "mysqlclient < 3", ] -test = [ - "opentelemetry-instrumentation-mysqlclient[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] mysqlclient = "opentelemetry.instrumentation.mysqlclient:MySQLClientInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/version.py b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/version.py +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt new file mode 100644 index 0000000000..78060fbccc --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +mysqlclient==2.2.4 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-mysqlclient diff --git a/instrumentation/opentelemetry-instrumentation-pika/LICENSE b/instrumentation/opentelemetry-instrumentation-pika/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-pika/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml index b11c9bbffc..8cff4d3d24 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pika/pyproject.toml @@ -22,8 +22,10 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ + "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-api ~= 1.5", "packaging >= 20.0", "wrapt >= 1.0.0, < 2.0.0", @@ -33,12 +35,6 @@ dependencies = [ instruments = [ "pika >= 0.12.0", ] -test = [ - "opentelemetry-instrumentation-pika[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "pytest", - "wrapt >= 1.0.0, < 2.0.0", -] [project.entry-points.opentelemetry_instrumentor] pika = "opentelemetry.instrumentation.pika:PikaInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/__init__.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/__init__.py index c745462cf3..d9cec06525 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/__init__.py @@ -77,6 +77,15 @@ def consume_hook(span: Span, body: bytes, properties: BasicProperties): PikaInstrumentor.instrument_channel(channel, publish_hook=publish_hook, consume_hook=consume_hook) +Consumer Instrumentation +------------------------ +For consumer instrumentation, pika supports two consuming modes: + +* Consumers using the `basic_consume` method which accepts a callback. This is supported for global instrumentation + (`PikaInstrumentor().instrument()`) as well channel specific instrumentation (`PikaInstrumentor().instrument_channel(channel)`) +* Consumers using the `consume` method which returns a generator over messages. This is supported for global + instrumentations only (`PikaInstrumentor().instrument()`) + API --- """ diff --git a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py index 56c78a85c3..f37f74e396 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py @@ -20,7 +20,10 @@ import wrapt from packaging import version from pika.adapters import BlockingConnection -from pika.adapters.blocking_connection import BlockingChannel +from pika.adapters.blocking_connection import ( + BlockingChannel, + _QueueConsumerGeneratorInfo, +) from opentelemetry import trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -153,9 +156,9 @@ def uninstrument_channel(channel: BlockingChannel) -> None: callback_attr = PikaInstrumentor.CONSUMER_CALLBACK_ATTR consumer_callback = getattr(client_info, callback_attr, None) if hasattr(consumer_callback, "_original_callback"): - channel._consumer_infos[ - consumers_tag - ] = consumer_callback._original_callback + channel._consumer_infos[consumers_tag] = ( + consumer_callback._original_callback + ) PikaInstrumentor._uninstrument_channel_functions(channel) def _decorate_channel_function( @@ -191,6 +194,24 @@ def wrapper(wrapped, instance, args, kwargs): wrapt.wrap_function_wrapper(channel, "basic_consume", wrapper) + @staticmethod + def _decorate_queue_consumer_generator( + tracer_provider: Optional[TracerProvider], + consume_hook: utils.HookT = utils.dummy_callback, + ) -> None: + tracer = trace.get_tracer(__name__, __version__, tracer_provider) + + def wrapper(wrapped, instance, args, kwargs): + res = wrapped(*args, **kwargs) + instance.pending_events = utils.ReadyMessagesDequeProxy( + instance.pending_events, instance, tracer, consume_hook + ) + return res + + wrapt.wrap_function_wrapper( + _QueueConsumerGeneratorInfo, "__init__", wrapper + ) + def _instrument(self, **kwargs: Dict[str, Any]) -> None: tracer_provider: TracerProvider = kwargs.get("tracer_provider", None) publish_hook: utils.HookT = kwargs.get( @@ -207,10 +228,15 @@ def _instrument(self, **kwargs: Dict[str, Any]) -> None: consume_hook=consume_hook, ) + self._decorate_queue_consumer_generator( + tracer_provider, consume_hook=consume_hook + ) + def _uninstrument(self, **kwargs: Dict[str, Any]) -> None: if hasattr(self, "__opentelemetry_tracer_provider"): delattr(self, "__opentelemetry_tracer_provider") unwrap(BlockingConnection, "channel") + unwrap(_QueueConsumerGeneratorInfo, "__init__") def instrumentation_dependencies(self) -> Collection[str]: return _instruments diff --git a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py index 6dab4fdfa9..2b4d1204ea 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py @@ -1,8 +1,13 @@ from logging import getLogger from typing import Any, Callable, List, Optional +from pika.adapters.blocking_connection import ( + _ConsumerDeliveryEvt, + _QueueConsumerGeneratorInfo, +) from pika.channel import Channel from pika.spec import Basic, BasicProperties +from wrapt import ObjectProxy from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.utils import is_instrumentation_enabled @@ -33,8 +38,7 @@ def keys(self, carrier: CarrierT) -> List[str]: HookT = Callable[[Span, bytes, BasicProperties], None] -def dummy_callback(span: Span, body: bytes, properties: BasicProperties): - ... +def dummy_callback(span: Span, body: bytes, properties: BasicProperties): ... def _decorate_callback( @@ -61,9 +65,9 @@ def decorated_callback( tracer, channel, properties, - destination=method.exchange - if method.exchange - else method.routing_key, + destination=( + method.exchange if method.exchange else method.routing_key + ), span_kind=SpanKind.CONSUMER, task_name=task_name, operation=MessagingOperationValues.RECEIVE, @@ -128,7 +132,7 @@ def decorated_function( def _get_span( tracer: Tracer, - channel: Channel, + channel: Optional[Channel], properties: BasicProperties, task_name: str, destination: str, @@ -157,7 +161,7 @@ def _generate_span_name( def _enrich_span( span: Span, - channel: Channel, + channel: Optional[Channel], properties: BasicProperties, task_destination: str, operation: Optional[MessagingOperationValues] = None, @@ -176,6 +180,8 @@ def _enrich_span( span.set_attribute( SpanAttributes.MESSAGING_CONVERSATION_ID, properties.correlation_id ) + if not channel: + return if not hasattr(channel.connection, "params"): span.set_attribute( SpanAttributes.NET_PEER_NAME, channel.connection._impl.params.host @@ -190,3 +196,77 @@ def _enrich_span( span.set_attribute( SpanAttributes.NET_PEER_PORT, channel.connection.params.port ) + + +# pylint:disable=abstract-method +class ReadyMessagesDequeProxy(ObjectProxy): + def __init__( + self, + wrapped, + queue_consumer_generator: _QueueConsumerGeneratorInfo, + tracer: Optional[Tracer], + consume_hook: HookT = dummy_callback, + ): + super().__init__(wrapped) + self._self_active_token = None + self._self_tracer = tracer + self._self_consume_hook = consume_hook + self._self_queue_consumer_generator = queue_consumer_generator + + def popleft(self, *args, **kwargs): + try: + # end active context if exists + if self._self_active_token: + context.detach(self._self_active_token) + except Exception as inst_exception: # pylint: disable=W0703 + _LOG.exception(inst_exception) + + evt = self.__wrapped__.popleft(*args, **kwargs) + + try: + # If a new message was received, create a span and set as active context + if isinstance(evt, _ConsumerDeliveryEvt): + method = evt.method + properties = evt.properties + if not properties: + properties = BasicProperties(headers={}) + if properties.headers is None: + properties.headers = {} + ctx = propagate.extract( + properties.headers, getter=_pika_getter + ) + if not ctx: + ctx = context.get_current() + message_ctx_token = context.attach(ctx) + span = _get_span( + self._self_tracer, + None, + properties, + destination=( + method.exchange + if method.exchange + else method.routing_key + ), + span_kind=SpanKind.CONSUMER, + task_name=self._self_queue_consumer_generator.consumer_tag, + operation=MessagingOperationValues.RECEIVE, + ) + try: + context.detach(message_ctx_token) + self._self_active_token = context.attach( + trace.set_span_in_context(span) + ) + self._self_consume_hook(span, evt.body, properties) + except Exception as hook_exception: # pylint: disable=W0703 + _LOG.exception(hook_exception) + finally: + # We must end the span here, because the next place we can hook + # is not the end of the user code, but only when the next message + # arrives. we still set this span's context as the active context + # so spans created by user code that handles this message will be + # children of this one. + span.end() + except Exception as inst_exception: # pylint: disable=W0703 + _LOG.exception(inst_exception) + + return evt diff --git a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt new file mode 100644 index 0000000000..b6aa239e0f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pika==0.13.1 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pika diff --git a/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt new file mode 100644 index 0000000000..334d08f537 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pika==1.3.2 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pika diff --git a/instrumentation/opentelemetry-instrumentation-pika/tests/test_pika_instrumentation.py b/instrumentation/opentelemetry-instrumentation-pika/tests/test_pika_instrumentation.py index 6e154c04f9..ad519c4a35 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/tests/test_pika_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-pika/tests/test_pika_instrumentation.py @@ -14,6 +14,7 @@ from unittest import TestCase, mock from pika.adapters import BlockingConnection +from pika.adapters.blocking_connection import _QueueConsumerGeneratorInfo from pika.channel import Channel from wrapt import BoundFunctionWrapper @@ -21,7 +22,10 @@ from opentelemetry.instrumentation.pika.pika_instrumentor import ( _consumer_callback_attribute_name, ) -from opentelemetry.instrumentation.pika.utils import dummy_callback +from opentelemetry.instrumentation.pika.utils import ( + ReadyMessagesDequeProxy, + dummy_callback, +) from opentelemetry.trace import Tracer @@ -40,13 +44,23 @@ def test_instrument_api(self) -> None: self.assertTrue( isinstance(BlockingConnection.channel, BoundFunctionWrapper) ) + self.assertTrue( + isinstance( + _QueueConsumerGeneratorInfo.__init__, BoundFunctionWrapper + ) + ) assert hasattr( instrumentation, "__opentelemetry_tracer_provider" ), "Tracer not stored for the object!" - instrumentation.uninstrument(channel=self.channel) + instrumentation.uninstrument() self.assertFalse( isinstance(BlockingConnection.channel, BoundFunctionWrapper) ) + self.assertFalse( + isinstance( + _QueueConsumerGeneratorInfo.__init__, BoundFunctionWrapper + ) + ) @mock.patch( "opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_channel_functions" @@ -57,7 +71,7 @@ def test_instrument_api(self) -> None: @mock.patch( "opentelemetry.instrumentation.pika.PikaInstrumentor._instrument_blocking_channel_consumers" ) - def test_instrument( + def test_instrument_channel( self, instrument_blocking_channel_consumers: mock.MagicMock, instrument_basic_consume: mock.MagicMock, @@ -110,6 +124,23 @@ def test_instrument_basic_publish( self.channel.basic_publish, decorate_basic_publish.return_value ) + def test_instrument_queue_consumer_generator(self) -> None: + instrumentation = PikaInstrumentor() + instrumentation.instrument() + generator_info = _QueueConsumerGeneratorInfo( + params=("queue", False, False), consumer_tag="tag" + ) + self.assertTrue( + isinstance(generator_info.pending_events, ReadyMessagesDequeProxy) + ) + instrumentation.uninstrument() + generator_info = _QueueConsumerGeneratorInfo( + params=("queue", False, False), consumer_tag="tag" + ) + self.assertFalse( + isinstance(generator_info.pending_events, ReadyMessagesDequeProxy) + ) + def test_uninstrument_channel_functions(self) -> None: original_function = self.channel.basic_publish self.channel.basic_publish = mock.MagicMock() diff --git a/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py index ed33593389..7c0aa7a715 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py @@ -11,8 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import collections from unittest import TestCase, mock +from pika.adapters.blocking_connection import ( + _ConsumerCancellationEvt, + _ConsumerDeliveryEvt, + _QueueConsumerGeneratorInfo, +) from pika.channel import Channel from pika.spec import Basic, BasicProperties @@ -448,3 +454,113 @@ def test_decorate_basic_publish_when_span_is_not_recording( exchange_name, routing_key, mock_body, properties, False ) self.assertEqual(retval, callback.return_value) + + # pylint: disable=too-many-statements + @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") + @mock.patch("opentelemetry.propagate.extract") + @mock.patch("opentelemetry.context.detach") + @mock.patch("opentelemetry.context.attach") + @mock.patch("opentelemetry.context.get_current") + def test_decorate_deque_proxy( + self, + context_get_current: mock.MagicMock, + context_attach: mock.MagicMock, + context_detach: mock.MagicMock, + extract: mock.MagicMock, + get_span: mock.MagicMock, + ) -> None: + returned_span = mock.MagicMock() + get_span.return_value = returned_span + consume_hook = mock.MagicMock() + tracer = mock.MagicMock() + generator_info = mock.MagicMock( + spec=_QueueConsumerGeneratorInfo, + pending_events=mock.MagicMock(spec=collections.deque), + consumer_tag="mock_task_name", + ) + method = mock.MagicMock(spec=Basic.Deliver) + method.exchange = "test_exchange" + properties = mock.MagicMock() + evt = _ConsumerDeliveryEvt(method, properties, b"mock_body") + generator_info.pending_events.popleft.return_value = evt + proxy = utils.ReadyMessagesDequeProxy( + generator_info.pending_events, generator_info, tracer, consume_hook + ) + + # First call (no detach cleanup) + res = proxy.popleft() + self.assertEqual(res, evt) + generator_info.pending_events.popleft.assert_called_once() + extract.assert_called_once_with( + properties.headers, getter=utils._pika_getter + ) + context_get_current.assert_called_once() + self.assertEqual(context_attach.call_count, 2) + self.assertEqual(context_detach.call_count, 1) + get_span.assert_called_once_with( + tracer, + None, + properties, + destination=method.exchange, + span_kind=SpanKind.CONSUMER, + task_name=generator_info.consumer_tag, + operation=MessagingOperationValues.RECEIVE, + ) + consume_hook.assert_called_once() + returned_span.end.assert_called_once() + + generator_info.pending_events.reset_mock() + extract.reset_mock() + context_get_current.reset_mock() + get_span.reset_mock() + context_attach.reset_mock() + context_detach.reset_mock() + returned_span.end.reset_mock() + consume_hook.reset_mock() + + # Second call (has detach cleanup) + res = proxy.popleft() + self.assertEqual(res, evt) + generator_info.pending_events.popleft.assert_called_once() + extract.assert_called_once_with( + properties.headers, getter=utils._pika_getter + ) + context_get_current.assert_called_once() + self.assertEqual(context_attach.call_count, 2) + self.assertEqual(context_detach.call_count, 2) + get_span.assert_called_once_with( + tracer, + None, + properties, + destination=method.exchange, + span_kind=SpanKind.CONSUMER, + task_name=generator_info.consumer_tag, + operation=MessagingOperationValues.RECEIVE, + ) + consume_hook.assert_called_once() + returned_span.end.assert_called_once() + generator_info.pending_events.reset_mock() + + extract.reset_mock() + context_get_current.reset_mock() + get_span.reset_mock() + context_attach.reset_mock() + context_detach.reset_mock() + returned_span.end.reset_mock() + consume_hook.reset_mock() + + # Third call (cancellation event) + evt = _ConsumerCancellationEvt("") + generator_info.pending_events.popleft.return_value = evt + + res = proxy.popleft() + + self.assertEqual(res, evt) + generator_info.pending_events.popleft.assert_called_once() + extract.assert_not_called() + context_get_current.assert_not_called() + context_detach.assert_called_once() + context_attach.assert_not_called() + get_span.assert_not_called() + consume_hook.assert_not_called() + returned_span.end.assert_not_called() diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE b/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml index a5a4da0b59..81baee5267 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml @@ -23,21 +23,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "psycopg >= 3.1.0", ] -test = [ - "opentelemetry-instrumentation-psycopg[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] psycopg = "opentelemetry.instrumentation.psycopg:PsycopgInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py index ab473c2fe4..5d7054151a 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -105,8 +105,13 @@ import typing from typing import Collection -import psycopg -from psycopg import Cursor as pg_cursor # pylint: disable=no-name-in-module +import psycopg # pylint: disable=import-self +from psycopg import ( + AsyncCursor as pg_async_cursor, # pylint: disable=import-self,no-name-in-module +) +from psycopg import ( + Cursor as pg_cursor, # pylint: disable=no-name-in-module,import-self +) from psycopg.sql import Composed # pylint: disable=no-name-in-module from opentelemetry.instrumentation import dbapi @@ -151,9 +156,40 @@ def _instrument(self, **kwargs): commenter_options=commenter_options, ) + dbapi.wrap_connect( + __name__, + psycopg.Connection, # pylint: disable=no-member + "connect", + self._DATABASE_SYSTEM, + self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + db_api_integration_factory=DatabaseApiIntegration, + enable_commenter=enable_sqlcommenter, + commenter_options=commenter_options, + ) + dbapi.wrap_connect( + __name__, + psycopg.AsyncConnection, # pylint: disable=no-member + "connect", + self._DATABASE_SYSTEM, + self._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + db_api_integration_factory=DatabaseApiAsyncIntegration, + enable_commenter=enable_sqlcommenter, + commenter_options=commenter_options, + ) + def _uninstrument(self, **kwargs): """ "Disable Psycopg instrumentation""" - dbapi.unwrap_connect(psycopg, "connect") + dbapi.unwrap_connect(psycopg, "connect") # pylint: disable=no-member + dbapi.unwrap_connect( + psycopg.Connection, "connect" # pylint: disable=no-member + ) + dbapi.unwrap_connect( + psycopg.AsyncConnection, "connect" # pylint: disable=no-member + ) # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql @staticmethod @@ -204,6 +240,26 @@ def wrapped_connection( return connection +class DatabaseApiAsyncIntegration(dbapi.DatabaseApiIntegration): + async def wrapped_connection( + self, + connect_method: typing.Callable[..., typing.Any], + args: typing.Tuple[typing.Any, typing.Any], + kwargs: typing.Dict[typing.Any, typing.Any], + ): + """Add object proxy to connection object.""" + base_cursor_factory = kwargs.pop("cursor_factory", None) + new_factory_kwargs = {"db_api": self} + if base_cursor_factory: + new_factory_kwargs["base_factory"] = base_cursor_factory + kwargs["cursor_factory"] = _new_cursor_async_factory( + **new_factory_kwargs + ) + connection = await connect_method(*args, **kwargs) + self.get_connection_attributes(connection) + return connection + + class CursorTracer(dbapi.CursorTracer): def get_operation_name(self, cursor, args): if not args: @@ -259,3 +315,36 @@ def callproc(self, *args, **kwargs): ) return TracedCursorFactory + + +def _new_cursor_async_factory( + db_api=None, base_factory=None, tracer_provider=None +): + if not db_api: + db_api = DatabaseApiAsyncIntegration( + __name__, + PsycopgInstrumentor._DATABASE_SYSTEM, + connection_attributes=PsycopgInstrumentor._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + ) + base_factory = base_factory or pg_async_cursor + _cursor_tracer = CursorTracer(db_api) + + class TracedCursorAsyncFactory(base_factory): + async def execute(self, *args, **kwargs): + return await _cursor_tracer.traced_execution( + self, super().execute, *args, **kwargs + ) + + async def executemany(self, *args, **kwargs): + return await _cursor_tracer.traced_execution( + self, super().executemany, *args, **kwargs + ) + + async def callproc(self, *args, **kwargs): + return await _cursor_tracer.traced_execution( + self, super().callproc, *args, **kwargs + ) + + return TracedCursorAsyncFactory diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt new file mode 100644 index 0000000000..d9e9b4de0b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +backports.zoneinfo==0.2.1 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +psycopg==3.1.18 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-psycopg diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt new file mode 100644 index 0000000000..9269a3c378 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +psycopg==3.1.18 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-psycopg diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index d5e4bc65f3..5a5b39d80b 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -13,7 +13,7 @@ # limitations under the License. import types -from unittest import mock +from unittest import IsolatedAsyncioTestCase, mock import psycopg @@ -45,6 +45,35 @@ def __exit__(self, *args): return self +class MockAsyncCursor: + def __init__(self, *args, **kwargs): + pass + + # pylint: disable=unused-argument, no-self-use + async def execute(self, query, params=None, throw_exception=False): + if throw_exception: + raise psycopg.Error("Test Exception") + + # pylint: disable=unused-argument, no-self-use + async def executemany(self, query, params=None, throw_exception=False): + if throw_exception: + raise psycopg.Error("Test Exception") + + # pylint: disable=unused-argument, no-self-use + async def callproc(self, query, params=None, throw_exception=False): + if throw_exception: + raise psycopg.Error("Test Exception") + + async def __aenter__(self, *args, **kwargs): + return self + + async def __aexit__(self, *args, **kwargs): + pass + + def close(self): + pass + + class MockConnection: commit = mock.MagicMock(spec=types.MethodType) commit.__name__ = "commit" @@ -64,25 +93,79 @@ def get_dsn_parameters(self): # pylint: disable=no-self-use return {"dbname": "test"} -class TestPostgresqlIntegration(TestBase): +class MockAsyncConnection: + commit = mock.MagicMock(spec=types.MethodType) + commit.__name__ = "commit" + + rollback = mock.MagicMock(spec=types.MethodType) + rollback.__name__ = "rollback" + + def __init__(self, *args, **kwargs): + self.cursor_factory = kwargs.pop("cursor_factory", None) + + @staticmethod + async def connect(*args, **kwargs): + return MockAsyncConnection(**kwargs) + + def cursor(self): + if self.cursor_factory: + cur = self.cursor_factory(self) + return cur + return MockAsyncCursor() + + def execute(self, query, params=None, *, prepare=None, binary=False): + cur = self.cursor() + return cur.execute(query, params, prepare=prepare) + + def get_dsn_parameters(self): # pylint: disable=no-self-use + return {"dbname": "test"} + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return mock.MagicMock(spec=types.MethodType) + + +class PostgresqlIntegrationTestMixin: + # pylint: disable=invalid-name def setUp(self): super().setUp() self.cursor_mock = mock.patch( "opentelemetry.instrumentation.psycopg.pg_cursor", MockCursor ) + self.cursor_async_mock = mock.patch( + "opentelemetry.instrumentation.psycopg.pg_async_cursor", + MockAsyncCursor, + ) self.connection_mock = mock.patch("psycopg.connect", MockConnection) + self.connection_sync_mock = mock.patch( + "psycopg.Connection.connect", MockConnection + ) + self.connection_async_mock = mock.patch( + "psycopg.AsyncConnection.connect", MockAsyncConnection.connect + ) self.cursor_mock.start() + self.cursor_async_mock.start() self.connection_mock.start() + self.connection_sync_mock.start() + self.connection_async_mock.start() + # pylint: disable=invalid-name def tearDown(self): super().tearDown() self.memory_exporter.clear() self.cursor_mock.stop() + self.cursor_async_mock.stop() self.connection_mock.stop() + self.connection_sync_mock.stop() + self.connection_async_mock.stop() with self.disable_logging(): PsycopgInstrumentor().uninstrument() + +class TestPostgresqlIntegration(PostgresqlIntegrationTestMixin, TestBase): # pylint: disable=unused-argument def test_instrumentor(self): PsycopgInstrumentor().instrument() @@ -114,6 +197,37 @@ def test_instrumentor(self): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) + # pylint: disable=unused-argument + def test_instrumentor_with_connection_class(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.Connection.connect(database="test") + + cursor = cnx.cursor() + + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.psycopg + ) + + # check that no spans are generated after uninstrument + PsycopgInstrumentor().uninstrument() + + cnx = psycopg.Connection.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + def test_span_name(self): PsycopgInstrumentor().instrument() @@ -269,3 +383,108 @@ def test_sqlcommenter_disabled(self, event_mocked): cursor.execute(query) kwargs = event_mocked.call_args[1] self.assertEqual(kwargs["enable_commenter"], False) + + +class TestPostgresqlIntegrationAsync( + PostgresqlIntegrationTestMixin, TestBase, IsolatedAsyncioTestCase +): + async def test_wrap_async_connection_class_with_cursor(self): + PsycopgInstrumentor().instrument() + + async def test_async_connection(): + acnx = await psycopg.AsyncConnection.connect("test") + async with acnx as cnx: + async with cnx.cursor() as cursor: + await cursor.execute("SELECT * FROM test") + + await test_async_connection() + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.psycopg + ) + + # check that no spans are generated after uninstrument + PsycopgInstrumentor().uninstrument() + + await test_async_connection() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + # pylint: disable=unused-argument + async def test_instrumentor_with_async_connection_class(self): + PsycopgInstrumentor().instrument() + + async def test_async_connection(): + acnx = await psycopg.AsyncConnection.connect("test") + async with acnx as cnx: + await cnx.execute("SELECT * FROM test") + + await test_async_connection() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.psycopg + ) + + # check that no spans are generated after uninstrument + PsycopgInstrumentor().uninstrument() + await test_async_connection() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + async def test_span_name_async(self): + PsycopgInstrumentor().instrument() + + cnx = await psycopg.AsyncConnection.connect("test") + async with cnx.cursor() as cursor: + await cursor.execute("Test query", ("param1Value", False)) + await cursor.execute( + """multi + line + query""" + ) + await cursor.execute("tab\tseparated query") + await cursor.execute("/* leading comment */ query") + await cursor.execute( + "/* leading comment */ query /* trailing comment */" + ) + await cursor.execute("query /* trailing comment */") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 6) + self.assertEqual(spans_list[0].name, "Test") + self.assertEqual(spans_list[1].name, "multi") + self.assertEqual(spans_list[2].name, "tab") + self.assertEqual(spans_list[3].name, "query") + self.assertEqual(spans_list[4].name, "query") + self.assertEqual(spans_list[5].name, "query") + + # pylint: disable=unused-argument + async def test_not_recording_async(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + PsycopgInstrumentor().instrument() + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + cnx = await psycopg.AsyncConnection.connect("test") + async with cnx.cursor() as cursor: + query = "SELECT * FROM test" + await cursor.execute(query) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + + PsycopgInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE b/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml b/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml index a300fa9fdd..581ff20229 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "psycopg2 >= 2.7.3.1", ] -test = [ - "opentelemetry-instrumentation-psycopg2[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] psycopg2 = "opentelemetry.instrumentation.psycopg2:Psycopg2Instrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt new file mode 100644 index 0000000000..28ad25715d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +psycopg2==2.9.9 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-psycopg2 diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE b/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml index e7a642b0aa..eab52fa474 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -34,10 +35,6 @@ dependencies = [ instruments = [ "pymemcache >= 1.3.5, < 5", ] -test = [ - "opentelemetry-instrumentation-pymemcache[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] pymemcache = "opentelemetry.instrumentation.pymemcache:PymemcacheInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py index 512ce9ea56..d763734aca 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py @@ -162,14 +162,14 @@ def _get_address_attributes(instance): host, port = instance.server address_attributes[SpanAttributes.NET_PEER_NAME] = host address_attributes[SpanAttributes.NET_PEER_PORT] = port - address_attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.IP_TCP.value + address_attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.IP_TCP.value + ) elif isinstance(instance.server, str): address_attributes[SpanAttributes.NET_PEER_NAME] = instance.server - address_attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.OTHER.value + address_attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.OTHER.value + ) return address_attributes diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt new file mode 100644 index 0000000000..ddb06914f7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymemcache==1.3.5 +pytest==7.4.4 +pytest-benchmark==4.0.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymemcache diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt new file mode 100644 index 0000000000..a6ad4d0248 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymemcache==2.2.2 +pytest==7.4.4 +pytest-benchmark==4.0.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymemcache diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt new file mode 100644 index 0000000000..7405224a8d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymemcache==3.4.1 +pytest==7.4.4 +pytest-benchmark==4.0.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymemcache diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt new file mode 100644 index 0000000000..d817e70c59 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymemcache==3.4.2 +pytest==7.4.4 +pytest-benchmark==4.0.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymemcache diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt new file mode 100644 index 0000000000..606d79143c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymemcache==4.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymemcache diff --git a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py index 35b672bac0..4e29091217 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py @@ -24,15 +24,14 @@ MemcacheUnknownError, ) -# pylint: disable=import-error,no-name-in-module -from tests.utils import MockSocket, _str - from opentelemetry import trace as trace_api from opentelemetry.instrumentation.pymemcache import PymemcacheInstrumentor from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import get_tracer +from .utils import MockSocket, _str + TEST_HOST = "localhost" TEST_PORT = 117711 diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE b/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-pymongo/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml index 6df765bfe0..d77e95b90e 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymongo/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "pymongo >= 3.1, < 5.0", ] -test = [ - "opentelemetry-instrumentation-pymongo[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] pymongo = "opentelemetry.instrumentation.pymongo:PymongoInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index 506669a5c6..f55aa2be33 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -98,8 +98,7 @@ def failed_hook(span, event): FailedHookT = Callable[[Span, monitoring.CommandFailedEvent], None] -def dummy_callback(span, event): - ... +def dummy_callback(span, event): ... class CommandTracer(monitoring.CommandListener): diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt new file mode 100644 index 0000000000..2fd3f4ed0e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +dnspython==2.6.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pymongo==4.6.3 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-pymongo diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/LICENSE b/instrumentation/opentelemetry-instrumentation-pymysql/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-pymysql/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml index 4018d29601..bc06e31b86 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pymysql/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "PyMySQL < 2", ] -test = [ - "opentelemetry-instrumentation-pymysql[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] pymysql = "opentelemetry.instrumentation.pymysql:PyMySQLInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt new file mode 100644 index 0000000000..5f2d8b7783 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +PyMySQL==1.1.1 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-pymysql diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 587ebc1b53..6f8af5d8df 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -17,6 +17,7 @@ import pymysql import opentelemetry.instrumentation.pymysql +from opentelemetry import trace as trace_api from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.test.test_base import TestBase @@ -78,6 +79,20 @@ def test_custom_tracer_provider(self, mock_connect): self.assertIs(span.resource, resource) + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_no_op_tracer_provider(self, mock_connect): + PyMySQLInstrumentor().instrument( + tracer_provider=trace_api.NoOpTracerProvider() + ) + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + @mock.patch("pymysql.connect") # pylint: disable=unused-argument def test_instrument_connection(self, mock_connect): diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE b/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-pyramid/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml b/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml index 1face37ccc..96edbff3b2 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-pyramid/pyproject.toml @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-wsgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-wsgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -36,11 +37,6 @@ dependencies = [ instruments = [ "pyramid >= 1.7", ] -test = [ - "opentelemetry-instrumentation-pyramid[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "werkzeug == 0.16.1", -] [project.entry-points.opentelemetry_instrumentor] pyramid = "opentelemetry.instrumentation.pyramid:PyramidInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index e3675fcfab..d0010ed8d0 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -31,6 +31,7 @@ from opentelemetry.metrics import get_meter from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import get_excluded_urls TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory" @@ -106,9 +107,9 @@ def _before_traversal(event): if span.is_recording(): attributes = otel_wsgi.collect_request_attributes(request_environ) if request.matched_route: - attributes[ - SpanAttributes.HTTP_ROUTE - ] = request.matched_route.pattern + attributes[SpanAttributes.HTTP_ROUTE] = ( + request.matched_route.pattern + ) for key, value in attributes.items(): span.set_attribute(key, value) if span.kind == trace.SpanKind.SERVER: @@ -140,7 +141,7 @@ def trace_tween_factory(handler, registry): duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration of the inbound HTTP request", + description="Duration of HTTP client requests.", ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, @@ -180,6 +181,7 @@ def trace_tween(request): response = None status = None + recordable_exc = None try: response = handler(request) @@ -190,20 +192,23 @@ def trace_tween(request): # As described in docs, Pyramid exceptions are all valid # response types response = exc + if isinstance(exc, HTTPServerError): + recordable_exc = exc raise - except BaseException: + except BaseException as exc: # In the case that a non-HTTPException is bubbled up we # should infer a internal server error and raise status = "500 InternalServerError" + recordable_exc = exc raise finally: duration = max(round((default_timer() - start) * 1000), 0) status = getattr(response, "status", status) status_code = otel_wsgi._parse_status_code(status) if status_code is not None: - duration_attrs[ - SpanAttributes.HTTP_STATUS_CODE - ] = otel_wsgi._parse_status_code(status) + duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( + otel_wsgi._parse_status_code(status) + ) duration_histogram.record(duration, duration_attrs) active_requests_counter.add(-1, active_requests_count_attrs) span = request.environ.get(_ENVIRON_SPAN_KEY) @@ -222,6 +227,12 @@ def trace_tween(request): getattr(response, "headerlist", None), ) + if recordable_exc is not None: + span.set_status( + Status(StatusCode.ERROR, str(recordable_exc)) + ) + span.record_exception(recordable_exc) + if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = ( otel_wsgi.collect_custom_response_headers_attributes( diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt new file mode 100644 index 0000000000..184b03fed4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt @@ -0,0 +1,28 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +hupper==1.12.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +PasteDeploy==3.1.0 +plaster==1.1.2 +plaster-pastedeploy==1.0.1 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pyramid==2.0.2 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +translationstring==1.4 +typing_extensions==4.9.0 +venusian==3.1.0 +WebOb==1.8.7 +Werkzeug==3.0.3 +wrapt==1.16.0 +zipp==3.17.0 +zope.deprecation==5.0 +zope.interface==6.2 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-pyramid diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py index c6b9faa196..9c177433ef 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/pyramid_base_test.py @@ -14,12 +14,7 @@ import pyramid.httpexceptions as exc from pyramid.response import Response -from werkzeug.test import Client - -# opentelemetry-instrumentation-pyramid uses werkzeug==0.16.1 which has -# werkzeug.wrappers.BaseResponse. This is not the case for newer versions of -# werkzeug like the one lint uses. -from werkzeug.wrappers import BaseResponse # pylint: disable=no-name-in-module +from werkzeug.test import Client, TestResponse class InstrumentationTest: @@ -35,7 +30,7 @@ def _hello_endpoint(request): if helloid == 204: raise exc.HTTPNoContent() if helloid == 900: - raise NotImplementedError() + raise NotImplementedError("error message") return Response("Hello: " + str(helloid)) @staticmethod @@ -77,4 +72,4 @@ def excluded2_endpoint(request): ) # pylint: disable=attribute-defined-outside-init - self.client = Client(config.make_wsgi_app(), BaseResponse) + self.client = Client(config.make_wsgi_app(), TestResponse) diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py index 2c3ec85e18..b1d854b371 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_automatic.py @@ -18,6 +18,10 @@ from pyramid.config import Configurator from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + _server_active_requests_count_attrs_old, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.pyramid import PyramidInstrumentor from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, @@ -31,8 +35,6 @@ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, - _active_requests_count_attrs, - _duration_attrs, ) # pylint: disable=import-error @@ -43,8 +45,8 @@ "http.server.duration", ] _recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": _duration_attrs, + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, } @@ -119,6 +121,7 @@ def test_redirect_response_is_not_an_error(self): span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].status.status_code, StatusCode.UNSET) + self.assertEqual(len(span_list[0].events), 0) PyramidInstrumentor().uninstrument() @@ -213,6 +216,7 @@ def test_basic_metric_success(self): "http.server_name": "localhost", "net.host.port": 80, "http.status_code": 200, + "net.host.name": "localhost", } expected_requests_count_attributes = { "http.method": "GET", @@ -220,6 +224,8 @@ def test_basic_metric_success(self): "http.scheme": "http", "http.flavor": "1.1", "http.server_name": "localhost", + "net.host.name": "localhost", + "net.host.port": 80, } metrics_list = self.memory_metrics_reader.get_metrics_data() for metric in ( @@ -240,7 +246,7 @@ def test_basic_metric_success(self): ) self.assertEqual(point.value, 0) - def test_metric_uninstruemnt(self): + def test_metric_uninstrument(self): self.client.get("/hello/756") PyramidInstrumentor().uninstrument() self.config = Configurator() diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py index 478eab1937..0e3a5dec19 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/tests/test_programmatic.py @@ -23,6 +23,7 @@ set_global_response_propagator, ) from opentelemetry.instrumentation.pyramid import PyramidInstrumentor +from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.util.http import get_excluded_urls @@ -37,6 +38,7 @@ def expected_attributes(override_attributes): SpanAttributes.HTTP_SERVER_NAME: "localhost", SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.NET_HOST_PORT: 80, + SpanAttributes.NET_HOST_NAME: "localhost", SpanAttributes.HTTP_HOST: "localhost", SpanAttributes.HTTP_TARGET: "/", SpanAttributes.HTTP_FLAVOR: "1.1", @@ -148,6 +150,7 @@ def test_404(self): self.assertEqual(span_list[0].name, "POST /bye") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual(len(span_list[0].events), 0) def test_internal_error(self): expected_attrs = expected_attributes( @@ -165,6 +168,18 @@ def test_internal_error(self): self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual( + span_list[0].status.status_code, trace.StatusCode.ERROR + ) + self.assertIn( + "HTTPInternalServerError", span_list[0].status.description + ) + self.assertEqual( + span_list[0] + .events[0] + .attributes[exception_attributes.EXCEPTION_TYPE], + "pyramid.httpexceptions.HTTPInternalServerError", + ) def test_internal_exception(self): expected_attrs = expected_attributes( @@ -183,6 +198,21 @@ def test_internal_exception(self): self.assertEqual(span_list[0].name, "/hello/{helloid}") self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER) self.assertEqual(span_list[0].attributes, expected_attrs) + self.assertEqual( + span_list[0].status.status_code, trace.StatusCode.ERROR + ) + self.assertEqual(span_list[0].status.description, "error message") + + expected_error_event_attrs = { + exception_attributes.EXCEPTION_TYPE: "NotImplementedError", + exception_attributes.EXCEPTION_MESSAGE: "error message", + } + self.assertEqual(span_list[0].events[0].name, "exception") + # Ensure exception event has specific attributes, but allow additional ones + self.assertLess( + expected_error_event_attrs.items(), + dict(span_list[0].events[0].attributes).items(), + ) def test_tween_list(self): tween_list = "opentelemetry.instrumentation.pyramid.trace_tween_factory\npyramid.tweens.excview_tween_factory" diff --git a/instrumentation/opentelemetry-instrumentation-redis/LICENSE b/instrumentation/opentelemetry-instrumentation-redis/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-redis/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml index 8511671ed6..af342aa076 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "wrapt >= 1.12.1", ] @@ -34,11 +35,6 @@ dependencies = [ instruments = [ "redis >= 2.6", ] -test = [ - "opentelemetry-instrumentation-redis[instruments]", - "opentelemetry-sdk ~= 1.3", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] redis = "opentelemetry.instrumentation.redis:RedisInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py index 3c274c8c43..4703bc271f 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py @@ -29,21 +29,21 @@ def _extract_conn_attributes(conn_kwargs): } db = conn_kwargs.get("db", 0) attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX] = db - try: + if "path" in conn_kwargs: + attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get("path", "") + attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.OTHER.value + ) + else: attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get( "host", "localhost" ) attributes[SpanAttributes.NET_PEER_PORT] = conn_kwargs.get( "port", 6379 ) - attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.IP_TCP.value - except KeyError: - attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get("path", "") - attributes[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.OTHER.value + attributes[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.IP_TCP.value + ) return attributes diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt new file mode 100644 index 0000000000..00b0a04b3e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +async-timeout==4.0.3 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +redis==5.0.1 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-redis diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py index 2d2670fee3..4a2fce5026 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -20,6 +20,11 @@ from opentelemetry import trace from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.semconv.trace import ( + DbSystemValues, + NetTransportValues, + SpanAttributes, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind @@ -226,3 +231,83 @@ def test_no_op_tracer_provider(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) + + def test_attributes_default(self): + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.set("key", "value") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual( + span.attributes[SpanAttributes.DB_SYSTEM], + DbSystemValues.REDIS.value, + ) + self.assertEqual( + span.attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX], 0 + ) + self.assertEqual( + span.attributes[SpanAttributes.NET_PEER_NAME], "localhost" + ) + self.assertEqual(span.attributes[SpanAttributes.NET_PEER_PORT], 6379) + self.assertEqual( + span.attributes[SpanAttributes.NET_TRANSPORT], + NetTransportValues.IP_TCP.value, + ) + + def test_attributes_tcp(self): + redis_client = redis.Redis.from_url("redis://foo:bar@1.1.1.1:6380/1") + + with mock.patch.object(redis_client, "connection"): + redis_client.set("key", "value") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual( + span.attributes[SpanAttributes.DB_SYSTEM], + DbSystemValues.REDIS.value, + ) + self.assertEqual( + span.attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX], 1 + ) + self.assertEqual( + span.attributes[SpanAttributes.NET_PEER_NAME], "1.1.1.1" + ) + self.assertEqual(span.attributes[SpanAttributes.NET_PEER_PORT], 6380) + self.assertEqual( + span.attributes[SpanAttributes.NET_TRANSPORT], + NetTransportValues.IP_TCP.value, + ) + + def test_attributes_unix_socket(self): + redis_client = redis.Redis.from_url( + "unix://foo@/path/to/socket.sock?db=3&password=bar" + ) + + with mock.patch.object(redis_client, "connection"): + redis_client.set("key", "value") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual( + span.attributes[SpanAttributes.DB_SYSTEM], + DbSystemValues.REDIS.value, + ) + self.assertEqual( + span.attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX], 3 + ) + self.assertEqual( + span.attributes[SpanAttributes.NET_PEER_NAME], + "/path/to/socket.sock", + ) + self.assertEqual( + span.attributes[SpanAttributes.NET_TRANSPORT], + NetTransportValues.OTHER.value, + ) diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/LICENSE b/instrumentation/opentelemetry-instrumentation-remoulade/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-remoulade/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml b/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml index b8c9d2709b..d3c1bd345d 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-remoulade/pyproject.toml @@ -22,22 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "remoulade >= 0.50", ] -test = [ - "opentelemetry-instrumentation-remoulade[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "opentelemetry-sdk ~= 1.10" -] [project.entry-points.opentelemetry_instrumentor] remoulade = "opentelemetry.instrumentation.remoulade:RemouladeInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py +++ b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt new file mode 100644 index 0000000000..0f6374b7ef --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +prometheus_client==0.20.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +pytz==2024.1 +remoulade==3.2.0 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-remoulade diff --git a/instrumentation/opentelemetry-instrumentation-requests/LICENSE b/instrumentation/opentelemetry-instrumentation-requests/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-requests/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml b/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml index a4f8255e00..504634007c 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-requests/pyproject.toml @@ -22,23 +22,19 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "requests ~= 2.0", ] -test = [ - "opentelemetry-instrumentation-requests[instruments]", - "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] requests = "opentelemetry.instrumentation.requests:RequestsInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 4c198596a2..18cc3e767c 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -59,22 +59,20 @@ from requests.structures import CaseInsensitiveDict from opentelemetry.instrumentation._semconv import ( - _METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, - _SPAN_ATTRIBUTES_ERROR_TYPE, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, - _filter_duration_attrs, + _client_duration_attrs_new, + _client_duration_attrs_old, + _filter_semconv_duration_attrs, _get_schema_url, + _HTTPStabilityMode, _OpenTelemetrySemanticConventionStability, - _OpenTelemetryStabilityMode, _OpenTelemetryStabilitySignalType, _report_new, _report_old, - _set_http_hostname, + _set_http_host, _set_http_method, - _set_http_net_peer_name, + _set_http_net_peer_name_client, _set_http_network_protocol_version, - _set_http_port, + _set_http_peer_port_client, _set_http_scheme, _set_http_status_code, _set_http_url, @@ -89,7 +87,15 @@ ) from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, +) from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_CLIENT_REQUEST_DURATION, +) from opentelemetry.trace import SpanKind, Tracer, get_tracer from opentelemetry.trace.span import Span from opentelemetry.trace.status import StatusCode @@ -105,7 +111,7 @@ _excluded_urls_from_env = get_excluded_urls("REQUESTS") _RequestHookT = Optional[Callable[[Span, PreparedRequest], None]] -_ResponseHookT = Optional[Callable[[Span, PreparedRequest], None]] +_ResponseHookT = Optional[Callable[[Span, PreparedRequest, Response], None]] # pylint: disable=unused-argument @@ -117,7 +123,7 @@ def _instrument( request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, excluded_urls: ExcludeList = None, - sem_conv_opt_in_mode: _OpenTelemetryStabilityMode = _OpenTelemetryStabilityMode.DEFAULT, + sem_conv_opt_in_mode: _HTTPStabilityMode = _HTTPStabilityMode.DEFAULT, ): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes @@ -170,38 +176,36 @@ def get_or_create_headers(): try: parsed_url = urlparse(url) if parsed_url.scheme: - _set_http_scheme( - metric_labels, parsed_url.scheme, sem_conv_opt_in_mode - ) + if _report_old(sem_conv_opt_in_mode): + # TODO: Support opt-in for url.scheme in new semconv + _set_http_scheme( + metric_labels, parsed_url.scheme, sem_conv_opt_in_mode + ) if parsed_url.hostname: - _set_http_hostname( + _set_http_host( metric_labels, parsed_url.hostname, sem_conv_opt_in_mode ) - _set_http_net_peer_name( + _set_http_net_peer_name_client( metric_labels, parsed_url.hostname, sem_conv_opt_in_mode ) if _report_new(sem_conv_opt_in_mode): - _set_http_hostname( + _set_http_host( span_attributes, parsed_url.hostname, sem_conv_opt_in_mode, ) # Use semconv library when available - span_attributes[ - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS - ] = parsed_url.hostname + span_attributes[NETWORK_PEER_ADDRESS] = parsed_url.hostname if parsed_url.port: - _set_http_port( + _set_http_peer_port_client( metric_labels, parsed_url.port, sem_conv_opt_in_mode ) if _report_new(sem_conv_opt_in_mode): - _set_http_port( + _set_http_peer_port_client( span_attributes, parsed_url.port, sem_conv_opt_in_mode ) # Use semconv library when available - span_attributes[ - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT - ] = parsed_url.port + span_attributes[NETWORK_PEER_PORT] = parsed_url.port except ValueError: pass @@ -225,9 +229,7 @@ def get_or_create_headers(): exception = exc result = getattr(exc, "response", None) finally: - elapsed_time = max( - round((default_timer() - start_time) * 1000), 0 - ) + elapsed_time = max(default_timer() - start_time, 0) if isinstance(result, Response): span_attributes = {} @@ -248,12 +250,8 @@ def get_or_create_headers(): _report_new(sem_conv_opt_in_mode) and status_code is StatusCode.ERROR ): - span_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( - result.status_code - ) - metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( - result.status_code - ) + span_attributes[ERROR_TYPE] = str(result.status_code) + metric_labels[ERROR_TYPE] = str(result.status_code) if result.raw is not None: version = getattr(result.raw, "version", None) @@ -276,24 +274,26 @@ def get_or_create_headers(): response_hook(span, request, result) if exception is not None and _report_new(sem_conv_opt_in_mode): - span.set_attribute( - _SPAN_ATTRIBUTES_ERROR_TYPE, type(exception).__qualname__ - ) - metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = type( - exception - ).__qualname__ + span.set_attribute(ERROR_TYPE, type(exception).__qualname__) + metric_labels[ERROR_TYPE] = type(exception).__qualname__ if duration_histogram_old is not None: - duration_attrs_old = _filter_duration_attrs( - metric_labels, _OpenTelemetryStabilityMode.DEFAULT + duration_attrs_old = _filter_semconv_duration_attrs( + metric_labels, + _client_duration_attrs_old, + _client_duration_attrs_new, + _HTTPStabilityMode.DEFAULT, ) duration_histogram_old.record( max(round(elapsed_time * 1000), 0), attributes=duration_attrs_old, ) if duration_histogram_new is not None: - duration_attrs_new = _filter_duration_attrs( - metric_labels, _OpenTelemetryStabilityMode.HTTP + duration_attrs_new = _filter_semconv_duration_attrs( + metric_labels, + _client_duration_attrs_old, + _client_duration_attrs_new, + _HTTPStabilityMode.HTTP, ) duration_histogram_new.record( elapsed_time, attributes=duration_attrs_new @@ -341,7 +341,10 @@ def get_default_span_name(method): Returns: span name """ - return sanitize_method(method.upper().strip()) + method = sanitize_method(method.upper().strip()) + if method == "_OTHER": + return "HTTP" + return method class RequestsInstrumentor(BaseInstrumentor): @@ -392,7 +395,7 @@ def _instrument(self, **kwargs): duration_histogram_new = None if _report_new(semconv_opt_in_mode): duration_histogram_new = meter.create_histogram( - name=_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, + name=HTTP_CLIENT_REQUEST_DURATION, unit="s", description="Duration of HTTP client requests.", ) @@ -402,9 +405,11 @@ def _instrument(self, **kwargs): duration_histogram_new, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), - excluded_urls=_excluded_urls_from_env - if excluded_urls is None - else parse_excluded_urls(excluded_urls), + excluded_urls=( + _excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls) + ), sem_conv_opt_in_mode=semconv_opt_in_mode, ) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py index 8424bfeb2a..9cd93a9150 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py @@ -16,3 +16,5 @@ _instruments = ("requests ~= 2.0",) _supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt new file mode 100644 index 0000000000..c5c8f84366 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +httpretty==1.1.4 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-requests diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 8817053068..75518fc8d3 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -23,10 +23,7 @@ import opentelemetry.instrumentation.requests from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( - _OTEL_SEMCONV_STABILITY_OPT_IN_KEY, - _SPAN_ATTRIBUTES_ERROR_TYPE, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, + OTEL_SEMCONV_STABILITY_OPT_IN, _OpenTelemetrySemanticConventionStability, ) from opentelemetry.instrumentation.requests import RequestsInstrumentor @@ -36,6 +33,21 @@ ) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PEER_ADDRESS, + NETWORK_PEER_PORT, + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import URL_FULL from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator from opentelemetry.test.test_base import TestBase @@ -88,7 +100,7 @@ def setUp(self): "os.environ", { "OTEL_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", - _OTEL_SEMCONV_STABILITY_OPT_IN_KEY: sem_conv_mode, + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) @@ -176,14 +188,14 @@ def test_basic_new_semconv(self): self.assertEqual( span.attributes, { - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.URL_FULL: url_with_port, - SpanAttributes.SERVER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url_with_port, + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", + HTTP_RESPONSE_STATUS_CODE: 200, + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, }, ) @@ -213,19 +225,19 @@ def test_basic_both_semconv(self): span.attributes, { SpanAttributes.HTTP_METHOD: "GET", - SpanAttributes.HTTP_REQUEST_METHOD: "GET", + HTTP_REQUEST_METHOD: "GET", SpanAttributes.HTTP_URL: url_with_port, - SpanAttributes.URL_FULL: url_with_port, + URL_FULL: url_with_port, SpanAttributes.HTTP_HOST: "mock", - SpanAttributes.SERVER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", + SERVER_ADDRESS: "mock", + NETWORK_PEER_ADDRESS: "mock", SpanAttributes.NET_PEER_PORT: 80, SpanAttributes.HTTP_STATUS_CODE: 200, - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + HTTP_RESPONSE_STATUS_CODE: 200, SpanAttributes.HTTP_FLAVOR: "1.1", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + NETWORK_PROTOCOL_VERSION: "1.1", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, }, ) @@ -328,12 +340,8 @@ def test_not_foundbasic_new_semconv(self): span = self.assert_span() - self.assertEqual( - span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 - ) - self.assertEqual( - span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" - ) + self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) + self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, @@ -355,12 +363,8 @@ def test_not_foundbasic_both_semconv(self): self.assertEqual( span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 404 ) - self.assertEqual( - span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 - ) - self.assertEqual( - span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" - ) + self.assertEqual(span.attributes.get(HTTP_RESPONSE_STATUS_CODE), 404) + self.assertEqual(span.attributes.get(ERROR_TYPE), "404") self.assertIs( span.status.status_code, @@ -524,17 +528,16 @@ def test_requests_exception_new_semconv(self, *_, **__): self.perform_request(url_with_port) span = self.assert_span() - print(span.attributes) self.assertEqual( span.attributes, { - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.URL_FULL: url_with_port, - SpanAttributes.SERVER_ADDRESS: "mock", - SpanAttributes.SERVER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, - _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", - _SPAN_ATTRIBUTES_ERROR_TYPE: "RequestException", + HTTP_REQUEST_METHOD: "GET", + URL_FULL: url_with_port, + SERVER_ADDRESS: "mock", + SERVER_PORT: 80, + NETWORK_PEER_PORT: 80, + NETWORK_PEER_ADDRESS: "mock", + ERROR_TYPE: "RequestException", }, ) self.assertEqual(span.status.status_code, StatusCode.ERROR) @@ -671,7 +674,7 @@ def setUp(self): self.env_patch = mock.patch.dict( "os.environ", { - _OTEL_SEMCONV_STABILITY_OPT_IN_KEY: sem_conv_mode, + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) self.env_patch.start() @@ -725,11 +728,11 @@ def test_basic_metric_new_semconv(self): self.perform_request(self.URL) expected_attributes = { - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.SERVER_ADDRESS: "examplehost", - SpanAttributes.SERVER_PORT: 8000, - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + SERVER_ADDRESS: "examplehost", + SERVER_PORT: 8000, + HTTP_REQUEST_METHOD: "GET", + NETWORK_PROTOCOL_VERSION: "1.1", } for ( resource_metrics @@ -761,11 +764,11 @@ def test_basic_metric_both_semconv(self): } expected_attributes_new = { - SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, - SpanAttributes.SERVER_ADDRESS: "examplehost", - SpanAttributes.SERVER_PORT: 8000, - SpanAttributes.HTTP_REQUEST_METHOD: "GET", - SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + HTTP_RESPONSE_STATUS_CODE: 200, + SERVER_ADDRESS: "examplehost", + SERVER_PORT: 8000, + HTTP_REQUEST_METHOD: "GET", + NETWORK_PROTOCOL_VERSION: "1.1", } for ( diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/LICENSE b/instrumentation/opentelemetry-instrumentation-sklearn/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-sklearn/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml index 28fa247159..6e57529ed1 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sklearn/pyproject.toml @@ -19,23 +19,16 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "scikit-learn ~= 0.24.0", ] -test = [ - "opentelemetry-instrumentation-sklearn[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] sklearn = "opentelemetry.instrumentation.sklearn:SklearnInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py index f7a124c7b8..deef26c62f 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py +++ b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt new file mode 100644 index 0000000000..885caae033 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +joblib==1.3.2 +numpy==1.24.4 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +scikit-learn==0.24.2 +scipy==1.10.1 +threadpoolctl==3.3.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-sklearn diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml index 1c63d671b7..c9a54920a2 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", "packaging >= 21.0", "wrapt >= 1.11.2", ] @@ -35,11 +36,6 @@ dependencies = [ instruments = [ "sqlalchemy", ] -test = [ - "opentelemetry-instrumentation-sqlalchemy[instruments]", - "opentelemetry-sdk ~= 1.12", - "pytest", -] [project.entry-points.opentelemetry_instrumentor] sqlalchemy = "opentelemetry.instrumentation.sqlalchemy:SQLAlchemyInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py index 0632d71faf..172c1193f3 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -15,13 +15,13 @@ import re import weakref +import sqlalchemy from sqlalchemy.event import ( # pylint: disable=no-name-in-module listen, remove, ) from opentelemetry import trace -from opentelemetry.instrumentation.sqlalchemy.version import __version__ from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import _get_opentelemetry_values from opentelemetry.semconv.trace import NetTransportValues, SpanAttributes @@ -227,7 +227,7 @@ def _before_cur_exec( commenter_data = { "db_driver": conn.engine.driver, # Driver/framework centric information. - "db_framework": f"sqlalchemy:{__version__}", + "db_framework": f"sqlalchemy:{sqlalchemy.__version__}", } if self.commenter_options.get("opentelemetry_values", True): @@ -296,18 +296,18 @@ def _get_attributes_from_cursor(vendor, cursor, attrs): is_unix_socket = info.host and info.host.startswith("/") if is_unix_socket: - attrs[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.OTHER.value + attrs[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.OTHER.value + ) if info.port: # postgresql enforces this pattern on all socket names attrs[SpanAttributes.NET_PEER_NAME] = os.path.join( info.host, f".s.PGSQL.{info.port}" ) else: - attrs[ - SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.IP_TCP.value + attrs[SpanAttributes.NET_TRANSPORT] = ( + NetTransportValues.IP_TCP.value + ) attrs[SpanAttributes.NET_PEER_NAME] = info.host if info.port: attrs[SpanAttributes.NET_PEER_PORT] = int(info.port) diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt new file mode 100644 index 0000000000..04f4fe0c4b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +cffi==1.15.1 +Deprecated==1.2.14 +greenlet==0.4.13 +hpy==0.0.4.dev179+g9b5d200 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +readline==6.2.4.1 +SQLAlchemy==1.1.18 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-sqlalchemy diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt new file mode 100644 index 0000000000..4e1620b772 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt @@ -0,0 +1,18 @@ +aiosqlite==0.20.0 +asgiref==3.7.2 +Deprecated==1.2.14 +greenlet==3.0.3 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +SQLAlchemy==1.4.51 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-sqlalchemy diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE b/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml b/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml index 0ccdc2da06..09f302e64e 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/pyproject.toml @@ -22,18 +22,16 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-dbapi == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-dbapi == 0.47b0.dev", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] sqlite3 = "opentelemetry.instrumentation.sqlite3:SQLite3Instrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py index 6340c1adfb..db4e3a0022 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt new file mode 100644 index 0000000000..848f207c8c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-dbapi +-e instrumentation/opentelemetry-instrumentation-sqlite3 diff --git a/instrumentation/opentelemetry-instrumentation-starlette/LICENSE b/instrumentation/opentelemetry-instrumentation-starlette/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-starlette/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml b/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml index 8b2f2a7014..9103dd6f2a 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-starlette/pyproject.toml @@ -22,25 +22,20 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-instrumentation-asgi == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-instrumentation-asgi == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "starlette ~= 0.13.0", ] -test = [ - "opentelemetry-instrumentation-starlette[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "requests ~= 2.23", # needed for testclient - "httpx ~= 0.22", # needed for testclient -] [project.entry-points.opentelemetry_instrumentor] starlette = "opentelemetry.instrumentation.starlette:StarletteInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 1ebc3348d4..474a942a98 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -55,20 +55,22 @@ def home(request): right after a span is created for a request and right before the span is finished for the response. - The server request hook is passed a server span and ASGI scope object for every incoming request. -- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. -- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. +- The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called. +- The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called. For example, .. code-block:: python - def server_request_hook(span: Span, scope: dict): + def server_request_hook(span: Span, scope: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_request_hook", "some-value") - def client_request_hook(span: Span, scope: dict): + + def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") - def client_response_hook(span: Span, message: dict): + + def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]): if span and span.is_recording(): span.set_attribute("custom_user_attribute_from_response_hook", "some-value") @@ -108,10 +110,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -142,10 +144,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** @@ -167,27 +169,27 @@ def client_response_hook(span: Span, message: dict): API --- """ -import typing from typing import Collection from starlette import applications from starlette.routing import Match from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.asgi.types import ( + ClientRequestHook, + ClientResponseHook, + ServerRequestHook, +) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.starlette.package import _instruments from opentelemetry.instrumentation.starlette.version import __version__ from opentelemetry.metrics import get_meter from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import Span +from opentelemetry.trace import get_tracer from opentelemetry.util.http import get_excluded_urls _excluded_urls = get_excluded_urls("STARLETTE") -_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]] -_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]] - class StarletteInstrumentor(BaseInstrumentor): """An instrumentor for starlette @@ -200,13 +202,19 @@ class StarletteInstrumentor(BaseInstrumentor): @staticmethod def instrument_app( app: applications.Starlette, - server_request_hook: _ServerRequestHookT = None, - client_request_hook: _ClientRequestHookT = None, - client_response_hook: _ClientResponseHookT = None, + server_request_hook: ServerRequestHook = None, + client_request_hook: ClientRequestHook = None, + client_response_hook: ClientResponseHook = None, meter_provider=None, tracer_provider=None, ): """Instrument an uninstrumented Starlette application.""" + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter = get_meter( __name__, __version__, @@ -221,7 +229,8 @@ def instrument_app( server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook, - tracer_provider=tracer_provider, + # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation + tracer=tracer, meter=meter, ) app.is_instrumented_by_opentelemetry = True @@ -270,13 +279,19 @@ def _uninstrument(self, **kwargs): class _InstrumentedStarlette(applications.Starlette): _tracer_provider = None _meter_provider = None - _server_request_hook: _ServerRequestHookT = None - _client_request_hook: _ClientRequestHookT = None - _client_response_hook: _ClientResponseHookT = None + _server_request_hook: ServerRequestHook = None + _client_request_hook: ClientRequestHook = None + _client_response_hook: ClientResponseHook = None _instrumented_starlette_apps = set() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + tracer = get_tracer( + __name__, + __version__, + _InstrumentedStarlette._tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter = get_meter( __name__, __version__, @@ -290,7 +305,8 @@ def __init__(self, *args, **kwargs): server_request_hook=_InstrumentedStarlette._server_request_hook, client_request_hook=_InstrumentedStarlette._client_request_hook, client_response_hook=_InstrumentedStarlette._client_response_hook, - tracer_provider=_InstrumentedStarlette._tracer_provider, + # Pass in tracer/meter to get __name__and __version__ of starlette instrumentation + tracer=tracer, meter=meter, ) self._is_instrumented_by_opentelemetry = True diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt new file mode 100644 index 0000000000..a582353901 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt @@ -0,0 +1,29 @@ +anyio==4.3.0 +asgiref==3.7.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +exceptiongroup==1.2.0 +h11==0.14.0 +httpcore==1.0.4 +httpx==0.27.0 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +sniffio==1.3.0 +starlette==0.13.8 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-asgi +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-starlette diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index 3784672fb5..1e768982b5 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -98,6 +98,10 @@ def test_basic_starlette_call(self): self.assertEqual(len(spans), 3) for span in spans: self.assertIn("GET /foobar", span.name) + self.assertEqual( + span.instrumentation_scope.name, + "opentelemetry.instrumentation.starlette", + ) def test_starlette_route_attribute_added(self): """Ensure that starlette routes are used as the span name.""" @@ -132,6 +136,10 @@ def test_starlette_metrics(self): for resource_metric in metrics_list.resource_metrics: self.assertTrue(len(resource_metric.scope_metrics) == 1) for scope_metric in resource_metric.scope_metrics: + self.assertEqual( + scope_metric.scope.name, + "opentelemetry.instrumentation.starlette", + ) self.assertTrue(len(scope_metric.metrics) == 3) for metric in scope_metric.metrics: self.assertIn(metric.name, _expected_metric_names) @@ -263,23 +271,23 @@ def server_request_hook(self, span, scope): if self._server_request_hook is not None: self._server_request_hook(span, scope) - def client_request_hook(self, receive_span, request): + def client_request_hook(self, receive_span, scope, message): if self._client_request_hook is not None: - self._client_request_hook(receive_span, request) + self._client_request_hook(receive_span, scope, message) - def client_response_hook(self, send_span, response): + def client_response_hook(self, send_span, scope, message): if self._client_response_hook is not None: - self._client_response_hook(send_span, response) + self._client_response_hook(send_span, scope, message) def test_hooks(self): def server_request_hook(span, scope): span.update_name("name from server hook") - def client_request_hook(receive_span, request): + def client_request_hook(receive_span, scope, message): receive_span.update_name("name from client hook") receive_span.set_attribute("attr-from-request-hook", "set") - def client_response_hook(send_span, response): + def client_response_hook(send_span, scope, message): send_span.update_name("name from response hook") send_span.set_attribute("attr-from-response-hook", "value") diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml index caf27ec5ab..8518ddc8db 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml @@ -22,21 +22,18 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ + "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-api ~= 1.11", - "opentelemetry-sdk ~= 1.11", - "psutil ~= 5.9", + "psutil >= 5.9.0, < 7", ] [project.optional-dependencies] instruments = [ "psutil >= 5", ] -test = [ - "opentelemetry-instrumentation-system-metrics[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] system_metrics = "opentelemetry.instrumentation.system_metrics:SystemMetricsInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 32766fa0c5..6342d287d5 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -91,7 +91,6 @@ from opentelemetry.instrumentation.system_metrics.package import _instruments from opentelemetry.instrumentation.system_metrics.version import __version__ from opentelemetry.metrics import CallbackOptions, Observation, get_meter -from opentelemetry.sdk.util import get_dict_as_key _logger = logging.getLogger(__name__) @@ -481,9 +480,11 @@ def _get_system_swap_utilization( if hasattr(system_swap, metric): self._system_swap_utilization_labels["state"] = metric yield Observation( - getattr(system_swap, metric) / system_swap.total - if system_swap.total - else 0, + ( + getattr(system_swap, metric) / system_swap.total + if system_swap.total + else 0 + ), self._system_swap_utilization_labels.copy(), ) @@ -556,9 +557,9 @@ def _get_system_network_dropped_packets( for metric in self._config["system.network.dropped.packets"]: in_out = {"receive": "in", "transmit": "out"}[metric] if hasattr(counters, f"drop{in_out}"): - self._system_network_dropped_packets_labels[ - "device" - ] = device + self._system_network_dropped_packets_labels["device"] = ( + device + ) self._system_network_dropped_packets_labels[ "direction" ] = metric @@ -629,15 +630,15 @@ def _get_system_network_connections( 1: "tcp", 2: "udp", }[net_connection.type.value] - self._system_network_connections_labels[ - "state" - ] = net_connection.status + self._system_network_connections_labels["state"] = ( + net_connection.status + ) self._system_network_connections_labels[metric] = getattr( net_connection, metric ) - connection_counters_key = get_dict_as_key( - self._system_network_connections_labels + connection_counters_key = tuple( + sorted(self._system_network_connections_labels.items()) ) if connection_counters_key in connection_counters: diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt new file mode 100644 index 0000000000..13573c65bd --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +psutil==6.0.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-system-metrics diff --git a/instrumentation/opentelemetry-instrumentation-threading/LICENSE b/instrumentation/opentelemetry-instrumentation-threading/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/instrumentation/opentelemetry-instrumentation-threading/README.rst b/instrumentation/opentelemetry-instrumentation-threading/README.rst new file mode 100644 index 0000000000..93097dfcb6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/README.rst @@ -0,0 +1,25 @@ +OpenTelemetry threading Instrumentation +======================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-threading.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-threading/ + +This library provides instrumentation for the `threading` module to ensure that +the OpenTelemetry context is propagated across threads. It is important to note +that this instrumentation does not produce any telemetry data on its own. It +merely ensures that the context is correctly propagated when threads are used. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-threading + +References +---------- + +* `OpenTelemetry Threading Tracing `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-threading/pyproject.toml b/instrumentation/opentelemetry-instrumentation-threading/pyproject.toml new file mode 100644 index 0000000000..d496b99f7a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-threading" +dynamic = ["version"] +description = "Thread context propagation support for OpenTelemetry" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.8" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-instrumentation == 0.47b0.dev", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [] + +[project.entry-points.opentelemetry_instrumentor] +threading = "opentelemetry.instrumentation.threading:ThreadingInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-threading" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/threading/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/__init__.py b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/__init__.py new file mode 100644 index 0000000000..be1eec139e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/__init__.py @@ -0,0 +1,149 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Instrument threading to propagate OpenTelemetry context. + +Usage +----- + +.. code-block:: python + + from opentelemetry.instrumentation.threading import ThreadingInstrumentor + + ThreadingInstrumentor().instrument() + +This library provides instrumentation for the `threading` module to ensure that +the OpenTelemetry context is propagated across threads. It is important to note +that this instrumentation does not produce any telemetry data on its own. It +merely ensures that the context is correctly propagated when threads are used. + + +When instrumented, new threads created using threading.Thread, threading.Timer, +or within futures.ThreadPoolExecutor will have the current OpenTelemetry +context attached, and this context will be re-activated in the thread's +run method or the executor's worker thread." +""" + +import threading +from concurrent import futures +from typing import Collection + +from wrapt import wrap_function_wrapper + +from opentelemetry import context +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.threading.package import _instruments +from opentelemetry.instrumentation.utils import unwrap + + +class ThreadingInstrumentor(BaseInstrumentor): + __WRAPPER_START_METHOD = "start" + __WRAPPER_RUN_METHOD = "run" + __WRAPPER_SUBMIT_METHOD = "submit" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + self._instrument_thread() + self._instrument_timer() + self._instrument_thread_pool() + + def _uninstrument(self, **kwargs): + self._uninstrument_thread() + self._uninstrument_timer() + self._uninstrument_thread_pool() + + @staticmethod + def _instrument_thread(): + wrap_function_wrapper( + threading.Thread, + ThreadingInstrumentor.__WRAPPER_START_METHOD, + ThreadingInstrumentor.__wrap_threading_start, + ) + wrap_function_wrapper( + threading.Thread, + ThreadingInstrumentor.__WRAPPER_RUN_METHOD, + ThreadingInstrumentor.__wrap_threading_run, + ) + + @staticmethod + def _instrument_timer(): + wrap_function_wrapper( + threading.Timer, + ThreadingInstrumentor.__WRAPPER_START_METHOD, + ThreadingInstrumentor.__wrap_threading_start, + ) + wrap_function_wrapper( + threading.Timer, + ThreadingInstrumentor.__WRAPPER_RUN_METHOD, + ThreadingInstrumentor.__wrap_threading_run, + ) + + @staticmethod + def _instrument_thread_pool(): + wrap_function_wrapper( + futures.ThreadPoolExecutor, + ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD, + ThreadingInstrumentor.__wrap_thread_pool_submit, + ) + + @staticmethod + def _uninstrument_thread(): + unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_START_METHOD) + unwrap(threading.Thread, ThreadingInstrumentor.__WRAPPER_RUN_METHOD) + + @staticmethod + def _uninstrument_timer(): + unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_START_METHOD) + unwrap(threading.Timer, ThreadingInstrumentor.__WRAPPER_RUN_METHOD) + + @staticmethod + def _uninstrument_thread_pool(): + unwrap( + futures.ThreadPoolExecutor, + ThreadingInstrumentor.__WRAPPER_SUBMIT_METHOD, + ) + + @staticmethod + def __wrap_threading_start(call_wrapped, instance, args, kwargs): + instance._otel_context = context.get_current() + return call_wrapped(*args, **kwargs) + + @staticmethod + def __wrap_threading_run(call_wrapped, instance, args, kwargs): + token = None + try: + token = context.attach(instance._otel_context) + return call_wrapped(*args, **kwargs) + finally: + context.detach(token) + + @staticmethod + def __wrap_thread_pool_submit(call_wrapped, instance, args, kwargs): + # obtain the original function and wrapped kwargs + original_func = args[0] + otel_context = context.get_current() + + def wrapped_func(*func_args, **func_kwargs): + token = None + try: + token = context.attach(otel_context) + return original_func(*func_args, **func_kwargs) + finally: + context.detach(token) + + # replace the original function with the wrapped function + new_args = (wrapped_func,) + args[1:] + return call_wrapped(*new_args, **kwargs) diff --git a/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/package.py b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/package.py new file mode 100644 index 0000000000..1bf177779b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/package.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_instruments = () + +_supports_metrics = False diff --git a/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/version.py b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/version.py new file mode 100644 index 0000000000..b6955b0eca --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry/instrumentation/threading/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt new file mode 100644 index 0000000000..eb37259669 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt @@ -0,0 +1,15 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-threading diff --git a/instrumentation/opentelemetry-instrumentation-threading/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-threading/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-threading/tests/test_threading.py b/instrumentation/opentelemetry-instrumentation-threading/tests/test_threading.py new file mode 100644 index 0000000000..15f67b8d61 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/tests/test_threading.py @@ -0,0 +1,226 @@ +# Copyright 2020, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import threading +from concurrent.futures import ThreadPoolExecutor +from typing import List + +from opentelemetry import trace +from opentelemetry.instrumentation.threading import ThreadingInstrumentor +from opentelemetry.test.test_base import TestBase + + +class TestThreading(TestBase): + def setUp(self): + super().setUp() + self._tracer = self.tracer_provider.get_tracer(__name__) + self._mock_span_contexts: List[trace.SpanContext] = [] + ThreadingInstrumentor().instrument() + + def tearDown(self): + ThreadingInstrumentor().uninstrument() + super().tearDown() + + def get_root_span(self): + return self._tracer.start_as_current_span("rootSpan") + + def test_trace_context_propagation_in_thread(self): + self.run_threading_test(threading.Thread(target=self.fake_func)) + + def test_trace_context_propagation_in_timer(self): + self.run_threading_test( + threading.Timer(interval=1, function=self.fake_func) + ) + + def run_threading_test(self, thread: threading.Thread): + with self.get_root_span() as span: + expected_span_context = span.get_span_context() + thread.start() + thread.join() + + # check result + self.assertEqual(len(self._mock_span_contexts), 1) + self.assertEqual( + self._mock_span_contexts[0], expected_span_context + ) + + def test_trace_context_propagation_in_thread_pool_with_multiple_workers( + self, + ): + max_workers = 10 + executor = ThreadPoolExecutor(max_workers=max_workers) + + expected_span_contexts: List[trace.SpanContext] = [] + futures_list = [] + for num in range(max_workers): + with self._tracer.start_as_current_span(f"trace_{num}") as span: + expected_span_context = span.get_span_context() + expected_span_contexts.append(expected_span_context) + future = executor.submit( + self.get_current_span_context_for_test + ) + futures_list.append(future) + + result_span_contexts = [future.result() for future in futures_list] + + # check result + self.assertEqual(result_span_contexts, expected_span_contexts) + + def test_trace_context_propagation_in_thread_pool_with_single_worker(self): + max_workers = 1 + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # test propagation of the same trace context across multiple tasks + with self._tracer.start_as_current_span("task") as task_span: + expected_task_context = task_span.get_span_context() + future1 = executor.submit( + self.get_current_span_context_for_test + ) + future2 = executor.submit( + self.get_current_span_context_for_test + ) + + # check result + self.assertEqual(future1.result(), expected_task_context) + self.assertEqual(future2.result(), expected_task_context) + + # test propagation of different trace contexts across tasks in sequence + with self._tracer.start_as_current_span("task1") as task1_span: + expected_task1_context = task1_span.get_span_context() + future1 = executor.submit( + self.get_current_span_context_for_test + ) + + # check result + self.assertEqual(future1.result(), expected_task1_context) + + with self._tracer.start_as_current_span("task2") as task2_span: + expected_task2_context = task2_span.get_span_context() + future2 = executor.submit( + self.get_current_span_context_for_test + ) + + # check result + self.assertEqual(future2.result(), expected_task2_context) + + def fake_func(self): + span_context = self.get_current_span_context_for_test() + self._mock_span_contexts.append(span_context) + + @staticmethod + def get_current_span_context_for_test() -> trace.SpanContext: + return trace.get_current_span().get_span_context() + + def print_square(self, num): + with self._tracer.start_as_current_span("square"): + return num * num + + def print_cube(self, num): + with self._tracer.start_as_current_span("cube"): + return num * num * num + + def print_square_with_thread(self, num): + with self._tracer.start_as_current_span("square"): + cube_thread = threading.Thread(target=self.print_cube, args=(10,)) + + cube_thread.start() + cube_thread.join() + return num * num + + def calculate(self, num): + with self._tracer.start_as_current_span("calculate"): + square_thread = threading.Thread( + target=self.print_square, args=(num,) + ) + cube_thread = threading.Thread(target=self.print_cube, args=(num,)) + square_thread.start() + square_thread.join() + + cube_thread.start() + cube_thread.join() + + def test_without_thread_nesting(self): + square_thread = threading.Thread(target=self.print_square, args=(10,)) + + with self._tracer.start_as_current_span("root"): + square_thread.start() + square_thread.join() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + + # pylint: disable=unbalanced-tuple-unpacking + target, root = spans[:2] + + self.assertIs(target.parent, root.get_span_context()) + self.assertIsNone(root.parent) + + def test_with_thread_nesting(self): + # + # Following scenario is tested. + # threadA -> methodA -> threadB -> methodB + # + + square_thread = threading.Thread( + target=self.print_square_with_thread, args=(10,) + ) + + with self._tracer.start_as_current_span("root"): + square_thread.start() + square_thread.join() + + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 3) + # pylint: disable=unbalanced-tuple-unpacking + cube, square, root = spans[:3] + + self.assertIs(cube.parent, square.get_span_context()) + self.assertIs(square.parent, root.get_span_context()) + self.assertIsNone(root.parent) + + def test_with_thread_multi_nesting(self): + # + # Following scenario is tested. + # / threadB -> methodB + # threadA -> methodA -> + # \ threadC -> methodC + # + calculate_thread = threading.Thread(target=self.calculate, args=(10,)) + + with self._tracer.start_as_current_span("root"): + calculate_thread.start() + calculate_thread.join() + + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 4) + + # pylint: disable=unbalanced-tuple-unpacking + cube, square, calculate, root = spans[:4] + + self.assertIs(cube.parent, calculate.get_span_context()) + self.assertIs(square.parent, calculate.get_span_context()) + self.assertIs(calculate.parent, root.get_span_context()) + self.assertIsNone(root.parent) + + def test_uninstrumented(self): + ThreadingInstrumentor().uninstrument() + + square_thread = threading.Thread(target=self.print_square, args=(10,)) + square_thread.start() + square_thread.join() + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + ThreadingInstrumentor().instrument() diff --git a/instrumentation/opentelemetry-instrumentation-tornado/LICENSE b/instrumentation/opentelemetry-instrumentation-tornado/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-tornado/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml b/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml index 54706b42f2..ab5d522aa2 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-tornado/pyproject.toml @@ -21,23 +21,19 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [ "tornado >= 5.1.1", ] -test = [ - "opentelemetry-instrumentation-tornado[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", - "http-server-mock" -] [project.entry-points.opentelemetry_instrumentor] tornado = "opentelemetry.instrumentation.tornado:TornadoInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index dfa4b217df..be9129bda0 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -87,14 +87,14 @@ def client_request_hook(span, request): # will be called after a outgoing request made with # `tornado.httpclient.AsyncHTTPClient.fetch` finishes. # `response`` is an instance of ``Future[tornado.httpclient.HTTPResponse]`. - def client_resposne_hook(span, future): + def client_response_hook(span, future): pass # apply tornado instrumentation with hooks TornadoInstrumentor().instrument( server_request_hook=server_request_hook, client_request_hook=client_request_hook, - client_response_hook=client_resposne_hook + client_response_hook=client_response_hook ) Capture HTTP request and response headers @@ -296,7 +296,7 @@ def _create_server_histograms(meter) -> Dict[str, Histogram]: MetricInstruments.HTTP_SERVER_DURATION: meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="measures the duration outbound HTTP requests", + description="Duration of HTTP client requests.", ), MetricInstruments.HTTP_SERVER_REQUEST_SIZE: meter.create_histogram( name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, @@ -455,9 +455,9 @@ def _get_attributes_from_request(request): if hasattr(request.connection, "context") and getattr( request.connection.context, "_orig_remote_ip", None ): - attrs[ - SpanAttributes.NET_PEER_IP - ] = request.connection.context._orig_remote_ip + attrs[SpanAttributes.NET_PEER_IP] = ( + request.connection.context._orig_remote_ip + ) return extract_attributes_from_object( request, _traced_request_attrs, attrs diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt new file mode 100644 index 0000000000..14ac028083 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt @@ -0,0 +1,30 @@ +asgiref==3.7.2 +blinker==1.7.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +Deprecated==1.2.14 +Flask==3.0.2 +http_server_mock==1.7 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +tomli==2.0.1 +tornado==6.4.1 +typing_extensions==4.9.0 +urllib3==2.2.2 +Werkzeug==3.0.3 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-tornado diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py index 0baaa348ab..01cdddceed 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -528,7 +528,7 @@ def index(): class TestTornadoInstrumentationWithXHeaders(TornadoTest): - def get_httpserver_options(self): + def get_httpserver_options(self): # pylint: disable=no-self-use return {"xheaders": True} def test_xheaders(self): diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/LICENSE b/instrumentation/opentelemetry-instrumentation-tortoiseorm/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst b/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst index e845fbf84d..22d6c01731 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst @@ -19,5 +19,5 @@ References ---------- * `OpenTelemetry Project `_ -* `Tortoise ORM `_ +* `Tortoise ORM `_ * `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/pyproject.toml b/instrumentation/opentelemetry-instrumentation-tortoiseorm/pyproject.toml index 9d32028bdd..dc0293d3d5 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", ] [project.optional-dependencies] @@ -34,10 +35,6 @@ instruments = [ "tortoise-orm >= 0.17.0", "pydantic >= 1.10.2" ] -test = [ - "opentelemetry-instrumentation-tortoiseorm[instruments]", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] tortoiseorm = "opentelemetry.instrumentation.tortoiseorm:TortoiseORMInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py index 7988daf130..cebcb81ced 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py @@ -229,17 +229,17 @@ def _hydrate_span_from_args(self, connection, query, parameters) -> dict: capabilities = getattr(connection, "capabilities", None) if capabilities is not None: if capabilities.dialect == "sqlite": - span_attributes[ - SpanAttributes.DB_SYSTEM - ] = DbSystemValues.SQLITE.value + span_attributes[SpanAttributes.DB_SYSTEM] = ( + DbSystemValues.SQLITE.value + ) elif capabilities.dialect == "postgres": - span_attributes[ - SpanAttributes.DB_SYSTEM - ] = DbSystemValues.POSTGRESQL.value + span_attributes[SpanAttributes.DB_SYSTEM] = ( + DbSystemValues.POSTGRESQL.value + ) elif capabilities.dialect == "mysql": - span_attributes[ - SpanAttributes.DB_SYSTEM - ] = DbSystemValues.MYSQL.value + span_attributes[SpanAttributes.DB_SYSTEM] = ( + DbSystemValues.MYSQL.value + ) dbname = getattr(connection, "filename", None) if dbname: span_attributes[SpanAttributes.DB_NAME] = dbname diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt new file mode 100644 index 0000000000..2d518c1192 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt @@ -0,0 +1,23 @@ +aiosqlite==0.17.0 +annotated-types==0.6.0 +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +iso8601==1.1.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pydantic==2.6.2 +pydantic_core==2.16.3 +pypika-tortoise==0.1.6 +pytest==7.4.4 +pytest-benchmark==4.0.0 +pytz==2024.1 +tomli==2.0.1 +tortoise-orm==0.20.0 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-tortoiseorm diff --git a/instrumentation/opentelemetry-instrumentation-urllib/LICENSE b/instrumentation/opentelemetry-instrumentation-urllib/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-urllib/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml b/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml index 3fe6d272cb..90799c4492 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-urllib/pyproject.toml @@ -22,20 +22,17 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [] -test = [ - "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] urllib = "opentelemetry.instrumentation.urllib:URLLibInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py index 3738c4d2c6..befc022b35 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py @@ -156,9 +156,11 @@ def _instrument(self, **kwargs): histograms, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), - excluded_urls=_excluded_urls_from_env - if excluded_urls is None - else parse_excluded_urls(excluded_urls), + excluded_urls=( + _excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls) + ), ) def _uninstrument(self, **kwargs): @@ -251,9 +253,9 @@ def _instrumented_open_call( ver_ = str(getattr(result, "version", "")) if ver_: - labels[ - SpanAttributes.HTTP_FLAVOR - ] = f"{ver_[:1]}.{ver_[:-1]}" + labels[SpanAttributes.HTTP_FLAVOR] = ( + f"{ver_[:1]}.{ver_[:-1]}" + ) _record_histograms( histograms, labels, request, result, elapsed_time diff --git a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py index 6340c1adfb..db4e3a0022 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/version.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" _instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt new file mode 100644 index 0000000000..0ac7842086 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-urllib diff --git a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py index 36189e12c1..f73f0d1b97 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py @@ -99,7 +99,7 @@ def timeout_exception_callback(*_, **__): @staticmethod def base_exception_callback(*_, **__): - raise Exception("test") + raise Exception("test") # pylint: disable=broad-exception-raised def assert_span(self, exporter=None, num_spans=1): if exporter is None: diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/LICENSE b/instrumentation/opentelemetry-instrumentation-urllib3/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-urllib3/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml b/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml index fbd27f7bd8..b9dd6d528f 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-urllib3/pyproject.toml @@ -22,12 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", "wrapt >= 1.0.0, < 2.0.0", ] @@ -35,11 +36,6 @@ dependencies = [ instruments = [ "urllib3 >= 1.0.0, < 3.0.0", ] -test = [ - "opentelemetry-instrumentation-urllib3[instruments]", - "httpretty ~= 1.0", - "opentelemetry-test-utils == 0.45b0.dev", -] [project.entry-points.opentelemetry_instrumentor] urllib3 = "opentelemetry.instrumentation.urllib3:URLLib3Instrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py index 985f291199..add5db8f19 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py @@ -200,9 +200,11 @@ def _instrument(self, **kwargs): request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), url_filter=kwargs.get("url_filter"), - excluded_urls=_excluded_urls_from_env - if excluded_urls is None - else parse_excluded_urls(excluded_urls), + excluded_urls=( + _excluded_urls_from_env + if excluded_urls is None + else parse_excluded_urls(excluded_urls) + ), ) def _uninstrument(self, **kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt new file mode 100644 index 0000000000..6eb6272dbf --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==1.26.19 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-urllib3 diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt new file mode 100644 index 0000000000..402beb85b9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-urllib3 diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE b/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE +++ b/instrumentation/opentelemetry-instrumentation-wsgi/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml b/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml index bb1b6ef286..e56e4e9eb9 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-wsgi/pyproject.toml @@ -22,19 +22,17 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", - "opentelemetry-semantic-conventions == 0.45b0.dev", - "opentelemetry-util-http == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", + "opentelemetry-semantic-conventions == 0.47b0.dev", + "opentelemetry-util-http == 0.47b0.dev", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.45b0.dev", -] [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-wsgi" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 87c73cc737..6a1883fa7e 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -213,14 +213,40 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he from timeit import default_timer from opentelemetry import context, trace -from opentelemetry.instrumentation.utils import ( - _start_internal_or_server_span, - http_status_to_status_code, +from opentelemetry.instrumentation._semconv import ( + _filter_semconv_active_request_count_attr, + _filter_semconv_duration_attrs, + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, + _set_http_flavor_version, + _set_http_method, + _set_http_net_host, + _set_http_net_host_port, + _set_http_net_peer_name_server, + _set_http_peer_ip, + _set_http_peer_port_server, + _set_http_scheme, + _set_http_target, + _set_http_user_agent, + _set_status, ) +from opentelemetry.instrumentation.utils import _start_internal_or_server_span from opentelemetry.instrumentation.wsgi.version import __version__ from opentelemetry.metrics import get_meter from opentelemetry.propagators.textmap import Getter +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.http import ( @@ -228,6 +254,7 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, + _parse_url_query, get_custom_headers, normalise_request_header_name, normalise_response_header_name, @@ -239,26 +266,6 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he _CARRIER_KEY_PREFIX = "HTTP_" _CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX) -# List of recommended attributes -_duration_attrs = [ - SpanAttributes.HTTP_METHOD, - SpanAttributes.HTTP_HOST, - SpanAttributes.HTTP_SCHEME, - SpanAttributes.HTTP_STATUS_CODE, - SpanAttributes.HTTP_FLAVOR, - SpanAttributes.HTTP_SERVER_NAME, - SpanAttributes.NET_HOST_NAME, - SpanAttributes.NET_HOST_PORT, -] - -_active_requests_count_attrs = [ - SpanAttributes.HTTP_METHOD, - SpanAttributes.HTTP_HOST, - SpanAttributes.HTTP_SCHEME, - SpanAttributes.HTTP_FLAVOR, - SpanAttributes.HTTP_SERVER_NAME, -] - class WSGIGetter(Getter[dict]): def get( @@ -296,53 +303,84 @@ def setifnotnone(dic, key, value): dic[key] = value -def collect_request_attributes(environ): +# pylint: disable=too-many-branches + + +def collect_request_attributes( + environ, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, +): """Collects HTTP request attributes from the PEP3333-conforming WSGI environ and returns a dictionary to be used as span creation attributes. """ + result = {} + _set_http_method( + result, + environ.get("REQUEST_METHOD", ""), + sanitize_method(environ.get("REQUEST_METHOD", "")), + sem_conv_opt_in_mode, + ) + # old semconv v1.12.0 + server_name = environ.get("SERVER_NAME") + if _report_old(sem_conv_opt_in_mode): + result[SpanAttributes.HTTP_SERVER_NAME] = server_name + + _set_http_scheme( + result, + environ.get("wsgi.url_scheme"), + sem_conv_opt_in_mode, + ) - result = { - SpanAttributes.HTTP_METHOD: sanitize_method( - environ.get("REQUEST_METHOD") - ), - SpanAttributes.HTTP_SERVER_NAME: environ.get("SERVER_NAME"), - SpanAttributes.HTTP_SCHEME: environ.get("wsgi.url_scheme"), - } - + host = environ.get("HTTP_HOST") host_port = environ.get("SERVER_PORT") - if host_port is not None and not host_port == "": - result.update({SpanAttributes.NET_HOST_PORT: int(host_port)}) + if host: + _set_http_net_host(result, host, sem_conv_opt_in_mode) + # old semconv v1.12.0 + if _report_old(sem_conv_opt_in_mode): + result[SpanAttributes.HTTP_HOST] = host + if host_port: + _set_http_net_host_port( + result, + int(host_port), + sem_conv_opt_in_mode, + ) - setifnotnone(result, SpanAttributes.HTTP_HOST, environ.get("HTTP_HOST")) target = environ.get("RAW_URI") if target is None: # Note: `"" or None is None` target = environ.get("REQUEST_URI") - if target is not None: - result[SpanAttributes.HTTP_TARGET] = target + if target: + path, query = _parse_url_query(target) + _set_http_target(result, target, path, query, sem_conv_opt_in_mode) else: - result[SpanAttributes.HTTP_URL] = remove_url_credentials( - wsgiref_util.request_uri(environ) - ) + # old semconv v1.20.0 + if _report_old(sem_conv_opt_in_mode): + result[SpanAttributes.HTTP_URL] = remove_url_credentials( + wsgiref_util.request_uri(environ) + ) remote_addr = environ.get("REMOTE_ADDR") if remote_addr: - result[SpanAttributes.NET_PEER_IP] = remote_addr + _set_http_peer_ip(result, remote_addr, sem_conv_opt_in_mode) + + peer_port = environ.get("REMOTE_PORT") + if peer_port: + _set_http_peer_port_server(result, peer_port, sem_conv_opt_in_mode) + remote_host = environ.get("REMOTE_HOST") if remote_host and remote_host != remote_addr: - result[SpanAttributes.NET_PEER_NAME] = remote_host + _set_http_net_peer_name_server( + result, remote_host, sem_conv_opt_in_mode + ) user_agent = environ.get("HTTP_USER_AGENT") if user_agent is not None and len(user_agent) > 0: - result[SpanAttributes.HTTP_USER_AGENT] = user_agent + _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode) - setifnotnone( - result, SpanAttributes.NET_PEER_PORT, environ.get("REMOTE_PORT") - ) flavor = environ.get("SERVER_PROTOCOL", "") if flavor.upper().startswith(_HTTP_VERSION_PREFIX): flavor = flavor[len(_HTTP_VERSION_PREFIX) :] if flavor: - result[SpanAttributes.HTTP_FLAVOR] = flavor + _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode) return result @@ -358,7 +396,6 @@ def collect_custom_request_headers_attributes(environ): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) - headers = { key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val for key, val in environ.items() @@ -387,7 +424,12 @@ def collect_custom_response_headers_attributes(response_headers): ) response_headers_dict = {} if response_headers: - response_headers_dict = dict(response_headers) + for key, val in response_headers: + key = key.lower() + if key in response_headers_dict: + response_headers_dict[key] += "," + val + else: + response_headers_dict[key] = val return sanitize.sanitize_header_values( response_headers_dict, @@ -406,46 +448,56 @@ def _parse_status_code(resp_status): return None -def _parse_active_request_count_attrs(req_attrs): - active_requests_count_attrs = {} - for attr_key in _active_requests_count_attrs: - if req_attrs.get(attr_key) is not None: - active_requests_count_attrs[attr_key] = req_attrs[attr_key] - return active_requests_count_attrs +def _parse_active_request_count_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_active_request_count_attr( + req_attrs, + _server_active_requests_count_attrs_old, + _server_active_requests_count_attrs_new, + sem_conv_opt_in_mode, + ) -def _parse_duration_attrs(req_attrs): - duration_attrs = {} - for attr_key in _duration_attrs: - if req_attrs.get(attr_key) is not None: - duration_attrs[attr_key] = req_attrs[attr_key] - return duration_attrs +def _parse_duration_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_duration_attrs( + req_attrs, + _server_duration_attrs_old, + _server_duration_attrs_new, + sem_conv_opt_in_mode, + ) def add_response_attributes( - span, start_response_status, response_headers + span, + start_response_status, + response_headers, + duration_attrs=None, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, ): # pylint: disable=unused-argument """Adds HTTP response attributes to span using the arguments passed to a PEP3333-conforming start_response callable. """ if not span.is_recording(): return - status_code, _ = start_response_status.split(" ", 1) + status_code_str, _ = start_response_status.split(" ", 1) + status_code = 0 try: - status_code = int(status_code) + status_code = int(status_code_str) except ValueError: - span.set_status( - Status( - StatusCode.ERROR, - "Non-integer HTTP status: " + repr(status_code), - ) - ) - else: - span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) - span.set_status( - Status(http_status_to_status_code(status_code, server_span=True)) - ) + status_code = -1 + if duration_attrs is None: + duration_attrs = {} + _set_status( + span, + duration_attrs, + status_code_str, + status_code, + sem_conv_opt_in_mode, + ) def get_default_span_name(environ): @@ -460,6 +512,8 @@ def get_default_span_name(environ): The span name. """ method = sanitize_method(environ.get("REQUEST_METHOD", "").strip()) + if method == "_OTHER": + return "HTTP" path = environ.get("PATH_INFO", "").strip() if method and path: return f"{method} {path}" @@ -481,6 +535,8 @@ class OpenTelemetryMiddleware: incoming request. tracer_provider: Optional tracer provider to use. If omitted the current globally configured one is used. + meter_provider: Optional meter provider to use. If omitted the current + globally configured one is used. """ def __init__( @@ -491,42 +547,66 @@ def __init__( tracer_provider=None, meter_provider=None, ): + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) self.wsgi = wsgi self.tracer = trace.get_tracer( __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) self.meter = get_meter( __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", - ) - self.duration_histogram = self.meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="measures the duration of the inbound HTTP request", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) + self.duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + self.duration_histogram_old = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="measures the duration of the inbound HTTP request", + ) + self.duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + self.duration_histogram_new = self.meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + unit="s", + description="measures the duration of the inbound HTTP request", + ) + # We don't need a separate active request counter for old/new semantic conventions + # because the new attributes are a subset of the old attributes self.active_requests_counter = self.meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, - unit="requests", - description="measures the number of concurrent HTTP requests that are currently in-flight", + unit="{request}", + description="Number of active HTTP server requests.", ) self.request_hook = request_hook self.response_hook = response_hook + self._sem_conv_opt_in_mode = sem_conv_opt_in_mode @staticmethod def _create_start_response( - span, start_response, response_hook, duration_attrs + span, + start_response, + response_hook, + duration_attrs, + sem_conv_opt_in_mode, ): @functools.wraps(start_response) def _start_response(status, response_headers, *args, **kwargs): - add_response_attributes(span, status, response_headers) - status_code = _parse_status_code(status) - if status_code is not None: - duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = status_code + add_response_attributes( + span, + status, + response_headers, + duration_attrs, + sem_conv_opt_in_mode, + ) if span.is_recording() and span.kind == trace.SpanKind.SERVER: custom_attributes = collect_custom_response_headers_attributes( response_headers @@ -547,11 +627,13 @@ def __call__(self, environ, start_response): environ: A WSGI environment. start_response: The WSGI start_response callable. """ - req_attrs = collect_request_attributes(environ) + req_attrs = collect_request_attributes( + environ, self._sem_conv_opt_in_mode + ) active_requests_count_attrs = _parse_active_request_count_attrs( - req_attrs + req_attrs, + self._sem_conv_opt_in_mode, ) - duration_attrs = _parse_duration_attrs(req_attrs) span, token = _start_internal_or_server_span( tracer=self.tracer, @@ -580,20 +662,40 @@ def __call__(self, environ, start_response): try: with trace.use_span(span): start_response = self._create_start_response( - span, start_response, response_hook, duration_attrs + span, + start_response, + response_hook, + req_attrs, + self._sem_conv_opt_in_mode, ) iterable = self.wsgi(environ, start_response) return _end_span_after_iterating(iterable, span, token) except Exception as ex: - if span.is_recording(): + if _report_new(self._sem_conv_opt_in_mode): + req_attrs[ERROR_TYPE] = type(ex).__qualname__ + if span.is_recording(): + span.set_attribute(ERROR_TYPE, type(ex).__qualname__) span.set_status(Status(StatusCode.ERROR, str(ex))) span.end() if token is not None: context.detach(token) raise finally: - duration = max(round((default_timer() - start) * 1000), 0) - self.duration_histogram.record(duration, duration_attrs) + duration_s = default_timer() - start + if self.duration_histogram_old: + duration_attrs_old = _parse_duration_attrs( + req_attrs, _HTTPStabilityMode.DEFAULT + ) + self.duration_histogram_old.record( + max(round(duration_s * 1000), 0), duration_attrs_old + ) + if self.duration_histogram_new: + duration_attrs_new = _parse_duration_attrs( + req_attrs, _HTTPStabilityMode.HTTP + ) + self.duration_histogram_new.record( + max(duration_s, 0), duration_attrs_new + ) self.active_requests_counter.add(-1, active_requests_count_attrs) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py index 942f175da1..1bb8350a06 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/package.py @@ -16,3 +16,5 @@ _instruments = tuple() _supports_metrics = True + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py index 2b23bc4994..b6955b0eca 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt new file mode 100644 index 0000000000..a25f8882d1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e util/opentelemetry-util-http +-e instrumentation/opentelemetry-instrumentation-wsgi diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index d71e584ca8..777d19f41d 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines + import sys import unittest import wsgiref.util as wsgiref_util @@ -20,11 +22,37 @@ import opentelemetry.instrumentation.wsgi as otel_wsgi from opentelemetry import trace as trace_api +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, + URL_PATH, + URL_QUERY, + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.test.wsgitestutil import WsgiTestBase @@ -115,17 +143,70 @@ def wsgi_with_custom_response_headers(environ, start_response): return [b"*"] -_expected_metric_names = [ +def wsgi_with_repeat_custom_response_headers(environ, start_response): + assert isinstance(environ, dict) + start_response( + "200 OK", + [ + ("my-custom-header", "my-custom-value-1"), + ("my-custom-header", "my-custom-value-2"), + ], + ) + return [b"*"] + + +_expected_metric_names_old = [ "http.server.active_requests", "http.server.duration", ] -_recommended_attrs = { - "http.server.active_requests": otel_wsgi._active_requests_count_attrs, - "http.server.duration": otel_wsgi._duration_attrs, +_expected_metric_names_new = [ + "http.server.active_requests", + "http.server.request.duration", +] +_recommended_metrics_attrs_old = { + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, +} +_recommended_metrics_attrs_new = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, +} +_server_active_requests_count_attrs_both = ( + _server_active_requests_count_attrs_old +) +_server_active_requests_count_attrs_both.extend( + _server_active_requests_count_attrs_new +) +_recommended_metrics_attrs_both = { + "http.server.active_requests": _server_active_requests_count_attrs_both, + "http.server.duration": _server_duration_attrs_old, + "http.server.request.duration": _server_duration_attrs_new, } class TestWsgiApplication(WsgiTestBase): + def setUp(self): + super().setUp() + + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" + self.env_patch = mock.patch.dict( + "os.environ", + { + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, + }, + ) + + _OpenTelemetrySemanticConventionStability._initialized = False + + self.env_patch.start() + def validate_response( self, response, @@ -134,6 +215,8 @@ def validate_response( http_method="GET", span_attributes=None, response_headers=None, + old_sem_conv=True, + new_sem_conv=False, ): while True: try: @@ -159,7 +242,8 @@ def validate_response( self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) - expected_attributes = { + expected_attributes = {} + expected_attributes_old = { SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.NET_HOST_PORT: 80, @@ -167,10 +251,26 @@ def validate_response( SpanAttributes.HTTP_FLAVOR: "1.0", SpanAttributes.HTTP_URL: "http://127.0.0.1/", SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.NET_HOST_NAME: "127.0.0.1", } + expected_attributes_new = { + SERVER_PORT: 80, + SERVER_ADDRESS: "127.0.0.1", + NETWORK_PROTOCOL_VERSION: "1.0", + HTTP_RESPONSE_STATUS_CODE: 200, + URL_SCHEME: "http", + } + if old_sem_conv: + expected_attributes.update(expected_attributes_old) + if new_sem_conv: + expected_attributes.update(expected_attributes_new) + expected_attributes.update(span_attributes or {}) if http_method is not None: - expected_attributes[SpanAttributes.HTTP_METHOD] = http_method + if old_sem_conv: + expected_attributes[SpanAttributes.HTTP_METHOD] = http_method + if new_sem_conv: + expected_attributes[HTTP_REQUEST_METHOD] = http_method self.assertEqual(span_list[0].attributes, expected_attributes) def test_basic_wsgi_call(self): @@ -178,6 +278,16 @@ def test_basic_wsgi_call(self): response = app(self.environ, self.start_response) self.validate_response(response) + def test_basic_wsgi_call_new_semconv(self): + app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) + response = app(self.environ, self.start_response) + self.validate_response(response, old_sem_conv=False, new_sem_conv=True) + + def test_basic_wsgi_call_both_semconv(self): + app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) + response = app(self.environ, self.start_response) + self.validate_response(response, old_sem_conv=True, new_sem_conv=True) + def test_hooks(self): hook_headers = ( "hook_attr", @@ -271,7 +381,38 @@ def test_wsgi_metrics(self): for scope_metric in resource_metric.scope_metrics: self.assertTrue(len(scope_metric.metrics) != 0) for metric in scope_metric.metrics: - self.assertIn(metric.name, _expected_metric_names) + self.assertIn(metric.name, _expected_metric_names_old) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_old[metric.name], + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_wsgi_metrics_new_semconv(self): + app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) + self.assertRaises(ValueError, app, self.environ, self.start_response) + self.assertRaises(ValueError, app, self.environ, self.start_response) + self.assertRaises(ValueError, app, self.environ, self.start_response) + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names_new) data_points = list(metric.data.data_points) self.assertEqual(len(data_points), 1) for point in data_points: @@ -282,7 +423,46 @@ def test_wsgi_metrics(self): number_data_point_seen = True for attr in point.attributes: self.assertIn( - attr, _recommended_attrs[metric.name] + attr, + _recommended_metrics_attrs_new[metric.name], + ) + self.assertTrue(number_data_point_seen and histogram_data_point_seen) + + def test_wsgi_metrics_both_semconv(self): + app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled) + self.assertRaises(ValueError, app, self.environ, self.start_response) + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histogram_data_point_seen = False + + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + for metric in scope_metric.metrics: + if metric.unit == "ms": + self.assertEqual(metric.name, "http.server.duration") + elif metric.unit == "s": + self.assertEqual( + metric.name, "http.server.request.duration" + ) + else: + self.assertEqual( + metric.name, "http.server.active_requests" + ) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 1) + histogram_data_point_seen = True + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + for attr in point.attributes: + self.assertIn( + attr, + _recommended_metrics_attrs_both[metric.name], ) self.assertTrue(number_data_point_seen and histogram_data_point_seen) @@ -291,7 +471,7 @@ def test_nonstandard_http_method(self): app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi) response = app(self.environ, self.start_response) self.validate_response( - response, span_name="UNKNOWN /", http_method="UNKNOWN" + response, span_name="HTTP", http_method="_OTHER" ) @mock.patch.dict( @@ -337,34 +517,93 @@ def test_request_attributes(self): SpanAttributes.HTTP_SCHEME: "http", SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", SpanAttributes.HTTP_FLAVOR: "1.0", + SpanAttributes.NET_HOST_NAME: "127.0.0.1", + }, + ) + + def test_request_attributes_new_semconv(self): + self.environ["QUERY_STRING"] = "foo=bar" + self.environ["REQUEST_URI"] = "http://127.0.0.1/?foo=bar" + + attrs = otel_wsgi.collect_request_attributes( + self.environ, + _HTTPStabilityMode.HTTP, + ) + self.assertDictEqual( + attrs, + { + HTTP_REQUEST_METHOD: "GET", + SERVER_ADDRESS: "127.0.0.1", + SERVER_PORT: 80, + NETWORK_PROTOCOL_VERSION: "1.0", + URL_PATH: "/", + URL_QUERY: "foo=bar", + URL_SCHEME: "http", }, ) - def validate_url(self, expected_url, raw=False, has_host=True): + def validate_url( + self, + expected_url, + raw=False, + has_host=True, + old_semconv=True, + new_semconv=False, + ): parts = urlsplit(expected_url) - expected = { + expected_old = { SpanAttributes.HTTP_SCHEME: parts.scheme, SpanAttributes.NET_HOST_PORT: parts.port or (80 if parts.scheme == "http" else 443), SpanAttributes.HTTP_SERVER_NAME: parts.hostname, # Not true in the general case, but for all tests. } - if raw: - expected[SpanAttributes.HTTP_TARGET] = expected_url.split( - parts.netloc, 1 - )[1] - else: - expected[SpanAttributes.HTTP_URL] = expected_url - if has_host: - expected[SpanAttributes.HTTP_HOST] = parts.hostname + expected_new = { + SERVER_PORT: parts.port or (80 if parts.scheme == "http" else 443), + SERVER_ADDRESS: parts.hostname, + URL_PATH: parts.path, + URL_QUERY: parts.query, + } + if old_semconv: + if raw: + expected_old[SpanAttributes.HTTP_TARGET] = expected_url.split( + parts.netloc, 1 + )[1] + else: + expected_old[SpanAttributes.HTTP_URL] = expected_url + if has_host: + expected_old[SpanAttributes.HTTP_HOST] = parts.hostname + if new_semconv: + if raw: + expected_new[URL_PATH] = expected_url.split(parts.path, 1)[1] + if parts.query: + expected_new[URL_QUERY] = expected_url.split( + parts.query, 1 + )[1] + else: + expected_new[URL_FULL] = expected_url + if has_host: + expected_new[SERVER_ADDRESS] = parts.hostname attrs = otel_wsgi.collect_request_attributes(self.environ) self.assertGreaterEqual( - attrs.items(), expected.items(), expected_url + " expected." + attrs.items(), expected_old.items(), expected_url + " expected." ) def test_request_attributes_with_partial_raw_uri(self): - self.environ["RAW_URI"] = "/#top" - self.validate_url("http://127.0.0.1/#top", raw=True) + self.environ["RAW_URI"] = "/?foo=bar/#top" + self.validate_url("http://127.0.0.1/?foo=bar/#top", raw=True) + self.validate_url( + "http://127.0.0.1/?foo=bar/#top", + raw=True, + old_semconv=False, + new_semconv=True, + ) + self.validate_url( + "http://127.0.0.1/?foo=bar/#top", + raw=True, + old_semconv=True, + new_semconv=True, + ) def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( self, @@ -373,18 +612,68 @@ def test_request_attributes_with_partial_raw_uri_and_nonstandard_port( del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "8080" self.validate_url("http://127.0.0.1:8080/?", raw=True, has_host=False) + self.validate_url( + "http://127.0.0.1:8080/?", + raw=True, + has_host=False, + old_semconv=False, + new_semconv=True, + ) + self.validate_url( + "http://127.0.0.1:8080/?", + raw=True, + has_host=False, + old_semconv=True, + new_semconv=True, + ) def test_https_uri_port(self): del self.environ["HTTP_HOST"] self.environ["SERVER_PORT"] = "443" self.environ["wsgi.url_scheme"] = "https" self.validate_url("https://127.0.0.1/", has_host=False) + self.validate_url( + "https://127.0.0.1/", + has_host=False, + old_semconv=False, + new_semconv=True, + ) + self.validate_url( + "https://127.0.0.1/", + has_host=False, + old_semconv=True, + new_semconv=True, + ) self.environ["SERVER_PORT"] = "8080" self.validate_url("https://127.0.0.1:8080/", has_host=False) + self.validate_url( + "https://127.0.0.1:8080/", + has_host=False, + old_semconv=False, + new_semconv=True, + ) + self.validate_url( + "https://127.0.0.1:8080/", + has_host=False, + old_semconv=True, + new_semconv=True, + ) self.environ["SERVER_PORT"] = "80" self.validate_url("https://127.0.0.1:80/", has_host=False) + self.validate_url( + "https://127.0.0.1:80/", + has_host=False, + old_semconv=False, + new_semconv=True, + ) + self.validate_url( + "https://127.0.0.1:80/", + has_host=False, + old_semconv=True, + new_semconv=True, + ) def test_http_uri_port(self): del self.environ["HTTP_HOST"] @@ -426,40 +715,69 @@ def test_request_attributes_with_faux_scheme_relative_raw_uri(self): def test_request_attributes_pathless(self): self.environ["RAW_URI"] = "" - expected = {SpanAttributes.HTTP_TARGET: ""} - self.assertGreaterEqual( - otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), + self.assertIsNone( + otel_wsgi.collect_request_attributes(self.environ).get( + SpanAttributes.HTTP_TARGET + ) ) def test_request_attributes_with_full_request_uri(self): self.environ["HTTP_HOST"] = "127.0.0.1:8080" self.environ["REQUEST_METHOD"] = "CONNECT" - self.environ[ - "REQUEST_URI" - ] = "127.0.0.1:8080" # Might happen in a CONNECT request - expected = { + self.environ["REQUEST_URI"] = ( + "http://docs.python.org:80/3/library/urllib.parse.html?highlight=params#url-parsing" # Might happen in a CONNECT request + ) + expected_old = { SpanAttributes.HTTP_HOST: "127.0.0.1:8080", - SpanAttributes.HTTP_TARGET: "127.0.0.1:8080", + SpanAttributes.HTTP_TARGET: "http://docs.python.org:80/3/library/urllib.parse.html?highlight=params#url-parsing", + } + expected_new = { + URL_PATH: "/3/library/urllib.parse.html", + URL_QUERY: "highlight=params", } self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), - expected.items(), + expected_old.items(), + ) + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes( + self.environ, + _HTTPStabilityMode.HTTP, + ).items(), + expected_new.items(), ) def test_http_user_agent_attribute(self): self.environ["HTTP_USER_AGENT"] = "test-useragent" expected = {SpanAttributes.HTTP_USER_AGENT: "test-useragent"} + expected_new = {SpanAttributes.USER_AGENT_ORIGINAL: "test-useragent"} self.assertGreaterEqual( otel_wsgi.collect_request_attributes(self.environ).items(), expected.items(), ) + self.assertGreaterEqual( + otel_wsgi.collect_request_attributes( + self.environ, + _HTTPStabilityMode.HTTP, + ).items(), + expected_new.items(), + ) def test_response_attributes(self): otel_wsgi.add_response_attributes(self.span, "404 Not Found", {}) + otel_wsgi.add_response_attributes( + self.span, + "404 Not Found", + {}, + sem_conv_opt_in_mode=_HTTPStabilityMode.HTTP, + ) expected = (mock.call(SpanAttributes.HTTP_STATUS_CODE, 404),) - self.assertEqual(self.span.set_attribute.call_count, len(expected)) + expected_new = ( + mock.call(SpanAttributes.HTTP_RESPONSE_STATUS_CODE, 404), + ) + self.assertEqual(self.span.set_attribute.call_count, 2) self.span.set_attribute.assert_has_calls(expected, any_order=True) + self.span.set_attribute.assert_has_calls(expected_new, any_order=True) def test_credential_removal(self): self.environ["HTTP_HOST"] = "username:password@mock" @@ -711,6 +1029,26 @@ def test_custom_response_headers_not_added_in_internal_span(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) + @mock.patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "my-custom-header", + }, + ) + def test_repeat_custom_response_headers_added_in_server_span(self): + app = otel_wsgi.OpenTelemetryMiddleware( + wsgi_with_repeat_custom_response_headers + ) + response = app(self.environ, self.start_response) + self.iterate_response(response) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.response.header.my_custom_header": ( + "my-custom-value-1,my-custom-value-2", + ), + } + self.assertSpanHasAttributes(span, expected) + if __name__ == "__main__": unittest.main() diff --git a/opentelemetry-contrib-instrumentations/LICENSE b/opentelemetry-contrib-instrumentations/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/opentelemetry-contrib-instrumentations/LICENSE +++ b/opentelemetry-contrib-instrumentations/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-contrib-instrumentations/README.rst b/opentelemetry-contrib-instrumentations/README.rst index 4e581c0a63..e0a76806e1 100644 --- a/opentelemetry-contrib-instrumentations/README.rst +++ b/opentelemetry-contrib-instrumentations/README.rst @@ -15,7 +15,7 @@ Installation This package installs all instrumentation packages hosted by the OpenTelemetry Python Contrib repository. -The list of packages can be found (here)[https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation] +The list of packages can be found `here `_. References diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index 2ed7ccec87..74c28f38cf 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -26,60 +26,59 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ - "opentelemetry-instrumentation-aio-pika==0.45b0.dev", - "opentelemetry-instrumentation-aiohttp-client==0.45b0.dev", - "opentelemetry-instrumentation-aiohttp-server==0.45b0.dev", - "opentelemetry-instrumentation-aiopg==0.45b0.dev", - "opentelemetry-instrumentation-asgi==0.45b0.dev", - "opentelemetry-instrumentation-asyncio==0.45b0.dev", - "opentelemetry-instrumentation-asyncpg==0.45b0.dev", - "opentelemetry-instrumentation-aws-lambda==0.45b0.dev", - "opentelemetry-instrumentation-boto==0.45b0.dev", - "opentelemetry-instrumentation-boto3sqs==0.45b0.dev", - "opentelemetry-instrumentation-botocore==0.45b0.dev", - "opentelemetry-instrumentation-cassandra==0.45b0.dev", - "opentelemetry-instrumentation-celery==0.45b0.dev", - "opentelemetry-instrumentation-confluent-kafka==0.45b0.dev", - "opentelemetry-instrumentation-dbapi==0.45b0.dev", - "opentelemetry-instrumentation-django==0.45b0.dev", - "opentelemetry-instrumentation-elasticsearch==0.45b0.dev", - "opentelemetry-instrumentation-falcon==0.45b0.dev", - "opentelemetry-instrumentation-fastapi==0.45b0.dev", - "opentelemetry-instrumentation-flask==0.45b0.dev", - "opentelemetry-instrumentation-grpc==0.45b0.dev", - "opentelemetry-instrumentation-httpx==0.45b0.dev", - "opentelemetry-instrumentation-jinja2==0.45b0.dev", - "opentelemetry-instrumentation-kafka-python==0.45b0.dev", - "opentelemetry-instrumentation-logging==0.45b0.dev", - "opentelemetry-instrumentation-mysql==0.45b0.dev", - "opentelemetry-instrumentation-mysqlclient==0.45b0.dev", - "opentelemetry-instrumentation-pika==0.45b0.dev", - "opentelemetry-instrumentation-psycopg==0.45b0.dev", - "opentelemetry-instrumentation-psycopg2==0.45b0.dev", - "opentelemetry-instrumentation-pymemcache==0.45b0.dev", - "opentelemetry-instrumentation-pymongo==0.45b0.dev", - "opentelemetry-instrumentation-pymysql==0.45b0.dev", - "opentelemetry-instrumentation-pyramid==0.45b0.dev", - "opentelemetry-instrumentation-redis==0.45b0.dev", - "opentelemetry-instrumentation-remoulade==0.45b0.dev", - "opentelemetry-instrumentation-requests==0.45b0.dev", - "opentelemetry-instrumentation-sklearn==0.45b0.dev", - "opentelemetry-instrumentation-sqlalchemy==0.45b0.dev", - "opentelemetry-instrumentation-sqlite3==0.45b0.dev", - "opentelemetry-instrumentation-starlette==0.45b0.dev", - "opentelemetry-instrumentation-system-metrics==0.45b0.dev", - "opentelemetry-instrumentation-tornado==0.45b0.dev", - "opentelemetry-instrumentation-tortoiseorm==0.45b0.dev", - "opentelemetry-instrumentation-urllib==0.45b0.dev", - "opentelemetry-instrumentation-urllib3==0.45b0.dev", - "opentelemetry-instrumentation-wsgi==0.45b0.dev", + "opentelemetry-instrumentation-aio-pika==0.47b0.dev", + "opentelemetry-instrumentation-aiohttp-client==0.47b0.dev", + "opentelemetry-instrumentation-aiohttp-server==0.47b0.dev", + "opentelemetry-instrumentation-aiopg==0.47b0.dev", + "opentelemetry-instrumentation-asgi==0.47b0.dev", + "opentelemetry-instrumentation-asyncio==0.47b0.dev", + "opentelemetry-instrumentation-asyncpg==0.47b0.dev", + "opentelemetry-instrumentation-aws-lambda==0.47b0.dev", + "opentelemetry-instrumentation-boto==0.47b0.dev", + "opentelemetry-instrumentation-boto3sqs==0.47b0.dev", + "opentelemetry-instrumentation-botocore==0.47b0.dev", + "opentelemetry-instrumentation-cassandra==0.47b0.dev", + "opentelemetry-instrumentation-celery==0.47b0.dev", + "opentelemetry-instrumentation-confluent-kafka==0.47b0.dev", + "opentelemetry-instrumentation-dbapi==0.47b0.dev", + "opentelemetry-instrumentation-django==0.47b0.dev", + "opentelemetry-instrumentation-elasticsearch==0.47b0.dev", + "opentelemetry-instrumentation-falcon==0.47b0.dev", + "opentelemetry-instrumentation-fastapi==0.47b0.dev", + "opentelemetry-instrumentation-flask==0.47b0.dev", + "opentelemetry-instrumentation-grpc==0.47b0.dev", + "opentelemetry-instrumentation-httpx==0.47b0.dev", + "opentelemetry-instrumentation-jinja2==0.47b0.dev", + "opentelemetry-instrumentation-kafka-python==0.47b0.dev", + "opentelemetry-instrumentation-logging==0.47b0.dev", + "opentelemetry-instrumentation-mysql==0.47b0.dev", + "opentelemetry-instrumentation-mysqlclient==0.47b0.dev", + "opentelemetry-instrumentation-pika==0.47b0.dev", + "opentelemetry-instrumentation-psycopg==0.47b0.dev", + "opentelemetry-instrumentation-psycopg2==0.47b0.dev", + "opentelemetry-instrumentation-pymemcache==0.47b0.dev", + "opentelemetry-instrumentation-pymongo==0.47b0.dev", + "opentelemetry-instrumentation-pymysql==0.47b0.dev", + "opentelemetry-instrumentation-pyramid==0.47b0.dev", + "opentelemetry-instrumentation-redis==0.47b0.dev", + "opentelemetry-instrumentation-remoulade==0.47b0.dev", + "opentelemetry-instrumentation-requests==0.47b0.dev", + "opentelemetry-instrumentation-sklearn==0.47b0.dev", + "opentelemetry-instrumentation-sqlalchemy==0.47b0.dev", + "opentelemetry-instrumentation-sqlite3==0.47b0.dev", + "opentelemetry-instrumentation-starlette==0.47b0.dev", + "opentelemetry-instrumentation-system-metrics==0.47b0.dev", + "opentelemetry-instrumentation-threading==0.47b0.dev", + "opentelemetry-instrumentation-tornado==0.47b0.dev", + "opentelemetry-instrumentation-tortoiseorm==0.47b0.dev", + "opentelemetry-instrumentation-urllib==0.47b0.dev", + "opentelemetry-instrumentation-urllib3==0.47b0.dev", + "opentelemetry-instrumentation-wsgi==0.47b0.dev", ] -[project.optional-dependencies] -test = [] - [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opentelemetry-contrib-instrumentations" diff --git a/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py b/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py index 2b23bc4994..b6955b0eca 100644 --- a/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py +++ b/opentelemetry-contrib-instrumentations/src/opentelemetry/contrib-instrumentations/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/opentelemetry-distro/pyproject.toml b/opentelemetry-distro/pyproject.toml index 43c646b1d9..17f8e5de43 100644 --- a/opentelemetry-distro/pyproject.toml +++ b/opentelemetry-distro/pyproject.toml @@ -19,19 +19,22 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Typing :: Typed", ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.45b0.dev", + "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-sdk ~= 1.13", ] [project.optional-dependencies] otlp = [ - "opentelemetry-exporter-otlp == 1.24.0.dev", + "opentelemetry-exporter-otlp == 1.26.0.dev", ] -test = [] [project.entry-points.opentelemetry_configurator] configurator = "opentelemetry.distro:OpenTelemetryConfigurator" diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py index 2b23bc4994..b6955b0eca 100644 --- a/opentelemetry-distro/src/opentelemetry/distro/version.py +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/opentelemetry-distro/test-requirements.txt b/opentelemetry-distro/test-requirements.txt new file mode 100644 index 0000000000..b93758ee31 --- /dev/null +++ b/opentelemetry-distro/test-requirements.txt @@ -0,0 +1,15 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e opentelemetry-distro diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index dd8f9c4cc4..ea7b9f08c8 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -14,7 +14,7 @@ # type: ignore import os -from unittest import TestCase +from unittest import TestCase, mock from pkg_resources import DistributionNotFound, require @@ -33,17 +33,10 @@ def test_package_available(self): except DistributionNotFound: self.fail("opentelemetry-distro not installed") + @mock.patch.dict("os.environ", {}, clear=True) def test_default_configuration(self): distro = OpenTelemetryDistro() - self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) - self.assertIsNone(os.environ.get(OTEL_METRICS_EXPORTER)) distro.configure() - self.assertEqual( - "otlp", os.environ.get(OTEL_TRACES_EXPORTER) - ) - self.assertEqual( - "otlp", os.environ.get(OTEL_METRICS_EXPORTER) - ) - self.assertEqual( - "grpc", os.environ.get(OTEL_EXPORTER_OTLP_PROTOCOL) - ) + self.assertEqual("otlp", os.environ.get(OTEL_TRACES_EXPORTER)) + self.assertEqual("otlp", os.environ.get(OTEL_METRICS_EXPORTER)) + self.assertEqual("grpc", os.environ.get(OTEL_EXPORTER_OTLP_PROTOCOL)) diff --git a/opentelemetry-instrumentation/LICENSE b/opentelemetry-instrumentation/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/opentelemetry-instrumentation/LICENSE +++ b/opentelemetry-instrumentation/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/opentelemetry-instrumentation/pyproject.toml b/opentelemetry-instrumentation/pyproject.toml index db389544c9..edaf400419 100644 --- a/opentelemetry-instrumentation/pyproject.toml +++ b/opentelemetry-instrumentation/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.4", @@ -29,9 +30,6 @@ dependencies = [ "wrapt >= 1.0.0, < 2.0.0", ] -[project.optional-dependencies] -test = [] - [project.scripts] opentelemetry-bootstrap = "opentelemetry.instrumentation.bootstrap:run" opentelemetry-instrument = "opentelemetry.instrumentation.auto_instrumentation:run" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index fbfc92cf21..baa06ff99b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -16,13 +16,27 @@ import threading from enum import Enum +from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_REQUEST_METHOD_ORIGINAL, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes - -# TODO: will come through semconv package once updated -_SPAN_ATTRIBUTES_ERROR_TYPE = "error.type" -_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS = "network.peer.address" -_SPAN_ATTRIBUTES_NETWORK_PEER_PORT = "network.peer.port" -_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME = "http.client.request.duration" +from opentelemetry.trace.status import Status, StatusCode _client_duration_attrs_old = [ SpanAttributes.HTTP_STATUS_CODE, @@ -35,23 +49,126 @@ ] _client_duration_attrs_new = [ - _SPAN_ATTRIBUTES_ERROR_TYPE, - SpanAttributes.HTTP_REQUEST_METHOD, - SpanAttributes.HTTP_RESPONSE_STATUS_CODE, - SpanAttributes.NETWORK_PROTOCOL_VERSION, - SpanAttributes.SERVER_ADDRESS, - SpanAttributes.SERVER_PORT, + ERROR_TYPE, + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + SERVER_PORT, # TODO: Support opt-in for scheme in new semconv - # SpanAttributes.URL_SCHEME, + # URL_SCHEME, ] +_server_duration_attrs_old = [ + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, +] + +_server_duration_attrs_new = [ + ERROR_TYPE, + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, + NETWORK_PROTOCOL_VERSION, + URL_SCHEME, +] + +_server_active_requests_count_attrs_old = [ + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, + SpanAttributes.NET_HOST_NAME, + SpanAttributes.NET_HOST_PORT, +] + +_server_active_requests_count_attrs_new = [ + HTTP_REQUEST_METHOD, + URL_SCHEME, +] + +OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN" + + +class _OpenTelemetryStabilitySignalType: + HTTP = "http" + + +class _HTTPStabilityMode(Enum): + # http - emit the new, stable HTTP and networking conventions ONLY + HTTP = "http" + # http/dup - emit both the old and the stable HTTP and networking conventions + HTTP_DUP = "http/dup" + # default - continue emitting old experimental HTTP and networking conventions + DEFAULT = "default" + + +def _report_new(mode): + return mode.name != _HTTPStabilityMode.DEFAULT.name + + +def _report_old(mode): + return mode.name != _HTTPStabilityMode.HTTP.name + + +class _OpenTelemetrySemanticConventionStability: + _initialized = False + _lock = threading.Lock() + _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {} + + @classmethod + def _initialize(cls): + with _OpenTelemetrySemanticConventionStability._lock: + if not _OpenTelemetrySemanticConventionStability._initialized: + # Users can pass in comma delimited string for opt-in options + # Only values for http stability are supported for now + opt_in = os.environ.get(OTEL_SEMCONV_STABILITY_OPT_IN, "") + opt_in_list = [] + if opt_in: + opt_in_list = [s.strip() for s in opt_in.split(",")] + http_opt_in = _HTTPStabilityMode.DEFAULT + if opt_in_list: + # Process http opt-in + # http/dup takes priority over http + if _HTTPStabilityMode.HTTP_DUP.value in opt_in_list: + http_opt_in = _HTTPStabilityMode.HTTP_DUP + elif _HTTPStabilityMode.HTTP.value in opt_in_list: + http_opt_in = _HTTPStabilityMode.HTTP + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ + _OpenTelemetryStabilitySignalType.HTTP + ] = http_opt_in + _OpenTelemetrySemanticConventionStability._initialized = True -def _filter_duration_attrs(attrs, sem_conv_opt_in_mode): + @classmethod + # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.) + def _get_opentelemetry_stability_opt_in_mode( + cls, + signal_type: _OpenTelemetryStabilitySignalType, + ) -> _HTTPStabilityMode: + return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get( + signal_type, _HTTPStabilityMode.DEFAULT + ) + + +def _filter_semconv_duration_attrs( + attrs, + old_attrs, + new_attrs, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, +): filtered_attrs = {} + # duration is two different metrics depending on sem_conv_opt_in_mode, so no DUP attributes allowed_attributes = ( - _client_duration_attrs_new - if sem_conv_opt_in_mode == _OpenTelemetryStabilityMode.HTTP - else _client_duration_attrs_old + new_attrs + if sem_conv_opt_in_mode == _HTTPStabilityMode.HTTP + else old_attrs ) for key, val in attrs.items(): if key in allowed_attributes: @@ -59,6 +176,24 @@ def _filter_duration_attrs(attrs, sem_conv_opt_in_mode): return filtered_attrs +def _filter_semconv_active_request_count_attr( + attrs, + old_attrs, + new_attrs, + sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, +): + filtered_attrs = {} + if _report_old(sem_conv_opt_in_mode): + for key, val in attrs.items(): + if key in old_attrs: + filtered_attrs[key] = val + if _report_new(sem_conv_opt_in_mode): + for key, val in attrs.items(): + if key in new_attrs: + filtered_attrs[key] = val + return filtered_attrs + + def set_string_attribute(result, key, value): if value: result[key] = value @@ -78,140 +213,173 @@ def _set_http_method(result, original, normalized, sem_conv_opt_in_mode): # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes # Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized. if original != normalized and _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.HTTP_REQUEST_METHOD_ORIGINAL, original - ) + set_string_attribute(result, HTTP_REQUEST_METHOD_ORIGINAL, original) if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_METHOD, normalized) if _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.HTTP_REQUEST_METHOD, normalized - ) + set_string_attribute(result, HTTP_REQUEST_METHOD, normalized) + + +def _set_http_status_code(result, code, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code) + if _report_new(sem_conv_opt_in_mode): + set_int_attribute(result, HTTP_RESPONSE_STATUS_CODE, code) def _set_http_url(result, url, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_URL, url) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.URL_FULL, url) + set_string_attribute(result, URL_FULL, url) def _set_http_scheme(result, scheme, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.HTTP_SCHEME, scheme) - # TODO: Support opt-in for scheme in new semconv - # if _report_new(sem_conv_opt_in_mode): - # set_string_attribute(result, SpanAttributes.URL_SCHEME, scheme) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, URL_SCHEME, scheme) -def _set_http_hostname(result, hostname, sem_conv_opt_in_mode): +def _set_http_host(result, host, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.HTTP_HOST, hostname) + set_string_attribute(result, SpanAttributes.HTTP_HOST, host) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, hostname) + set_string_attribute(result, SERVER_ADDRESS, host) + + +# Client -def _set_http_net_peer_name(result, peer_name, sem_conv_opt_in_mode): +def _set_http_net_peer_name_client(result, peer_name, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, SpanAttributes.NET_PEER_NAME, peer_name) if _report_new(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, peer_name) + set_string_attribute(result, SERVER_ADDRESS, peer_name) -def _set_http_port(result, port, sem_conv_opt_in_mode): +def _set_http_peer_port_client(result, port, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port) if _report_new(sem_conv_opt_in_mode): - set_int_attribute(result, SpanAttributes.SERVER_PORT, port) + set_int_attribute(result, SERVER_PORT, port) -def _set_http_status_code(result, code, sem_conv_opt_in_mode): +def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): - set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code) + set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version) if _report_new(sem_conv_opt_in_mode): - set_int_attribute( - result, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, code - ) + set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version) -def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode): +# Server + + +def _set_http_net_host(result, host, sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode): - set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version) + set_string_attribute(result, SpanAttributes.NET_HOST_NAME, host) if _report_new(sem_conv_opt_in_mode): - set_string_attribute( - result, SpanAttributes.NETWORK_PROTOCOL_VERSION, version - ) + set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, host) -_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN" +def _set_http_net_host_port(result, port, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.NET_HOST_PORT, port) + if _report_new(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.SERVER_PORT, port) -class _OpenTelemetryStabilitySignalType: - HTTP = "http" +def _set_http_target(result, target, path, query, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_TARGET, target) + if _report_new(sem_conv_opt_in_mode): + if path: + set_string_attribute(result, SpanAttributes.URL_PATH, path) + if query: + set_string_attribute(result, SpanAttributes.URL_QUERY, query) -class _OpenTelemetryStabilityMode(Enum): - # http - emit the new, stable HTTP and networking conventions ONLY - HTTP = "http" - # http/dup - emit both the old and the stable HTTP and networking conventions - HTTP_DUP = "http/dup" - # default - continue emitting old experimental HTTP and networking conventions - DEFAULT = "default" +def _set_http_peer_ip(result, ip, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.NET_PEER_IP, ip) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.CLIENT_ADDRESS, ip) -def _report_new(mode): - return mode.name != _OpenTelemetryStabilityMode.DEFAULT.name +def _set_http_peer_port_server(result, port, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port) + if _report_new(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.CLIENT_PORT, port) -def _report_old(mode): - return mode.name != _OpenTelemetryStabilityMode.HTTP.name +def _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.HTTP_USER_AGENT, user_agent + ) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.USER_AGENT_ORIGINAL, user_agent + ) -class _OpenTelemetrySemanticConventionStability: - _initialized = False - _lock = threading.Lock() - _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {} +def _set_http_net_peer_name_server(result, name, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.NET_PEER_NAME, name) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.CLIENT_ADDRESS, name) - @classmethod - def _initialize(cls): - with _OpenTelemetrySemanticConventionStability._lock: - if not _OpenTelemetrySemanticConventionStability._initialized: - # Users can pass in comma delimited string for opt-in options - # Only values for http stability are supported for now - opt_in = os.environ.get(_OTEL_SEMCONV_STABILITY_OPT_IN_KEY, "") - opt_in_list = [] - if opt_in: - opt_in_list = [s.strip() for s in opt_in.split(",")] - http_opt_in = _OpenTelemetryStabilityMode.DEFAULT - if opt_in_list: - # Process http opt-in - # http/dup takes priority over http - if ( - _OpenTelemetryStabilityMode.HTTP_DUP.value - in opt_in_list - ): - http_opt_in = _OpenTelemetryStabilityMode.HTTP_DUP - elif _OpenTelemetryStabilityMode.HTTP.value in opt_in_list: - http_opt_in = _OpenTelemetryStabilityMode.HTTP - _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ - _OpenTelemetryStabilitySignalType.HTTP - ] = http_opt_in - _OpenTelemetrySemanticConventionStability._initialized = True - @classmethod - # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.) - def _get_opentelemetry_stability_opt_in_mode( - cls, - signal_type: _OpenTelemetryStabilitySignalType, - ) -> _OpenTelemetryStabilityMode: - return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get( - signal_type, _OpenTelemetryStabilityMode.DEFAULT +def _set_http_flavor_version(result, version, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.NETWORK_PROTOCOL_VERSION, version + ) + + +def _set_status( + span, + metrics_attributes, + status_code_str, + status_code, + sem_conv_opt_in_mode, +): + if status_code < 0: + if _report_new(sem_conv_opt_in_mode): + span.set_attribute(ERROR_TYPE, status_code_str) + metrics_attributes[ERROR_TYPE] = status_code_str + + span.set_status( + Status( + StatusCode.ERROR, + "Non-integer HTTP status: " + status_code_str, + ) ) + else: + status = http_status_to_status_code(status_code, server_span=True) + + if _report_old(sem_conv_opt_in_mode): + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + metrics_attributes[SpanAttributes.HTTP_STATUS_CODE] = status_code + if _report_new(sem_conv_opt_in_mode): + span.set_attribute( + SpanAttributes.HTTP_RESPONSE_STATUS_CODE, status_code + ) + metrics_attributes[SpanAttributes.HTTP_RESPONSE_STATUS_CODE] = ( + status_code + ) + if status == StatusCode.ERROR: + span.set_attribute(ERROR_TYPE, status_code_str) + metrics_attributes[ERROR_TYPE] = status_code_str + span.set_status(Status(status)) # Get schema version based off of opt-in mode -def _get_schema_url(mode: _OpenTelemetryStabilityMode) -> str: - if mode is _OpenTelemetryStabilityMode.DEFAULT: +def _get_schema_url(mode: _HTTPStabilityMode) -> str: + if mode is _HTTPStabilityMode.DEFAULT: return "https://opentelemetry.io/schemas/1.11.0" return SpanAttributes.SCHEMA_URL diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 0c8f0aa3c4..6f86a539b2 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -14,8 +14,14 @@ import argparse import logging -import subprocess import sys +from subprocess import ( + PIPE, + CalledProcessError, + Popen, + SubprocessError, + check_call, +) import pkg_resources @@ -34,7 +40,7 @@ def wrapper(package=None): if package: return func(package) return func() - except subprocess.SubprocessError as exp: + except SubprocessError as exp: cmd = getattr(exp, "cmd", None) if cmd: msg = f'Error calling system command "{" ".join(cmd)}"' @@ -48,18 +54,21 @@ def wrapper(package=None): @_syscall def _sys_pip_install(package): # explicit upgrade strategy to override potential pip config - subprocess.check_call( - [ - sys.executable, - "-m", - "pip", - "install", - "-U", - "--upgrade-strategy", - "only-if-needed", - package, - ] - ) + try: + check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + except CalledProcessError as error: + print(error) def _pip_check(): @@ -70,8 +79,8 @@ def _pip_check(): 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' To not be too restrictive, we'll only check for relevant packages. """ - with subprocess.Popen( - [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + with Popen( + [sys.executable, "-m", "pip", "check"], stdout=PIPE ) as check_pipe: pip_check = check_pipe.communicate()[0].decode() pip_check_lower = pip_check.lower() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 3591581c97..3dfd97e0b2 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -18,179 +18,180 @@ libraries = [ { "library": "aio_pika >= 7.2.0, < 10.0.0", - "instrumentation": "opentelemetry-instrumentation-aio-pika==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-aio-pika==0.47b0.dev", }, { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.47b0.dev", }, { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.47b0.dev", }, { "library": "aiopg >= 0.13.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.47b0.dev", }, { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-asgi==0.47b0.dev", }, { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.47b0.dev", }, { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-boto==0.47b0.dev", }, { "library": "boto3 ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.47b0.dev", }, { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-botocore==0.47b0.dev", }, { "library": "cassandra-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-cassandra==0.47b0.dev", }, { "library": "scylla-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-cassandra==0.47b0.dev", }, { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-celery==0.47b0.dev", }, { - "library": "confluent-kafka >= 1.8.2, <= 2.3.0", - "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.45b0.dev", + "library": "confluent-kafka >= 1.8.2, <= 2.4.0", + "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.47b0.dev", }, { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-django==0.47b0.dev", }, { - "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.45b0.dev", + "library": "elasticsearch >= 6.0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.47b0.dev", }, { "library": "falcon >= 1.4.1, < 3.1.2", - "instrumentation": "opentelemetry-instrumentation-falcon==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-falcon==0.47b0.dev", }, { "library": "fastapi ~= 0.58", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.47b0.dev", }, { "library": "flask >= 1.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-flask==0.47b0.dev", }, { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-grpc==0.47b0.dev", }, { "library": "httpx >= 0.18.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-httpx==0.47b0.dev", }, { "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.47b0.dev", }, { "library": "kafka-python >= 2.0", - "instrumentation": "opentelemetry-instrumentation-kafka-python==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-kafka-python==0.47b0.dev", }, { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-mysql==0.47b0.dev", }, { "library": "mysqlclient < 3", - "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.47b0.dev", }, { "library": "pika >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-pika==0.47b0.dev", }, { "library": "psycopg >= 3.1.0", - "instrumentation": "opentelemetry-instrumentation-psycopg==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-psycopg==0.47b0.dev", }, { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.47b0.dev", }, { "library": "pymemcache >= 1.3.5, < 5", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.47b0.dev", }, { "library": "pymongo >= 3.1, < 5.0", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.47b0.dev", }, { "library": "PyMySQL < 2", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.47b0.dev", }, { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.47b0.dev", }, { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-redis==0.47b0.dev", }, { "library": "remoulade >= 0.50", - "instrumentation": "opentelemetry-instrumentation-remoulade==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-remoulade==0.47b0.dev", }, { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-requests==0.47b0.dev", }, { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.47b0.dev", }, { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.47b0.dev", }, { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-starlette==0.47b0.dev", }, { "library": "psutil >= 5", - "instrumentation": "opentelemetry-instrumentation-system-metrics==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-system-metrics==0.47b0.dev", }, { "library": "tornado >= 5.1.1", - "instrumentation": "opentelemetry-instrumentation-tornado==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-tornado==0.47b0.dev", }, { "library": "tortoise-orm >= 0.17.0", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.47b0.dev", }, { "library": "pydantic >= 1.10.2", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.47b0.dev", }, { "library": "urllib3 >= 1.0.0, < 3.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.45b0.dev", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.47b0.dev", }, ] default_instrumentations = [ - "opentelemetry-instrumentation-asyncio==0.45b0.dev", - "opentelemetry-instrumentation-aws-lambda==0.45b0.dev", - "opentelemetry-instrumentation-dbapi==0.45b0.dev", - "opentelemetry-instrumentation-logging==0.45b0.dev", - "opentelemetry-instrumentation-sqlite3==0.45b0.dev", - "opentelemetry-instrumentation-urllib==0.45b0.dev", - "opentelemetry-instrumentation-wsgi==0.45b0.dev", + "opentelemetry-instrumentation-asyncio==0.47b0.dev", + "opentelemetry-instrumentation-aws-lambda==0.47b0.dev", + "opentelemetry-instrumentation-dbapi==0.47b0.dev", + "opentelemetry-instrumentation-logging==0.47b0.dev", + "opentelemetry-instrumentation-sqlite3==0.47b0.dev", + "opentelemetry-instrumentation-threading==0.47b0.dev", + "opentelemetry-instrumentation-urllib==0.47b0.dev", + "opentelemetry-instrumentation-wsgi==0.47b0.dev", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py index cc1c99c1e0..93646bbb2f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -50,9 +50,10 @@ def configure(self, **kwargs): def load_instrumentor( # pylint: disable=no-self-use self, entry_point: EntryPoint, **kwargs ): - """Takes a collection of instrumentation entry points - and activates them by instantiating and calling instrument() - on each one. + """Takes an instrumentation entry point and activates it by instantiating + and calling instrument() on it. + This is called for each opentelemetry_instrumentor entry point by auto + instrumentation. Distros can override this method to customize the behavior by inspecting each entry point and configuring them in special ways, diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 318aaeaa74..73c000ee9c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -37,6 +37,10 @@ propagator = TraceContextTextMapPropagator() +_SUPPRESS_INSTRUMENTATION_KEY_PLAIN = ( + "suppress_instrumentation" # Set for backward compatibility +) + def extract_attributes_from_object( obj: any, attributes: Sequence[str], existing: Dict[str, str] = None @@ -161,9 +165,10 @@ def _python_path_without_directory(python_path, directory, path_separator): def is_instrumentation_enabled() -> bool: - if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): - return False - return True + return not ( + context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) + or context.get_value(_SUPPRESS_INSTRUMENTATION_KEY_PLAIN) + ) def is_http_instrumentation_enabled() -> bool: @@ -188,7 +193,9 @@ def _suppress_instrumentation(*keys: str) -> Iterable[None]: @contextmanager def suppress_instrumentation() -> Iterable[None]: """Suppress instrumentation within the context.""" - with _suppress_instrumentation(_SUPPRESS_INSTRUMENTATION_KEY): + with _suppress_instrumentation( + _SUPPRESS_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY_PLAIN + ): yield diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index 2b23bc4994..b6955b0eca 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/opentelemetry-instrumentation/test-requirements.txt b/opentelemetry-instrumentation/test-requirements.txt new file mode 100644 index 0000000000..2ef62218b1 --- /dev/null +++ b/opentelemetry-instrumentation/test-requirements.txt @@ -0,0 +1,14 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py index cf6cfdfd37..d3807a1bdb 100644 --- a/opentelemetry-instrumentation/tests/test_utils.py +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -15,10 +15,20 @@ import unittest from http import HTTPStatus +from opentelemetry.context import ( + _SUPPRESS_HTTP_INSTRUMENTATION_KEY, + _SUPPRESS_INSTRUMENTATION_KEY, + get_current, + get_value, +) from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment from opentelemetry.instrumentation.utils import ( _python_path_without_directory, http_status_to_status_code, + is_http_instrumentation_enabled, + is_instrumentation_enabled, + suppress_http_instrumentation, + suppress_instrumentation, ) from opentelemetry.trace import StatusCode @@ -186,3 +196,47 @@ def test_add_sql_comments_without_comments(self): ) self.assertEqual(commented_sql_without_semicolon, "Select 1") + + def test_is_instrumentation_enabled_by_default(self): + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_instrumentation(self): + with suppress_instrumentation(): + self.assertFalse(is_instrumentation_enabled()) + self.assertFalse(is_http_instrumentation_enabled()) + + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_http_instrumentation(self): + with suppress_http_instrumentation(): + self.assertFalse(is_http_instrumentation_enabled()) + self.assertTrue(is_instrumentation_enabled()) + + self.assertTrue(is_instrumentation_enabled()) + self.assertTrue(is_http_instrumentation_enabled()) + + def test_suppress_instrumentation_key(self): + self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertIsNone(get_value("suppress_instrumentation")) + + with suppress_instrumentation(): + ctx = get_current() + self.assertIn(_SUPPRESS_INSTRUMENTATION_KEY, ctx) + self.assertIn("suppress_instrumentation", ctx) + self.assertTrue(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertTrue(get_value("suppress_instrumentation")) + + self.assertIsNone(get_value(_SUPPRESS_INSTRUMENTATION_KEY)) + self.assertIsNone(get_value("suppress_instrumentation")) + + def test_suppress_http_instrumentation_key(self): + self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) + + with suppress_http_instrumentation(): + ctx = get_current() + self.assertIn(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, ctx) + self.assertTrue(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) + + self.assertIsNone(get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)) diff --git a/processor/opentelemetry-processor-baggage/LICENSE b/processor/opentelemetry-processor-baggage/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/processor/opentelemetry-processor-baggage/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/processor/opentelemetry-processor-baggage/README.rst b/processor/opentelemetry-processor-baggage/README.rst new file mode 100644 index 0000000000..6d3a439d8a --- /dev/null +++ b/processor/opentelemetry-processor-baggage/README.rst @@ -0,0 +1,70 @@ +OpenTelemetry Baggage Span Processor +==================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-processor-baggage.svg + :target: https://pypi.org/project/opentelemetry-processor-baggage/ + +The BaggageSpanProcessor reads entries stored in Baggage +from the parent context and adds the baggage entries' keys and +values to the span as attributes on span start. + +Installation +------------ + +:: + + pip install opentelemetry-processor-baggage + +Add this span processor to a tracer provider. + +Keys and values added to Baggage will appear on subsequent child +spans for a trace within this service *and* be propagated to external +services in accordance with any configured propagation formats +configured. If the external services also have a Baggage span +processor, the keys and values will appear in those child spans as +well. + +[!WARNING] + +Do not put sensitive information in Baggage. + +To repeat: a consequence of adding data to Baggage is that the keys and +values will appear in all outgoing HTTP headers from the application. + +## Usage + +Add the span processor when configuring the tracer provider. + +To configure the span processor to copy all baggage entries during configuration: + +:: + + from opentelemetry.processor.baggage import BaggageSpanProcessor, ALLOW_ALL_BAGGAGE_KEYS + + tracer_provider = TracerProvider() + tracer_provider.add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)) + + +Alternatively, you can provide a custom baggage key predicate to select which baggage keys you want to copy. + +For example, to only copy baggage entries that start with `my-key`: + +:: + + starts_with_predicate = lambda baggage_key: baggage_key.startswith("my-key") + tracer_provider.add_span_processor(BaggageSpanProcessor(starts_with_predicate)) + + +For example, to only copy baggage entries that match the regex `^key.+`: + +:: + + regex_predicate = lambda baggage_key: baggage_key.startswith("^key.+") + tracer_provider.add_span_processor(BaggageSpanProcessor(regex_predicate)) + + +References +---------- +* `OpenTelemetry Project `_ diff --git a/processor/opentelemetry-processor-baggage/pyproject.toml b/processor/opentelemetry-processor-baggage/pyproject.toml new file mode 100644 index 0000000000..3fa80e1517 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-processor-baggage" +dynamic = ["version"] +description = "OpenTelemetry Baggage Span Processor" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.8" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "opentelemetry-api ~= 1.5", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/processor/opentelemetry-processor-baggage" + +[tool.hatch.version] +path = "src/opentelemetry/processor/baggage/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/__init__.py b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/__init__.py new file mode 100644 index 0000000000..fcff749d64 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/__init__.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error + +from .processor import ALLOW_ALL_BAGGAGE_KEYS, BaggageSpanProcessor +from .version import __version__ + +__all__ = ["ALLOW_ALL_BAGGAGE_KEYS", "BaggageSpanProcessor", "__version__"] diff --git a/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py new file mode 100644 index 0000000000..d14cf3a7e6 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/processor.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Callable, Optional + +from opentelemetry.baggage import get_all as get_all_baggage +from opentelemetry.context import Context +from opentelemetry.sdk.trace.export import SpanProcessor +from opentelemetry.trace import Span + +# A BaggageKeyPredicate is a function that takes a baggage key and returns a boolean +BaggageKeyPredicateT = Callable[[str], bool] + +# A BaggageKeyPredicate that always returns True, allowing all baggage keys to be added to spans +ALLOW_ALL_BAGGAGE_KEYS: BaggageKeyPredicateT = lambda _: True + + +class BaggageSpanProcessor(SpanProcessor): + """ + The BaggageSpanProcessor reads entries stored in Baggage + from the parent context and adds the baggage entries' keys and + values to the span as attributes on span start. + + Add this span processor to a tracer provider. + + Keys and values added to Baggage will appear on subsequent child + spans for a trace within this service *and* be propagated to external + services in accordance with any configured propagation formats + configured. If the external services also have a Baggage span + processor, the keys and values will appear in those child spans as + well. + + ⚠ Warning ⚠️ + + Do not put sensitive information in Baggage. + + To repeat: a consequence of adding data to Baggage is that the keys and + values will appear in all outgoing HTTP headers from the application. + + """ + + def __init__(self, baggage_key_predicate: BaggageKeyPredicateT) -> None: + self._baggage_key_predicate = baggage_key_predicate + + def on_start( + self, span: "Span", parent_context: Optional[Context] = None + ) -> None: + baggage = get_all_baggage(parent_context) + for key, value in baggage.items(): + if self._baggage_key_predicate(key): + span.set_attribute(key, value) diff --git a/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/version.py b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/version.py new file mode 100644 index 0000000000..b6955b0eca --- /dev/null +++ b/processor/opentelemetry-processor-baggage/src/opentelemetry/processor/baggage/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.47b0.dev" diff --git a/processor/opentelemetry-processor-baggage/test-requirements.txt b/processor/opentelemetry-processor-baggage/test-requirements.txt new file mode 100644 index 0000000000..fa7ad3d793 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/test-requirements.txt @@ -0,0 +1,2 @@ + +-e processor/opentelemetry-processor-baggage \ No newline at end of file diff --git a/processor/opentelemetry-processor-baggage/tests/__init__.py b/processor/opentelemetry-processor-baggage/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/processor/opentelemetry-processor-baggage/tests/test_baggage_processor.py b/processor/opentelemetry-processor-baggage/tests/test_baggage_processor.py new file mode 100644 index 0000000000..fb89ef5eb1 --- /dev/null +++ b/processor/opentelemetry-processor-baggage/tests/test_baggage_processor.py @@ -0,0 +1,167 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import unittest + +from opentelemetry.baggage import get_all as get_all_baggage +from opentelemetry.baggage import set_baggage +from opentelemetry.context import attach, detach +from opentelemetry.processor.baggage import ( + ALLOW_ALL_BAGGAGE_KEYS, + BaggageSpanProcessor, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SpanProcessor +from opentelemetry.trace import Span, Tracer + + +class BaggageSpanProcessorTest(unittest.TestCase): + def test_check_the_baggage(self): + self.assertIsInstance( + BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS), SpanProcessor + ) + + def test_set_baggage_attaches_to_child_spans_and_detaches_properly_with_context( + self, + ): + tracer_provider = TracerProvider() + tracer_provider.add_span_processor( + BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS) + ) + + # tracer has no baggage to start + tracer = tracer_provider.get_tracer("my-tracer") + self.assertIsInstance(tracer, Tracer) + self.assertEqual(get_all_baggage(), {}) + # set baggage in context + ctx = set_baggage("queen", "bee") + with tracer.start_as_current_span( + name="bumble", context=ctx + ) as bumble_span: + # span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # span should have baggage key-value pair in attribute + self.assertEqual(bumble_span._attributes["queen"], "bee") + with tracer.start_as_current_span( + name="child_span", context=ctx + ) as child_span: + self.assertIsInstance(child_span, Span) + # child span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # child span should have baggage key-value pair in attribute + self.assertEqual(child_span._attributes["queen"], "bee") + + def test_baggage_span_processor_with_string_prefix( + self, + ): + tracer_provider = TracerProvider() + tracer_provider.add_span_processor( + BaggageSpanProcessor(self.has_prefix) + ) + + # tracer has no baggage to start + tracer = tracer_provider.get_tracer("my-tracer") + self.assertIsInstance(tracer, Tracer) + self.assertEqual(get_all_baggage(), {}) + # set baggage in context + ctx = set_baggage("queen", "bee") + with tracer.start_as_current_span( + name="bumble", context=ctx + ) as bumble_span: + # span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # span should have baggage key-value pair in attribute + self.assertEqual(bumble_span._attributes["queen"], "bee") + with tracer.start_as_current_span( + name="child_span", context=ctx + ) as child_span: + self.assertIsInstance(child_span, Span) + # child span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # child span should have baggage key-value pair in attribute + self.assertEqual(child_span._attributes["queen"], "bee") + + def test_baggage_span_processor_with_regex( + self, + ): + tracer_provider = TracerProvider() + tracer_provider.add_span_processor( + BaggageSpanProcessor(self.matches_regex) + ) + + # tracer has no baggage to start + tracer = tracer_provider.get_tracer("my-tracer") + self.assertIsInstance(tracer, Tracer) + self.assertEqual(get_all_baggage(), {}) + # set baggage in context + ctx = set_baggage("queen", "bee") + with tracer.start_as_current_span( + name="bumble", context=ctx + ) as bumble_span: + # span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # span should have baggage key-value pair in attribute + self.assertEqual(bumble_span._attributes["queen"], "bee") + with tracer.start_as_current_span( + name="child_span", context=ctx + ) as child_span: + self.assertIsInstance(child_span, Span) + # child span should have baggage key-value pair in context + self.assertEqual(get_all_baggage(ctx), {"queen": "bee"}) + # child span should have baggage key-value pair in attribute + self.assertEqual(child_span._attributes["queen"], "bee") + + def test_set_baggage_attaches_to_child_spans_and_detaches_properly_with_token( + self, + ): + tracer_provider = TracerProvider() + tracer_provider.add_span_processor( + BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS) + ) + + # tracer has no baggage to start + tracer = tracer_provider.get_tracer("my-tracer") + self.assertIsInstance(tracer, Tracer) + self.assertEqual(get_all_baggage(), {}) + # create a context token and set baggage + honey_token = attach(set_baggage("bumble", "bee")) + self.assertEqual(get_all_baggage(), {"bumble": "bee"}) + # in a new span, ensure the baggage is there + with tracer.start_as_current_span("parent") as span: + self.assertEqual(get_all_baggage(), {"bumble": "bee"}) + self.assertEqual(span._attributes["bumble"], "bee") + # create a second context token and set more baggage + moar_token = attach(set_baggage("moar", "bee")) + self.assertEqual( + get_all_baggage(), {"bumble": "bee", "moar": "bee"} + ) + # in a child span, ensure all baggage is there as attributes + with tracer.start_as_current_span("child") as child_span: + self.assertEqual( + get_all_baggage(), {"bumble": "bee", "moar": "bee"} + ) + self.assertEqual(child_span._attributes["bumble"], "bee") + self.assertEqual(child_span._attributes["moar"], "bee") + detach(moar_token) + detach(honey_token) + self.assertEqual(get_all_baggage(), {}) + + @staticmethod + def has_prefix(baggage_key: str) -> bool: + return baggage_key.startswith("que") + + @staticmethod + def matches_regex(baggage_key: str) -> bool: + return re.match(r"que.*", baggage_key) is not None diff --git a/propagator/opentelemetry-propagator-aws-xray/LICENSE b/propagator/opentelemetry-propagator-aws-xray/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/propagator/opentelemetry-propagator-aws-xray/LICENSE +++ b/propagator/opentelemetry-propagator-aws-xray/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml index 4ece31fc53..4a3e22269a 100644 --- a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml +++ b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml @@ -22,16 +22,15 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_propagator] xray = "opentelemetry.propagators.aws:AwsXRayPropagator" +xray_lambda = "opentelemetry.propagators.aws:AwsXRayLambdaPropagator" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/propagator/opentelemetry-propagator-aws-xray" diff --git a/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py b/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py index 4e4a6872ea..4966218211 100644 --- a/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py +++ b/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry/propagators/aws/aws_xray_propagator.py @@ -58,6 +58,7 @@ import logging import typing +from os import environ from opentelemetry import trace from opentelemetry.context import Context @@ -71,6 +72,7 @@ ) TRACE_HEADER_KEY = "X-Amzn-Trace-Id" +AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID" KV_PAIR_DELIMITER = ";" KEY_AND_VALUE_DELIMITER = "=" @@ -324,3 +326,33 @@ def fields(self): """Returns a set with the fields set in `inject`.""" return {TRACE_HEADER_KEY} + + +class AwsXrayLambdaPropagator(AwsXRayPropagator): + """Implementation of the AWS X-Ray Trace Header propagation protocol but + with special handling for Lambda's ``_X_AMZN_TRACE_ID` environment + variable. + """ + + def extract( + self, + carrier: CarrierT, + context: typing.Optional[Context] = None, + getter: Getter[CarrierT] = default_getter, + ) -> Context: + + xray_context = super().extract(carrier, context=context, getter=getter) + + if trace.get_current_span(context=context).get_span_context().is_valid: + return xray_context + + trace_header = environ.get(AWS_TRACE_HEADER_ENV_KEY) + + if trace_header is None: + return xray_context + + return super().extract( + {TRACE_HEADER_KEY: trace_header}, + context=xray_context, + getter=getter, + ) diff --git a/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt b/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt new file mode 100644 index 0000000000..5e15e1c4a1 --- /dev/null +++ b/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +idna==3.7 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +requests==2.32.3 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.2 +wrapt==1.16.0 +zipp==3.17.0 +-e propagator/opentelemetry-propagator-aws-xray diff --git a/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py b/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py new file mode 100644 index 0000000000..a0432d1457 --- /dev/null +++ b/propagator/opentelemetry-propagator-aws-xray/tests/test_aws_xray_lambda_propagator.py @@ -0,0 +1,164 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ +from unittest import TestCase +from unittest.mock import patch + +from requests.structures import CaseInsensitiveDict + +from opentelemetry.context import get_current +from opentelemetry.propagators.aws.aws_xray_propagator import ( + TRACE_HEADER_KEY, + AwsXrayLambdaPropagator, +) +from opentelemetry.propagators.textmap import DefaultGetter +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.trace import ( + Link, + NonRecordingSpan, + SpanContext, + TraceState, + get_current_span, + use_span, +) + + +class AwsXRayLambdaPropagatorTest(TestCase): + + def test_extract_no_environment_variable(self): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual(hex(actual_context.trace_id), "0x0") + self.assertEqual(hex(actual_context.span_id), "0x0") + self.assertFalse( + actual_context.trace_flags.sampled, + ) + self.assertEqual(actual_context.trace_state, TraceState.get_default()) + + def test_extract_no_environment_variable_valid_context(self): + + with use_span(NonRecordingSpan(SpanContext(1, 2, False))): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual(hex(actual_context.trace_id), "0x1") + self.assertEqual(hex(actual_context.span_id), "0x2") + self.assertFalse( + actual_context.trace_flags.sampled, + ) + self.assertEqual( + actual_context.trace_state, TraceState.get_default() + ) + + @patch.dict( + environ, + { + "_X_AMZN_TRACE_ID": ( + "Root=1-00000001-d188f8fa79d48a391a778fa6;" + "Parent=53995c3f42cd8ad8;Sampled=1;Foo=Bar" + ) + }, + ) + def test_extract_from_environment_variable(self): + + actual_context = get_current_span( + AwsXrayLambdaPropagator().extract( + {}, context=get_current(), getter=DefaultGetter() + ) + ).get_span_context() + + self.assertEqual( + hex(actual_context.trace_id), "0x1d188f8fa79d48a391a778fa6" + ) + self.assertEqual(hex(actual_context.span_id), "0x53995c3f42cd8ad8") + self.assertTrue( + actual_context.trace_flags.sampled, + ) + self.assertEqual(actual_context.trace_state, TraceState.get_default()) + + @patch.dict( + environ, + { + "_X_AMZN_TRACE_ID": ( + "Root=1-00000002-240000000000000000000002;" + "Parent=1600000000000002;Sampled=1;Foo=Bar" + ) + }, + ) + def test_add_link_from_environment_variable(self): + + propagator = AwsXrayLambdaPropagator() + + default_getter = DefaultGetter() + + carrier = CaseInsensitiveDict( + { + TRACE_HEADER_KEY: ( + "Root=1-00000001-240000000000000000000001;" + "Parent=1600000000000001;Sampled=1" + ) + } + ) + + extracted_context = propagator.extract( + carrier, context=get_current(), getter=default_getter + ) + + link_context = propagator.extract( + carrier, context=extracted_context, getter=default_getter + ) + + span = ReadableSpan( + "test", parent=extracted_context, links=[Link(link_context)] + ) + + span_parent_context = get_current_span(span.parent).get_span_context() + + self.assertEqual( + hex(span_parent_context.trace_id), "0x2240000000000000000000002" + ) + self.assertEqual( + hex(span_parent_context.span_id), "0x1600000000000002" + ) + self.assertTrue( + span_parent_context.trace_flags.sampled, + ) + self.assertEqual( + span_parent_context.trace_state, TraceState.get_default() + ) + + span_link_context = get_current_span( + span.links[0].context + ).get_span_context() + + self.assertEqual( + hex(span_link_context.trace_id), "0x1240000000000000000000001" + ) + self.assertEqual(hex(span_link_context.span_id), "0x1600000000000001") + self.assertTrue( + span_link_context.trace_flags.sampled, + ) + self.assertEqual( + span_link_context.trace_state, TraceState.get_default() + ) diff --git a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml index 4b73034617..93c7ad6784 100644 --- a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml +++ b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml @@ -22,15 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_propagator] ottrace = "opentelemetry.propagators.ot_trace:OTTracePropagator" diff --git a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/__init__.py b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/__init__.py index c5c3496248..7924fe0b57 100644 --- a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/__init__.py +++ b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/__init__.py @@ -98,9 +98,9 @@ def extract( if not key.startswith(OT_BAGGAGE_PREFIX): continue - baggage[ - key[len(OT_BAGGAGE_PREFIX) :] - ] = _extract_first_element(getter.get(carrier, key)) + baggage[key[len(OT_BAGGAGE_PREFIX) :]] = ( + _extract_first_element(getter.get(carrier, key)) + ) for key, value in baggage.items(): context = set_baggage(key, value, context) diff --git a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py index 2b23bc4994..b6955b0eca 100644 --- a/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py +++ b/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry/propagators/ot_trace/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt new file mode 100644 index 0000000000..59c30eabf4 --- /dev/null +++ b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt @@ -0,0 +1,14 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e propagator/opentelemetry-propagator-ot-trace diff --git a/resource/opentelemetry-resource-detector-azure/CHANGELOG.md b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md new file mode 100644 index 0000000000..5e16c83d63 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +- Ensure consistently use of suppress_instrumentation utils + ([#2590](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2590)) + +## Version 0.1.5 (2024-05-16) + +- Ignore vm detector if already in other rps + ([#2456](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2456)) +- Implement functions resource detector + ([#2523](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2523)) + +## Version 0.1.4 (2024-04-05) + +- Fix windows tests/suppress instrumentation for urllib call + ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) + +## Version 0.1.3 (2024-01-25) + +- Change meta data service timeout to 200ms + ([#2387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2387)) + +## Version 0.1.1 (2024-01-10) + +- Initial CHANGELOG.md entry diff --git a/resource/opentelemetry-resource-detector-azure/LICENSE b/resource/opentelemetry-resource-detector-azure/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/resource/opentelemetry-resource-detector-azure/LICENSE +++ b/resource/opentelemetry-resource-detector-azure/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/resource/opentelemetry-resource-detector-azure/README.rst b/resource/opentelemetry-resource-detector-azure/README.rst index 6a376534ad..19a0f97f32 100644 --- a/resource/opentelemetry-resource-detector-azure/README.rst +++ b/resource/opentelemetry-resource-detector-azure/README.rst @@ -9,6 +9,7 @@ OpenTelemetry Resource detectors for Azure This library contains OpenTelemetry `Resource Detectors `_ for the following Azure resources: * `Azure App Service `_ * `Azure Virtual Machines `_ + * `Azure Functions (Experimental) `_ Installation ------------ @@ -60,7 +61,7 @@ The Azure App Service Resource Detector sets the following Resource Attributes: * ``service.instance.id`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. * ``azure.app.service.stamp`` set to the value of the ``WEBSITE_HOME_STAMPNAME`` environment variable. -The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: +The Azure VM Resource Detector sets the following Resource Attributes according to the response from the `Azure Metadata Service `_: * ``azure.vm.scaleset.name`` set to the value of the ``vmScaleSetName`` field. * ``azure.vm.sku`` set to the value of the ``sku`` field. * ``cloud.platform`` set to the value of the ``azure_vm``. @@ -74,6 +75,16 @@ The Azure VM Resource Detector sets the following Resource Attributes according * ``os.version`` set to the value of the ``version`` field. * ``service.instance.id`` set to the value of the ``vmId`` field. +The Azure Functions Resource Detector is currently experimental. It sets the following Resource Attributes: + * ``service.name`` set to the value of the ``WEBSITE_SITE_NAME`` environment variable. + * ``process.id`` set to the process ID collected from the running process. + * ``cloud.platform`` set to ``azure_functions``. + * ``cloud.provider`` set to ``azure``. + * ``cloud.resource_id`` set using the ``WEBSITE_RESOURCE_GROUP``, ``WEBSITE_OWNER_NAME``, and ``WEBSITE_SITE_NAME`` environment variables. + * ``cloud.region`` set to the value of the ``REGION_NAME`` environment variable. + * ``faas.instance`` set to the value of the ``WEBSITE_INSTANCE_ID`` environment variable. + * ``faas.max_memory`` set to the value of the ``WEBSITE_MEMORY_LIMIT_MB`` environment variable. + For more information, see the `Semantic Conventions for Cloud Resource Attributes `_. References diff --git a/resource/opentelemetry-resource-detector-azure/pyproject.toml b/resource/opentelemetry-resource-detector-azure/pyproject.toml index ea4be96948..f86f1f097b 100644 --- a/resource/opentelemetry-resource-detector-azure/pyproject.toml +++ b/resource/opentelemetry-resource-detector-azure/pyproject.toml @@ -22,16 +22,15 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-sdk ~= 1.21", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_resource_detector] azure_app_service = "opentelemetry.resource.detector.azure.app_service:AzureAppServiceResourceDetector" +azure_functions = "opentelemetry.resource.detector.azure.functions:AzureFunctionsResourceDetector" azure_vm = "opentelemetry.resource.detector.azure.vm:AzureVMResourceDetector" [project.urls] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py new file mode 100644 index 0000000000..628a8ab781 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/__init__.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=import-error + +from .app_service import AzureAppServiceResourceDetector +from .functions import AzureFunctionsResourceDetector +from .version import __version__ +from .vm import AzureVMResourceDetector + +__all__ = [ + "AzureAppServiceResourceDetector", + "AzureFunctionsResourceDetector", + "AzureVMResourceDetector", + "__version__", +] diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py new file mode 100644 index 0000000000..3a6415e0d5 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_constants.py @@ -0,0 +1,74 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry.semconv.resource import ResourceAttributes + +# cSpell:disable + +# Azure Kubernetes + +_AKS_ARM_NAMESPACE_ID = "AKS_ARM_NAMESPACE_ID" + +# AppService + +_AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE = "azure.app.service.stamp" +_REGION_NAME = "REGION_NAME" +_WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME" +_WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME" +_WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID" +_WEBSITE_OWNER_NAME = "WEBSITE_OWNER_NAME" +_WEBSITE_RESOURCE_GROUP = "WEBSITE_RESOURCE_GROUP" +_WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME" +_WEBSITE_SLOT_NAME = "WEBSITE_SLOT_NAME" + +_APP_SERVICE_ATTRIBUTE_ENV_VARS = { + ResourceAttributes.CLOUD_REGION: _REGION_NAME, + ResourceAttributes.DEPLOYMENT_ENVIRONMENT: _WEBSITE_SLOT_NAME, + ResourceAttributes.HOST_ID: _WEBSITE_HOSTNAME, + ResourceAttributes.SERVICE_INSTANCE_ID: _WEBSITE_INSTANCE_ID, + _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE: _WEBSITE_HOME_STAMPNAME, +} + +# Functions + +_FUNCTIONS_WORKER_RUNTIME = "FUNCTIONS_WORKER_RUNTIME" +_WEBSITE_MEMORY_LIMIT_MB = "WEBSITE_MEMORY_LIMIT_MB" + +_FUNCTIONS_ATTRIBUTE_ENV_VARS = { + ResourceAttributes.FAAS_INSTANCE: _WEBSITE_INSTANCE_ID, + ResourceAttributes.FAAS_MAX_MEMORY: _WEBSITE_MEMORY_LIMIT_MB, +} + +# Vm + +_AZURE_VM_METADATA_ENDPOINT = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json" +_AZURE_VM_SCALE_SET_NAME_ATTRIBUTE = "azure.vm.scaleset.name" +_AZURE_VM_SKU_ATTRIBUTE = "azure.vm.sku" + +_EXPECTED_AZURE_AMS_ATTRIBUTES = [ + _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE, + _AZURE_VM_SKU_ATTRIBUTE, + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CLOUD_PROVIDER, + ResourceAttributes.CLOUD_REGION, + ResourceAttributes.CLOUD_RESOURCE_ID, + ResourceAttributes.HOST_ID, + ResourceAttributes.HOST_NAME, + ResourceAttributes.HOST_TYPE, + ResourceAttributes.OS_TYPE, + ResourceAttributes.OS_VERSION, + ResourceAttributes.SERVICE_INSTANCE_ID, +] + +# cSpell:enable diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py new file mode 100644 index 0000000000..62d00c5a6c --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/_utils.py @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from os import environ +from typing import Optional + +from ._constants import ( + _AKS_ARM_NAMESPACE_ID, + _FUNCTIONS_WORKER_RUNTIME, + _WEBSITE_OWNER_NAME, + _WEBSITE_RESOURCE_GROUP, + _WEBSITE_SITE_NAME, +) + + +def _is_on_aks() -> bool: + return environ.get(_AKS_ARM_NAMESPACE_ID) is not None + + +def _is_on_app_service() -> bool: + return environ.get(_WEBSITE_SITE_NAME) is not None + + +def _is_on_functions() -> bool: + return environ.get(_FUNCTIONS_WORKER_RUNTIME) is not None + + +def _can_ignore_vm_detect() -> bool: + return _is_on_aks() or _is_on_app_service() or _is_on_functions() + + +def _get_azure_resource_uri() -> Optional[str]: + website_site_name = environ.get(_WEBSITE_SITE_NAME) + website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) + website_owner_name = environ.get(_WEBSITE_OWNER_NAME) + + subscription_id = website_owner_name + if website_owner_name and "+" in website_owner_name: + subscription_id = website_owner_name[0 : website_owner_name.index("+")] + + if not (website_site_name and website_resource_group and subscription_id): + return None + + return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py index 1e853acc57..41371b8eec 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/app_service.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from os import environ from opentelemetry.sdk.resources import Resource, ResourceDetector @@ -20,25 +21,14 @@ CloudProviderValues, ResourceAttributes, ) +from opentelemetry.resource.detector.azure._utils import _get_azure_resource_uri -_AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE = "azure.app.service.stamp" -_REGION_NAME = "REGION_NAME" -_WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME" -_WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME" -_WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID" -_WEBSITE_OWNER_NAME = "WEBSITE_OWNER_NAME" -_WEBSITE_RESOURCE_GROUP = "WEBSITE_RESOURCE_GROUP" -_WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME" -_WEBSITE_SLOT_NAME = "WEBSITE_SLOT_NAME" - +from ._constants import ( + _APP_SERVICE_ATTRIBUTE_ENV_VARS, + _WEBSITE_SITE_NAME, +) -_APP_SERVICE_ATTRIBUTE_ENV_VARS = { - ResourceAttributes.CLOUD_REGION: _REGION_NAME, - ResourceAttributes.DEPLOYMENT_ENVIRONMENT: _WEBSITE_SLOT_NAME, - ResourceAttributes.HOST_ID: _WEBSITE_HOSTNAME, - ResourceAttributes.SERVICE_INSTANCE_ID: _WEBSITE_INSTANCE_ID, - _AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE: _WEBSITE_HOME_STAMPNAME, -} +from opentelemetry.resource.detector.azure._utils import _is_on_functions class AzureAppServiceResourceDetector(ResourceDetector): @@ -46,36 +36,24 @@ def detect(self) -> Resource: attributes = {} website_site_name = environ.get(_WEBSITE_SITE_NAME) if website_site_name: - attributes[ResourceAttributes.SERVICE_NAME] = website_site_name - attributes[ - ResourceAttributes.CLOUD_PROVIDER - ] = CloudProviderValues.AZURE.value - attributes[ - ResourceAttributes.CLOUD_PLATFORM - ] = CloudPlatformValues.AZURE_APP_SERVICE.value - - azure_resource_uri = _get_azure_resource_uri(website_site_name) + # Functions resource detector takes priority with `service.name` and `cloud.platform` + if not _is_on_functions(): + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_APP_SERVICE.value + ) + attributes[ResourceAttributes.CLOUD_PROVIDER] = ( + CloudProviderValues.AZURE.value + ) + + azure_resource_uri = _get_azure_resource_uri() if azure_resource_uri: - attributes[ - ResourceAttributes.CLOUD_RESOURCE_ID - ] = azure_resource_uri - for (key, env_var) in _APP_SERVICE_ATTRIBUTE_ENV_VARS.items(): + attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( + azure_resource_uri + ) + for key, env_var in _APP_SERVICE_ATTRIBUTE_ENV_VARS.items(): value = environ.get(env_var) if value: attributes[key] = value return Resource(attributes) - - -def _get_azure_resource_uri(website_site_name): - website_resource_group = environ.get(_WEBSITE_RESOURCE_GROUP) - website_owner_name = environ.get(_WEBSITE_OWNER_NAME) - - subscription_id = website_owner_name - if website_owner_name and "+" in website_owner_name: - subscription_id = website_owner_name[0 : website_owner_name.index("+")] - - if not (website_resource_group and subscription_id): - return None - - return f"/subscriptions/{subscription_id}/resourceGroups/{website_resource_group}/providers/Microsoft.Web/sites/{website_site_name}" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py new file mode 100644 index 0000000000..0bf9a10f86 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/functions.py @@ -0,0 +1,68 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import environ, getpid + +from opentelemetry.sdk.resources import Resource, ResourceDetector +from opentelemetry.semconv.resource import ( + CloudPlatformValues, + CloudProviderValues, + ResourceAttributes, +) + +from ._constants import ( + _FUNCTIONS_ATTRIBUTE_ENV_VARS, + _REGION_NAME, + _WEBSITE_SITE_NAME, +) +from opentelemetry.resource.detector.azure._utils import ( + _get_azure_resource_uri, + _is_on_functions, +) + + +class AzureFunctionsResourceDetector(ResourceDetector): + def detect(self) -> Resource: + attributes = {} + if _is_on_functions(): + website_site_name = environ.get(_WEBSITE_SITE_NAME) + if website_site_name: + attributes[ResourceAttributes.SERVICE_NAME] = website_site_name + attributes[ResourceAttributes.PROCESS_PID] = getpid() + attributes[ResourceAttributes.CLOUD_PROVIDER] = ( + CloudProviderValues.AZURE.value + ) + attributes[ResourceAttributes.CLOUD_PLATFORM] = ( + CloudPlatformValues.AZURE_FUNCTIONS.value + ) + cloud_region = environ.get(_REGION_NAME) + if cloud_region: + attributes[ResourceAttributes.CLOUD_REGION] = cloud_region + azure_resource_uri = _get_azure_resource_uri() + if azure_resource_uri: + attributes[ResourceAttributes.CLOUD_RESOURCE_ID] = ( + azure_resource_uri + ) + for key, env_var in _FUNCTIONS_ATTRIBUTE_ENV_VARS.items(): + value = environ.get(env_var) + if value: + if key == ResourceAttributes.FAAS_MAX_MEMORY: + try: + value = int(value) + except ValueError: + continue + attributes[key] = value + + return Resource(attributes) + diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py index 97109f529b..fac29d773f 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.2" +__version__ = "0.1.5" diff --git a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py index a4ba295ab9..63281a46e5 100644 --- a/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py +++ b/resource/opentelemetry-resource-detector-azure/src/opentelemetry/resource/detector/azure/vm.py @@ -17,12 +17,7 @@ from urllib.error import URLError from urllib.request import Request, urlopen -from opentelemetry.context import ( - _SUPPRESS_INSTRUMENTATION_KEY, - attach, - detach, - set_value, -) +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, @@ -30,40 +25,30 @@ ResourceAttributes, ) -_AZURE_VM_METADATA_ENDPOINT = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json" -_AZURE_VM_SCALE_SET_NAME_ATTRIBUTE = "azure.vm.scaleset.name" -_AZURE_VM_SKU_ATTRIBUTE = "azure.vm.sku" -_logger = getLogger(__name__) - -EXPECTED_AZURE_AMS_ATTRIBUTES = [ +from ._constants import ( + _AZURE_VM_METADATA_ENDPOINT, _AZURE_VM_SCALE_SET_NAME_ATTRIBUTE, _AZURE_VM_SKU_ATTRIBUTE, - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CLOUD_PROVIDER, - ResourceAttributes.CLOUD_REGION, - ResourceAttributes.CLOUD_RESOURCE_ID, - ResourceAttributes.HOST_ID, - ResourceAttributes.HOST_NAME, - ResourceAttributes.HOST_TYPE, - ResourceAttributes.OS_TYPE, - ResourceAttributes.OS_VERSION, - ResourceAttributes.SERVICE_INSTANCE_ID, -] + _EXPECTED_AZURE_AMS_ATTRIBUTES, +) +from ._utils import _can_ignore_vm_detect + +_logger = getLogger(__name__) class AzureVMResourceDetector(ResourceDetector): # pylint: disable=no-self-use def detect(self) -> "Resource": attributes = {} - token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - metadata_json = _get_azure_vm_metadata() - if not metadata_json: - return Resource(attributes) - for attribute_key in EXPECTED_AZURE_AMS_ATTRIBUTES: - attributes[attribute_key] = _get_attribute_from_metadata( - metadata_json, attribute_key - ) - detach(token) + if not _can_ignore_vm_detect(): + with suppress_instrumentation(): + metadata_json = _get_azure_vm_metadata() + if not metadata_json: + return Resource(attributes) + for attribute_key in _EXPECTED_AZURE_AMS_ATTRIBUTES: + attributes[attribute_key] = _get_attribute_from_metadata( + metadata_json, attribute_key + ) return Resource(attributes) @@ -71,10 +56,8 @@ def _get_azure_vm_metadata(): request = Request(_AZURE_VM_METADATA_ENDPOINT) request.add_header("Metadata", "True") try: - # TODO: Changed to 4s to fit into OTel SDK's 5 second timeout. - # Lengthen or allow user input if issue is resolved. - # See https://github.com/open-telemetry/opentelemetry-python/issues/3644 - with urlopen(request, timeout=4) as response: + # VM metadata service should not take more than 200ms on success case + with urlopen(request, timeout=0.2) as response: return loads(response.read()) except URLError: # Not on Azure VM diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py index c5d2396dab..6c3d395994 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_app_service.py @@ -68,6 +68,45 @@ def test_on_app_service(self): self.assertEqual( attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME ) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_SLOT_NAME": TEST_WEBSITE_SLOT_NAME, + "WEBSITE_HOSTNAME": TEST_WEBSITE_HOSTNAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_HOME_STAMPNAME": TEST_WEBSITE_HOME_STAMPNAME, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + }, + clear=True, + ) + def test_on_app_service_with_functions(self): + resource = AzureAppServiceResourceDetector().detect() + attributes = resource.attributes + self.assertIsNone(attributes.get("service.name")) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertIsNone(attributes.get("cloud.platform")) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual( + attributes["deployment.environment"], TEST_WEBSITE_SLOT_NAME + ) + self.assertEqual(attributes["host.id"], TEST_WEBSITE_HOSTNAME) + self.assertEqual( + attributes["service.instance.id"], TEST_WEBSITE_INSTANCE_ID + ) + self.assertEqual( + attributes["azure.app.service.stamp"], TEST_WEBSITE_HOME_STAMPNAME + ) @patch.dict( "os.environ", diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_functions.py b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py new file mode 100644 index 0000000000..1f5354c500 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/tests/test_functions.py @@ -0,0 +1,110 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.resource.detector.azure.functions import ( + AzureFunctionsResourceDetector, +) + +TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME" +TEST_REGION_NAME = "TEST_REGION_NAME" +TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID" + +TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP" +TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME" +TEST_WEBSITE_MEMORY_LIMIT_MB = "1024" + + +class TestAzureAppServiceResourceDetector(unittest.TestCase): + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertEqual(attributes["faas.max_memory"], 1024) + + @patch.dict( + "os.environ", + { + "FUNCTIONS_WORKER_RUNTIME": "1", + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": "error", + }, + clear=True, + ) + @patch("opentelemetry.resource.detector.azure.functions.getpid") + def test_on_functions_error_memory(self, pid_mock): + pid_mock.return_value = 1000 + resource = AzureFunctionsResourceDetector().detect() + attributes = resource.attributes + self.assertEqual(attributes["service.name"], TEST_WEBSITE_SITE_NAME) + self.assertEqual(attributes["cloud.provider"], "azure") + self.assertEqual(attributes["cloud.platform"], "azure_functions") + self.assertEqual(attributes["process.pid"], 1000) + + self.assertEqual( + attributes["cloud.resource_id"], + f"/subscriptions/{TEST_WEBSITE_OWNER_NAME}/resourceGroups/{TEST_WEBSITE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/{TEST_WEBSITE_SITE_NAME}", + ) + + self.assertEqual(attributes["cloud.region"], TEST_REGION_NAME) + self.assertEqual(attributes["faas.instance"], TEST_WEBSITE_INSTANCE_ID) + self.assertIsNone(attributes.get("faas.max_memory")) + + @patch.dict( + "os.environ", + { + "WEBSITE_SITE_NAME": TEST_WEBSITE_SITE_NAME, + "REGION_NAME": TEST_REGION_NAME, + "WEBSITE_INSTANCE_ID": TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_RESOURCE_GROUP": TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME": TEST_WEBSITE_OWNER_NAME, + "WEBSITE_MEMORY_LIMIT_MB": TEST_WEBSITE_MEMORY_LIMIT_MB, + }, + clear=True, + ) + def test_off_app_service(self): + resource = AzureFunctionsResourceDetector().detect() + self.assertEqual(resource.attributes, {}) diff --git a/resource/opentelemetry-resource-detector-azure/tests/test_vm.py b/resource/opentelemetry-resource-detector-azure/tests/test_vm.py index 86aa373d3c..56d7d39104 100644 --- a/resource/opentelemetry-resource-detector-azure/tests/test_vm.py +++ b/resource/opentelemetry-resource-detector-azure/tests/test_vm.py @@ -378,3 +378,13 @@ def test_windows(self, mock_urlopen): attributes = AzureVMResourceDetector().detect().attributes for attribute_key, attribute_value in WINDOWS_ATTRIBUTES.items(): self.assertEqual(attributes[attribute_key], attribute_value) + + @patch("opentelemetry.resource.detector.azure.vm._can_ignore_vm_detect") + @patch("opentelemetry.resource.detector.azure.vm.urlopen") + def test_in_another_rp(self, mock_urlopen, detect_mock): + mock_urlopen.return_value.__enter__.return_value.read.return_value = ( + LINUX_JSON + ) + detect_mock.return_value = True + attributes = AzureVMResourceDetector().detect().attributes + self.assertEqual(attributes, {}) diff --git a/resource/opentelemetry-resource-detector-container/LICENSE b/resource/opentelemetry-resource-detector-container/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/resource/opentelemetry-resource-detector-container/LICENSE +++ b/resource/opentelemetry-resource-detector-container/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/resource/opentelemetry-resource-detector-container/pyproject.toml b/resource/opentelemetry-resource-detector-container/pyproject.toml index 9cb6685a2a..3993ecd06c 100644 --- a/resource/opentelemetry-resource-detector-container/pyproject.toml +++ b/resource/opentelemetry-resource-detector-container/pyproject.toml @@ -22,14 +22,12 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-sdk ~= 1.12", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_resource_detector] container = "opentelemetry.resource.detector.container:ContainerResourceDetector" diff --git a/resource/opentelemetry-resource-detector-container/src/opentelemetry/resource/detector/container/version.py b/resource/opentelemetry-resource-detector-container/src/opentelemetry/resource/detector/container/version.py index 2b23bc4994..b6955b0eca 100644 --- a/resource/opentelemetry-resource-detector-container/src/opentelemetry/resource/detector/container/version.py +++ b/resource/opentelemetry-resource-detector-container/src/opentelemetry/resource/detector/container/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/resource/opentelemetry-resource-detector-container/test-requirements.txt b/resource/opentelemetry-resource-detector-container/test-requirements.txt new file mode 100644 index 0000000000..ecacb62b12 --- /dev/null +++ b/resource/opentelemetry-resource-detector-container/test-requirements.txt @@ -0,0 +1,14 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e resource/opentelemetry-resource-detector-container diff --git a/scripts/build.sh b/scripts/build.sh index fa490a6a35..93dc0edce1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,15 +8,15 @@ set -ev # Get the latest versions of packaging tools python3 -m pip install --upgrade pip build setuptools wheel -BASEDIR=$(dirname $(readlink -f $(dirname $0))) +BASEDIR=$(dirname "$(readlink -f "$(dirname $0)")") DISTDIR=dist ( cd $BASEDIR mkdir -p $DISTDIR - rm -rf $DISTDIR/* + rm -rf ${DISTDIR:?}/* - for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ ; do + for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ processor/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ ; do ( echo "building $d" cd "$d" diff --git a/scripts/build_a_package.sh b/scripts/build_a_package.sh index f3baa8a65a..333deccd80 100755 --- a/scripts/build_a_package.sh +++ b/scripts/build_a_package.sh @@ -22,7 +22,7 @@ set -ev if [ -z $GITHUB_REF ]; then echo 'Failed to run script, missing workflow env variable GITHUB_REF.' - exit -1 + exit 1 fi pkg_name_and_version=${GITHUB_REF#refs/tags/*} @@ -40,13 +40,13 @@ cd $basedir distdir=${basedir}/dist mkdir -p $distdir -rm -rf $distdir/* +rm -rf ${distdir:?}/* pyproject_toml_file_path=$(ls **/$pkg_name/pyproject.toml) if [ -z $pyproject_toml_file_path ]; then echo "Error! pyproject.toml not found for $pkg_name, can't build." - exit -1 + exit 1 fi directory_with_package=$(dirname $pyproject_toml_file_path) @@ -61,7 +61,7 @@ pkg_tar_gz_file=${pkg_name}-${pkg_version}.tar.gz if ! [ -f $pkg_tar_gz_file ]; then echo 'Error! Tag version does not match version built using latest package files.' - exit -1 + exit 1 fi # Build a wheel for the source distribution diff --git a/scripts/check_for_valid_readme.py b/scripts/check_for_valid_readme.py index 42446dd741..11b1fa81ac 100644 --- a/scripts/check_for_valid_readme.py +++ b/scripts/check_for_valid_readme.py @@ -1,4 +1,5 @@ """Test script to check given paths for valid README.rst files.""" + import argparse import sys from pathlib import Path diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 4015c6884a..02ce1eed55 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -11,9 +11,6 @@ function cov { ${1} } -PYTHON_VERSION=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') -PYTHON_VERSION_INFO=(${PYTHON_VERSION//./ }) - coverage erase cov instrumentation/opentelemetry-instrumentation-flask diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 570a0cd0e5..2eaabee2c7 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -198,7 +198,6 @@ def setup_instparser(instparser): setup_instparser(instparser) instparser.add_argument("--editable", "-e", action="store_true") - instparser.add_argument("--with-test-deps", action="store_true") instparser.add_argument("--with-dev-deps", action="store_true") instparser.add_argument("--eager-upgrades", action="store_true") @@ -211,7 +210,6 @@ def setup_instparser(instparser): editable=True, with_dev_deps=True, eager_upgrades=True, - with_test_deps=True, ) lintparser = subparsers.add_parser( @@ -467,16 +465,7 @@ def install_args(args): check=True, ) - allfmt = "-e 'file://{}" if args.editable else "'file://{}" - # packages should provide an extra_requires that is named - # 'test', to denote test dependencies. - extras = [] - if args.with_test_deps: - extras.append("test") - if extras: - allfmt += f"[{','.join(extras)}]" - # note the trailing single quote, to close the quote opened above. - allfmt += "'" + allfmt = "-e 'file://{}'" if args.editable else "'file://{}'" execute_args( parse_subargs( @@ -489,6 +478,7 @@ def install_args(args): ), ) ) + if args.with_dev_deps: rootpath = find_projectroot() runsubprocess( diff --git a/scripts/generate_instrumentation_readme.py b/scripts/generate_instrumentation_readme.py index 00045cd73c..79b8a5533d 100755 --- a/scripts/generate_instrumentation_readme.py +++ b/scripts/generate_instrumentation_readme.py @@ -23,8 +23,8 @@ _prefix = "opentelemetry-instrumentation-" header = """ -| Instrumentation | Supported Packages | Metrics support | -| --------------- | ------------------ | --------------- |""" +| Instrumentation | Supported Packages | Metrics support | Semconv status | +| --------------- | ------------------ | --------------- | -------------- |""" def main(): @@ -63,13 +63,17 @@ def main(): instruments = pkg_info["_instruments"] supports_metrics = pkg_info.get("_supports_metrics") + semconv_status = pkg_info.get("_semconv_status") if not instruments: instruments = (name,) + if not semconv_status: + semconv_status = "experimental" + metric_column = "Yes" if supports_metrics else "No" table.append( - f"| [{instrumentation}](./{instrumentation}) | {','.join(instruments)} | {metric_column}" + f"| [{instrumentation}](./{instrumentation}) | {','.join(instruments)} | {metric_column} | {semconv_status}" ) with open( diff --git a/scripts/prepare_release.sh b/scripts/prepare_release.sh index a0bb15f216..66ed3c593e 100755 --- a/scripts/prepare_release.sh +++ b/scripts/prepare_release.sh @@ -1,5 +1,6 @@ #!/bin/bash -# +set -e + # This script: # 1. parses the version number from the branch name # 2. updates version.py files to match that version diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE b/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE index 1ef7dad2c5..261eeb9e9f 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE +++ b/sdk-extension/opentelemetry-sdk-extension-aws/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright The OpenTelemetry Authors + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml index 252f9bb6f7..767bebdd10 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml +++ b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml @@ -22,17 +22,22 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "opentelemetry-sdk ~= 1.12", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_id_generator] xray = "opentelemetry.sdk.extension.aws.trace.aws_xray_id_generator:AwsXRayIdGenerator" +[project.entry-points.opentelemetry_resource_detector] +aws_ec2 = "opentelemetry.sdk.extension.aws.resource.ec2:AwsEc2ResourceDetector" +aws_ecs = "opentelemetry.sdk.extension.aws.resource.ecs:AwsEcsResourceDetector" +aws_eks = "opentelemetry.sdk.extension.aws.resource.eks:AwsEksResourceDetector" +aws_elastic_beanstalk = "opentelemetry.sdk.extension.aws.resource.beanstalk:AwsBeanstalkResourceDetector" +aws_lambda = "opentelemetry.sdk.extension.aws.resource._lambda:AwsLambdaResourceDetector" + [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/sdk-extension/opentelemetry-sdk-extension-aws" diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt b/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt new file mode 100644 index 0000000000..4ed081e748 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt @@ -0,0 +1,14 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +wrapt==1.16.0 +zipp==3.17.0 +-e sdk-extension/opentelemetry-sdk-extension-aws diff --git a/tox.ini b/tox.ini index 7b7b2a0710..e18bdf6a6f 100644 --- a/tox.ini +++ b/tox.ini @@ -7,241 +7,358 @@ envlist = ; for specifying supported Python versions per package. ; opentelemetry-resource-detector-container - py3{8,9,10,11}-test-resource-detector-container + py3{8,9,10,11,12}-test-resource-detector-container pypy3-test-resource-detector-container + lint-resource-detector-container ; opentelemetry-sdk-extension-aws - py3{8,9,10,11}-test-sdkextension-aws - pypy3-test-sdkextension-aws + py3{8,9,10,11,12}-test-sdk-extension-aws + pypy3-test-sdk-extension-aws + lint-sdk-extension-aws ; opentelemetry-distro - py3{8,9,10,11}-test-distro + py3{8,9,10,11,12}-test-distro pypy3-test-distro + lint-distro ; opentelemetry-instrumentation - py3{8,9,10,11}-test-opentelemetry-instrumentation + py3{8,9,10,11,12}-test-opentelemetry-instrumentation pypy3-test-opentelemetry-instrumentation + lint-opentelemetry-instrumentation ; opentelemetry-instrumentation-aiohttp-client - py3{8,9,10,11}-test-instrumentation-aiohttp-client + py3{8,9,10,11,12}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client + lint-instrumentation-aiohttp-client ; opentelemetry-instrumentation-aiohttp-server - py3{6,8,9,10,11}-test-instrumentation-aiohttp-server + py3{8,9,10,11,12}-test-instrumentation-aiohttp-server pypy3-test-instrumentation-aiohttp-server + lint-instrumentation-aiohttp-server ; opentelemetry-instrumentation-aiopg - py3{8,9,10,11}-test-instrumentation-aiopg + py3{8,9,10,11,12}-test-instrumentation-aiopg ; instrumentation-aiopg intentionally excluded from pypy3 + lint-instrumentation-aiopg ; opentelemetry-instrumentation-aws-lambda - py3{8,9,10,11}-test-instrumentation-aws-lambda + py3{8,9,10,11,12}-test-instrumentation-aws-lambda pypy3-test-instrumentation-aws-lambda + lint-instrumentation-aws-lambda ; opentelemetry-instrumentation-botocore - py3{8,9,10,11}-test-instrumentation-botocore + py3{8,9,10,11,12}-test-instrumentation-botocore ; FIXME: see https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1736 ; pypy3-test-instrumentation-botocore + lint-instrumentation-botocore ; opentelemetry-instrumentation-boto3sqs - py3{6,8,9,10,11}-test-instrumentation-boto3sqs - ; FIXME: see https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1736 - ; pypy3-test-instrumentation-boto3sqs + py3{8,9,10,11,12}-test-instrumentation-boto3sqs + pypy3-test-instrumentation-boto3sqs + lint-instrumentation-boto3sqs ; opentelemetry-instrumentation-django ; Only officially supported Python versions are tested for each Django ; major release. Updated list can be found at: ; https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: django~=2.0 + ; 1: django~=3.0 + ; 2: django>=4.0b1,<5.0 backports.zoneinfo==0.2.1 + ; 3: django>=4.0b1,<5.0 + py3{8,9}-test-instrumentation-django-0 + py3{8,9}-test-instrumentation-django-1 py3{8,9}-test-instrumentation-django-2 - py3{8,9,10,11}-test-instrumentation-django-3 - py3{8,9,10,11}-test-instrumentation-django-4 - pypy3-test-instrumentation-django-{2,3} + py3{10,11,12}-test-instrumentation-django-1 + py3{10,11,12}-test-instrumentation-django-3 + pypy3-test-instrumentation-django-{0,1} + lint-instrumentation-django ; opentelemetry-instrumentation-dbapi - py3{8,9,10,11}-test-instrumentation-dbapi + py3{8,9,10,11,12}-test-instrumentation-dbapi pypy3-test-instrumentation-dbapi + lint-instrumentation-dbapi ; opentelemetry-instrumentation-boto py3{8,9,10,11}-test-instrumentation-boto ; FIXME: see https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1736 ; pypy3-test-instrumentation-boto + lint-instrumentation-boto ; opentelemetry-instrumentation-elasticsearch - py3{8,9,10,11}-test-instrumentation-elasticsearch-{2,6} - pypy3-test-instrumentation-elasticsearch-{2,6} - py3{8,9}-test-instrumentation-elasticsearch-5 - pypy3-test-instrumentation-elasticsearch-5 + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: elasticsearch-dsl==6.4.0 elasticsearch==6.8.2 + ; 1: elasticsearch-dsl==7.4.1 elasticsearch==7.17.9 + ; 2: elasticsearch-dsl==8.13.1 elasticsearch==8.13.1 + py3{8,9,10,11,12}-test-instrumentation-elasticsearch-{0,1,2} + pypy3-test-instrumentation-elasticsearch-{0,1,2} + lint-instrumentation-elasticsearch ; opentelemetry-instrumentation-falcon ; py310 does not work with falcon 1 - py3{8,9}-test-instrumentation-falcon-1 - py3{8,9,10,11}-test-instrumentation-falcon-{2,3} - pypy3-test-instrumentation-falcon-{1,2,3} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: falcon ==1.4.1 + ; 1: falcon >=2.0.0,<3.0.0 + ; 2: falcon >=3.0.0,<4.0.0 + py3{8,9}-test-instrumentation-falcon-0 + py3{8,9,10,11,12}-test-instrumentation-falcon-{1,2} + pypy3-test-instrumentation-falcon-{0,1,2} + lint-instrumentation-falcon ; opentelemetry-instrumentation-fastapi - py3{8,9,10,11}-test-instrumentation-fastapi + py3{8,9,10,11,12}-test-instrumentation-fastapi pypy3-test-instrumentation-fastapi + lint-instrumentation-fastapi ; opentelemetry-instrumentation-flask - py3{8,9,10,11}-test-instrumentation-flask-{213,220} - py3{8,9,10,11}-test-instrumentation-flask-{300} - pypy3-test-instrumentation-flask-{213,220} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: Flask ==2.1.3 Werkzeug <3.0.0 + ; 1: Flask ==2.2.0 Werkzeug <3.0.0 + ; 2: Flask >=3.0.0 Werkzeug >=3.0.0 + py3{8,9,10,11,12}-test-instrumentation-flask-{0,1} + py3{8,9,10,11,12}-test-instrumentation-flask-{2} + pypy3-test-instrumentation-flask-{0,1} + lint-instrumentation-flask ; opentelemetry-instrumentation-urllib - py3{8,9,10,11}-test-instrumentation-urllib + py3{8,9,10,11,12}-test-instrumentation-urllib pypy3-test-instrumentation-urllib + lint-instrumentation-urllib ; opentelemetry-instrumentation-urllib3 - py3{8,9,10,11}-test-instrumentation-urllib3v-{1,2} - pypy3-test-instrumentation-urllib3v-{1,2} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: urllib3 >=1.0.0,<2.0.0 + ; 1: urllib3 >=2.0.0,<3.0.0 + py3{8,9,10,11,12}-test-instrumentation-urllib3-{0,1} + pypy3-test-instrumentation-urllib3-{0,1} + lint-instrumentation-urllib3 ; opentelemetry-instrumentation-requests - py3{8,9,10,11}-test-instrumentation-requests + py3{8,9,10,11,12}-test-instrumentation-requests ;pypy3-test-instrumentation-requests + lint-instrumentation-requests ; opentelemetry-instrumentation-starlette - py3{8,9,10,11}-test-instrumentation-starlette + py3{8,9,10,11,12}-test-instrumentation-starlette pypy3-test-instrumentation-starlette + lint-instrumentation-starlette ; opentelemetry-instrumentation-jinja2 - py3{8,9,10,11}-test-instrumentation-jinja2 + py3{8,9,10,11,12}-test-instrumentation-jinja2 pypy3-test-instrumentation-jinja2 + lint-instrumentation-jinja2 ; opentelemetry-instrumentation-logging - py3{8,9,10,11}-test-instrumentation-logging + py3{8,9,10,11,12}-test-instrumentation-logging pypy3-test-instrumentation-logging + lint-instrumentation-logging ; opentelemetry-exporter-richconsole - py3{8,9,10,11}-test-exporter-richconsole + py3{8,9,10,11,12}-test-exporter-richconsole pypy3-test-exporter-richconsole + lint-exporter-richconsole ; opentelemetry-exporter-prometheus-remote-write - py3{6,8,9,10,11}-test-exporter-prometheus-remote-write + py3{8,9,10,11,12}-test-exporter-prometheus-remote-write pypy3-test-exporter-prometheus-remote-write + lint-exporter-prometheus-remote-write ; opentelemetry-instrumentation-mysql - py3{8,9,10,11}-test-instrumentation-mysql + py3{8,9,10,11,12}-test-instrumentation-mysql pypy3-test-instrumentation-mysql + lint-instrumentation-mysql ; opentelemetry-instrumentation-mysqlclient - py3{8,9,10,11}-test-instrumentation-mysqlclient + py3{8,9,10,11,12}-test-instrumentation-mysqlclient pypy3-test-instrumentation-mysqlclient + ; prerequisite: follow the instructions here + ; https://github.com/PyMySQL/mysqlclient#install + ; for your OS to install the required dependencies + lint-instrumentation-mysqlclient ; opentelemetry-instrumentation-psycopg2 - py3{8,9,10,11}-test-instrumentation-psycopg2 + py3{8,9,10,11,12}-test-instrumentation-psycopg2 ; ext-psycopg2 intentionally excluded from pypy3 + lint-instrumentation-psycopg2 ; opentelemetry-instrumentation-psycopg - py3{7,8,9,10,11}-test-instrumentation-psycopg + py3{8,9,10,11,12}-test-instrumentation-psycopg pypy3-test-instrumentation-psycopg + lint-instrumentation-psycopg ; opentelemetry-instrumentation-pymemcache - py3{8,9,10,11}-test-instrumentation-pymemcache-{135,200,300,342,400} - pypy3-test-instrumentation-pymemcache-{135,200,300,342,400} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: pymemcache ==1.3.5 + ; 1: pymemcache >2.0.0,<3.0.0 + ; 2: pymemcache >3.0.0,<3.4.2 + ; 3: pymemcache ==3.4.2 + ; 4: pymemcache ==4.0.0 + py3{8,9,10,11,12}-test-instrumentation-pymemcache-{0,1,2,3,4} + pypy3-test-instrumentation-pymemcache-{0,1,2,3,4} + lint-instrumentation-pymemcache ; opentelemetry-instrumentation-pymongo - py3{8,9,10,11}-test-instrumentation-pymongo + py3{8,9,10,11,12}-test-instrumentation-pymongo pypy3-test-instrumentation-pymongo + lint-instrumentation-pymongo ; opentelemetry-instrumentation-pymysql - py3{8,9,10,11}-test-instrumentation-pymysql + py3{8,9,10,11,12}-test-instrumentation-pymysql pypy3-test-instrumentation-pymysql + lint-instrumentation-pymysql ; opentelemetry-instrumentation-pyramid - py3{8,9,10,11}-test-instrumentation-pyramid + py3{8,9,10,11,12}-test-instrumentation-pyramid pypy3-test-instrumentation-pyramid + lint-instrumentation-pyramid ; opentelemetry-instrumentation-asgi - py3{8,9,10,11}-test-instrumentation-asgi + py3{8,9,10,11,12}-test-instrumentation-asgi pypy3-test-instrumentation-asgi + lint-instrumentation-asgi ; opentelemetry-instrumentation-asyncpg - py3{8,9,10,11}-test-instrumentation-asyncpg + py3{8,9,10,11,12}-test-instrumentation-asyncpg ; ext-asyncpg intentionally excluded from pypy3 + lint-instrumentation-asyncpg ; opentelemetry-instrumentation-sqlite3 - py3{8,9,10,11}-test-instrumentation-sqlite3 + py3{8,9,10,11,12}-test-instrumentation-sqlite3 pypy3-test-instrumentation-sqlite3 + lint-instrumentation-sqlite3 ; opentelemetry-instrumentation-wsgi - py3{8,9,10,11}-test-instrumentation-wsgi + py3{8,9,10,11,12}-test-instrumentation-wsgi pypy3-test-instrumentation-wsgi + lint-instrumentation-wsgi ; opentelemetry-instrumentation-grpc - py3{8,9,10,11}-test-instrumentation-grpc + py3{8,9,10,11,12}-test-instrumentation-grpc pypy3-test-instrumentation-grpc + lint-instrumentation-grpc ; opentelemetry-instrumentation-sqlalchemy - py3{8,9,10,11}-test-instrumentation-sqlalchemy-{14} - pypy3-test-instrumentation-sqlalchemy-{11,14} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: sqlalchemy>=1.1,<1.2 + ; 1: sqlalchemy~=1.4 aiosqlite + py3{8,9,10,11,12}-test-instrumentation-sqlalchemy-{1} + pypy3-test-instrumentation-sqlalchemy-{0,1} + lint-instrumentation-sqlalchemy ; opentelemetry-instrumentation-redis - py3{8,9,10,11}-test-instrumentation-redis + py3{8,9,10,11,12}-test-instrumentation-redis pypy3-test-instrumentation-redis + lint-instrumentation-redis ; opentelemetry-instrumentation-remoulade - py3{8,9,10,11}-test-instrumentation-remoulade + py3{8,9,10,11,12}-test-instrumentation-remoulade ; instrumentation-remoulade intentionally excluded from pypy3 + lint-instrumentation-remoulade ; opentelemetry-instrumentation-celery - py3{8,9,10,11}-test-instrumentation-celery + py3{8,9,10,11,12}-test-instrumentation-celery pypy3-test-instrumentation-celery + lint-instrumentation-celery ; opentelemetry-instrumentation-sklearn py3{8}-test-instrumentation-sklearn + lint-instrumentation-sklearn ; opentelemetry-instrumentation-system-metrics - py3{6,8,9,10,11}-test-instrumentation-system-metrics + py3{8,9,10,11,12}-test-instrumentation-system-metrics pypy3-test-instrumentation-system-metrics + lint-instrumentation-system-metrics + + ; opentelemetry-instrumentation-threading + py3{8,9,10,11,12}-test-instrumentation-threading + pypy3-test-instrumentation-threading + lint-instrumentation-threading ; opentelemetry-instrumentation-tornado - py3{8,9,10,11}-test-instrumentation-tornado + py3{8,9,10,11,12}-test-instrumentation-tornado pypy3-test-instrumentation-tornado + lint-instrumentation-tornado ; opentelemetry-instrumentation-tortoiseorm - py3{8,9,10,11}-test-instrumentation-tortoiseorm + py3{8,9,10,11,12}-test-instrumentation-tortoiseorm pypy3-test-instrumentation-tortoiseorm + lint-instrumentation-tortoiseorm ; opentelemetry-instrumentation-httpx - py3{8,9,10,11}-test-instrumentation-httpx-{18,21} - pypy3-test-instrumentation-httpx-{18,21} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: httpx>=0.18.0,<0.19.0 respx~=0.17.0 + ; 1: httpx>=0.19.0 respx~=0.20.1 + py3{8,9,10,11,12}-test-instrumentation-httpx-{0,1} + pypy3-test-instrumentation-httpx-{0,1} + lint-instrumentation-httpx ; opentelemetry-util-http - py3{8,9,10,11}-test-util-http + py3{8,9,10,11,12}-test-util-http pypy3-test-util-http ; opentelemetry-propagator-aws-xray - py3{8,9,10,11}-test-propagator-aws-xray + py3{8,9,10,11,12}-test-propagator-aws-xray pypy3-test-propagator-aws-xray + lint-propagator-aws-xray ; opentelemetry-propagator-ot-trace - py3{8,9,10,11}-test-propagator-ot-trace + py3{8,9,10,11,12}-test-propagator-ot-trace pypy3-test-propagator-ot-trace + lint-propagator-ot-trace ; opentelemetry-instrumentation-sio-pika - py3{8,9,10,11}-test-instrumentation-sio-pika-{0,1} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: pika>=0.12.0,<1.0.0 + ; 1: pika>=1.0.0 + py3{8,9,10,11,12}-test-instrumentation-sio-pika-{0,1} pypy3-test-instrumentation-sio-pika-{0,1} + lint-instrumentation-sio-pika ; opentelemetry-instrumentation-aio-pika - py3{8,9,10,11}-test-instrumentation-aio-pika-{8,9} - pypy3-test-instrumentation-aio-pika-{8,9} + ; The numbers at the end of the environment names + ; below mean these dependencies are being used: + ; 0: aio_pika==7.2.0 + ; 1: aio_pika==8.3.0 + ; 2: aio_pika==9.0.5 + ; 3: aio_pika==9.4.1 + py3{8,9,10,11,12}-test-instrumentation-aio-pika-{0,1,2,3} + pypy3-test-instrumentation-aio-pika-{0,1,2,3} + lint-instrumentation-aio-pika ; opentelemetry-instrumentation-kafka-python py3{8,9,10,11}-test-instrumentation-kafka-python pypy3-test-instrumentation-kafka-python + lint-instrumentation-kafka-python ; opentelemetry-instrumentation-confluent-kafka - py3{8,9,10,11}-test-instrumentation-confluent-kafka + py3{8,9,10,11,12}-test-instrumentation-confluent-kafka pypy3-test-instrumentation-confluent-kafka + lint-instrumentation-confluent-kafka ; opentelemetry-instrumentation-asyncio - py3{8,9,10,11}-test-instrumentation-asyncio + py3{8,9,10,11,12}-test-instrumentation-asyncio + lint-instrumentation-asyncio ; opentelemetry-instrumentation-cassandra - py3{8,9,10,11}-test-instrumentation-cassandra + py3{8,9,10,11,12}-test-instrumentation-cassandra pypy3-test-instrumentation-cassandra + lint-instrumentation-cassandra + + ; opentelemetry-processor-baggage + py3{8,9,10,11,12}-test-processor-baggage + pypy3-test-processor-baggage + ; requires snappy headers to be available on the system + lint-processor-baggage - lint spellcheck docker-tests docs @@ -251,53 +368,11 @@ envlist = [testenv] deps = -c dev-requirements.txt + lint: -r dev-requirements.txt test: pytest test: pytest-benchmark coverage: pytest coverage: pytest-cov - django-2: django~=2.0 - django-3: django~=3.0 - django-4: django>=4.0b1,<5.0 - elasticsearch-2: elasticsearch-dsl>=2.0,<3.0 - elasticsearch-2: elasticsearch>=2.0,<3.0 - elasticsearch-5: elasticsearch-dsl>=5.0,<6.0 - elasticsearch-5: elasticsearch>=5.0,<6.0 - elasticsearch-6: elasticsearch-dsl>=6.0,<7.0 - elasticsearch-6: elasticsearch>=6.0,<7.0 - ; FIXME: Elasticsearch >=7 causes CI workflow tests to hang, see open-telemetry/opentelemetry-python-contrib#620 - ; elasticsearch-7: elasticsearch-dsl>=7.0,<8.0 - ; elasticsearch-7: elasticsearch>=7.0,<8.0 - ; elasticsearch-8: elasticsearch-dsl>=8.0,<9.0 - ; elasticsearch-8: elasticsearch>=8.0,<9.0 - falcon-1: falcon ==1.4.1 - falcon-2: falcon >=2.0.0,<3.0.0 - falcon-3: falcon >=3.0.0,<4.0.0 - flask-213: Flask ==2.1.3 - flask-213: Werkzeug <3.0.0 - flask-220: Flask ==2.2.0 - flask-220: Werkzeug <3.0.0 - flask-300: Flask >=3.0.0 - flask-300: Werkzeug >=3.0.0 - grpc: pytest-asyncio - sqlalchemy-11: sqlalchemy>=1.1,<1.2 - sqlalchemy-14: aiosqlite - sqlalchemy-14: sqlalchemy~=1.4 - sio-pika-0: pika>=0.12.0,<1.0.0 - sio-pika-1: pika>=1.0.0 - aio-pika-7: aio_pika~=7.2.0 - aio-pika-8: aio_pika>=8.0.0,<9.0.0 - aio-pika-9: aio_pika>=9.0.0,<10.0.0 - pymemcache-135: pymemcache ==1.3.5 - pymemcache-200: pymemcache >2.0.0,<3.0.0 - pymemcache-300: pymemcache >3.0.0,<3.4.2 - pymemcache-342: pymemcache ==3.4.2 - pymemcache-400: pymemcache ==4.0.0 - httpx-18: httpx>=0.18.0,<0.19.0 - httpx-18: respx~=0.17.0 - httpx-21: httpx>=0.19.0 - httpx-21: respx~=0.20.1 - urllib3v-1: urllib3 >=1.0.0,<2.0.0 - urllib3v-2: urllib3 >=2.0.0,<3.0.0 ; FIXME: add coverage testing ; FIXME: add mypy testing @@ -309,189 +384,796 @@ setenv = CORE_REPO=git+https://github.com/open-telemetry/opentelemetry-python.git@{env:CORE_REPO_SHA} commands_pre = -; Install without -e to test the actual installation - py3{8,9,10,11}: 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 opentelemetry-api[test]@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api - test: pip install opentelemetry-semantic-conventions[test]@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions - test: pip install opentelemetry-sdk[test]@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk - test: pip install opentelemetry-test-utils[test]@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils - test: pip install {toxinidir}/opentelemetry-instrumentation - - distro: pip install {toxinidir}/opentelemetry-distro - - celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] - - sio-pika-{0,1}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] - - aio-pika-{8,9}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika[test] - - kafka-python: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python[test] - - confluent-kafka: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka[test] - - grpc: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] - - falcon-{1,2,3},flask-{213,220,300},django-{1,2,3,4},pyramid,tornado,starlette,fastapi,aiohttp,asgi,httpx-{18,21},requests,urllib,urllib3v-{1,2},wsgi: pip install {toxinidir}/util/opentelemetry-util-http - wsgi,falcon-{1,2,3},flask-{213,220,300},django-{1,2,3,4},pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test] - asgi,django-{3,4},starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] - - asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test] - - aws-lambda: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda[test] - - boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] - boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] - - boto3sqs: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs[test] - - falcon-{1,2,3}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test] - - flask-{213,220,300}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] - - urllib: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib[test] - - urllib3v-{1,2}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3[test] - - botocore: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] - - cassandra: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra[test] - - dbapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] - - django-{1,2,3,4}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] - - fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] - - mysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test] - - mysqlclient: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient[test] - - pymemcache-{135,200,300,342,400}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache[test] - - pymongo: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo[test] - - psycopg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg[test] - - psycopg2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2[test] - - pymysql: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql[test] - - pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test] - - sqlite3: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3[test] - - redis: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] - - remoulade: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade[test] - - requests: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] - - starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] - - system-metrics: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] - - tornado: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado[test] - - tortoiseorm: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm[test] - - jinja2: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] - - logging: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-logging[test] - - aio-pika-{8,9}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika[test] - - aiohttp-client: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client[test] - - aiohttp-server: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server[test] - - aiopg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] - - richconsole: pip install flaky {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] - - prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write[test] - - sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] - - sqlalchemy-{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] - - elasticsearch-{2,5,6}: pip install {toxinidir}/opentelemetry-instrumentation[test] {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] - - asyncio: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio[test] - - httpx-{18,21}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test] - - sdkextension-aws: pip install {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] - - resource-detector-container: pip install {toxinidir}/resource/opentelemetry-resource-detector-container[test] - + opentelemetry-instrumentation: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + opentelemetry-instrumentation: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + opentelemetry-instrumentation: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + opentelemetry-instrumentation: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + opentelemetry-instrumentation: pip install -r {toxinidir}/opentelemetry-instrumentation/test-requirements.txt + + distro: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + distro: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + distro: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + distro: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + distro: pip install -r {toxinidir}/opentelemetry-distro/test-requirements.txt + + asgi: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + asgi: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + asgi: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + asgi: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + asgi: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt + + celery: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + celery: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + celery: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + celery: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + py3{8,9}-test-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt + py3{10,11,12}-test-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt + pypy3-test-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt + lint-instrumentation-celery: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt + + sio-pika: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + sio-pika: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + sio-pika: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + sio-pika-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt + sio-pika-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt + lint-instrumentation-sio-pika: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt + + aio-pika: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + aio-pika: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + aio-pika: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + aio-pika-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt + aio-pika-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt + aio-pika-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt + aio-pika-3: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt + lint-instrumentation-aio-pika: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-3.txt + + kafka-python: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + kafka-python: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + kafka-python: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + kafka-python: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt + + confluent-kafka: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + confluent-kafka: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + confluent-kafka: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + confluent-kafka: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + confluent-kafka: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt + + grpc: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + grpc: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + grpc: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + grpc: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + grpc: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt + + wsgi: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + wsgi: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + wsgi: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + wsgi: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + wsgi: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt + + asyncpg: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + asyncpg: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + asyncpg: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + asyncpg: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + asyncpg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt + + aws-lambda: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + aws-lambda: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + aws-lambda: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + aws-lambda: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + aws-lambda: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt + + boto: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + boto: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + boto: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + boto: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + boto: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt + + boto3sqs: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + boto3sqs: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + boto3sqs: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + boto3sqs: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + boto3sqs: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt + + falcon: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + falcon: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + falcon: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + falcon: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + falcon-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt + falcon-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt + falcon-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt + lint-instrumentation-falcon: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt + + flask: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + flask: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + flask: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + flask: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + flask-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt + flask-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt + flask-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt + lint-instrumentation-flask: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt + + urllib: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + urllib: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + urllib: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + urllib: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + urllib: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt + + urllib3: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + urllib3: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + urllib3: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + urllib3: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + urllib3-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt + urllib3-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt + lint-instrumentation-urllib3: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt + + botocore: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + botocore: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + botocore: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + botocore: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + botocore: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt + + cassandra: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + cassandra: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + cassandra: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + cassandra: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + cassandra: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt + + dbapi: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + dbapi: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + dbapi: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + dbapi: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + dbapi: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt + + django: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + django: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + django: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + django: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + py3{8,9}-test-instrumentation-django-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt + py3{8,9}-test-instrumentation-django-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt + py3{8,9}-test-instrumentation-django-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt + py3{10,11,12}-test-instrumentation-django-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt + py3{10,11,12}-test-instrumentation-django-3: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt + pypy3-test-instrumentation-django-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt + pypy3-test-instrumentation-django-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt + lint-instrumentation-django: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt + + fastapi: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + fastapi: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + fastapi: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + fastapi: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + fastapi: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt + + mysql: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + mysql: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + mysql: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + mysql: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + mysql: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt + + mysqlclient: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + mysqlclient: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + mysqlclient: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + mysqlclient: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + mysqlclient: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt + + pymemcache: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + pymemcache: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + pymemcache: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + pymemcache: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + pymemcache-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt + pymemcache-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt + pymemcache-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt + pymemcache-3: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt + pymemcache-4: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt + lint-instrumentation-pymemcache: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt + + pymongo: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + pymongo: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + pymongo: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + pymongo: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + pymongo: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt + + psycopg: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + psycopg: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + psycopg: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + psycopg: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + py3{8,9}-test-instrumentation-psycopg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt + py3{10,11,12}-test-instrumentation-psycopg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt + pypy3-test-instrumentation-psycopg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt + lint-instrumentation-psycopg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt + + psycopg2: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + psycopg2: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + psycopg2: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + psycopg2: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + psycopg2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt + + pymysql: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + pymysql: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + pymysql: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + pymysql: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + pymysql: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt + + pyramid: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + pyramid: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + pyramid: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + pyramid: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + pyramid: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt + + sqlite3: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + sqlite3: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + sqlite3: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + sqlite3: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + sqlite3: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt + + redis: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + redis: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + redis: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + redis: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + redis: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt + + remoulade: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + remoulade: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + remoulade: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + remoulade: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + remoulade: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt + + requests: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + requests: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + requests: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + requests: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + requests: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt + + starlette: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + starlette: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + starlette: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + starlette: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + starlette: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt + + system-metrics: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + system-metrics: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + system-metrics: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + system-metrics: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + system-metrics: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt + + threading: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + threading: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + threading: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + threading: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + threading: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt + + tornado: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + tornado: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + tornado: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + tornado: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + tornado: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt + + tortoiseorm: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + tortoiseorm: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + tortoiseorm: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + tortoiseorm: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + tortoiseorm: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt + + jinja2: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + jinja2: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + jinja2: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + jinja2: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + jinja2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt + + logging: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + logging: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + logging: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + logging: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + logging: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt + + aiohttp-client: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + aiohttp-client: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + aiohttp-client: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + aiohttp-client: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + aiohttp-client: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt + + aiohttp-server: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + aiohttp-server: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + aiohttp-server: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + aiohttp-server: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + aiohttp-server: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt + + aiopg: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + aiopg: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + aiopg: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + aiopg: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + aiopg: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt + + richconsole: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + richconsole: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + richconsole: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + richconsole: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + richconsole: pip install -r {toxinidir}/exporter/opentelemetry-exporter-richconsole/test-requirements.txt + + prometheus: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + prometheus: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + prometheus: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + prometheus: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + prometheus: pip install -r {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt + + sklearn: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + sklearn: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + sklearn: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + sklearn: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + sklearn: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt + + sqlalchemy: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + sqlalchemy: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + sqlalchemy: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + sqlalchemy: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + sqlalchemy-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt + sqlalchemy-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt + lint-instrumentation-sqlalchemy: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt + + elasticsearch: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + elasticsearch: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + elasticsearch: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + elasticsearch: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + elasticsearch-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt + elasticsearch-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt + elasticsearch-2: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt + lint-instrumentation-elasticsearch: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt + + asyncio: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + asyncio: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + asyncio: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + asyncio: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + asyncio: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt + + httpx: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + httpx: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + httpx: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + httpx: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + httpx-0: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt + httpx-1: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt + lint-instrumentation-httpx: pip install -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt + + sdk-extension-aws: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + sdk-extension-aws: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + sdk-extension-aws: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + sdk-extension-aws: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + sdk-extension-aws: pip install -r {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt + + resource-detector-container: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + resource-detector-container: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + resource-detector-container: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + resource-detector-container: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + resource-detector-container: pip install -r {toxinidir}/resource/opentelemetry-resource-detector-container/test-requirements.txt + + propagator-ot-trace: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + propagator-ot-trace: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + propagator-ot-trace: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + propagator-ot-trace: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + propagator-ot-trace: pip install -r {toxinidir}/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt + + propagator-aws-xray: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + propagator-aws-xray: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + propagator-aws-xray: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + propagator-aws-xray: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + propagator-aws-xray: pip install -r {toxinidir}/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt + + processor-baggage: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + processor-baggage: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + processor-baggage: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + processor-baggage: pip install -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt + + http: pip install opentelemetry-api@{env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api + http: pip install opentelemetry-semantic-conventions@{env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions + http: pip install opentelemetry-sdk@{env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk + http: pip install opentelemetry-test-utils@{env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils + http: pip install -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt http: pip install {toxinidir}/util/opentelemetry-util-http -; In order to get a health coverage report, - propagator-ot-trace: pip install {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] - - propagator-aws-xray: pip install requests {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test] +; In order to get a health coverage report, ; we have to install packages in editable mode. coverage: python {toxinidir}/scripts/eachdist.py install --editable commands = test-distro: pytest {toxinidir}/opentelemetry-distro/tests {posargs} + lint-distro: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/opentelemetry-distro + lint-distro: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/opentelemetry-distro + lint-distro: flake8 --config {toxinidir}/.flake8 {toxinidir}/opentelemetry-distro + lint-distro: pylint {toxinidir}/opentelemetry-distro/src/opentelemetry + lint-distro: pylint {toxinidir}/opentelemetry-distro/tests + test-opentelemetry-instrumentation: pytest {toxinidir}/opentelemetry-instrumentation/tests {posargs} + lint-opentelemetry-instrumentation: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/opentelemetry-instrumentation + lint-opentelemetry-instrumentation: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/opentelemetry-instrumentation + lint-opentelemetry-instrumentation: flake8 --config {toxinidir}/.flake8 {toxinidir}/opentelemetry-instrumentation + lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation/src/opentelemetry + lint-opentelemetry-instrumentation: pylint {toxinidir}/opentelemetry-instrumentation/tests + test-instrumentation-aiohttp-client: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests {posargs} + lint-instrumentation-aiohttp-client: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client + lint-instrumentation-aiohttp-client: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client + lint-instrumentation-aiohttp-client: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client + lint-instrumentation-aiohttp-client: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry + lint-instrumentation-aiohttp-client: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests + test-instrumentation-aiohttp-server: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests {posargs} + lint-instrumentation-aiohttp-server: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server + lint-instrumentation-aiohttp-server: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server + lint-instrumentation-aiohttp-server: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server + lint-instrumentation-aiohttp-server: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry + lint-instrumentation-aiohttp-server: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests + test-instrumentation-aiopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/tests {posargs} + lint-instrumentation-aiopg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg + lint-instrumentation-aiopg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg + lint-instrumentation-aiopg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg + lint-instrumentation-aiopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry + lint-instrumentation-aiopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg/tests + test-instrumentation-asgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests {posargs} + lint-instrumentation-asgi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi + lint-instrumentation-asgi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi + lint-instrumentation-asgi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi + lint-instrumentation-asgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry + lint-instrumentation-asgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests + test-instrumentation-asyncpg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests {posargs} + lint-instrumentation-asyncpg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg + lint-instrumentation-asyncpg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg + lint-instrumentation-asyncpg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg + lint-instrumentation-asyncpg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry + lint-instrumentation-asyncpg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests + test-instrumentation-aws-lambda: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/tests {posargs} + lint-instrumentation-aws-lambda: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda + lint-instrumentation-aws-lambda: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda + lint-instrumentation-aws-lambda: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda + lint-instrumentation-aws-lambda: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry + lint-instrumentation-aws-lambda: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda/tests + test-instrumentation-boto: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/tests {posargs} + lint-instrumentation-boto: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-boto + lint-instrumentation-boto: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-boto + lint-instrumentation-boto: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-boto + lint-instrumentation-boto: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry + lint-instrumentation-boto: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto/tests + test-instrumentation-botocore: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/tests {posargs} + lint-instrumentation-botocore: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore + lint-instrumentation-botocore: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore + lint-instrumentation-botocore: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore + lint-instrumentation-botocore: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry + lint-instrumentation-botocore: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore/tests + test-instrumentation-boto3sqs: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/tests {posargs} + lint-instrumentation-boto3sqs: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs + lint-instrumentation-boto3sqs: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs + lint-instrumentation-boto3sqs: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs + lint-instrumentation-boto3sqs: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry + lint-instrumentation-boto3sqs: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs/tests + test-instrumentation-cassandra: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/tests {posargs} + lint-instrumentation-cassandra: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra + lint-instrumentation-cassandra: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra + lint-instrumentation-cassandra: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra + lint-instrumentation-cassandra: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/src/opentelemetry + lint-instrumentation-cassandra: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra/tests + test-instrumentation-celery: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests {posargs} + lint-instrumentation-celery: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-celery + lint-instrumentation-celery: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-celery + lint-instrumentation-celery: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-celery + lint-instrumentation-celery: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/src/opentelemetry + lint-instrumentation-celery: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/tests + test-instrumentation-dbapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests {posargs} - test-instrumentation-django-{1,2,3,4}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests {posargs} - test-instrumentation-elasticsearch-{2,5,6}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/tests {posargs} - test-instrumentation-falcon-{1,2,3}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests {posargs} + lint-instrumentation-dbapi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi + lint-instrumentation-dbapi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi + lint-instrumentation-dbapi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi + lint-instrumentation-dbapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry + lint-instrumentation-dbapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi/tests + + test-instrumentation-django: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests {posargs} + lint-instrumentation-django: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-django + lint-instrumentation-django: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-django + lint-instrumentation-django: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-django + lint-instrumentation-django: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry + lint-instrumentation-django: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-django/tests + + test-instrumentation-elasticsearch: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/tests {posargs} + lint-instrumentation-elasticsearch: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch + lint-instrumentation-elasticsearch: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch + lint-instrumentation-elasticsearch: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch + lint-instrumentation-elasticsearch: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry + lint-instrumentation-elasticsearch: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch/tests + + test-instrumentation-falcon: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests {posargs} + lint-instrumentation-falcon: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon + lint-instrumentation-falcon: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon + lint-instrumentation-falcon: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon + lint-instrumentation-falcon: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry + lint-instrumentation-falcon: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon/tests + test-instrumentation-fastapi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/tests {posargs} - test-instrumentation-flask-{213,220,300}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests {posargs} + lint-instrumentation-fastapi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi + lint-instrumentation-fastapi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi + lint-instrumentation-fastapi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi + lint-instrumentation-fastapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry + lint-instrumentation-fastapi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi/tests + + test-instrumentation-flask: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests {posargs} + lint-instrumentation-flask: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-flask + lint-instrumentation-flask: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-flask + lint-instrumentation-flask: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-flask + lint-instrumentation-flask: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry + lint-instrumentation-flask: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-flask/tests + test-instrumentation-urllib: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/tests {posargs} - test-instrumentation-urllib3v-{1,2}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests {posargs} + lint-instrumentation-urllib: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib + lint-instrumentation-urllib: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib + lint-instrumentation-urllib: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib + lint-instrumentation-urllib: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry + lint-instrumentation-urllib: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib/tests + + test-instrumentation-urllib3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests {posargs} + lint-instrumentation-urllib3: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 + lint-instrumentation-urllib3: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 + lint-instrumentation-urllib3: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3 + lint-instrumentation-urllib3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry + lint-instrumentation-urllib3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3/tests + test-instrumentation-grpc: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/tests {posargs} + lint-instrumentation-grpc: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc + lint-instrumentation-grpc: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc + lint-instrumentation-grpc: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc + lint-instrumentation-grpc: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry + lint-instrumentation-grpc: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc/tests + test-instrumentation-jinja2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/tests {posargs} + lint-instrumentation-jinja2: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 + lint-instrumentation-jinja2: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 + lint-instrumentation-jinja2: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2 + lint-instrumentation-jinja2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry + lint-instrumentation-jinja2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2/tests + test-instrumentation-kafka-python: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/tests {posargs} + lint-instrumentation-kafka-python: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python + lint-instrumentation-kafka-python: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python + lint-instrumentation-kafka-python: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python + lint-instrumentation-kafka-python: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry + lint-instrumentation-kafka-python: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python/tests + test-instrumentation-confluent-kafka: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests {posargs} + lint-instrumentation-confluent-kafka: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka + lint-instrumentation-confluent-kafka: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka + lint-instrumentation-confluent-kafka: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka + lint-instrumentation-confluent-kafka: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/src/opentelemetry + lint-instrumentation-confluent-kafka: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka/tests + test-instrumentation-logging: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/tests {posargs} + lint-instrumentation-logging: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-logging + lint-instrumentation-logging: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-logging + lint-instrumentation-logging: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-logging + lint-instrumentation-logging: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry + lint-instrumentation-logging: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/tests + test-instrumentation-mysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/tests {posargs} + lint-instrumentation-mysql: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql + lint-instrumentation-mysql: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql + lint-instrumentation-mysql: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql + lint-instrumentation-mysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry + lint-instrumentation-mysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql/tests + test-instrumentation-mysqlclient: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/tests {posargs} - test-instrumentation-sio-pika-{0,1}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests {posargs} - test-instrumentation-aio-pika-{8,9}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests {posargs} + lint-instrumentation-mysqlclient: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient + lint-instrumentation-mysqlclient: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient + lint-instrumentation-mysqlclient: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient + lint-instrumentation-mysqlclient: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry + lint-instrumentation-mysqlclient: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient/tests + + test-instrumentation-sio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests {posargs} + lint-instrumentation-sio-pika: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pika + lint-instrumentation-sio-pika: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pika + lint-instrumentation-sio-pika: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pika + lint-instrumentation-sio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry + lint-instrumentation-sio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pika/tests + + test-instrumentation-aio-pika: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests {posargs} + lint-instrumentation-aio-pika: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika + lint-instrumentation-aio-pika: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika + lint-instrumentation-aio-pika: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika + lint-instrumentation-aio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry + lint-instrumentation-aio-pika: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika/tests + test-instrumentation-psycopg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/tests {posargs} + lint-instrumentation-psycopg: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg + lint-instrumentation-psycopg: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg + lint-instrumentation-psycopg: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg + lint-instrumentation-psycopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry + lint-instrumentation-psycopg: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg/tests + test-instrumentation-psycopg2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests {posargs} - test-instrumentation-pymemcache-{135,200,300,342,400}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests {posargs} + lint-instrumentation-psycopg2: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 + lint-instrumentation-psycopg2: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 + lint-instrumentation-psycopg2: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2 + lint-instrumentation-psycopg2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry + lint-instrumentation-psycopg2: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2/tests + + test-instrumentation-pymemcache: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests {posargs} + lint-instrumentation-pymemcache: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache + lint-instrumentation-pymemcache: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache + lint-instrumentation-pymemcache: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache + lint-instrumentation-pymemcache: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry + lint-instrumentation-pymemcache: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache/tests + test-instrumentation-pymongo: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/tests {posargs} + lint-instrumentation-pymongo: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo + lint-instrumentation-pymongo: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo + lint-instrumentation-pymongo: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo + lint-instrumentation-pymongo: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry + lint-instrumentation-pymongo: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo/tests + test-instrumentation-pymysql: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/tests {posargs} + lint-instrumentation-pymysql: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql + lint-instrumentation-pymysql: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql + lint-instrumentation-pymysql: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql + lint-instrumentation-pymysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry + lint-instrumentation-pymysql: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql/tests + test-instrumentation-pyramid: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/tests {posargs} + lint-instrumentation-pyramid: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid + lint-instrumentation-pyramid: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid + lint-instrumentation-pyramid: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid + lint-instrumentation-pyramid: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry + lint-instrumentation-pyramid: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid/tests + test-instrumentation-redis: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests {posargs} + lint-instrumentation-redis: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-redis + lint-instrumentation-redis: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-redis + lint-instrumentation-redis: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-redis + lint-instrumentation-redis: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry + lint-instrumentation-redis: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests + test-instrumentation-remoulade: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests {posargs} + lint-instrumentation-remoulade: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade + lint-instrumentation-remoulade: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade + lint-instrumentation-remoulade: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade + lint-instrumentation-remoulade: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry + lint-instrumentation-remoulade: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests + test-instrumentation-requests: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/tests {posargs} + lint-instrumentation-requests: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-requests + lint-instrumentation-requests: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-requests + lint-instrumentation-requests: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-requests + lint-instrumentation-requests: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry + lint-instrumentation-requests: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-requests/tests + test-instrumentation-sklearn: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/tests {posargs} - test-instrumentation-sqlalchemy-{11,14}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests {posargs} + lint-instrumentation-sklearn: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn + lint-instrumentation-sklearn: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn + lint-instrumentation-sklearn: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn + lint-instrumentation-sklearn: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry + lint-instrumentation-sklearn: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn/tests + + test-instrumentation-sqlalchemy: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests {posargs} + lint-instrumentation-sqlalchemy: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy + lint-instrumentation-sqlalchemy: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy + lint-instrumentation-sqlalchemy: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy + lint-instrumentation-sqlalchemy: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry + lint-instrumentation-sqlalchemy: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests + test-instrumentation-sqlite3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/tests {posargs} + lint-instrumentation-sqlite3: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 + lint-instrumentation-sqlite3: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 + lint-instrumentation-sqlite3: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3 + lint-instrumentation-sqlite3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry + lint-instrumentation-sqlite3: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3/tests + test-instrumentation-starlette: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/tests {posargs} + lint-instrumentation-starlette: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette + lint-instrumentation-starlette: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette + lint-instrumentation-starlette: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette + lint-instrumentation-starlette: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry + lint-instrumentation-starlette: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette/tests + test-instrumentation-system-metrics: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/tests {posargs} + lint-instrumentation-system-metrics: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics + lint-instrumentation-system-metrics: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics + lint-instrumentation-system-metrics: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics + lint-instrumentation-system-metrics: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry + lint-instrumentation-system-metrics: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics/tests + + test-instrumentation-threading: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/tests {posargs} + lint-instrumentation-threading: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-threading + lint-instrumentation-threading: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-threading + lint-instrumentation-threading: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-threading + lint-instrumentation-threading: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/src/opentelemetry + lint-instrumentation-threading: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-threading/tests + test-instrumentation-tornado: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/tests {posargs} + lint-instrumentation-tornado: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado + lint-instrumentation-tornado: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado + lint-instrumentation-tornado: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado + lint-instrumentation-tornado: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry + lint-instrumentation-tornado: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado/tests + test-instrumentation-tortoiseorm: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests {posargs} + lint-instrumentation-tortoiseorm: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm + lint-instrumentation-tortoiseorm: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm + lint-instrumentation-tortoiseorm: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm + lint-instrumentation-tortoiseorm: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry + lint-instrumentation-tortoiseorm: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests + test-instrumentation-wsgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests {posargs} - test-instrumentation-httpx-{18,21}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs} + lint-instrumentation-wsgi: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi + lint-instrumentation-wsgi: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi + lint-instrumentation-wsgi: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi + lint-instrumentation-wsgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry + lint-instrumentation-wsgi: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests + + test-instrumentation-httpx: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs} + lint-instrumentation-httpx: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx + lint-instrumentation-httpx: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx + lint-instrumentation-httpx: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx + lint-instrumentation-httpx: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry + lint-instrumentation-httpx: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests + test-instrumentation-asyncio: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests {posargs} + lint-instrumentation-asyncio: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio + lint-instrumentation-asyncio: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio + lint-instrumentation-asyncio: flake8 --config {toxinidir}/.flake8 {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio + lint-instrumentation-asyncio: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry + lint-instrumentation-asyncio: pylint {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests + test-util-http: pytest {toxinidir}/util/opentelemetry-util-http/tests {posargs} - test-sdkextension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests {posargs} + + test-sdk-extension-aws: pytest {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests {posargs} + lint-sdk-extension-aws: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws + lint-sdk-extension-aws: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws + lint-sdk-extension-aws: flake8 --config {toxinidir}/.flake8 {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws + lint-sdk-extension-aws: pylint {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry + lint-sdk-extension-aws: pylint {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws/tests + test-resource-detector-container: pytest {toxinidir}/resource/opentelemetry-resource-detector-container/tests {posargs} - test-propagator-aws: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests {posargs} + lint-resource-detector-container: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/resource/opentelemetry-resource-detector-container + lint-resource-detector-container: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/resource/opentelemetry-resource-detector-container + lint-resource-detector-container: flake8 --config {toxinidir}/.flake8 {toxinidir}/resource/opentelemetry-resource-detector-container + lint-resource-detector-container: pylint {toxinidir}/resource/opentelemetry-resource-detector-container/src/opentelemetry + lint-resource-detector-container: pylint {toxinidir}/resource/opentelemetry-resource-detector-container/tests + + test-processor-baggage: pytest {toxinidir}/processor/opentelemetry-processor-baggage/tests {posargs} + lint-processor-baggage: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/processor/opentelemetry-processor-baggage + lint-processor-baggage: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/processor/opentelemetry-processor-baggage + lint-processor-baggage: flake8 --config {toxinidir}/.flake8 {toxinidir}/processor/opentelemetry-processor-baggage + lint-processor-baggage: pylint {toxinidir}/processor/opentelemetry-processor-baggage/src/opentelemetry + lint-processor-baggage: pylint {toxinidir}/processor/opentelemetry-processor-baggage/tests + + test-propagator-aws-xray: pytest {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests {posargs} + lint-propagator-aws-xray: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/propagator/opentelemetry-propagator-aws-xray + lint-propagator-aws-xray: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/propagator/opentelemetry-propagator-aws-xray + lint-propagator-aws-xray: flake8 --config {toxinidir}/.flake8 {toxinidir}/propagator/opentelemetry-propagator-aws-xray + lint-propagator-aws-xray: pylint {toxinidir}/propagator/opentelemetry-propagator-aws-xray/src/opentelemetry + lint-propagator-aws-xray: pylint {toxinidir}/propagator/opentelemetry-propagator-aws-xray/tests + test-propagator-ot-trace: pytest {toxinidir}/propagator/opentelemetry-propagator-ot-trace/tests {posargs} + lint-propagator-ot-trace: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/propagator/opentelemetry-propagator-ot-trace + lint-propagator-ot-trace: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/propagator/opentelemetry-propagator-ot-trace + lint-propagator-ot-trace: flake8 --config {toxinidir}/.flake8 {toxinidir}/propagator/opentelemetry-propagator-ot-trace + lint-propagator-ot-trace: pylint {toxinidir}/propagator/opentelemetry-propagator-ot-trace/src/opentelemetry + lint-propagator-ot-trace: pylint {toxinidir}/propagator/opentelemetry-propagator-ot-trace/tests + test-exporter-richconsole: pytest {toxinidir}/exporter/opentelemetry-exporter-richconsole/tests {posargs} + lint-exporter-richconsole: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/exporter/opentelemetry-exporter-richconsole + lint-exporter-richconsole: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/exporter/opentelemetry-exporter-richconsole + lint-exporter-richconsole: flake8 --config {toxinidir}/.flake8 {toxinidir}/exporter/opentelemetry-exporter-richconsole + lint-exporter-richconsole: pylint {toxinidir}/exporter/opentelemetry-exporter-richconsole/src/opentelemetry + lint-exporter-richconsole: pylint {toxinidir}/exporter/opentelemetry-exporter-richconsole/tests + test-exporter-prometheus-remote-write: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests {posargs} + lint-exporter-prometheus-remote-write: black --diff --check --config {toxinidir}/pyproject.toml {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write + lint-exporter-prometheus-remote-write: isort --diff --check-only --settings-path {toxinidir}/.isort.cfg {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write + lint-exporter-prometheus-remote-write: flake8 --config {toxinidir}/.flake8 {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write + lint-exporter-prometheus-remote-write: pylint {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry + lint-exporter-prometheus-remote-write: pylint {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests + coverage: {toxinidir}/scripts/coverage.sh [testenv:docs] @@ -512,115 +1194,105 @@ changedir = docs commands = sphinx-build -E -a -W -b html -T . _build/html -[testenv:spellcheck] +[testenv:lint] basepython: python3 recreate = True deps = - codespell + -r dev-requirements.txt commands = - codespell + black --config {toxinidir}/pyproject.toml {toxinidir} --diff --check + isort --settings-path {toxinidir}/.isort.cfg {toxinidir} --diff --check-only + flake8 --config {toxinidir}/.flake8 {toxinidir} -[testenv:lint] +[testenv:spellcheck] basepython: python3 recreate = True deps = - -r dev-requirements.txt - -commands_pre = - python -m pip install {env:CORE_REPO}\#egg=opentelemetry-api&subdirectory=opentelemetry-api - python -m pip install {env:CORE_REPO}\#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions - python -m pip install {env:CORE_REPO}\#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk - python -m pip install {env:CORE_REPO}\#egg=opentelemetry-test-utils&subdirectory=tests/opentelemetry-test-utils - python -m pip install -e {toxinidir}/util/opentelemetry-util-http[test] - python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-boto3sqs[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-django[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-grpc[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-falcon[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-flask[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-cassandra[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pika[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aio-pika[test] - ; python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-kafka-python[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-confluent-kafka[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-logging[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-urllib3[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymysql[test] - # prerequisite: follow the instructions here https://github.com/PyMySQL/mysqlclient#install - # for your OS to install the required dependencies - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysqlclient[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymongo[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-tortoiseorm[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] - python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio[test] - python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] - # requires snappy headers to be available on the system - python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] - python -m pip install -e {toxinidir}/resource/opentelemetry-resource-detector-container[test] - python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test] - python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] - python -m pip install -e {toxinidir}/opentelemetry-distro[test] + codespell==2.2.6 commands = - python scripts/eachdist.py lint --check-only + codespell [testenv:docker-tests] basepython: python3 deps = - pip >= 20.3.3 - pytest - asyncpg==0.27.0 - docker-compose >= 1.25.2 - mysql-connector-python ~= 8.0 - pymongo >= 3.1, < 5.0 - PyMySQL ~= 0.10.1 + aiopg==1.4.0 + amqp==5.2.0 + asgiref==3.7.2 + async-timeout==4.0.3 + asyncpg==0.29.0 + attrs==23.2.0 + bcrypt==4.1.2 + billiard==4.2.0 + celery==5.3.6 + certifi==2024.2.2 + cffi==1.16.0 + chardet==3.0.4 + click==8.1.7 + click-didyoumean==0.3.0 + click-plugins==1.1.1 + click-repl==0.3.0 + cryptography==42.0.5 + Deprecated==1.2.14 + distro==1.9.0 + dnspython==2.6.1 + docker==5.0.3 + docker-compose==1.29.2 + dockerpty==0.4.1 + docopt==0.6.2 + exceptiongroup==1.2.0 + flaky==3.7.0 + greenlet==3.0.3 + grpcio==1.62.1 + idna==2.10 + importlib-metadata==6.11.0 + iniconfig==2.0.0 + jsonschema==3.2.0 + kombu==5.3.5 + mysql-connector-python==8.3.0 + mysqlclient==2.1.1 + opencensus-proto==0.1.0 + packaging==24.0 + paramiko==3.4.0 + pluggy==1.4.0 + prometheus_client==0.20.0 + prompt-toolkit==3.0.43 + protobuf==3.20.3 # prerequisite: install libpq-dev (debian) or postgresql-devel (rhel), postgresql (mac) # see https://www.psycopg.org/docs/install.html#build-prerequisites # you might have to install additional packages depending on your OS - psycopg ~= 3.1.17 - psycopg2 ~= 2.9.5 - aiopg >= 0.13.0, < 1.3.0 - sqlalchemy ~= 1.4 - redis ~= 4.3 - celery[pytest] >= 4.0, < 6.0 - protobuf~=3.13 - requests==2.25.0 + psycopg==3.1.18 + psycopg2==2.9.9 + psycopg2-binary==2.9.9 + pycparser==2.21 + pymongo==4.6.3 + PyMySQL==0.10.1 + PyNaCl==1.5.0 # prerequisite: install unixodbc - pyodbc~=4.0.30 - flaky==3.7.0 - remoulade>=0.50 - mysqlclient~=2.1.1 - pyyaml==5.3.1 + pyodbc==4.0.39 + pyrsistent==0.20.0 + pytest==8.0.2 + pytest-celery==0.0.0 + python-dateutil==2.9.0.post0 + python-dotenv==0.21.1 + pytz==2024.1 + PyYAML==5.3.1 + redis==5.0.1 + remoulade==3.2.0 + requests==2.25.0 + six==1.16.0 + SQLAlchemy==1.4.52 + texttable==1.7.0 + tomli==2.0.1 + typing_extensions==4.10.0 + tzdata==2024.1 + urllib3==1.26.19 + vine==5.1.0 + wcwidth==0.2.13 + websocket-client==0.59.0 + wrapt==1.16.0 + zipp==3.18.0 changedir = tests/opentelemetry-docker-tests/tests diff --git a/util/opentelemetry-util-http/pyproject.toml b/util/opentelemetry-util-http/pyproject.toml index 88724caf1a..0e632a81b3 100644 --- a/util/opentelemetry-util-http/pyproject.toml +++ b/util/opentelemetry-util-http/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] [project.urls] diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 523f9400b1..f5dacf0fff 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -14,6 +14,7 @@ from __future__ import annotations +from collections.abc import Mapping from os import environ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile @@ -87,32 +88,32 @@ def sanitize_header_value(self, header: str, value: str) -> str: def sanitize_header_values( self, - headers: dict[str, str], + headers: Mapping[str, str | list[str]], header_regexes: list[str], normalize_function: Callable[[str], str], - ) -> dict[str, str]: - values: dict[str, str] = {} + ) -> dict[str, list[str]]: + values: dict[str, list[str]] = {} if header_regexes: header_regexes_compiled = re_compile( - "|".join("^" + i + "$" for i in header_regexes), + "|".join(header_regexes), RE_IGNORECASE, ) - for header_name in list( - filter( - header_regexes_compiled.match, - headers.keys(), - ) - ): - header_values = headers.get(header_name) - if header_values: + for header_name, header_value in headers.items(): + if header_regexes_compiled.fullmatch(header_name): key = normalize_function(header_name.lower()) - values[key] = [ - self.sanitize_header_value( - header=header_name, value=header_values - ) - ] + if isinstance(header_value, str): + values[key] = [ + self.sanitize_header_value( + header_name, header_value + ) + ] + else: + values[key] = [ + self.sanitize_header_value(header_name, value) + for value in header_value + ] return values @@ -166,11 +167,7 @@ def remove_url_credentials(url: str) -> str: parsed = urlparse(url) if all([parsed.scheme, parsed.netloc]): # checks for valid url parsed_url = urlparse(url) - netloc = ( - (":".join(((parsed_url.hostname or ""), str(parsed_url.port)))) - if parsed_url.port - else (parsed_url.hostname or "") - ) + _, _, netloc = parsed.netloc.rpartition("@") return urlunparse( ( parsed_url.scheme, @@ -218,7 +215,7 @@ def sanitize_method(method: Optional[str]) -> Optional[str]: ] ): return method - return "UNKNOWN" + return "_OTHER" def get_custom_headers(env_var: str) -> list[str]: @@ -245,3 +242,10 @@ def _parse_duration_attrs(req_attrs): for key in _duration_attrs.intersection(req_attrs.keys()) } return duration_attrs + + +def _parse_url_query(url: str): + parsed_url = urlparse(url) + path = parsed_url.path + query_params = parsed_url.query + return path, query_params diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py index 2b23bc4994..b6955b0eca 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.45b0.dev" +__version__ = "0.47b0.dev" diff --git a/util/opentelemetry-util-http/test-requirements.txt b/util/opentelemetry-util-http/test-requirements.txt new file mode 100644 index 0000000000..0e28bbdd05 --- /dev/null +++ b/util/opentelemetry-util-http/test-requirements.txt @@ -0,0 +1,12 @@ +asgiref==3.7.2 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==24.0 +pluggy==1.5.0 +py-cpuinfo==9.0.0 +pytest==7.4.4 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +-e opentelemetry-instrumentation \ No newline at end of file diff --git a/util/opentelemetry-util-http/tests/test_remove_credentials.py b/util/opentelemetry-util-http/tests/test_remove_credentials.py new file mode 100644 index 0000000000..b6243145f5 --- /dev/null +++ b/util/opentelemetry-util-http/tests/test_remove_credentials.py @@ -0,0 +1,27 @@ +import unittest + +from opentelemetry.util.http import remove_url_credentials + + +class TestRemoveUrlCredentials(unittest.TestCase): + def test_remove_no_credentials(self): + url = "http://opentelemetry.io:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, url) + + def test_remove_credentials(self): + url = "http://someuser:somepass@opentelemetry.io:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual( + cleaned_url, "http://opentelemetry.io:8080/test/path?query=value" + ) + + def test_remove_credentials_ipv4_literal(self): + url = "http://someuser:somepass@127.0.0.1:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, "http://127.0.0.1:8080/test/path?query=value") + + def test_remove_credentials_ipv6_literal(self): + url = "http://someuser:somepass@[::1]:8080/test/path?query=value" + cleaned_url = remove_url_credentials(url) + self.assertEqual(cleaned_url, "http://[::1]:8080/test/path?query=value") diff --git a/util/opentelemetry-util-http/tests/test_sanitize_method.py b/util/opentelemetry-util-http/tests/test_sanitize_method.py index b4095324a6..bd14d88ee5 100644 --- a/util/opentelemetry-util-http/tests/test_sanitize_method.py +++ b/util/opentelemetry-util-http/tests/test_sanitize_method.py @@ -32,7 +32,7 @@ def test_standard_method_lowercase(self): def test_nonstandard_method(self): method = sanitize_method("UNKNOWN") - self.assertEqual(method, "NONSTANDARD") + self.assertEqual(method, "_OTHER") @patch.dict( "os.environ", @@ -42,4 +42,4 @@ def test_nonstandard_method(self): ) def test_nonstandard_method_allowed(self): method = sanitize_method("UNKNOWN") - self.assertEqual(method, "NONSTANDARD") + self.assertEqual(method, "UNKNOWN")