diff --git a/.github/workflows/instrumentations_0.yml b/.github/workflows/instrumentations_0.yml new file mode 100644 index 0000000000..256d160640 --- /dev/null +++ b/.github/workflows/instrumentations_0.yml @@ -0,0 +1,123 @@ +name: Contrib Repo Tests + +on: + push: + branches-ignore: + - 'release/*' + pull_request: +env: + CORE_REPO_SHA: 955c92e91b5cd4bcfb43c39efcef086b040471d2 + +jobs: + instrumentations-0: + env: + # We use these variables to convert between tox and GHA version literals + py38: 3.8 + py39: 3.9 + py310: "3.10" + py311: "3.11" + 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] + 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 + # the limit of jobs that can be run from a Github actions matrix: + # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + # "A matrix will generate a maximum of 256 jobs per workflow run. This limit applies + # to both GitHub-hosted and self-hosted runners." + - "aiohttp-client" + - "aiohttp-server" + - "aiopg" + - "aio-pika" + - "asgi" + - "asyncpg" + - "aws-lambda" + - "boto" + - "boto3sqs" + - "botocore" + - "cassandra" + - "celery" + - "confluent-kafka" + - "dbapi" + - "django" + - "elasticsearch" + - "falcon" + - "fastapi" + - "flask" + - "grpc" + - "httpx" + - "jinja2" + - "kafka-python" + - "logging" + - "mysql" + - "mysqlclient" + - "sio-pika" + - "psycopg2" + - "pymemcache" + - "pymongo" + - "pymysql" + - "pyramid" + - "redis" + - "remoulade" + - "requests" + - "sklearn" + - "sqlalchemy" + - "sqlite3" + - "starlette" + - "system-metrics" + - "tornado" + - "tortoiseorm" + os: [ubuntu-20.04] + exclude: + - python-version: py39 + package: "sklearn" + - python-version: py310 + package: "sklearn" + - python-version: py311 + package: "sklearn" + - python-version: pypy3 + package: "aiopg" + - python-version: pypy3 + package: "asyncpg" + - python-version: pypy3 + package: "boto" + - python-version: pypy3 + package: "boto3sqs" + - python-version: pypy3 + package: "botocore" + - python-version: pypy3 + package: "psycopg2" + - python-version: pypy3 + package: "remoulade" + - python-version: pypy3 + package: "requests" + - python-version: pypy3 + package: "sklearn" + - python-version: pypy3 + package: "confluent-kafka" + - python-version: pypy3 + package: "grpc" + steps: + - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v2 + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v4 + 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 + with: + path: | + .tox + ~/.cache/pip + key: v7-build-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json diff --git a/.github/workflows/instrumentations_1.yml b/.github/workflows/instrumentations_1.yml new file mode 100644 index 0000000000..59db21529a --- /dev/null +++ b/.github/workflows/instrumentations_1.yml @@ -0,0 +1,62 @@ +name: Contrib Repo Tests + +on: + push: + branches-ignore: + - 'release/*' + pull_request: +env: + CORE_REPO_SHA: 955c92e91b5cd4bcfb43c39efcef086b040471d2 + +jobs: + instrumentations-1: + env: + # We use these variables to convert between tox and GHA version literals + py38: 3.8 + py39: 3.9 + py310: "3.10" + py311: "3.11" + 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] + package: + - "urllib" + - "urllib3" + - "wsgi" + - "distro" + - "richconsole" + - "psycopg" + - "prometheus-remote-write" + - "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 + - name: Set up Python ${{ env[matrix.python-version] }} + uses: actions/setup-python@v4 + 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 + with: + path: | + .tox + ~/.cache/pip + key: v7-build-tox-cache-${{ env.RUN_MATRIX_COMBINATION }}-${{ hashFiles('tox.ini', 'gen-requirements.txt', 'dev-requirements.txt') }} + - name: run tox + run: tox -f ${{ matrix.python-version }}-${{ matrix.package }} -- -ra --benchmark-json=${{ env.RUN_MATRIX_COMBINATION }}-benchmark.json diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 42e287f981..49b9c89560 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -54,7 +54,7 @@ jobs: with: python-version: 3.9 - name: Install tox - run: pip install tox==3.27.1 + run: pip install tox - name: run tox run: tox -e generate diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml index 229c8ac7e6..a4caf86ebe 100644 --- a/.github/workflows/prepare-release-branch.yml +++ b/.github/workflows/prepare-release-branch.yml @@ -81,7 +81,7 @@ jobs: with: python-version: 3.9 - name: Install tox - run: pip install tox==3.27.1 + run: pip install tox - name: run tox run: tox -e generate @@ -165,7 +165,7 @@ jobs: with: python-version: 3.9 - name: Install tox - run: pip install tox==3.27.1 + run: pip install tox - name: run tox run: tox -e generate diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e3c0c6f30..f6c267003a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,12 +45,6 @@ jobs: echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV - # 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 - with: - ref: main - - run: | if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then # not making a patch release @@ -60,13 +54,19 @@ jobs: fi fi + # 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 + with: + ref: main + # back to the release branch - uses: actions/checkout@v3 # next few steps publish to pypi - uses: actions/setup-python@v1 with: - python-version: '3.7' + python-version: '3.8' - name: Build wheels run: ./scripts/build.sh @@ -202,4 +202,4 @@ jobs: gh pr create --title "$message" \ --body "$body" \ --head $branch \ - --base main \ No newline at end of file + --base main diff --git a/.pylintrc b/.pylintrc index 30d60bc2d3..114dadef75 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. -extension-pkg-whitelist= +extension-pkg-whitelist=cassandra # Add list of files or directories to be excluded. They should be base names, not # paths. @@ -29,7 +29,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=pylint.extensions.no_self_use # Pickle collected data for later comparisons. persistent=yes @@ -69,7 +69,6 @@ disable=missing-docstring, duplicate-code, ungrouped-imports, # Leave this up to isort wrong-import-order, # Leave this up to isort - bad-continuation, # Leave this up to black line-too-long, # Leave this up to black exec-used, super-with-arguments, # temp-pylint-upgrade @@ -81,6 +80,8 @@ disable=missing-docstring, invalid-overridden-method, # temp-pylint-upgrade 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 @@ -268,13 +269,6 @@ max-line-length=79 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41f2aa08cb..0c18392bc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,26 @@ 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! +## 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,8 +50,8 @@ 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 -$ pip install tox==3.27.1 +```sh +pip install tox ``` You can run `tox` with the following arguments: @@ -40,7 +59,7 @@ You can run `tox` with the following arguments: - `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 py37-test-instrumentation-aiopg` to e.g. run the aiopg instrumentation unit tests under a specific +- `tox -e py311-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 @@ -52,9 +71,13 @@ An easier way to do so is: 2. Run `.tox/lint/bin/isort .` See -[`tox.ini`](https://github.com/open-telemetry/opentelemetry-python/blob/main/tox.ini) +[`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. + ### Benchmarks Performance progression of benchmarks for packages distributed by OpenTelemetry Python can be viewed as a [graph of throughput vs commit history](https://opentelemetry-python-contrib.readthedocs.io/en/latest/performance/benchmarks.html). From the linked page, you can download a JSON file with the performance results. @@ -90,30 +113,30 @@ pull requests (PRs). 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 +git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git ``` Add your fork as an origin: ```sh -$ git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python-contrib.git +git remote add fork https://github.com/YOUR_GITHUB_USERNAME/opentelemetry-python-contrib.git ``` Run tests: ```sh # make sure you have all supported versions of Python installed -$ pip install tox==3.27.1 # only first time. +$ pip install tox # only first time. $ tox # execute in the root of the repository ``` Check out a new branch, make modifications and push the branch to your fork: ```sh -$ git checkout -b feature +git checkout -b feature # edit files -$ git commit -$ git push fork feature +git commit +git push fork feature ``` Open a pull request against the main `opentelemetry-python-contrib` repo. @@ -138,6 +161,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). @@ -173,7 +197,7 @@ For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-sp ## Running Tests Locally 1. Go to your Contrib repo directory. `git clone git@github.com:open-telemetry/opentelemetry-python-contrib.git && cd opentelemetry-python-contrib`. -2. Make sure you have `tox` installed. `pip install tox==3.27.1`. +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/). ### Testing against a different Core repo branch/commit @@ -182,8 +206,7 @@ Some of the tox targets install packages from the [OpenTelemetry Python Core Rep 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 continuation integration overrides that environment variable with as per the configuration [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/2518a4ac07cb62ad6587dd8f6cbb5f8663a7e179/.github/workflows/test.yml#L9). ## Style Guide @@ -198,28 +221,26 @@ The continuation integration overrides that environment variable with as per the Below is a checklist of things to be mindful of when implementing a new instrumentation or working on a specific instrumentation. It is one of our goals as a community to keep the implementation specific details of instrumentations as similar across the board as possible for ease of testing and feature parity. It is also good to abstract as much common functionality as possible. - Follow semantic conventions - - The instrumentation should follow the semantic conventions defined [here](https://github.com/open-telemetry/opentelemetry-specification/tree/main/semantic_conventions) -- Extends from [BaseInstrumentor](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py#L26) + - 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/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. - diff --git a/RELEASING.md b/RELEASING.md index a30838130f..256674d1b8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,8 +7,10 @@ * If making a pre-release of stable components (e.g. release candidate), enter the pre-release version number, e.g. `1.9.0rc2`. (otherwise the workflow will pick up the version from `main` and just remove the `.dev` suffix). - * Review and merge the two pull requests that it creates + * Review the two pull requests that it creates. (one is targeted to the release branch and one is targeted to `main`). + * Merge the one targeted towards the release branch. + * The builds will fail for the `main` pr because of validation rules. Follow the [release workflow](https://github.com/open-telemetry/opentelemetry-python/blob/main/RELEASING.md) for the core repo up until this same point. Change the SHAs of each PR to point at each other to get the `main` builds to pass. ## Preparing a new patch release @@ -75,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 @@ -96,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/README.rst b/_template/README.rst index 7eb48e6b15..78226bba43 100644 --- a/_template/README.rst +++ b/_template/README.rst @@ -11,6 +11,7 @@ This library allows tracing requests made by the library. Installation ------------ + :: pip install opentelemetry-instrumentation- diff --git a/_template/pyproject.toml b/_template/pyproject.toml index f088661c3a..ca3da89a30 100644 --- a/_template/pyproject.toml +++ b/_template/pyproject.toml @@ -12,7 +12,7 @@ dynamic = ["version"] description = "" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -22,7 +22,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -32,12 +31,6 @@ 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/dev-requirements.txt b/dev-requirements.txt index feab6b4b02..fffb4c445d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,20 +1,19 @@ -pylint==2.12.2 -flake8~=3.7 -isort~=5.6 -black>=22.1.0 -httpretty~=1.0 -mypy==0.790 -sphinx -sphinx-rtd-theme~=0.4 -sphinx-autodoc-typehints -pytest!=5.2.3 -pytest-cov>=2.8 -readme-renderer~=24.0 +pylint==3.0.2 +flake8==6.1.0 +isort==5.12.0 +black==22.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-cov==4.1.0 +readme-renderer==42.0 bleach==4.1.0 # transient dependency for readme-renderer -grpcio-tools==1.29.0 -mypy-protobuf>=1.23 protobuf~=3.13 markupsafe>=2.0.1 codespell==2.1.0 requests==2.31.0 ruamel.yaml==0.17.21 +flaky==3.7.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index 32f4a406aa..aff449fcf8 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,6 +1,6 @@ -sphinx==4.5.0 -sphinx-rtd-theme~=0.4 -sphinx-autodoc-typehints +sphinx==7.1.2 +sphinx-rtd-theme==2.0.0rc4 +sphinx-autodoc-typehints==1.25.2 # Need to install the api/sdk in the venv for autodoc. Modifying sys.path # doesn't work for pkg_resources. @@ -27,7 +27,7 @@ botocore~=1.0 boto3~=1.0 cassandra-driver~=3.25 celery>=4.0 -confluent-kafka>= 1.8.2,<= 2.2.0 +confluent-kafka>= 1.8.2,<= 2.3.0 elasticsearch>=2.0,<9.0 flask~=2.0 falcon~=2.0 @@ -36,6 +36,7 @@ kafka-python>=2.0,<3.0 mysql-connector-python~=8.0 mysqlclient~=2.1.1 psutil>=5 +psycopg~=3.1.17 pika>=0.12.0 pymongo~=3.1 PyMySQL~=0.9.3 @@ -45,11 +46,8 @@ remoulade>=0.50 sqlalchemy>=1.0 tornado>=5.1.1 tortoise-orm>=0.17.0 -ddtrace>=0.34.0 httpx>=0.18.0 # indirect dependency pins markupsafe==2.0.1 itsdangerous==2.0.1 - -docutils==0.16 \ No newline at end of file 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/asyncio/asyncio.rst b/docs/instrumentation/asyncio/asyncio.rst new file mode 100644 index 0000000000..4cbcc70f09 --- /dev/null +++ b/docs/instrumentation/asyncio/asyncio.rst @@ -0,0 +1,7 @@ +OpenTelemetry asyncio Instrumentation +============================================== + +.. automodule:: opentelemetry.instrumentation.asyncio + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/psycopg/psycopg.rst b/docs/instrumentation/psycopg/psycopg.rst new file mode 100644 index 0000000000..3ba7556359 --- /dev/null +++ b/docs/instrumentation/psycopg/psycopg.rst @@ -0,0 +1,7 @@ +OpenTelemetry Psycopg Instrumentation +===================================== + +.. automodule:: opentelemetry.instrumentation.psycopg + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/instrumentation/psycopg2/psycopg2.rst b/docs/instrumentation/psycopg2/psycopg2.rst index 69be31b2d1..0ae76580c0 100644 --- a/docs/instrumentation/psycopg2/psycopg2.rst +++ b/docs/instrumentation/psycopg2/psycopg2.rst @@ -1,5 +1,5 @@ -OpenTelemetry Psycopg Instrumentation -===================================== +OpenTelemetry Psycopg2 Instrumentation +====================================== .. automodule:: opentelemetry.instrumentation.psycopg2 :members: 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/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..5f6129df55 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,4 +1,5 @@ #!/bin/bash +set -e PROM_VERSION=v2.39.0 PROTO_VERSION=v1.3.2 diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml index 49ae48d397..b0006a0682 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/pyproject.toml @@ -9,7 +9,7 @@ dynamic = ["version"] description = "Prometheus Remote Write Metrics Exporter for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -19,7 +19,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -32,9 +31,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/test-requirements.txt b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt new file mode 100644 index 0000000000..40c4886fd7 --- /dev/null +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/test-requirements.txt @@ -0,0 +1,24 @@ +asgiref==3.7.2 +attrs==23.2.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +cramjam==2.8.1 +Deprecated==1.2.14 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +protobuf==4.25.3 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +python-snappy==0.7.1 +requests==2.31.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.1 +wrapt==1.16.0 +zipp==3.17.0 +-e exporter/opentelemetry-exporter-prometheus-remote-write diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py index 259de7b7a2..78e9c49bee 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/conftest.py @@ -3,6 +3,8 @@ import pytest import opentelemetry.test.metrictestutil as metric_util + +# pylint: disable=no-name-in-module from opentelemetry.exporter.prometheus_remote_write import ( PrometheusRemoteWriteMetricsExporter, ) diff --git a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py index d64a8f04a8..785c6bdc29 100644 --- a/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus-remote-write/tests/test_prometheus_remote_write_exporter.py @@ -17,6 +17,7 @@ import pytest +# pylint: disable=no-name-in-module from opentelemetry.exporter.prometheus_remote_write import ( PrometheusRemoteWriteMetricsExporter, ) diff --git a/exporter/opentelemetry-exporter-richconsole/test-requirements.txt b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt new file mode 100644 index 0000000000..42d2ec7b4c --- /dev/null +++ b/exporter/opentelemetry-exporter-richconsole/test-requirements.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +Pygments==2.17.2 +pytest==7.1.3 +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/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/src/opentelemetry/instrumentation/aio_pika/aio_pika_instrumentor.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/aio_pika_instrumentor.py index 99420d0892..caf0e5b1a9 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/aio_pika_instrumentor.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/aio_pika_instrumentor.py @@ -64,7 +64,10 @@ async def wrapper(wrapped, instance, args, kwargs): def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider", None) tracer = trace.get_tracer( - _INSTRUMENTATION_MODULE_NAME, __version__, tracer_provider + _INSTRUMENTATION_MODULE_NAME, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._instrument_queue(tracer) self._instrument_exchange(tracer) diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/callback_decorator.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/callback_decorator.py index a2169b6d18..f10415bdd2 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/callback_decorator.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/callback_decorator.py @@ -18,9 +18,7 @@ from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.aio_pika.span_builder import SpanBuilder -from opentelemetry.instrumentation.aio_pika.utils import ( - is_instrumentation_enabled, -) +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv.trace import MessagingOperationValues from opentelemetry.trace import Span, Tracer diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/publish_decorator.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/publish_decorator.py index cae834a031..03937290ee 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/publish_decorator.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/publish_decorator.py @@ -45,8 +45,7 @@ async def decorated_publish( if not span: return await publish(message, routing_key, **kwargs) with trace.use_span(span, end_on_exit=True): - if span.is_recording(): - propagate.inject(message.properties.headers) + propagate.inject(message.properties.headers) return_value = await publish(message, routing_key, **kwargs) return return_value 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 056f3dab25..b73afa62b3 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 @@ -15,9 +15,7 @@ from aio_pika.abc import AbstractChannel, AbstractMessage -from opentelemetry.instrumentation.aio_pika.utils import ( - is_instrumentation_enabled, -) +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv.trace import ( MessagingOperationValues, SpanAttributes, diff --git a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/utils.py b/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/utils.py deleted file mode 100644 index fb94ddf468..0000000000 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/src/opentelemetry/instrumentation/aio_pika/utils.py +++ /dev/null @@ -1,9 +0,0 @@ -from opentelemetry import context - - -def is_instrumentation_enabled() -> bool: - if context.get_value("suppress_instrumentation") or context.get_value( - context._SUPPRESS_INSTRUMENTATION_KEY - ): - return False - return True 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..c8ffea4e89 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-0.txt @@ -0,0 +1,23 @@ +aio-pika==7.2.0 +aiormq==6.2.3 +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==23.2 +pamqp==3.1.0 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..1e2ea5a1e5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-1.txt @@ -0,0 +1,23 @@ +aio-pika==8.3.0 +aiormq==6.6.4 +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==23.2 +pamqp==3.2.1 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..3250c93947 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/test-requirements-2.txt @@ -0,0 +1,23 @@ +aio-pika==9.4.0 +aiormq==6.8.0 +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==23.2 +pamqp==3.3.0 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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 d5291e07d9..90a029531d 100644 --- a/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py +++ b/instrumentation/opentelemetry-instrumentation-aio-pika/tests/test_publish_decorator.py @@ -14,6 +14,7 @@ import asyncio from typing import Type from unittest import TestCase, mock, skipIf +from unittest.mock import MagicMock from aio_pika import Exchange, RobustExchange @@ -74,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) @@ -92,6 +91,34 @@ def test_publish(self): def test_robust_publish(self): self._test_publish(RobustExchange) + def _test_publish_works_with_not_recording_span(self, exchange_type): + exchange = exchange_type(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) + with mock.patch.object( + PublishDecorator, "_get_publish_span" + ) as mock_get_publish_span: + 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") as mock_publish: + with mock.patch( + "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" + ) as mock_inject: + decorated_publish = PublishDecorator( + self.tracer, exchange + ).decorate(mock_publish) + self.loop.run_until_complete( + decorated_publish(MESSAGE, ROUTING_KEY) + ) + mock_publish.assert_called_once() + mock_get_publish_span.assert_called_once() + mock_inject.assert_called_once() + + def test_publish_works_with_not_recording_span(self): + self._test_publish_works_with_not_recording_span(Exchange) + + def test_publish_works_with_not_recording_span_robust(self): + self._test_publish_works_with_not_recording_span(RobustExchange) + @skipIf(AIOPIKA_VERSION_INFO <= (8, 0), "Only for aio_pika 8") class TestInstrumentedExchangeAioRmq8(TestCase): @@ -127,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) @@ -144,3 +169,31 @@ def test_publish(self): def test_robust_publish(self): self._test_publish(RobustExchange) + + def _test_publish_works_with_not_recording_span(self, exchange_type): + exchange = exchange_type(CONNECTION_7, CHANNEL_7, EXCHANGE_NAME) + with mock.patch.object( + PublishDecorator, "_get_publish_span" + ) as mock_get_publish_span: + 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") as mock_publish: + with mock.patch( + "opentelemetry.instrumentation.aio_pika.publish_decorator.propagate.inject" + ) as mock_inject: + decorated_publish = PublishDecorator( + self.tracer, exchange + ).decorate(mock_publish) + self.loop.run_until_complete( + decorated_publish(MESSAGE, ROUTING_KEY) + ) + mock_publish.assert_called_once() + mock_get_publish_span.assert_called_once() + mock_inject.assert_called_once() + + def test_publish_works_with_not_recording_span(self): + self._test_publish_works_with_not_recording_span(Exchange) + + def test_publish_works_with_not_recording_span_robust(self): + self._test_publish_works_with_not_recording_span(RobustExchange) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 65e1601f34..9f842bde79 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -94,8 +94,8 @@ def response_hook(span: Span, params: typing.Union[ from opentelemetry.instrumentation.aiohttp_client.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( - _SUPPRESS_INSTRUMENTATION_KEY, http_status_to_status_code, + is_instrumentation_enabled, unwrap, ) from opentelemetry.propagate import inject @@ -163,7 +163,12 @@ def create_trace_config( # Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work # around this issue. - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) def _end_trace(trace_config_ctx: types.SimpleNamespace): context_api.detach(trace_config_ctx.token) @@ -174,7 +179,7 @@ async def on_request_start( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestStartParams, ): - if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + if not is_instrumentation_enabled(): trace_config_ctx.span = None return @@ -277,7 +282,7 @@ def _instrument( # pylint:disable=unused-argument def instrumented_init(wrapped, instance, args, kwargs): - if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + if not is_instrumentation_enabled(): return wrapped(*args, **kwargs) client_trace_configs = list(kwargs.get("trace_configs") or []) 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..b8ccfd2a50 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/test-requirements.txt @@ -0,0 +1,37 @@ +aiohttp==3.9.3 +aiosignal==1.3.1 +asgiref==3.7.2 +async-timeout==4.0.3 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +multidict==6.0.5 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.1 +Werkzeug==3.0.1 +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 6af9d41900..2fa97f40b0 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,6 +14,7 @@ import asyncio import contextlib +import sys import typing import unittest import urllib.parse @@ -26,13 +27,12 @@ from http_server_mock import HttpServerMock from pkg_resources import iter_entry_points -from opentelemetry import context from opentelemetry import trace as trace_api from opentelemetry.instrumentation import aiohttp_client from opentelemetry.instrumentation.aiohttp_client import ( AioHttpClientInstrumentor, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import Span, StatusCode @@ -116,6 +116,11 @@ def test_status_codes(self): status_code=status_code, ) + 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( [ ( @@ -123,7 +128,7 @@ def test_status_codes(self): (span_status, None), { SpanAttributes.HTTP_METHOD: "GET", - SpanAttributes.HTTP_URL: f"http://{host}:{port}/test-path#foobar", + SpanAttributes.HTTP_URL: url, SpanAttributes.HTTP_STATUS_CODE: int( status_code ), @@ -134,6 +139,21 @@ def test_status_codes(self): self.memory_exporter.clear() + def test_schema_url(self): + with self.subTest(status_code=200): + self._http_request( + trace_config=aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + status_code=200, + ) + + span = self.memory_exporter.get_finished_spans()[0] + self.assertEqual( + span.instrumentation_info.schema_url, + "https://opentelemetry.io/schemas/1.11.0", + ) + self.memory_exporter.clear() + def test_not_recording(self): mock_tracer = mock.Mock() mock_span = mock.Mock() @@ -141,7 +161,7 @@ def test_not_recording(self): mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer"): # pylint: disable=W0612 - host, port = self._http_request( + self._http_request( trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", ) @@ -491,25 +511,17 @@ async def uninstrument_request(server: aiohttp.test_utils.TestServer): self.assert_spans(1) def test_suppress_instrumentation(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): run_with_test_server( self.get_default_request(), self.URL, self.default_handler ) - finally: - context.detach(token) self.assert_spans(0) @staticmethod async def suppressed_request(server: aiohttp.test_utils.TestServer): async with aiohttp.test_utils.TestClient(server) as client: - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - await client.get(TestAioHttpClientInstrumentor.URL) - context.detach(token) + with suppress_instrumentation(): + await client.get(TestAioHttpClientInstrumentor.URL) def test_suppress_instrumentation_after_creation(self): run_with_test_server( diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/README.rst b/instrumentation/opentelemetry-instrumentation-aiohttp-server/README.rst new file mode 100644 index 0000000000..b8606ad687 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry aiohttp server Integration +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ + +This library allows tracing HTTP requests made by the +`aiohttp server `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-aiohttp-server + +References +---------- + +* `OpenTelemetry Project `_ +* `aiohttp client Tracing `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml new file mode 100644 index 0000000000..f85cfd6e39 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-aiohttp-server" +dynamic = ["version"] +description = "Aiohttp server instrumentation 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" +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-instrumentation == 0.46b0.dev", + "opentelemetry-semantic-conventions == 0.46b0.dev", + "opentelemetry-util-http == 0.46b0.dev", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [ + "aiohttp ~= 3.0", +] + +[project.entry-points.opentelemetry_instrumentor] +aiohttp-server = "opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-server" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/aiohttp_server/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] 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 new file mode 100644 index 0000000000..c1ab960818 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -0,0 +1,267 @@ +# 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 urllib +from timeit import default_timer +from typing import Dict, List, Tuple, Union + +from aiohttp import web +from multidict import CIMultiDictProxy + +from opentelemetry import context, metrics, trace +from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +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.propagate import extract +from opentelemetry.propagators.textmap import Getter +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, remove_url_credentials + +_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, + SpanAttributes.HTTP_ROUTE, +] + +_active_requests_count_attrs = [ + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_HOST, + SpanAttributes.HTTP_SCHEME, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SERVER_NAME, +] + +tracer = trace.get_tracer(__name__) +meter = metrics.get_meter(__name__, __version__) +_excluded_urls = get_excluded_urls("AIOHTTP_SERVER") + + +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_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 get_default_span_details(request: web.Request) -> Tuple[str, dict]: + """Default implementation for get_default_span_details + Args: + request: the request object itself. + Returns: + a tuple of the span name, and any attributes to attach to the span. + """ + span_name = request.path.strip() or f"HTTP {request.method}" + return span_name, {} + + +def _get_view_func(request: web.Request) -> str: + """Returns the name of the request handler. + Args: + request: the request object itself. + Returns: + a string containing the name of the handler function + """ + try: + return request.match_info.handler.__name__ + except AttributeError: + return "unknown" + + +def collect_request_attributes(request: web.Request) -> Dict: + """Collects HTTP request attributes from the ASGI scope and returns a + dictionary to be used as span creation attributes.""" + + server_host, port, http_url = ( + request.url.host, + request.url.port, + str(request.url), + ) + query_string = request.query_string + if query_string and http_url: + if isinstance(query_string, bytes): + query_string = query_string.decode("utf8") + http_url += "?" + urllib.parse.unquote(query_string) + + result = { + SpanAttributes.HTTP_SCHEME: request.scheme, + SpanAttributes.HTTP_HOST: server_host, + SpanAttributes.NET_HOST_PORT: port, + SpanAttributes.HTTP_ROUTE: _get_view_func(request), + SpanAttributes.HTTP_FLAVOR: f"{request.version.major}.{request.version.minor}", + SpanAttributes.HTTP_TARGET: request.path, + SpanAttributes.HTTP_URL: remove_url_credentials(http_url), + } + + http_method = request.method + if http_method: + result[SpanAttributes.HTTP_METHOD] = http_method + + http_host_value_list = ( + [request.host] if not isinstance(request.host, list) else request.host + ) + if http_host_value_list: + result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( + http_host_value_list + ) + http_user_agent = request.headers.get("user-agent") + if http_user_agent: + result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent + + # remove None values + result = {k: v for k, v in result.items() if v is not None} + + return result + + +def set_status_code(span, status_code: int) -> None: + """Adds HTTP response attributes to span using the status_code argument.""" + + try: + status_code = int(status_code) + 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)) + ) + + +class AiohttpGetter(Getter): + """Extract current trace from headers""" + + def get(self, carrier, key: str) -> Union[List, None]: + """Getter implementation to retrieve an HTTP header value from the ASGI + scope. + + Args: + carrier: ASGI scope object + key: header name in scope + Returns: + A list of all header values matching the key, or None if the key + does not match any header. + """ + headers: CIMultiDictProxy = carrier.headers + if not headers: + return None + return headers.getall(key, None) + + def keys(self, carrier: Dict) -> List: + return list(carrier.keys()) + + +getter = AiohttpGetter() + + +@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) + ): + return await handler(request) + + span_name, additional_attributes = get_default_span_details(request) + + req_attrs = collect_request_attributes(request) + duration_attrs = _parse_duration_attrs(req_attrs) + active_requests_count_attrs = _parse_active_request_count_attrs(req_attrs) + + duration_histogram = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="Duration of HTTP client requests.", + ) + + active_requests_counter = meter.create_up_down_counter( + name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, + unit="requests", + description="measures the number of concurrent HTTP requests those are currently in flight", + ) + + with tracer.start_as_current_span( + span_name, + context=extract(request, getter=getter), + kind=trace.SpanKind.SERVER, + ) as span: + attributes = collect_request_attributes(request) + attributes.update(additional_attributes) + span.set_attributes(attributes) + start = default_timer() + active_requests_counter.add(1, active_requests_count_attrs) + try: + resp = await handler(request) + set_status_code(span, resp.status) + except web.HTTPException as ex: + set_status_code(span, ex.status_code) + raise + finally: + duration = max((default_timer() - start) * 1000, 0) + duration_histogram.record(duration, duration_attrs) + active_requests_counter.add(-1, active_requests_count_attrs) + return resp + + +class _InstrumentedApplication(web.Application): + """Insert tracing middleware""" + + def __init__(self, *args, **kwargs): + middlewares = kwargs.pop("middlewares", []) + middlewares.insert(0, middleware) + kwargs["middlewares"] = middlewares + super().__init__(*args, **kwargs) + + +class AioHttpServerInstrumentor(BaseInstrumentor): + # pylint: disable=protected-access,attribute-defined-outside-init + """An instrumentor for aiohttp.web.Application + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + self._original_app = web.Application + setattr(web, "Application", _InstrumentedApplication) + + def _uninstrument(self, **kwargs): + setattr(web, "Application", self._original_app) + + def instrumentation_dependencies(self): + return _instruments diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/package.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/package.py new file mode 100644 index 0000000000..557f1a54a9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/package.py @@ -0,0 +1,16 @@ +# 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 = ("aiohttp ~= 3.0",) 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 new file mode 100644 index 0000000000..ff4933b20b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/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.46b0.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..9f6d7accce --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt @@ -0,0 +1,27 @@ +aiohttp==3.9.3 +aiosignal==1.3.1 +asgiref==3.7.2 +async-timeout==4.0.3 +attrs==23.2.0 +Deprecated==1.2.14 +frozenlist==1.4.1 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +multidict==6.0.5 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..b5e8ec468f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py @@ -0,0 +1,130 @@ +# 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. + +from enum import Enum +from http import HTTPStatus + +import aiohttp +import pytest +import pytest_asyncio + +from opentelemetry import trace as trace_api +from opentelemetry.instrumentation.aiohttp_server import ( + AioHttpServerInstrumentor, +) +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.globals_test import reset_trace_globals +from opentelemetry.test.test_base import TestBase +from opentelemetry.util._importlib_metadata import entry_points + + +class HTTPMethod(Enum): + """HTTP methods and descriptions""" + + def __repr__(self): + return f"{self.value}" + + CONNECT = "CONNECT" + DELETE = "DELETE" + GET = "GET" + HEAD = "HEAD" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + POST = "POST" + PUT = "PUT" + TRACE = "TRACE" + + +@pytest.fixture(name="tracer", scope="session") +def fixture_tracer(): + test_base = TestBase() + + tracer_provider, memory_exporter = test_base.create_tracer_provider() + + reset_trace_globals() + trace_api.set_tracer_provider(tracer_provider) + + yield tracer_provider, memory_exporter + + reset_trace_globals() + + +async def default_handler(request, status=200): + return aiohttp.web.Response(status=status) + + +@pytest_asyncio.fixture(name="server_fixture") +async def fixture_server_fixture(tracer, aiohttp_server): + _, memory_exporter = tracer + + AioHttpServerInstrumentor().instrument() + + app = aiohttp.web.Application() + app.add_routes([aiohttp.web.get("/test-path", default_handler)]) + + server = await aiohttp_server(app) + yield server, app + + memory_exporter.clear() + + AioHttpServerInstrumentor().uninstrument() + + +def test_checking_instrumentor_pkg_installed(): + + (instrumentor_entrypoint,) = entry_points( + group="opentelemetry_instrumentor", name="aiohttp-server" + ) + instrumentor = instrumentor_entrypoint.load()() + assert isinstance(instrumentor, AioHttpServerInstrumentor) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "url, expected_method, expected_status_code", + [ + ("/test-path", HTTPMethod.GET, HTTPStatus.OK), + ("/not-found", HTTPMethod.GET, HTTPStatus.NOT_FOUND), + ], +) +async def test_status_code_instrumentation( + tracer, + server_fixture, + aiohttp_client, + url, + expected_method, + expected_status_code, +): + _, memory_exporter = tracer + server, _ = server_fixture + + assert len(memory_exporter.get_finished_spans()) == 0 + + client = await aiohttp_client(server) + await client.get(url) + + assert len(memory_exporter.get_finished_spans()) == 1 + + [span] = memory_exporter.get_finished_spans() + + assert expected_method.value == span.attributes[SpanAttributes.HTTP_METHOD] + assert ( + expected_status_code + == span.attributes[SpanAttributes.HTTP_STATUS_CODE] + ) + + assert ( + f"http://{server.host}:{server.port}{url}" + == span.attributes[SpanAttributes.HTTP_URL] + ) diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py index 6cc87f4900..a4bde482db 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/src/opentelemetry/instrumentation/aiopg/aiopg_integration.py @@ -215,6 +215,7 @@ async def __aexit__(self, exc_type, exc, tb): class _PoolAcquireContextManager(_ContextManager): + # pylint: disable=redefined-slots-in-subclass __slots__ = ("_coro", "_obj", "_pool") def __init__(self, coro, pool): diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt new file mode 100644 index 0000000000..2b03677f42 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiopg/test-requirements.txt @@ -0,0 +1,22 @@ +aiopg==1.4.0 +asgiref==3.7.2 +async-timeout==4.0.3 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +install==1.3.5 +packaging==23.2 +pluggy==1.4.0 +psycopg2-binary==2.9.9 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..fb76bd0f38 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -76,7 +76,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 +127,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) 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 c0dcd39fd2..fc80d9a5e5 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -189,11 +189,13 @@ def client_response_hook(span: Span, message: dict): --- """ +from __future__ import annotations + import typing import urllib from functools import wraps from timeit import default_timer -from typing import Tuple +from typing import Any, Awaitable, Callable, Tuple from asgiref.compatibility import guarantee_single_callable @@ -332,55 +334,34 @@ def collect_request_attributes(scope): return result -def collect_custom_request_headers_attributes(scope): - """returns custom HTTP request headers to be added into SERVER span as span attributes - Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers +def collect_custom_headers_attributes( + scope_or_response_message: dict[str, Any], + sanitize: SanitizeValue, + header_regexes: list[str], + normalize_names: Callable[[str], str], +) -> dict[str, str]: """ + Returns custom HTTP request or response headers to be added into SERVER span as span attributes. - sanitize = SanitizeValue( - get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS - ) - ) - - # Decode headers before processing. - headers = { - _key.decode("utf8"): _value.decode("utf8") - for (_key, _value) in scope.get("headers") - } - - return sanitize.sanitize_header_values( - headers, - get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST - ), - normalise_request_header_name, - ) - - -def collect_custom_response_headers_attributes(message): - """returns custom HTTP response headers to be added into SERVER span as span attributes - Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers + Refer specifications: + - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers """ - - sanitize = SanitizeValue( - get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS - ) - ) - # Decode headers before processing. - headers = { - _key.decode("utf8"): _value.decode("utf8") - for (_key, _value) in message.get("headers") - } + headers: dict[str, str] = {} + raw_headers = scope_or_response_message.get("headers") + if raw_headers: + for _key, _value in raw_headers: + key = _key.decode().lower() + value = _value.decode() + if key in headers: + headers[key] += f",{value}" + else: + headers[key] = value return sanitize.sanitize_header_values( headers, - get_custom_headers( - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE - ), - normalise_response_header_name, + header_regexes, + normalize_names, ) @@ -493,18 +474,41 @@ def __init__( tracer_provider=None, meter_provider=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) + self.tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) self.meter = ( - get_meter(__name__, __version__, meter_provider) + get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) if meter is None else meter ) 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, + unit="By", + description="measures the size of HTTP response messages (compressed).", + ) + self.server_request_size_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, + unit="By", + description="Measures the size of HTTP request messages (compressed).", ) self.server_response_size_histogram = self.meter.create_histogram( name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE, @@ -530,7 +534,41 @@ def __init__( self.client_response_hook = client_response_hook self.content_length_header = None - async def __call__(self, scope, receive, send): + # Environment variables as constructor parameters + self.http_capture_headers_server_request = ( + http_capture_headers_server_request + or ( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + ) + ) + or None + ) + self.http_capture_headers_server_response = ( + http_capture_headers_server_response + or ( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + ) + ) + or None + ) + self.http_capture_headers_sanitize_fields = SanitizeValue( + http_capture_headers_sanitize_fields + or ( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ) + or [] + ) + + async def __call__( + self, + scope: dict[str, Any], + receive: Callable[[], Awaitable[dict[str, Any]]], + send: Callable[[dict[str, Any]], Awaitable[None]], + ) -> None: """The ASGI application Args: @@ -566,14 +604,21 @@ async def __call__(self, scope, receive, send): if scope["type"] == "http": self.active_requests_counter.add(1, active_requests_count_attrs) try: - with trace.use_span(span, end_on_exit=True) as current_span: + with trace.use_span(span, end_on_exit=False) as current_span: if current_span.is_recording(): for key, value in attributes.items(): current_span.set_attribute(key, value) if current_span.kind == trace.SpanKind.SERVER: custom_attributes = ( - collect_custom_request_headers_attributes(scope) + collect_custom_headers_attributes( + scope, + self.http_capture_headers_sanitize_fields, + self.http_capture_headers_server_request, + normalise_request_header_name, + ) + if self.http_capture_headers_server_request + else {} ) if len(custom_attributes) > 0: current_span.set_attributes(custom_attributes) @@ -620,6 +665,8 @@ async def __call__(self, scope, receive, send): ) if token: context.detach(token) + if span.is_recording(): + span.end() # pylint: enable=too-many-branches @@ -635,7 +682,9 @@ async def otel_receive(): 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 @@ -643,8 +692,11 @@ async def otel_receive(): def _get_otel_send( self, server_span, server_span_name, scope, send, duration_attrs ): + expecting_trailers = False + @wraps(send) - async def otel_send(message): + async def otel_send(message: dict[str, Any]): + nonlocal expecting_trailers with self.tracer.start_as_current_span( " ".join((server_span_name, scope["type"], "send")) ) as send_span: @@ -658,17 +710,26 @@ async def otel_send(message): ] = status_code set_status_code(server_span, status_code) set_status_code(send_span, status_code) + + expecting_trailers = message.get("trailers", False) 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 and "headers" in message ): custom_response_attributes = ( - collect_custom_response_headers_attributes(message) + collect_custom_headers_attributes( + message, + self.http_capture_headers_sanitize_fields, + self.http_capture_headers_server_response, + normalise_response_header_name, + ) + if self.http_capture_headers_server_response + else {} ) if len(custom_response_attributes) > 0: server_span.set_attributes( @@ -693,5 +754,16 @@ async def otel_send(message): pass await send(message) + # pylint: disable=too-many-boolean-expressions + if ( + not expecting_trailers + and message["type"] == "http.response.body" + and not message.get("more_body", False) + ) or ( + expecting_trailers + and message["type"] == "http.response.trailers" + and not message.get("more_trailers", False) + ): + server_span.end() return otel_send diff --git a/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt new file mode 100644 index 0000000000..f3ee9764cf --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asgi/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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 2d50d0704f..7a9c91a3e7 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -1,4 +1,4 @@ -from unittest import mock +import os import opentelemetry.instrumentation.asgi as otel_asgi from opentelemetry.test.asgitestutil import AsgiTestBase @@ -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: @@ -72,21 +90,22 @@ async def websocket_app_with_custom_headers(scope, receive, send): break -@mock.patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) class TestCustomHeaders(AsgiTestBase, TestBase): + constructor_params = {} + __test__ = False + + def __init_subclass__(cls) -> None: + if cls is not TestCustomHeaders: + cls.__test__ = True + def setUp(self): super().setUp() self.tracer_provider, self.exporter = TestBase.create_tracer_provider() self.tracer = self.tracer_provider.get_tracer(__name__) self.app = otel_asgi.OpenTelemetryMiddleware( - simple_asgi, tracer_provider=self.tracer_provider + simple_asgi, + tracer_provider=self.tracer_provider, + **self.constructor_params, ) def test_http_custom_request_headers_in_span_attributes(self): @@ -120,6 +139,25 @@ 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( [ @@ -148,7 +186,9 @@ def test_http_custom_request_headers_not_in_span_attributes(self): def test_http_custom_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( - http_app_with_custom_headers, tracer_provider=self.tracer_provider + http_app_with_custom_headers, + tracer_provider=self.tracer_provider, + **self.constructor_params, ) self.seed_app(self.app) self.send_default_request() @@ -173,9 +213,29 @@ 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, tracer_provider=self.tracer_provider + http_app_with_custom_headers, + tracer_provider=self.tracer_provider, + **self.constructor_params, ) self.seed_app(self.app) self.send_default_request() @@ -277,6 +337,7 @@ def test_websocket_custom_response_headers_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( websocket_app_with_custom_headers, tracer_provider=self.tracer_provider, + **self.constructor_params, ) self.seed_app(self.app) self.send_input({"type": "websocket.connect"}) @@ -317,6 +378,7 @@ def test_websocket_custom_response_headers_not_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( websocket_app_with_custom_headers, tracer_provider=self.tracer_provider, + **self.constructor_params, ) self.seed_app(self.app) self.send_input({"type": "websocket.connect"}) @@ -333,3 +395,46 @@ def test_websocket_custom_response_headers_not_in_span_attributes(self): if span.kind == SpanKind.SERVER: for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) + + +SANITIZE_FIELDS_TEST_VALUE = ".*my-secret.*" +SERVER_REQUEST_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*" +SERVER_RESPONSE_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*" + + +class TestCustomHeadersEnv(TestCustomHeaders): + def setUp(self): + os.environ.update( + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: SANITIZE_FIELDS_TEST_VALUE, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: SERVER_REQUEST_TEST_VALUE, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: SERVER_RESPONSE_TEST_VALUE, + } + ) + super().setUp() + + def tearDown(self): + os.environ.pop( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, None + ) + os.environ.pop( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, None + ) + os.environ.pop( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, None + ) + super().tearDown() + + +class TestCustomHeadersConstructor(TestCustomHeaders): + constructor_params = { + "http_capture_headers_sanitize_fields": SANITIZE_FIELDS_TEST_VALUE.split( + "," + ), + "http_capture_headers_server_request": SERVER_REQUEST_TEST_VALUE.split( + "," + ), + "http_capture_headers_server_response": SERVER_RESPONSE_TEST_VALUE.split( + "," + ), + } diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE b/instrumentation/opentelemetry-instrumentation-asyncio/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/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 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/instrumentation/opentelemetry-instrumentation-asyncio/README.rst b/instrumentation/opentelemetry-instrumentation-asyncio/README.rst new file mode 100644 index 0000000000..d78e846793 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/README.rst @@ -0,0 +1,110 @@ +OpenTelemetry asyncio Instrumentation +====================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncio.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-asyncio/ + +AsyncioInstrumentor: Tracing Requests Made by the Asyncio Library + + +The opentelemetry-instrumentation-asyncio package allows tracing asyncio applications. +It also includes metrics for duration and counts of coroutines and futures. Metrics are generated even if coroutines are not traced. + + +Set the names of coroutines you want to trace. +------------------------------------------------- +.. code:: bash + + export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=coro_name,coro_name2,coro_name3 + +If you want to trace specific blocking functions executed with the ``to_thread`` function of asyncio, set the name of the functions in ``OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE``. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +.. code:: bash + + export OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE=func_name,func_name2,func_name3 + +You can enable tracing futures with ``OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED`` +----------------------------------------------------------------------------------------------- +.. code:: bash + + export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true + +Run instrumented application +----------------------------- +1. coroutine +-------------------- +.. code:: python + + # export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=sleep + + import asyncio + from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor + + AsyncioInstrumentor().instrument() + + async def main(): + await asyncio.create_task(asyncio.sleep(0.1)) + + asyncio.run(main()) + +2. future +-------------------- +.. code:: python + + # export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true + + loop = asyncio.get_event_loop() + + future = asyncio.Future() + future.set_result(1) + task = asyncio.ensure_future(future) + loop.run_until_complete(task) + +3. to_thread +-------------------- +.. code:: python + + # export OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE=func + + import asyncio + from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor + + AsyncioInstrumentor().instrument() + + async def main(): + await asyncio.to_thread(func) + + def func(): + pass + + asyncio.run(main()) + + +asyncio metric types +---------------------- + +* `asyncio.process.duration` (seconds) - Duration of asyncio process +* `asyncio.process.count` (count) - Number of asyncio process + + +API +--- + + + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-asyncio + + +References +----------- + +* `OpenTelemetry asyncio/ Tracing `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml new file mode 100644 index 0000000000..540da184be --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-asyncio" +dynamic = ["version"] +description = "OpenTelemetry instrumentation for asyncio" +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", +] +dependencies = [ + "opentelemetry-api ~= 1.14", + "opentelemetry-instrumentation == 0.46b0.dev", + "opentelemetry-semantic-conventions == 0.46b0.dev", + "opentelemetry-test-utils == 0.46b0.dev", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [] + +[project.entry-points.opentelemetry_instrumentor] +asyncio = "opentelemetry.instrumentation.asyncio:AsyncioInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-asyncio" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/asyncio/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py new file mode 100644 index 0000000000..68e3d0839f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py @@ -0,0 +1,375 @@ +# 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. +""" +.. asyncio: https://github.com/python/asyncio + +The opentelemetry-instrumentation-asycnio package allows tracing asyncio applications. +The metric for coroutine, future, is generated even if there is no setting to generate a span. + +Run instrumented application +------------------------------ +1. coroutine +---------------- +.. code:: python + + # export OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE=sleep + + import asyncio + from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor + + AsyncioInstrumentor().instrument() + + async def main(): + await asyncio.create_task(asyncio.sleep(0.1)) + + asyncio.run(main()) + +2. future +------------ +.. code:: python + + # export OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED=true + + loop = asyncio.get_event_loop() + + future = asyncio.Future() + future.set_result(1) + task = asyncio.ensure_future(future) + loop.run_until_complete(task) + +3. to_thread +------------- +.. code:: python + + import asyncio + from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor + + AsyncioInstrumentor().instrument() + + async def main(): + await asyncio.to_thread(func) + + def func(): + pass + + asyncio.run(main()) + + +asyncio metric types +--------------------- + +* asyncio.process.duration (seconds) - Duration of asyncio process +* asyncio.process.count (count) - Number of asyncio process + + +API +--- +""" +import asyncio +import sys +from asyncio import futures +from timeit import default_timer +from typing import Collection + +from wrapt import wrap_function_wrapper as _wrap + +from opentelemetry.instrumentation.asyncio.package import _instruments +from opentelemetry.instrumentation.asyncio.utils import ( + get_coros_to_trace, + get_future_trace_enabled, + get_to_thread_to_trace, +) +from opentelemetry.instrumentation.asyncio.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.metrics import get_meter +from opentelemetry.trace import get_tracer +from opentelemetry.trace.status import Status, StatusCode + +ASYNCIO_PREFIX = "asyncio" + + +class AsyncioInstrumentor(BaseInstrumentor): + """ + An instrumentor for asyncio + + See `BaseInstrumentor` + """ + + methods_with_coroutine = [ + "create_task", + "ensure_future", + "wait_for", + "wait", + "as_completed", + "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): + self._tracer = get_tracer( + __name__, __version__, kwargs.get("tracer_provider") + ) + self._meter = get_meter( + __name__, __version__, kwargs.get("meter_provider") + ) + + self._coros_name_to_trace = get_coros_to_trace() + self._future_active_enabled = get_future_trace_enabled() + self._to_thread_name_to_trace = get_to_thread_to_trace() + + self.process_duration_histogram = self._meter.create_histogram( + name="asyncio.process.duration", + description="Duration of asyncio process", + unit="s", + ) + self.process_created_counter = self._meter.create_counter( + name="asyncio.process.created", + description="Number of asyncio process", + unit="{process}", + ) + + for method in self.methods_with_coroutine: + self.instrument_method_with_coroutine(method) + + self.instrument_gather() + self.instrument_to_thread() + self.instrument_taskgroup_create_task() + + def _uninstrument(self, **kwargs): + for method in self.methods_with_coroutine: + uninstrument_method_with_coroutine(method) + uninstrument_gather() + uninstrument_to_thread() + uninstrument_taskgroup_create_task() + + def instrument_method_with_coroutine(self, method_name: str): + """ + Instruments specified asyncio method. + """ + + def wrap_coro_or_future(method, instance, args, kwargs): + + # If the first argument is a coroutine or future, + # we decorate it with a span and return the task. + if args and len(args) > 0: + first_arg = args[0] + # Check if it's a coroutine or future and wrap it + if asyncio.iscoroutine(first_arg) or futures.isfuture( + first_arg + ): + args = (self.trace_item(first_arg),) + args[1:] + # Check if it's a list and wrap each item + elif isinstance(first_arg, list): + args = ( + [self.trace_item(item) for item in first_arg], + ) + args[1:] + return method(*args, **kwargs) + + _wrap(asyncio, method_name, wrap_coro_or_future) + + def instrument_gather(self): + def wrap_coros_or_futures(method, instance, args, kwargs): + if args and len(args) > 0: + # Check if it's a coroutine or future and wrap it + wrapped_args = tuple(self.trace_item(item) for item in args) + return method(*wrapped_args, **kwargs) + return method(*args, **kwargs) + + _wrap(asyncio, "gather", wrap_coros_or_futures) + + def instrument_to_thread(self) -> None: + # to_thread was added in Python 3.9 + if sys.version_info < (3, 9): + return + + def wrap_to_thread(method, instance, args, kwargs) -> None: + if args: + first_arg = args[0] + # Wrap the first argument + wrapped_first_arg = self.trace_to_thread(first_arg) + wrapped_args = (wrapped_first_arg,) + args[1:] + + return method(*wrapped_args, **kwargs) + return method(*args, **kwargs) + + _wrap(asyncio, "to_thread", wrap_to_thread) + + def instrument_taskgroup_create_task(self) -> None: + # TaskGroup.create_task was added in Python 3.11 + if sys.version_info < (3, 11): + return + + def wrap_taskgroup_create_task(method, instance, args, kwargs) -> None: + if args: + coro = args[0] + wrapped_coro = self.trace_coroutine(coro) + wrapped_args = (wrapped_coro,) + args[1:] + return method(*wrapped_args, **kwargs) + return method(*args, **kwargs) + + _wrap( + asyncio.TaskGroup, # pylint: disable=no-member + "create_task", + wrap_taskgroup_create_task, + ) + + def trace_to_thread(self, func: callable): + """Trace a function.""" + start = default_timer() + span = ( + self._tracer.start_span( + f"{ASYNCIO_PREFIX} to_thread-" + func.__name__ + ) + if func.__name__ in self._to_thread_name_to_trace + else None + ) + attr = {"type": "to_thread", "name": func.__name__} + exception = None + try: + attr["state"] = "finished" + return func + except Exception: + attr["state"] = "exception" + raise + finally: + self.record_process(start, attr, span, exception) + + def trace_item(self, coro_or_future): + """Trace a coroutine or future item.""" + # Task is already traced, return it + if isinstance(coro_or_future, asyncio.Task): + return coro_or_future + if asyncio.iscoroutine(coro_or_future): + return self.trace_coroutine(coro_or_future) + if futures.isfuture(coro_or_future): + return self.trace_future(coro_or_future) + return coro_or_future + + async def trace_coroutine(self, coro): + start = default_timer() + attr = { + "type": "coroutine", + "name": coro.__name__, + } + span = ( + self._tracer.start_span(f"{ASYNCIO_PREFIX} coro-" + coro.__name__) + if coro.__name__ in self._coros_name_to_trace + else None + ) + exception = None + try: + attr["state"] = "finished" + return await coro + # CancelledError is raised when a coroutine is cancelled + # before it has a chance to run. We don't want to record + # this as an error. + except asyncio.CancelledError: + attr["state"] = "cancelled" + except Exception as exc: + exception = exc + state = determine_state(exception) + attr["state"] = state + raise + finally: + self.record_process(start, attr, span, exception) + + def trace_future(self, future): + start = default_timer() + span = ( + self._tracer.start_span(f"{ASYNCIO_PREFIX} future") + if self._future_active_enabled + else None + ) + + def callback(f): + exception = f.exception() + attr = { + "type": "future", + } + state = determine_state(exception) + attr["state"] = state + self.record_process(start, attr, span, exception) + + future.add_done_callback(callback) + return future + + def record_process( + self, start: float, attr: dict, span=None, exception=None + ) -> None: + """ + Record the processing time, update histogram and counter, and handle span. + + :param start: Start time of the process. + :param attr: Attributes for the histogram and counter. + :param span: Optional span for tracing. + :param exception: Optional exception occurred during the process. + """ + duration = max(default_timer() - start, 0) + self.process_duration_histogram.record(duration, attr) + self.process_created_counter.add(1, attr) + + if span: + if span.is_recording() and exception: + span.set_status(Status(StatusCode.ERROR)) + span.record_exception(exception) + span.end() + + +def determine_state(exception: Exception) -> str: + if isinstance(exception, asyncio.CancelledError): + return "cancelled" + if isinstance(exception, asyncio.TimeoutError): + return "timeout" + if exception: + return "exception" + return "finished" + + +def uninstrument_taskgroup_create_task() -> None: + # TaskGroup.create_task was added in Python 3.11 + if sys.version_info < (3, 11): + return + unwrap(asyncio.TaskGroup, "create_task") # pylint: disable=no-member + + +def uninstrument_to_thread() -> None: + # to_thread was added in Python 3.9 + if sys.version_info < (3, 9): + return + unwrap(asyncio, "to_thread") + + +def uninstrument_gather() -> None: + unwrap(asyncio, "gather") + + +def uninstrument_method_with_coroutine(method_name: str) -> None: + """ + Uninstrument specified asyncio method. + """ + unwrap(asyncio, method_name) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/environment_variables.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/environment_variables.py new file mode 100644 index 0000000000..7420ea362f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/environment_variables.py @@ -0,0 +1,34 @@ +# 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. + +""" +Enter the names of the coroutines to be traced through the environment variable below, separated by commas. +""" +OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE = ( + "OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE" +) + +""" +To determines whether the tracing feature for Future of Asyncio in Python is enabled or not. +""" +OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED = ( + "OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED" +) + +""" +Enter the names of the functions to be traced through the environment variable below, separated by commas. +""" +OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE = ( + "OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE" +) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/package.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/package.py new file mode 100644 index 0000000000..484f6e9c58 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/package.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. + +_instruments = tuple() diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/utils.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/utils.py new file mode 100644 index 0000000000..15c78ae1ce --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/utils.py @@ -0,0 +1,67 @@ +# 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 os +from typing import Set + +# pylint: disable=no-name-in-module +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, + OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, + OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE, +) + + +def separate_coro_names_by_comma(coro_names: str) -> Set[str]: + """ + Function to separate the coroutines to be traced by comma + """ + if coro_names is None: + return set() + return {coro_name.strip() for coro_name in coro_names.split(",")} + + +def get_coros_to_trace() -> set: + """ + Function to get the coroutines to be traced from the environment variable + """ + coro_names = os.getenv(OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE) + return separate_coro_names_by_comma(coro_names) + + +def get_future_trace_enabled() -> bool: + """ + Function to get the future active enabled flag from the environment variable + default value is False + """ + return ( + os.getenv(OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, "False").lower() + == "true" + ) + + +def get_to_thread_to_trace() -> set: + """ + Function to get the functions to be traced from the environment variable + """ + func_names = os.getenv( + OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE + ) + return separate_coro_names_by_comma(func_names) + + +__all__ = [ + "get_coros_to_trace", + "get_future_trace_enabled", + "get_to_thread_to_trace", +] diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/version.py new file mode 100644 index 0000000000..ff4933b20b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/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.46b0.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..14f724888b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/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/instrumentation/opentelemetry-instrumentation-asyncio/tests/common_test_func.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/common_test_func.py new file mode 100644 index 0000000000..5d06641bc0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/common_test_func.py @@ -0,0 +1,48 @@ +# 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 + + +async def async_func(): + await asyncio.sleep(0.1) + + +async def factorial(number: int): + factorial_value = 1 + for value in range(2, number + 1): + await asyncio.sleep(0) + factorial_value *= value + return factorial_value + + +async def cancellable_coroutine(): + await asyncio.sleep(2) + + +async def cancellation_coro(): + task = asyncio.create_task(cancellable_coroutine()) + + await asyncio.sleep(0.1) + task.cancel() + + await task + + +async def cancellation_create_task(): + await asyncio.create_task(cancellation_coro()) + + +async def ensure_future(): + await asyncio.ensure_future(asyncio.sleep(0)) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_cancellation.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_cancellation.py new file mode 100644 index 0000000000..9172cd4458 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_cancellation.py @@ -0,0 +1,78 @@ +# 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 SpanKind, get_tracer + +from .common_test_func import cancellation_create_task + + +class TestAsyncioCancel(TestBase): + @patch.dict( + "os.environ", + { + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "cancellation_coro, cancellable_coroutine" + }, + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + def test_cancel(self): + with self._tracer.start_as_current_span("root", kind=SpanKind.SERVER): + asyncio.run(cancellation_create_task()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + self.assertEqual(spans[0].context.trace_id, spans[1].context.trace_id) + self.assertEqual(spans[2].context.trace_id, spans[1].context.trace_id) + + self.assertEqual(spans[0].name, "asyncio coro-cancellable_coroutine") + self.assertEqual(spans[1].name, "asyncio coro-cancellation_coro") + for metric in ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ): + if metric.name == "asyncio.process.duration": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "coroutine") + self.assertIn( + point.attributes["name"], + ["cancellation_coro", "cancellable_coroutine"], + ) + if metric.name == "asyncio.process.created": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "coroutine") + self.assertIn( + point.attributes["name"], + ["cancellation_coro", "cancellable_coroutine"], + ) + self.assertIn( + point.attributes["state"], ["finished", "cancelled"] + ) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_create_task.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_create_task.py new file mode 100644 index 0000000000..9df8d9d14b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_create_task.py @@ -0,0 +1,52 @@ +# 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 + +from .common_test_func import factorial + + +class TestAsyncioCreateTask(TestBase): + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "sleep"} + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + def test_asyncio_create_task(self): + async def async_func(): + await asyncio.create_task(asyncio.sleep(0)) + await asyncio.create_task(factorial(3)) + + asyncio.run(async_func()) + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].name, "asyncio coro-sleep") diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_ensure_future.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_ensure_future.py new file mode 100644 index 0000000000..4907aa4bf8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_ensure_future.py @@ -0,0 +1,91 @@ +# 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 + +import pytest + +# pylint: disable=no-name-in-module +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 + +from .common_test_func import async_func + + +class TestAsyncioEnsureFuture(TestBase): + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"} + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + @pytest.mark.asyncio + def test_asyncio_loop_ensure_future(self): + """ + async_func is not traced because it is not set in the environment variable + """ + + async def test(): + task = asyncio.ensure_future(async_func()) + await task + + asyncio.run(test()) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + + @pytest.mark.asyncio + def test_asyncio_ensure_future_with_future(self): + async def test(): + with self._tracer.start_as_current_span("root"): + future = asyncio.Future() + future.set_result(1) + task = asyncio.ensure_future(future) + await task + + asyncio.run(test()) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "root": + self.assertEqual(span.parent, None) + if span.name == "asyncio future": + self.assertNotEqual(span.parent.trace_id, 0) + + for metric in ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ): + if metric.name == "asyncio.process.duration": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "future") + if metric.name == "asyncio.process.created": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "future") + self.assertEqual(point.attributes["state"], "finished") diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_gather.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_gather.py new file mode 100644 index 0000000000..395b46b698 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_gather.py @@ -0,0 +1,53 @@ +# 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 + +from .common_test_func import factorial + + +class TestAsyncioGather(TestBase): + @patch.dict( + "os.environ", + {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "factorial"}, + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + def test_asyncio_gather(self): + async def gather_factorial(): + await asyncio.gather(factorial(2), factorial(3), factorial(4)) + + asyncio.run(gather_factorial()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 3) + self.assertEqual(spans[0].name, "asyncio coro-factorial") + self.assertEqual(spans[1].name, "asyncio coro-factorial") + self.assertEqual(spans[2].name, "asyncio coro-factorial") diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_integration.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_integration.py new file mode 100644 index 0000000000..7f4723ec24 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_integration.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. +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 + +from .common_test_func import ensure_future + + +class TestAsyncioInstrumentor(TestBase): + def setUp(self): + super().setUp() + self._tracer = get_tracer( + __name__, + ) + + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "sleep"} + ) + def test_asyncio_integration(self): + AsyncioInstrumentor().instrument() + + asyncio.run(ensure_future()) + spans = self.memory_exporter.get_finished_spans() + self.memory_exporter.clear() + assert spans + AsyncioInstrumentor().uninstrument() + + asyncio.run(ensure_future()) + spans = self.memory_exporter.get_finished_spans() + assert not spans diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_run_coroutine_threadsafe.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_run_coroutine_threadsafe.py new file mode 100644 index 0000000000..fdf4bcb353 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_run_coroutine_threadsafe.py @@ -0,0 +1,61 @@ +# 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 +import threading +from concurrent.futures import ThreadPoolExecutor +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 TestRunCoroutineThreadSafe(TestBase): + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "coro"} + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self.loop = asyncio.new_event_loop() + self.executor = ThreadPoolExecutor(max_workers=1) + self.loop.set_default_executor(self.executor) + self.thread = threading.Thread(target=self.loop.run_forever) + self.thread.start() + + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + self.loop.call_soon_threadsafe(self.loop.stop) + self.thread.join() + self.loop.close() + + AsyncioInstrumentor().uninstrument() + + def test_run_coroutine_threadsafe(self): + async def coro(): + return 42 + + future = asyncio.run_coroutine_threadsafe(coro(), self.loop) + result = future.result(timeout=1) + self.assertEqual(result, 42) + spans = self.memory_exporter.get_finished_spans() + assert spans diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_taskgroup.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_taskgroup.py new file mode 100644 index 0000000000..e02f63aa42 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_taskgroup.py @@ -0,0 +1,63 @@ +# 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 +import sys +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 + +from .common_test_func import async_func + +py11 = False +if sys.version_info >= (3, 11): + py11 = True + + +class TestAsyncioTaskgroup(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() + + def test_task_group_create_task(self): + # TaskGroup is only available in Python 3.11+ + if not py11: + return + + async def main(): + async with asyncio.TaskGroup() as tg: # pylint: disable=no-member + for _ in range(10): + tg.create_task(async_func()) + + with self._tracer.start_as_current_span("root"): + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 11) + self.assertEqual(spans[4].context.trace_id, spans[5].context.trace_id) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_to_thread.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_to_thread.py new file mode 100644 index 0000000000..b53a6edc08 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_to_thread.py @@ -0,0 +1,73 @@ +# 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 +import sys +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_TO_THREAD_FUNCTION_NAMES_TO_TRACE, +) +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import get_tracer + + +class TestAsyncioToThread(TestBase): + @patch.dict( + "os.environ", + {OTEL_PYTHON_ASYNCIO_TO_THREAD_FUNCTION_NAMES_TO_TRACE: "multiply"}, + ) + def setUp(self): + super().setUp() + AsyncioInstrumentor().instrument() + self._tracer = get_tracer( + __name__, + ) + + def tearDown(self): + super().tearDown() + AsyncioInstrumentor().uninstrument() + + def test_to_thread(self): + # to_thread is only available in Python 3.9+ + if sys.version_info >= (3, 9): + + def multiply(x, y): + return x * y + + async def to_thread(): + result = await asyncio.to_thread(multiply, 2, 3) + assert result == 6 + + with self._tracer.start_as_current_span("root"): + asyncio.run(to_thread()) + spans = self.memory_exporter.get_finished_spans() + + self.assertEqual(len(spans), 2) + assert spans[0].name == "asyncio to_thread-multiply" + for metric in ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ): + if metric.name == "asyncio.process.duration": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "to_thread") + self.assertEqual(point.attributes["name"], "multiply") + if metric.name == "asyncio.process.created": + for point in metric.data.data_points: + self.assertEqual(point.attributes["type"], "to_thread") + self.assertEqual(point.attributes["name"], "multiply") diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_utils.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_utils.py new file mode 100644 index 0000000000..f3974e2d43 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_utils.py @@ -0,0 +1,44 @@ +# 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 unittest import TestCase +from unittest.mock import patch + +# pylint: disable=no-name-in-module +from opentelemetry.instrumentation.asyncio.environment_variables import ( + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE, + OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED, +) +from opentelemetry.instrumentation.asyncio.utils import ( + get_coros_to_trace, + get_future_trace_enabled, +) + + +class TestAsyncioToThread(TestCase): + @patch.dict( + "os.environ", + { + OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "test1,test2,test3 ,test4" + }, + ) + def test_separator(self): + self.assertEqual( + get_coros_to_trace(), {"test1", "test2", "test3", "test4"} + ) + + @patch.dict( + "os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"} + ) + def test_future_trace_enabled(self): + self.assertEqual(get_future_trace_enabled(), True) diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py new file mode 100644 index 0000000000..77064aeafa --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_wait.py @@ -0,0 +1,88 @@ +# 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 +import sys +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 + +from .common_test_func import async_func + + +class TestAsyncioWait(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() + + def test_asyncio_wait_with_create_task(self): + async def main(): + if sys.version_info >= (3, 11): + # In Python 3.11, you can't send coroutines directly to asyncio.wait(). + # Instead, you must wrap them in asyncio.create_task(). + tasks = [ + asyncio.create_task(async_func()), + asyncio.create_task(async_func()), + ] + await asyncio.wait(tasks) + else: + await asyncio.wait([async_func(), async_func()]) + + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + + def test_asyncio_wait_for(self): + async def main(): + await asyncio.wait_for(async_func(), 1) + await asyncio.wait_for(async_func(), 1) + + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + + def test_asyncio_as_completed(self): + async def main(): + if sys.version_info >= (3, 11): + # In Python 3.11, you can't send coroutines directly to asyncio.as_completed(). + # Instead, you must wrap them in asyncio.create_task(). + tasks = [ + asyncio.create_task(async_func()), + asyncio.create_task(async_func()), + ] + for task in asyncio.as_completed(tasks): + await task + else: + for task in asyncio.as_completed([async_func(), async_func()]): + await task + + asyncio.run(main()) + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) 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 4c9bc8c727..11c579f96a 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -84,7 +84,7 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict: span_attributes[SpanAttributes.NET_PEER_NAME] = addr span_attributes[ SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.UNIX.value + ] = NetTransportValues.OTHER.value if query is not None: span_attributes[SpanAttributes.DB_STATEMENT] = query @@ -96,18 +96,25 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict: class AsyncPGInstrumentor(BaseInstrumentor): + + _leading_comment_remover = re.compile(r"^/\*.*?\*/") + _tracer = None + def __init__(self, capture_parameters=False): super().__init__() self.capture_parameters = capture_parameters - self._tracer = None - self._leading_comment_remover = re.compile(r"^/\*.*?\*/") def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") - self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self._tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) for method in [ "Connection.execute", diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt new file mode 100644 index 0000000000..02d8fb2041 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +async-timeout==4.0.3 +asyncpg==0.29.0 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-asyncpg/tests/test_asyncpg_wrapper.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py index a3cbbfcecb..12aad0c6dc 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py @@ -5,7 +5,7 @@ class TestAsyncPGInstrumentation(TestBase): - def test_duplicated_instrumentation(self): + def test_duplicated_instrumentation_can_be_uninstrumented(self): AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().instrument() @@ -16,6 +16,14 @@ def test_duplicated_instrumentation(self): hasattr(method, "_opentelemetry_ext_asyncpg_applied") ) + def test_duplicated_instrumentation_works(self): + first = AsyncPGInstrumentor() + first.instrument() + second = AsyncPGInstrumentor() + second.instrument() + self.assertIsNotNone(first._tracer) + self.assertIsNotNone(second._tracer) + def test_duplicated_uninstrumentation(self): AsyncPGInstrumentor().instrument() AsyncPGInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml index 97c0a01245..3aa065a09a 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/pyproject.toml @@ -22,16 +22,13 @@ classifiers = [ "Programming Language :: Python :: 3.8", ] dependencies = [ - "opentelemetry-instrumentation == 0.41b0", + "opentelemetry-instrumentation == 0.45b0", "opentelemetry-propagator-aws-xray == 1.0.1", - "opentelemetry-semantic-conventions == 0.41b0", + "opentelemetry-semantic-conventions == 0.45b0", ] [project.optional-dependencies] instruments = [] -test = [ - "opentelemetry-test-utils == 0.41b0", -] [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/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt new file mode 100644 index 0000000000..53e5b9ce6f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 259375c481..a878d0f06a 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -19,3 +19,7 @@ def handler(event, context): def rest_api_handler(event, context): return {"statusCode": 200, "body": "200 ok"} + + +def handler_exc(event, context): + 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 1df7499d31..9bf6f47d7b 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,10 +17,12 @@ from typing import Any, Callable, Dict from unittest import mock -from mocks.api_gateway_http_api_event import ( +from tests.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 +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 ( @@ -38,7 +40,7 @@ from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import NoOpTracerProvider, SpanKind +from opentelemetry.trace import NoOpTracerProvider, SpanKind, StatusCode from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) @@ -52,7 +54,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 @@ -103,7 +105,7 @@ def setUp(self): super().setUp() self.common_env_patch = mock.patch.dict( "os.environ", - {_HANDLER: "mocks.lambda_function.handler"}, + {_HANDLER: "tests.mocks.lambda_function.handler"}, ) self.common_env_patch.start() @@ -145,6 +147,11 @@ def test_active_tracing(self): { ResourceAttributes.FAAS_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn, SpanAttributes.FAAS_EXECUTION: MOCK_LAMBDA_CONTEXT.aws_request_id, + ResourceAttributes.CLOUD_ACCOUNT_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn.split( + ":" + )[ + 4 + ], }, ) @@ -356,7 +363,7 @@ def test_lambda_handles_multiple_consumers(self): def test_api_gateway_proxy_event_sets_attributes(self): handler_patch = mock.patch.dict( "os.environ", - {_HANDLER: "mocks.lambda_function.rest_api_handler"}, + {_HANDLER: "tests.mocks.lambda_function.rest_api_handler"}, ) handler_patch.start() @@ -408,6 +415,27 @@ def test_lambda_handles_list_event(self): assert spans + def test_lambda_handles_handler_exception(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() + + 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/src/opentelemetry/instrumentation/boto/__init__.py b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py index 84c4e54a86..c92ccc8106 100644 --- a/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto/src/opentelemetry/instrumentation/boto/__init__.py @@ -91,7 +91,10 @@ def _instrument(self, **kwargs): # pylint: disable=attribute-defined-outside-init self._tracer = get_tracer( - __name__, __version__, kwargs.get("tracer_provider") + __name__, + __version__, + kwargs.get("tracer_provider"), + schema_url="https://opentelemetry.io/schemas/1.11.0", ) wrap_function_wrapper( diff --git a/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt new file mode 100644 index 0000000000..92c356ebe1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-boto/test-requirements.txt @@ -0,0 +1,71 @@ +annotated-types==0.6.0 +asgiref==3.7.2 +attrs==23.2.0 +aws-sam-translator==1.85.0 +aws-xray-sdk==2.12.1 +boto==2.49.0 +boto3==1.34.44 +botocore==1.34.44 +certifi==2024.2.2 +cffi==1.16.0 +cfn-lint==0.85.2 +charset-normalizer==3.3.2 +cryptography==42.0.3 +Deprecated==1.2.14 +docker==7.0.0 +ecdsa==0.18.0 +graphql-core==3.2.3 +idna==3.6 +importlib-metadata==6.11.0 +importlib-resources==6.1.1 +iniconfig==2.0.0 +Jinja2==3.1.3 +jmespath==1.0.1 +jschema-to-python==1.2.3 +jsondiff==2.0.0 +jsonpatch==1.33 +jsonpickle==3.0.2 +jsonpointer==2.4 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +junit-xml==1.9 +MarkupSafe==2.1.5 +moto==2.3.2 +mpmath==1.3.0 +networkx==3.1 +packaging==23.2 +pbr==6.0.0 +pkgutil_resolve_name==1.3.10 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pyasn1==0.5.1 +pycparser==2.21 +pydantic==2.6.1 +pydantic_core==2.16.2 +pytest==7.1.3 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +python-jose==3.3.0 +pytz==2024.1 +PyYAML==6.0.1 +referencing==0.33.0 +regex==2023.12.25 +requests==2.31.0 +responses==0.25.0 +rpds-py==0.18.0 +rsa==4.9 +s3transfer==0.10.0 +sarif-om==1.0.4 +six==1.16.0 +sshpubkeys==3.3.1 +sympy==1.12 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==1.26.18 +Werkzeug==3.0.1 +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/src/opentelemetry/instrumentation/boto3sqs/__init__.py b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py index c34be82189..ee7f4a59a6 100644 --- a/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/src/opentelemetry/instrumentation/boto3sqs/__init__.py @@ -38,7 +38,7 @@ from opentelemetry import context, propagate, trace from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import ( - _SUPPRESS_INSTRUMENTATION_KEY, + is_instrumentation_enabled, unwrap, ) from opentelemetry.propagators.textmap import CarrierT, Getter, Setter @@ -218,7 +218,7 @@ def _create_processing_span( def _wrap_send_message(self, sqs_class: type) -> None: def send_wrapper(wrapped, instance, args, kwargs): - if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + if not is_instrumentation_enabled(): return wrapped(*args, **kwargs) queue_url = kwargs.get("QueueUrl") # The method expect QueueUrl and Entries params, so if they are None, we call wrapped to receive the @@ -252,7 +252,7 @@ def send_batch_wrapper(wrapped, instance, args, kwargs): # The method expect QueueUrl and Entries params, so if they are None, we call wrapped to receive the # original exception if ( - context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) + not is_instrumentation_enabled() or not queue_url or not entries ): @@ -422,7 +422,10 @@ def _instrument(self, **kwargs: Dict[str, Any]) -> None: "tracer_provider" ) self._tracer: Tracer = trace.get_tracer( - __name__, __version__, self._tracer_provider + __name__, + __version__, + self._tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._wrap_client_creation() diff --git a/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt new file mode 100644 index 0000000000..2af3346e6d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-boto3sqs/test-requirements.txt @@ -0,0 +1,24 @@ +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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.18 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-boto3sqs diff --git a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml index c02c3bdf80..ad92b35417 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-botocore/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.12", - "opentelemetry-instrumentation == 0.41b0", - "opentelemetry-semantic-conventions == 0.41b0", + "opentelemetry-instrumentation == 0.45b0", + "opentelemetry-semantic-conventions == 0.45b0", "opentelemetry-propagator-aws-xray == 1.0.1", ] diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/dynamodb.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/dynamodb.py index da389415c7..1a5f01b6ce 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/dynamodb.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/dynamodb.py @@ -28,6 +28,7 @@ from opentelemetry.trace.span import Span from opentelemetry.util.types import AttributeValue +# pylint: disable=invalid-name _AttributePathT = Union[str, Tuple[str]] 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 d90ca0c530..a2b5ac0874 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 @@ -102,13 +102,13 @@ def _inject_current_span(cls, call_context: _AwsSdkCallContext): # Lambda extension ################################################################################ -_OPERATION_MAPPING = { +_OPERATION_MAPPING: Dict[str, _LambdaOperation] = { op.operation_name(): op for op in globals().values() if inspect.isclass(op) and issubclass(op, _LambdaOperation) and not inspect.isabstract(op) -} # type: Dict[str, _LambdaOperation] +} class _LambdaExtension(_AwsSdkExtension): 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 7849daa286..aa55ae697f 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 @@ -81,6 +81,10 @@ def extract_attributes( ] = MessagingDestinationKindValues.TOPIC.value attributes[SpanAttributes.MESSAGING_DESTINATION] = destination_name + # TODO: Use SpanAttributes.MESSAGING_DESTINATION_NAME when opentelemetry-semantic-conventions 0.42b0 is released + attributes["messaging.destination.name"] = cls._extract_input_arn( + call_context + ) call_context.span_name = ( f"{'phone_number' if is_phone_number else destination_name} send" ) @@ -139,13 +143,13 @@ def before_service_call(cls, call_context: _AwsSdkCallContext, span: Span): # SNS extension ################################################################################ -_OPERATION_MAPPING = { +_OPERATION_MAPPING: Dict[str, _SnsOperation] = { op.operation_name(): op for op in globals().values() if inspect.isclass(op) and issubclass(op, _SnsOperation) and not inspect.isabstract(op) -} # type: Dict[str, _SnsOperation] +} class _SnsExtension(_AwsSdkExtension): diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/types.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/types.py index b6a1c3aa57..a3c73af65c 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/types.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/types.py @@ -57,23 +57,21 @@ def __init__(self, client: _BotoClientT, args: Tuple[str, Dict[str, Any]]): boto_meta = client.meta service_model = boto_meta.service_model - self.service = service_model.service_name.lower() # type: str - self.operation = operation # type: str - self.params = params # type: Dict[str, Any] + self.service = service_model.service_name.lower() + self.operation = operation + self.params = params # 'operation' and 'service' are essential for instrumentation. # for all other attributes we extract them defensively. All of them should # usually exist unless some future botocore version moved things. - self.region = self._get_attr( - boto_meta, "region_name" - ) # type: Optional[str] - self.endpoint_url = self._get_attr( + self.region: Optional[str] = self._get_attr(boto_meta, "region_name") + self.endpoint_url: Optional[str] = self._get_attr( boto_meta, "endpoint_url" - ) # type: Optional[str] + ) - self.api_version = self._get_attr( + self.api_version: Optional[str] = self._get_attr( service_model, "api_version" - ) # type: Optional[str] + ) # name of the service in proper casing self.service_id = str( self._get_attr(service_model, "service_id", self.service) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt new file mode 100644 index 0000000000..9dcf9f9c0d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-botocore/test-requirements.txt @@ -0,0 +1,70 @@ +annotated-types==0.6.0 +asgiref==3.7.2 +attrs==23.2.0 +aws-sam-translator==1.85.0 +aws-xray-sdk==2.12.1 +boto3==1.28.80 +botocore==1.31.80 +certifi==2024.2.2 +cffi==1.16.0 +cfn-lint==0.85.2 +charset-normalizer==3.3.2 +cryptography==42.0.5 +Deprecated==1.2.14 +docker==7.0.0 +ecdsa==0.18.0 +idna==3.6 +importlib-metadata==6.11.0 +importlib-resources==6.1.1 +iniconfig==2.0.0 +Jinja2==3.1.3 +jmespath==1.0.1 +jschema-to-python==1.2.3 +jsondiff==2.0.0 +jsonpatch==1.33 +jsonpickle==3.0.3 +jsonpointer==2.4 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +junit-xml==1.9 +MarkupSafe==2.0.1 +moto==2.2.20 +mpmath==1.3.0 +networkx==3.1 +packaging==23.2 +pbr==6.0.0 +pkgutil_resolve_name==1.3.10 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pyasn1==0.5.1 +pycparser==2.21 +pydantic==2.6.2 +pydantic_core==2.16.3 +pytest==7.1.3 +pytest-benchmark==4.0.0 +python-dateutil==2.8.2 +python-jose==3.3.0 +pytz==2024.1 +PyYAML==6.0.1 +referencing==0.33.0 +regex==2023.12.25 +requests==2.31.0 +responses==0.25.0 +rpds-py==0.18.0 +rsa==4.9 +s3transfer==0.7.0 +sarif-om==1.0.4 +six==1.16.0 +sshpubkeys==3.3.1 +sympy==1.12 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==1.26.18 +Werkzeug==2.1.2 +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_instrumentation.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py index 3d25dcbf2d..bb6d283399 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py @@ -27,14 +27,11 @@ ) from opentelemetry import trace as trace_api -from opentelemetry.context import ( - _SUPPRESS_HTTP_INSTRUMENTATION_KEY, - attach, - detach, - set_value, -) from opentelemetry.instrumentation.botocore import BotocoreInstrumentor -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import ( + suppress_http_instrumentation, + suppress_instrumentation, +) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.propagators.aws.aws_xray_propagator import TRACE_HEADER_KEY from opentelemetry.semconv.trace import SpanAttributes @@ -341,23 +338,17 @@ def check_headers(**kwargs): @mock_xray def test_suppress_instrumentation_xray_client(self): xray_client = self._make_client("xray") - token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - try: + with suppress_instrumentation(): xray_client.put_trace_segments(TraceSegmentDocuments=["str1"]) xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) - finally: - detach(token) self.assertEqual(0, len(self.get_finished_spans())) @mock_xray def test_suppress_http_instrumentation_xray_client(self): xray_client = self._make_client("xray") - token = attach(set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)) - try: + with suppress_http_instrumentation(): xray_client.put_trace_segments(TraceSegmentDocuments=["str1"]) xray_client.put_trace_segments(TraceSegmentDocuments=["str2"]) - finally: - detach(token) self.assertEqual(2, len(self.get_finished_spans())) @mock_s3 diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py index 33f2531027..e2b4c55732 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_sns.py @@ -118,6 +118,12 @@ def _test_publish_to_arn(self, arg_name: str): self.topic_name, span.attributes[SpanAttributes.MESSAGING_DESTINATION], ) + self.assertEqual( + target_arn, + # TODO: Use SpanAttributes.MESSAGING_DESTINATION_NAME when + # opentelemetry-semantic-conventions 0.42b0 is released + span.attributes["messaging.destination.name"], + ) @mock_sns def test_publish_to_phone_number(self): @@ -184,6 +190,12 @@ def test_publish_batch_to_topic(self): self.topic_name, span.attributes[SpanAttributes.MESSAGING_DESTINATION], ) + self.assertEqual( + topic_arn, + # TODO: Use SpanAttributes.MESSAGING_DESTINATION_NAME when + # opentelemetry-semantic-conventions 0.42b0 is released + span.attributes["messaging.destination.name"], + ) self.assert_injected_span(message1_attrs, span) self.assert_injected_span(message2_attrs, span) diff --git a/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt new file mode 100644 index 0000000000..f55190171d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-cassandra/test-requirements.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/test-requirements-0.txt b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt new file mode 100644 index 0000000000..98c661ca67 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-0.txt @@ -0,0 +1,32 @@ +amqp==5.2.0 +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +prompt-toolkit==3.0.43 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..516e1a78b9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt @@ -0,0 +1,31 @@ +amqp==5.2.0 +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +prompt-toolkit==3.0.43 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-confluent-kafka/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt new file mode 100644 index 0000000000..be859a2ce1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-confluent-kafka/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +confluent-kafka==2.3.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index 6d7e37a45f..b0acbed185 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -264,6 +264,7 @@ def __init__( self._name, instrumenting_library_version=self._version, tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self.capture_parameters = capture_parameters self.enable_commenter = enable_commenter @@ -426,14 +427,14 @@ def traced_execution( if args and self._commenter_enabled: try: args_list = list(args) - commenter_data = dict( + 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__, - driver_paramstyle=self._connect_module.paramstyle, - ) + "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__, + "driver_paramstyle": self._connect_module.paramstyle, + } if self._commenter_options.get( "opentelemetry_values", True ): diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt new file mode 100644 index 0000000000..46c02707c1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-dbapi/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index d545a1950b..37ac760283 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -300,8 +300,14 @@ def _instrument(self, **kwargs): __name__, __version__, tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) + meter = get_meter( + __name__, + __version__, + meter_provider=meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) - meter = get_meter(__name__, __version__, meter_provider=meter_provider) _DjangoMiddleware._tracer = tracer _DjangoMiddleware._meter = meter _DjangoMiddleware._excluded_urls = ( @@ -316,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 491e78cab5..bc677a81cf 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 @@ -43,10 +43,17 @@ from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + SanitizeValue, _parse_active_request_count_attrs, _parse_duration_attrs, + get_custom_headers, get_excluded_urls, get_traced_request_attrs, + normalise_request_header_name, + normalise_response_header_name, ) try: @@ -91,10 +98,7 @@ def __call__(self, request): try: from opentelemetry.instrumentation.asgi import asgi_getter, asgi_setter from opentelemetry.instrumentation.asgi import ( - collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes, - ) - from opentelemetry.instrumentation.asgi import ( - collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes, + collect_custom_headers_attributes as asgi_collect_custom_headers_attributes, ) from opentelemetry.instrumentation.asgi import ( collect_request_attributes as asgi_collect_request_attributes, @@ -108,7 +112,6 @@ def __call__(self, request): set_status_code = None _is_asgi_supported = False - _logger = getLogger(__name__) _attributes_by_preference = [ [ @@ -249,7 +252,18 @@ def process_request(self, request): ) if span.is_recording() and span.kind == SpanKind.SERVER: attributes.update( - asgi_collect_custom_request_attributes(carrier) + asgi_collect_custom_headers_attributes( + carrier, + SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ), + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST + ), + normalise_request_header_name, + ) ) else: if span.is_recording() and span.kind == SpanKind.SERVER: @@ -336,8 +350,17 @@ def process_response(self, request, response): for key, value in response.items(): asgi_setter.set(custom_headers, key, value) - custom_res_attributes = ( - asgi_collect_custom_response_attributes(custom_headers) + custom_res_attributes = asgi_collect_custom_headers_attributes( + custom_headers, + SanitizeValue( + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS + ) + ), + get_custom_headers( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE + ), + normalise_response_header_name, ) for key, value in custom_res_attributes.items(): span.set_attribute(key, value) 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 89d8a9b776..30492a8be5 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 @@ -1,5 +1,3 @@ -#!/usr/bin/python -# # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); 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..6dce957000 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-0.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +Django==2.2.28 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +pytz==2024.1 +sqlparse==0.4.4 +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..116dc015ec --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-1.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +Django==3.2.24 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +pytz==2024.1 +sqlparse==0.4.4 +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..ac3b40e16b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-2.txt @@ -0,0 +1,23 @@ +asgiref==3.7.2 +attrs==23.2.0 +backports.zoneinfo==0.2.1 +Deprecated==1.2.14 +Django==4.2.10 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +sqlparse==0.4.4 +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..3bb32c4c6b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/test-requirements-3.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +Django==4.2.10 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +sqlparse==0.4.4 +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..4d221fae62 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -390,7 +390,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 +398,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-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py index 480ccb6402..0f5056de83 100644 --- a/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/src/opentelemetry/instrumentation/elasticsearch/__init__.py @@ -140,7 +140,12 @@ def _instrument(self, **kwargs): Instruments Elasticsearch module """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) request_hook = kwargs.get("request_hook") response_hook = kwargs.get("response_hook") if es_transport_split: @@ -167,6 +172,7 @@ def _instrument(self, **kwargs): ) def _uninstrument(self, **kwargs): + # pylint: disable=no-member unwrap(elasticsearch.Transport, "perform_request") @@ -239,9 +245,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: 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..216d1c0b02 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-0.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +elasticsearch==2.4.1 +elasticsearch-dsl==2.2.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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.18 +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..2c51c87508 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-1.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +elasticsearch==5.5.3 +elasticsearch-dsl==5.4.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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.1 +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..4bd1d0d318 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-elasticsearch/test-requirements-2.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +elasticsearch==6.8.2 +elasticsearch-dsl==6.4.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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.1 +wrapt==1.16.0 +zipp==3.17.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-elasticsearch 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 669f41b0ab..06a550cf3f 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -254,13 +254,21 @@ def __init__(self, *args, **kwargs): self._middlewares_list = [self._middlewares_list] self._otel_tracer = trace.get_tracer( - __name__, __version__, tracer_provider + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) + self._otel_meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) - self._otel_meter = get_meter(__name__, __version__, meter_provider) 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, 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..31c7f1d7c8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-0.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +falcon==1.4.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..ad476d7c22 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-1.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +falcon==2.0.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..6c5c3e8ac9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-falcon/test-requirements-2.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +falcon==3.1.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index e99c8be6ed..10b73c7a5b 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -222,7 +222,12 @@ def instrument_app( excluded_urls = _excluded_urls_from_env else: excluded_urls = parse_excluded_urls(excluded_urls) - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) app.add_middleware( OpenTelemetryMiddleware, @@ -295,7 +300,10 @@ class _InstrumentedFastAPI(fastapi.FastAPI): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) meter = get_meter( - __name__, __version__, _InstrumentedFastAPI._meter_provider + __name__, + __version__, + _InstrumentedFastAPI._meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self.add_middleware( OpenTelemetryMiddleware, diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt new file mode 100644 index 0000000000..8d7bf3ad78 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.txt @@ -0,0 +1,35 @@ +annotated-types==0.6.0 +anyio==4.3.0 +asgiref==3.7.2 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pydantic==2.6.2 +pydantic_core==2.16.3 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +sniffio==1.3.0 +starlette==0.36.3 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.1 +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-flask/src/opentelemetry/instrumentation/flask/package.py b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py index 33bfe4ccba..d83adbede0 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py +++ b/instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/package.py @@ -13,6 +13,6 @@ # limitations under the License. -_instruments = ("flask >= 1.0, < 3.0",) +_instruments = ("flask >= 1.0",) _supports_metrics = True 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..fbefeebdb4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-0.txt @@ -0,0 +1,25 @@ +asgiref==3.7.2 +attrs==23.2.0 +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.3 +MarkupSafe==2.1.2 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..41583ad7f9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-1.txt @@ -0,0 +1,25 @@ +asgiref==3.7.2 +attrs==23.2.0 +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.3 +MarkupSafe==2.1.2 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..3a89328861 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-flask/test-requirements-2.txt @@ -0,0 +1,26 @@ +asgiref==3.7.2 +attrs==23.2.0 +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.3 +MarkupSafe==2.1.2 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.9.0 +Werkzeug==3.0.1 +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..3c8073f261 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py @@ -88,6 +88,14 @@ def _custom_response_headers(): 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-grpc/src/opentelemetry/instrumentation/grpc/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py index 440d1facc8..717977146e 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/__init__.py @@ -576,7 +576,12 @@ def client_interceptor( """ from . import _client - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) return _client.OpenTelemetryClientInterceptor( tracer, @@ -601,7 +606,12 @@ def server_interceptor(tracer_provider=None, filter_=None): """ from . import _server - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) return _server.OpenTelemetryServerInterceptor(tracer, filter_=filter_) @@ -619,7 +629,12 @@ def aio_client_interceptors( """ from . import _aio_client - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) return [ _aio_client.UnaryUnaryAioClientInterceptor( @@ -660,7 +675,12 @@ def aio_server_interceptor(tracer_provider=None, filter_=None): """ from . import _aio_server - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) return _aio_server.OpenTelemetryAioServerInterceptor( tracer, filter_=filter_ 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 5d5a5ccc46..8fc992be73 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 @@ -19,12 +19,11 @@ import grpc from grpc.aio import ClientCallDetails -from opentelemetry import context from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, _carrier_setter, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagate import inject from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace.status import Status, StatusCode @@ -139,9 +138,10 @@ async def _wrap_stream_response(self, span, call): span.end() def tracing_skipped(self, client_call_details): - return context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ) or not self.rpc_matches_filters(client_call_details) + return ( + not is_instrumentation_enabled() + or not self.rpc_matches_filters(client_call_details) + ) def rpc_matches_filters(self, client_call_details): return self._filter is None or self._filter(client_call_details) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py index d64dcf000b..ba255ef3bf 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_aio_server.py @@ -12,13 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. +import grpc import grpc.aio - -from ._server import ( - OpenTelemetryServerInterceptor, - _OpenTelemetryServicerContext, - _wrap_rpc_behavior, -) +import wrapt + +from opentelemetry.semconv.trace import SpanAttributes + +from ._server import OpenTelemetryServerInterceptor, _wrap_rpc_behavior +from ._utilities import _server_status + + +# pylint:disable=abstract-method +class _OpenTelemetryAioServicerContext(wrapt.ObjectProxy): + def __init__(self, servicer_context, active_span): + super().__init__(servicer_context) + self._self_active_span = active_span + self._self_code = grpc.StatusCode.OK + self._self_details = None + + async def abort(self, code, details="", trailing_metadata=tuple()): + self._self_code = code + self._self_details = details + self._self_active_span.set_attribute( + SpanAttributes.RPC_GRPC_STATUS_CODE, code.value[0] + ) + status = _server_status(code, details) + self._self_active_span.set_status(status) + return await self.__wrapped__.abort(code, details, trailing_metadata) + + def set_code(self, code): + self._self_code = code + details = self._self_details or code.value[1] + self._self_active_span.set_attribute( + SpanAttributes.RPC_GRPC_STATUS_CODE, code.value[0] + ) + if code != grpc.StatusCode.OK: + status = _server_status(code, details) + self._self_active_span.set_status(status) + return self.__wrapped__.set_code(code) + + def set_details(self, details): + self._self_details = details + if self._self_code != grpc.StatusCode.OK: + status = _server_status(self._self_code, details) + self._self_active_span.set_status(status) + return self.__wrapped__.set_details(details) class OpenTelemetryAioServerInterceptor( @@ -66,7 +104,7 @@ async def _unary_interceptor(request_or_iterator, context): set_status_on_exception=False, ) as span: # wrap the context - context = _OpenTelemetryServicerContext(context, span) + context = _OpenTelemetryAioServicerContext(context, span) # And now we run the actual RPC. try: @@ -77,7 +115,7 @@ async def _unary_interceptor(request_or_iterator, context): # we handle in our context wrapper. # Here, we're interested in uncaught exceptions. # pylint:disable=unidiomatic-typecheck - if type(error) != Exception: + if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error @@ -91,7 +129,7 @@ async def _stream_interceptor(request_or_iterator, context): context, set_status_on_exception=False, ) as span: - context = _OpenTelemetryServicerContext(context, span) + context = _OpenTelemetryAioServicerContext(context, span) try: async for response in behavior( @@ -101,7 +139,7 @@ async def _stream_interceptor(request_or_iterator, context): except Exception as error: # pylint:disable=unidiomatic-typecheck - if type(error) != Exception: + if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py index b966fff4db..e27c9e826f 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_client.py @@ -25,10 +25,10 @@ import grpc -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.grpc import grpcext from opentelemetry.instrumentation.grpc._utilities import RpcInfo -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagate import inject from opentelemetry.propagators.textmap import Setter from opentelemetry.semconv.trace import SpanAttributes @@ -123,7 +123,7 @@ def _trace_result(self, span, rpc_info, result): return result def _intercept(self, request, metadata, client_info, invoker): - if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + if not is_instrumentation_enabled(): return invoker(request, metadata) if not metadata: @@ -219,7 +219,7 @@ def _intercept_server_stream( def intercept_stream( self, request_or_iterator, metadata, client_info, invoker ): - if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + if not is_instrumentation_enabled(): return invoker(request_or_iterator, metadata) if self._filter is not None and not self._filter(client_info): diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py index dcee959b4d..71697ef8bc 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_server.py @@ -31,7 +31,8 @@ from opentelemetry.context import attach, detach from opentelemetry.propagate import extract from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace.status import Status, StatusCode + +from ._utilities import _server_status logger = logging.getLogger(__name__) @@ -124,12 +125,8 @@ def abort(self, code, details): self._active_span.set_attribute( SpanAttributes.RPC_GRPC_STATUS_CODE, code.value[0] ) - self._active_span.set_status( - Status( - status_code=StatusCode.ERROR, - description=f"{code}:{details}", - ) - ) + status = _server_status(code, details) + self._active_span.set_status(status) return self._servicer_context.abort(code, details) def abort_with_status(self, status): @@ -158,23 +155,15 @@ def set_code(self, code): SpanAttributes.RPC_GRPC_STATUS_CODE, code.value[0] ) if code != grpc.StatusCode.OK: - self._active_span.set_status( - Status( - status_code=StatusCode.ERROR, - description=f"{code}:{details}", - ) - ) + status = _server_status(code, details) + self._active_span.set_status(status) return self._servicer_context.set_code(code) def set_details(self, details): self._details = details if self._code != grpc.StatusCode.OK: - self._active_span.set_status( - Status( - status_code=StatusCode.ERROR, - description=f"{self._code}:{details}", - ) - ) + status = _server_status(self._code, details) + self._active_span.set_status(status) return self._servicer_context.set_details(details) @@ -315,7 +304,7 @@ def telemetry_interceptor(request_or_iterator, context): # we handle in our context wrapper. # Here, we're interested in uncaught exceptions. # pylint:disable=unidiomatic-typecheck - if type(error) != Exception: + if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error @@ -342,6 +331,6 @@ def _intercept_server_stream( except Exception as error: # pylint:disable=unidiomatic-typecheck - if type(error) != Exception: + if type(error) != Exception: # noqa: E721 span.record_exception(error) raise error diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py index b6ff7d311a..8a6365b742 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/_utilities.py @@ -14,6 +14,10 @@ """Internal utilities.""" +import grpc + +from opentelemetry.trace.status import Status, StatusCode + class RpcInfo: def __init__( @@ -31,3 +35,21 @@ def __init__( self.request = request self.response = response self.error = error + + +def _server_status(code, details): + error_status = Status( + status_code=StatusCode.ERROR, description=f"{code}:{details}" + ) + status_codes = { + grpc.StatusCode.UNKNOWN: error_status, + grpc.StatusCode.DEADLINE_EXCEEDED: error_status, + grpc.StatusCode.UNIMPLEMENTED: error_status, + grpc.StatusCode.INTERNAL: error_status, + grpc.StatusCode.UNAVAILABLE: error_status, + grpc.StatusCode.DATA_LOSS: error_status, + } + + return status_codes.get( + code, Status(status_code=StatusCode.UNSET, description="") + ) diff --git a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py index 8100a2d17f..858d5d9e40 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/src/opentelemetry/instrumentation/grpc/filters/__init__.py @@ -17,13 +17,14 @@ import grpc -TCallDetails = TypeVar( - "TCallDetails", +CallDetailsT = TypeVar( + "CallDetailsT", grpc.HandlerCallDetails, grpc.ClientCallDetails, grpc.aio.ClientCallDetails, ) -Condition = Callable[[TCallDetails], bool] +# pylint: disable=invalid-name +Condition = Callable[[CallDetailsT], bool] def _full_method(metadata): @@ -61,7 +62,7 @@ def _split_full_method(metadata): return (service, method) -def all_of(*args: Condition[TCallDetails]) -> Condition[TCallDetails]: +def all_of(*args: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that returns True if all filter functions assigned matches conditions. @@ -79,7 +80,7 @@ def filter_fn(metadata): return filter_fn -def any_of(*args: Condition[TCallDetails]) -> Condition[TCallDetails]: +def any_of(*args: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that returns True if any of filter functions assigned matches conditions. @@ -97,7 +98,7 @@ def filter_fn(metadata): return filter_fn -def negate(func: Condition[TCallDetails]) -> Condition[TCallDetails]: +def negate(func: Condition[CallDetailsT]) -> Condition[CallDetailsT]: """Returns a filter function that negate the result of func Args: @@ -113,7 +114,7 @@ def filter_fn(metadata): return filter_fn -def method_name(name: str) -> Condition[TCallDetails]: +def method_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC method name matches name. @@ -132,7 +133,7 @@ def filter_fn(metadata): return filter_fn -def method_prefix(prefix: str) -> Condition[TCallDetails]: +def method_prefix(prefix: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC method name starts with prefix. @@ -151,7 +152,7 @@ def filter_fn(metadata): return filter_fn -def full_method_name(name: str) -> Condition[TCallDetails]: +def full_method_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC full method name matches name. @@ -170,7 +171,7 @@ def filter_fn(metadata): return filter_fn -def service_name(name: str) -> Condition[TCallDetails]: +def service_name(name: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC service name matches name. @@ -189,7 +190,7 @@ def filter_fn(metadata): return filter_fn -def service_prefix(prefix: str) -> Condition[TCallDetails]: +def service_prefix(prefix: str) -> Condition[CallDetailsT]: """Returns a filter function that return True if request's gRPC service name starts with prefix. @@ -208,7 +209,7 @@ def filter_fn(metadata): return filter_fn -def health_check() -> Condition[TCallDetails]: +def health_check() -> Condition[CallDetailsT]: """Returns a Filter that returns true if the request's service name is health check defined by gRPC Health Checking Protocol. https://github.com/grpc/grpc/blob/master/doc/health-checking.md diff --git a/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt new file mode 100644 index 0000000000..56d47af0df --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-grpc/test-requirements.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +grpcio==1.62.0 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +protobuf==3.20.3 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-grpc 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 6ca5ce92d5..6b1006b8a3 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_client_interceptor.py @@ -31,7 +31,7 @@ def run(self, result=None): import pytest import opentelemetry.instrumentation.grpc -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.grpc import ( GrpcAioInstrumentorClient, aio_client_interceptors, @@ -39,7 +39,7 @@ def run(self, result=None): from opentelemetry.instrumentation.grpc._aio_client import ( UnaryUnaryAioClientInterceptor, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator @@ -314,53 +314,33 @@ async def test_client_interceptor_trace_context_propagation(self): set_global_textmap(previous_propagator) async def test_unary_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): response = await simple_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) - finally: - context.detach(token) async def test_unary_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): async for response in server_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) - finally: - context.detach(token) async def test_stream_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): response = await client_streaming_method(self._stub) assert response.response_data == "data" spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) - finally: - context.detach(token) async def test_stream_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): async for response in bidirectional_streaming_method(self._stub): self.assertEqual(response.response_data, "data") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) - finally: - context.detach(token) 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 52391124b7..7b31b085de 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_aio_server_interceptor.py @@ -88,8 +88,11 @@ async def run_with_test_server( channel = grpc.aio.insecure_channel(f"localhost:{port:d}") await server.start() - resp = await runnable(channel) - await server.stop(1000) + + try: + resp = await runnable(channel) + finally: + await server.stop(1000) return resp @@ -504,9 +507,7 @@ async def test_abort(self): class AbortServicer(GRPCTestServerServicer): # pylint:disable=C0103 async def SimpleMethod(self, request, context): - await context.abort( - grpc.StatusCode.FAILED_PRECONDITION, failure_message - ) + await context.abort(grpc.StatusCode.INTERNAL, failure_message) testcase = self @@ -514,9 +515,12 @@ async def request(channel): request = Request(client_id=1, request_data=failure_message) msg = request.SerializeToString() - with testcase.assertRaises(Exception): + with testcase.assertRaises(grpc.RpcError) as cm: await channel.unary_unary(rpc_call)(msg) + self.assertEqual(cm.exception.code(), grpc.StatusCode.INTERNAL) + self.assertEqual(cm.exception.details(), failure_message) + await run_with_test_server(request, servicer=AbortServicer()) spans_list = self.memory_exporter.get_finished_spans() @@ -535,9 +539,71 @@ async def request(channel): self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual( span.status.description, - f"{grpc.StatusCode.FAILED_PRECONDITION}:{failure_message}", + f"{grpc.StatusCode.INTERNAL}:{failure_message}", ) + # Check attributes + self.assertSpanHasAttributes( + span, + { + SpanAttributes.NET_PEER_IP: "[::1]", + SpanAttributes.NET_PEER_NAME: "localhost", + SpanAttributes.RPC_METHOD: "SimpleMethod", + SpanAttributes.RPC_SERVICE: "GRPCTestServer", + SpanAttributes.RPC_SYSTEM: "grpc", + SpanAttributes.RPC_GRPC_STATUS_CODE: grpc.StatusCode.INTERNAL.value[ + 0 + ], + }, + ) + + async def test_abort_with_trailing_metadata(self): + """Check that we can catch an abort properly when trailing_metadata provided""" + rpc_call = "/GRPCTestServer/SimpleMethod" + failure_message = "failure message" + + class AbortServicer(GRPCTestServerServicer): + # pylint:disable=C0103 + async def SimpleMethod(self, request, context): + metadata = (("meta", "data"),) + await context.abort( + grpc.StatusCode.FAILED_PRECONDITION, + failure_message, + trailing_metadata=metadata, + ) + + testcase = self + + async def request(channel): + request = Request(client_id=1, request_data=failure_message) + msg = request.SerializeToString() + + with testcase.assertRaises(grpc.RpcError) as cm: + await channel.unary_unary(rpc_call)(msg) + + self.assertEqual( + cm.exception.code(), grpc.StatusCode.FAILED_PRECONDITION + ) + self.assertEqual(cm.exception.details(), failure_message) + + await run_with_test_server(request, servicer=AbortServicer()) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.name, rpc_call) + self.assertIs(span.kind, trace.SpanKind.SERVER) + + # Check version and name in span's instrumentation info + self.assertEqualSpanInstrumentationInfo( + span, opentelemetry.instrumentation.grpc + ) + + # make sure this span errored, with the right status and detail + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.status.description, None) + # Check attributes self.assertSpanHasAttributes( span, diff --git a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py index 810ee930dd..2436aca40c 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor.py @@ -11,6 +11,7 @@ # 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=cyclic-import import grpc from tests.protobuf import ( # pylint: disable=no-name-in-module @@ -18,7 +19,7 @@ ) import opentelemetry.instrumentation.grpc -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, @@ -26,7 +27,7 @@ from opentelemetry.instrumentation.grpc.grpcext._interceptor import ( _UnaryClientInfo, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator @@ -306,45 +307,25 @@ def invoker(request, metadata): set_global_textmap(previous_propagator) def test_unary_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_unary_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_stream_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_stream_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) 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 a15268464b..9a9aefad59 100644 --- a/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py +++ b/instrumentation/opentelemetry-instrumentation-grpc/tests/test_client_interceptor_filter.py @@ -11,6 +11,7 @@ # 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=cyclic-import import os from unittest import mock @@ -21,7 +22,7 @@ ) import opentelemetry.instrumentation.grpc -from opentelemetry import context, trace +from opentelemetry import trace from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient, filters from opentelemetry.instrumentation.grpc._client import ( OpenTelemetryClientInterceptor, @@ -29,7 +30,7 @@ from opentelemetry.instrumentation.grpc.grpcext._interceptor import ( _UnaryClientInfo, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator @@ -638,45 +639,25 @@ def invoker(request, metadata): set_global_textmap(previous_propagator) def test_unary_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): simple_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_unary_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): server_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_stream_unary_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): client_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) def test_stream_stream_with_suppress_key(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): bidirectional_streaming_method(self._stub) spans = self.memory_exporter.get_finished_spans() - finally: - context.detach(token) self.assertEqual(len(spans), 0) diff --git a/instrumentation/opentelemetry-instrumentation-httpx/README.rst b/instrumentation/opentelemetry-instrumentation-httpx/README.rst index 1e03eb128e..cc465dd615 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/README.rst +++ b/instrumentation/opentelemetry-instrumentation-httpx/README.rst @@ -136,7 +136,21 @@ The hooks can be configured as follows: # status_code, headers, stream, extensions = response pass - HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) + async def async_request_hook(span, request): + # method, url, headers, stream, extensions = request + pass + + async def async_response_hook(span, request, response): + # method, url, headers, stream, extensions = request + # status_code, headers, stream, extensions = response + pass + + HTTPXClientInstrumentor().instrument( + request_hook=request_hook, + response_hook=response_hook, + async_request_hook=async_request_hook, + async_response_hook=async_response_hook + ) Or if you are using the transport classes directly: @@ -144,7 +158,7 @@ Or if you are using the transport classes directly: .. code-block:: python - from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport + from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport def request_hook(span, request): # method, url, headers, stream, extensions = request @@ -155,6 +169,15 @@ Or if you are using the transport classes directly: # status_code, headers, stream, extensions = response pass + async def async_request_hook(span, request): + # method, url, headers, stream, extensions = request + pass + + async def async_response_hook(span, request, response): + # method, url, headers, stream, extensions = request + # status_code, headers, stream, extensions = response + pass + transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport( transport, @@ -162,6 +185,13 @@ Or if you are using the transport classes directly: response_hook=response_hook ) + async_transport = httpx.AsyncHTTPTransport() + async_telemetry_transport = AsyncOpenTelemetryTransport( + async_transport, + request_hook=async_request_hook, + response_hook=async_response_hook + ) + References ---------- 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 bb40adbc26..e6609157c4 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py @@ -131,7 +131,21 @@ def response_hook(span, request, response): # status_code, headers, stream, extensions = response pass - HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) + async def async_request_hook(span, request): + # method, url, headers, stream, extensions = request + pass + + async def async_response_hook(span, request, response): + # method, url, headers, stream, extensions = request + # status_code, headers, stream, extensions = response + pass + + HTTPXClientInstrumentor().instrument( + request_hook=request_hook, + response_hook=response_hook, + async_request_hook=async_request_hook, + async_response_hook=async_response_hook + ) Or if you are using the transport classes directly: @@ -139,7 +153,7 @@ def response_hook(span, request, response): .. code-block:: python - from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport + from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport def request_hook(span, request): # method, url, headers, stream, extensions = request @@ -150,6 +164,15 @@ def response_hook(span, request, response): # status_code, headers, stream, extensions = response pass + async def async_request_hook(span, request): + # method, url, headers, stream, extensions = request + pass + + async def async_response_hook(span, request, response): + # method, url, headers, stream, extensions = request + # status_code, headers, stream, extensions = response + pass + transport = httpx.HTTPTransport() telemetry_transport = SyncOpenTelemetryTransport( transport, @@ -157,6 +180,13 @@ def response_hook(span, request, response): response_hook=response_hook ) + async_transport = httpx.AsyncHTTPTransport() + async_telemetry_transport = AsyncOpenTelemetryTransport( + async_transport, + request_hook=async_request_hook, + response_hook=async_response_hook + ) + API --- """ @@ -176,6 +206,7 @@ def response_hook(span, request, response): from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.span import Span from opentelemetry.trace.status import Status +from opentelemetry.util.http import remove_url_credentials _logger = logging.getLogger(__name__) @@ -239,7 +270,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 = request.url + url = httpx.URL(remove_url_credentials(str(request.url))) headers = request.headers stream = request.stream extensions = request.extensions @@ -290,6 +321,7 @@ def __init__( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._request_hook = request_hook self._response_hook = response_hook @@ -376,14 +408,15 @@ def __init__( self, transport: httpx.AsyncBaseTransport, tracer_provider: typing.Optional[TracerProvider] = None, - request_hook: typing.Optional[RequestHook] = None, - response_hook: typing.Optional[ResponseHook] = None, + request_hook: typing.Optional[AsyncRequestHook] = None, + response_hook: typing.Optional[AsyncResponseHook] = None, ): self._transport = transport self._tracer = get_tracer( __name__, instrumenting_library_version=__version__, tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self._request_hook = request_hook self._response_hook = response_hook @@ -509,21 +542,27 @@ def _instrument(self, **kwargs): Args: **kwargs: Optional arguments ``tracer_provider``: a TracerProvider, defaults to global - ``request_hook``: A hook that receives the span and request that is called - right after the span is created - ``response_hook``: A hook that receives the span, request, and response - that is called right before the span ends + ``request_hook``: A ``httpx.Client`` hook that receives the span and request + that is called right after the span is created + ``response_hook``: A ``httpx.Client`` hook that receives the span, request, + and response that is called right before the span ends + ``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient`` + ``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient`` """ self._original_client = httpx.Client self._original_async_client = httpx.AsyncClient request_hook = kwargs.get("request_hook") response_hook = kwargs.get("response_hook") + async_request_hook = kwargs.get("async_request_hook", request_hook) + async_response_hook = kwargs.get("async_response_hook", response_hook) if callable(request_hook): _InstrumentedClient._request_hook = request_hook - _InstrumentedAsyncClient._request_hook = request_hook + if callable(async_request_hook): + _InstrumentedAsyncClient._request_hook = async_request_hook if callable(response_hook): _InstrumentedClient._response_hook = response_hook - _InstrumentedAsyncClient._response_hook = response_hook + if callable(async_response_hook): + _InstrumentedAsyncClient._response_hook = async_response_hook tracer_provider = kwargs.get("tracer_provider") _InstrumentedClient._tracer_provider = tracer_provider _InstrumentedAsyncClient._tracer_provider = tracer_provider @@ -544,8 +583,12 @@ def _uninstrument(self, **kwargs): def instrument_client( client: typing.Union[httpx.Client, httpx.AsyncClient], tracer_provider: TracerProvider = None, - request_hook: typing.Optional[RequestHook] = None, - response_hook: typing.Optional[ResponseHook] = None, + request_hook: typing.Union[ + typing.Optional[RequestHook], typing.Optional[AsyncRequestHook] + ] = None, + response_hook: typing.Union[ + typing.Optional[ResponseHook], typing.Optional[AsyncResponseHook] + ] = None, ) -> None: """Instrument httpx Client or AsyncClient 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..2d3a8399d8 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0.txt @@ -0,0 +1,28 @@ +anyio==3.7.1 +asgiref==3.7.2 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..4a36398fc1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1.txt @@ -0,0 +1,27 @@ +anyio==4.3.0 +asgiref==3.7.2 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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 daddaad306..c3f668cafe 100644 --- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py +++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py @@ -51,12 +51,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 +72,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 +80,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)) @@ -421,6 +430,28 @@ def test_response_hook(self): ) HTTPXClientInstrumentor().uninstrument() + def test_response_hook_sync_async_kwargs(self): + HTTPXClientInstrumentor().instrument( + tracer_provider=self.tracer_provider, + response_hook=_response_hook, + async_response_hook=_async_response_hook, + ) + client = self.create_client() + result = self.perform_request(self.URL, client=client) + + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_URL: self.URL, + SpanAttributes.HTTP_STATUS_CODE: 200, + HTTP_RESPONSE_BODY: "Hello!", + }, + ) + HTTPXClientInstrumentor().uninstrument() + def test_request_hook(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, @@ -434,6 +465,20 @@ def test_request_hook(self): self.assertEqual(span.name, "GET" + self.URL) HTTPXClientInstrumentor().uninstrument() + def test_request_hook_sync_async_kwargs(self): + HTTPXClientInstrumentor().instrument( + tracer_provider=self.tracer_provider, + request_hook=_request_hook, + async_request_hook=_async_request_hook, + ) + client = self.create_client() + result = self.perform_request(self.URL, client=client) + + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + self.assertEqual(span.name, "GET" + self.URL) + HTTPXClientInstrumentor().uninstrument() + def test_request_hook_no_span_update(self): HTTPXClientInstrumentor().instrument( tracer_provider=self.tracer_provider, @@ -568,6 +613,13 @@ def perform_request( return self.client.request(method, url, headers=headers) return client.request(method, url, headers=headers) + def test_credential_removal(self): + new_url = "http://username:password@mock/status/200" + self.perform_request(new_url) + span = self.assert_span() + + self.assertEqual(span.attributes[SpanAttributes.HTTP_URL], self.URL) + class TestAsyncIntegration(BaseTestCases.BaseManualTest): response_hook = staticmethod(_async_response_hook) @@ -628,6 +680,13 @@ def test_basic_multiple(self): ) self.assert_span(num_spans=2) + def test_credential_removal(self): + new_url = "http://username:password@mock/status/200" + self.perform_request(new_url) + span = self.assert_span() + + self.assertEqual(span.attributes[SpanAttributes.HTTP_URL], self.URL) + class TestSyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest): def create_client( diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py index 735f808e90..0b199cbe64 100644 --- a/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-jinja2/src/opentelemetry/instrumentation/jinja2/__init__.py @@ -130,7 +130,12 @@ def instrumentation_dependencies(self) -> Collection[str]: def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) _wrap(jinja2, "environment.Template.render", _wrap_render(tracer)) _wrap(jinja2, "environment.Template.generate", _wrap_render(tracer)) diff --git a/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt new file mode 100644 index 0000000000..d8ab59ac2d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-jinja2/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +Jinja2==3.1.3 +MarkupSafe==2.0.1 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/README.rst b/instrumentation/opentelemetry-instrumentation-kafka-python/README.rst index 426a371908..2d20e5c6d7 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/README.rst +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/README.rst @@ -17,6 +17,6 @@ Installation References ---------- -* `OpenTelemetry kafka-python Instrumentation `_ +* `OpenTelemetry kafka-python Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/__init__.py b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/__init__.py index ad94a4fb04..8d7378dfdf 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/src/opentelemetry/instrumentation/kafka/__init__.py @@ -102,7 +102,10 @@ def _instrument(self, **kwargs): consume_hook = kwargs.get("consume_hook") tracer = trace.get_tracer( - __name__, __version__, tracer_provider=tracer_provider + __name__, + __version__, + tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) wrap_function_wrapper( 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..96bef86dbe --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +kafka-python==2.0.2 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-kafka-python/tests/test_utils.py b/instrumentation/opentelemetry-instrumentation-kafka-python/tests/test_utils.py index 7da1ed0596..85397bcb73 100644 --- a/instrumentation/opentelemetry-instrumentation-kafka-python/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-kafka-python/tests/test_utils.py @@ -1,3 +1,18 @@ +# 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=unnecessary-dunder-call + from unittest import TestCase, mock from opentelemetry.instrumentation.kafka.utils import ( diff --git a/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt new file mode 100644 index 0000000000..f376796169 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt new file mode 100644 index 0000000000..f113b768b1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-mysql/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +mysql-connector-python==8.3.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt new file mode 100644 index 0000000000..afa2ccae6c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +mysqlclient==2.2.4 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/src/opentelemetry/instrumentation/pika/pika_instrumentor.py b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/pika_instrumentor.py index b09c3a0f9c..56c78a85c3 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 @@ -11,6 +11,8 @@ # 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=unnecessary-dunder-call + from logging import getLogger from typing import Any, Collection, Dict, Optional @@ -122,7 +124,12 @@ def instrument_channel( "Attempting to instrument Pika channel while already instrumented!" ) return - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) PikaInstrumentor._instrument_blocking_channel_consumers( channel, tracer, consume_hook ) 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 e9f819f2d6..6dab4fdfa9 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py +++ b/instrumentation/opentelemetry-instrumentation-pika/src/opentelemetry/instrumentation/pika/utils.py @@ -5,7 +5,7 @@ from pika.spec import Basic, BasicProperties from opentelemetry import context, propagate, trace -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.propagators.textmap import CarrierT, Getter from opentelemetry.semconv.trace import ( MessagingOperationValues, @@ -113,12 +113,11 @@ def decorated_function( exchange, routing_key, body, properties, mandatory ) with trace.use_span(span, end_on_exit=True): - if span.is_recording(): - propagate.inject(properties.headers) - try: - publish_hook(span, body, properties) - except Exception as hook_exception: # pylint: disable=W0703 - _LOG.exception(hook_exception) + propagate.inject(properties.headers) + try: + publish_hook(span, body, properties) + except Exception as hook_exception: # pylint: disable=W0703 + _LOG.exception(hook_exception) retval = original_function( exchange, routing_key, body, properties, mandatory ) @@ -136,9 +135,7 @@ def _get_span( span_kind: SpanKind, operation: Optional[MessagingOperationValues] = None, ) -> Optional[Span]: - if context.get_value("suppress_instrumentation") or context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ): + if not is_instrumentation_enabled(): return None task_name = properties.type if properties.type else task_name span = tracer.start_span( 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..fb3c6def5a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-0.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pika==0.13.1 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..d3ce673dab --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pika/test-requirements-1.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pika==1.3.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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_utils.py b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py index 9b1aed7f49..ed33593389 100644 --- a/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py +++ b/instrumentation/opentelemetry-instrumentation-pika/tests/test_utils.py @@ -292,7 +292,6 @@ def test_decorate_basic_publish( use_span.assert_called_once_with( get_span.return_value, end_on_exit=True ) - get_span.return_value.is_recording.assert_called_once() inject.assert_called_once_with(properties.headers) callback.assert_called_once_with( exchange_name, routing_key, mock_body, properties, False @@ -323,7 +322,6 @@ def test_decorate_basic_publish_no_properties( use_span.assert_called_once_with( get_span.return_value, end_on_exit=True ) - get_span.return_value.is_recording.assert_called_once() inject.assert_called_once_with(basic_properties.return_value.headers) self.assertEqual(retval, callback.return_value) @@ -393,7 +391,55 @@ def test_decorate_basic_publish_with_hook( use_span.assert_called_once_with( get_span.return_value, end_on_exit=True ) - get_span.return_value.is_recording.assert_called_once() + inject.assert_called_once_with(properties.headers) + publish_hook.assert_called_once_with( + get_span.return_value, mock_body, properties + ) + callback.assert_called_once_with( + exchange_name, routing_key, mock_body, properties, False + ) + self.assertEqual(retval, callback.return_value) + + @mock.patch("opentelemetry.instrumentation.pika.utils._get_span") + @mock.patch("opentelemetry.propagate.inject") + @mock.patch("opentelemetry.trace.use_span") + def test_decorate_basic_publish_when_span_is_not_recording( + self, + use_span: mock.MagicMock, + inject: mock.MagicMock, + get_span: mock.MagicMock, + ) -> None: + callback = mock.MagicMock() + tracer = mock.MagicMock() + channel = mock.MagicMock(spec=Channel) + exchange_name = "test-exchange" + routing_key = "test-routing-key" + properties = mock.MagicMock() + mock_body = b"mock_body" + publish_hook = mock.MagicMock() + + mocked_span = mock.MagicMock() + mocked_span.is_recording.return_value = False + get_span.return_value = mocked_span + + decorated_basic_publish = utils._decorate_basic_publish( + callback, channel, tracer, publish_hook + ) + retval = decorated_basic_publish( + exchange_name, routing_key, mock_body, properties + ) + get_span.assert_called_once_with( + tracer, + channel, + properties, + destination=exchange_name, + span_kind=SpanKind.PRODUCER, + task_name="(temporary)", + operation=None, + ) + use_span.assert_called_once_with( + get_span.return_value, end_on_exit=True + ) inject.assert_called_once_with(properties.headers) publish_hook.assert_called_once_with( get_span.return_value, mock_body, properties diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE b/instrumentation/opentelemetry-instrumentation-psycopg/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/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 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/instrumentation/opentelemetry-instrumentation-psycopg/README.rst b/instrumentation/opentelemetry-instrumentation-psycopg/README.rst new file mode 100644 index 0000000000..7feb4dc36a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/README.rst @@ -0,0 +1,21 @@ +OpenTelemetry Psycopg Instrumentation +===================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-psycopg.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-psycopg/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-psycopg + + +References +---------- +* `OpenTelemetry Psycopg Instrumentation `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml b/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml new file mode 100644 index 0000000000..d2328035fb --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-psycopg" +dynamic = ["version"] +description = "OpenTelemetry psycopg instrumentation" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.7" +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.7", + "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.46b0.dev", + "opentelemetry-instrumentation-dbapi == 0.46b0.dev", +] + +[project.optional-dependencies] +instruments = [ + "psycopg >= 3.1.0", +] + +[project.entry-points.opentelemetry_instrumentor] +psycopg = "opentelemetry.instrumentation.psycopg:PsycopgInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-psycopg" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/psycopg/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py new file mode 100644 index 0000000000..5d7054151a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -0,0 +1,350 @@ +# 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. + +""" +The integration with PostgreSQL supports the `Psycopg`_ library, it can be enabled by +using ``PsycopgInstrumentor``. + +.. _Psycopg: http://initd.org/psycopg/ + +SQLCOMMENTER +***************************************** +You can optionally configure Psycopg instrumentation to enable sqlcommenter which enriches +the query with contextual information. + +Usage +----- + +.. code:: python + + from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor + + PsycopgInstrumentor().instrument(enable_commenter=True, commenter_options={}) + + +For example, +:: + + Invoking cursor.execute("select * from auth_users") will lead to sql query "select * from auth_users" but when SQLCommenter is enabled + the query will get appended with some configurable tags like "select * from auth_users /*tag=value*/;" + + +SQLCommenter Configurations +*************************** +We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword + +db_driver = True(Default) or False + +For example, +:: +Enabling this flag will add psycopg and it's version which is /*psycopg%%3A2.9.3*/ + +dbapi_threadsafety = True(Default) or False + +For example, +:: +Enabling this flag will add threadsafety /*dbapi_threadsafety=2*/ + +dbapi_level = True(Default) or False + +For example, +:: +Enabling this flag will add dbapi_level /*dbapi_level='2.0'*/ + +libpq_version = True(Default) or False + +For example, +:: +Enabling this flag will add libpq_version /*libpq_version=140001*/ + +driver_paramstyle = True(Default) or False + +For example, +:: +Enabling this flag will add driver_paramstyle /*driver_paramstyle='pyformat'*/ + +opentelemetry_values = True(Default) or False + +For example, +:: +Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/ + +Usage +----- + +.. code-block:: python + + import psycopg + from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor + + + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database='Database') + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + +API +--- +""" + +import logging +import typing +from typing import Collection + +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 +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.psycopg.package import _instruments +from opentelemetry.instrumentation.psycopg.version import __version__ + +_logger = logging.getLogger(__name__) +_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory" + + +class PsycopgInstrumentor(BaseInstrumentor): + _CONNECTION_ATTRIBUTES = { + "database": "info.dbname", + "port": "info.port", + "host": "info.host", + "user": "info.user", + } + + _DATABASE_SYSTEM = "postgresql" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Integrate with PostgreSQL Psycopg library. + Psycopg: http://initd.org/psycopg/ + """ + tracer_provider = kwargs.get("tracer_provider") + enable_sqlcommenter = kwargs.get("enable_commenter", False) + commenter_options = kwargs.get("commenter_options", {}) + dbapi.wrap_connect( + __name__, + psycopg, + "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.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") # 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 + def instrument_connection(connection, tracer_provider=None): + if not hasattr(connection, "_is_instrumented_by_opentelemetry"): + connection._is_instrumented_by_opentelemetry = False + + if not connection._is_instrumented_by_opentelemetry: + setattr( + connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory + ) + connection.cursor_factory = _new_cursor_factory( + tracer_provider=tracer_provider + ) + connection._is_instrumented_by_opentelemetry = True + else: + _logger.warning( + "Attempting to instrument Psycopg connection while already instrumented" + ) + return connection + + # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql + @staticmethod + def uninstrument_connection(connection): + connection.cursor_factory = getattr( + connection, _OTEL_CURSOR_FACTORY_KEY, None + ) + + return connection + + +# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql +class DatabaseApiIntegration(dbapi.DatabaseApiIntegration): + 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_factory(**new_factory_kwargs) + connection = connect_method(*args, **kwargs) + self.get_connection_attributes(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: + return "" + + statement = args[0] + if isinstance(statement, Composed): + statement = statement.as_string(cursor) + + if isinstance(statement, str): + # Strip leading comments so we get the operation name. + return self._leading_comment_remover.sub("", statement).split()[0] + + return "" + + def get_statement(self, cursor, args): + if not args: + return "" + + statement = args[0] + if isinstance(statement, Composed): + statement = statement.as_string(cursor) + return statement + + +def _new_cursor_factory(db_api=None, base_factory=None, tracer_provider=None): + if not db_api: + db_api = DatabaseApiIntegration( + __name__, + PsycopgInstrumentor._DATABASE_SYSTEM, + connection_attributes=PsycopgInstrumentor._CONNECTION_ATTRIBUTES, + version=__version__, + tracer_provider=tracer_provider, + ) + + base_factory = base_factory or pg_cursor + _cursor_tracer = CursorTracer(db_api) + + class TracedCursorFactory(base_factory): + def execute(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().execute, *args, **kwargs + ) + + def executemany(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().executemany, *args, **kwargs + ) + + def callproc(self, *args, **kwargs): + return _cursor_tracer.traced_execution( + self, super().callproc, *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/package.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/package.py new file mode 100644 index 0000000000..635edfb4db --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/package.py @@ -0,0 +1,16 @@ +# 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 = ("psycopg >= 3.1.0",) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/version.py new file mode 100644 index 0000000000..ff4933b20b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/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.46b0.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..4b57cb1ca5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +backports.zoneinfo==0.2.1 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +psycopg==3.1.18 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..d449374047 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/test-requirements-1.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +psycopg==3.1.18 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py new file mode 100644 index 0000000000..4fbcac6042 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -0,0 +1,479 @@ +# 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 +import types +from unittest import mock + +import psycopg + +import opentelemetry.instrumentation.psycopg +from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor +from opentelemetry.sdk import resources +from opentelemetry.test.test_base import TestBase + + +class MockCursor: + execute = mock.MagicMock(spec=types.MethodType) + execute.__name__ = "execute" + + executemany = mock.MagicMock(spec=types.MethodType) + executemany.__name__ = "executemany" + + callproc = mock.MagicMock(spec=types.MethodType) + callproc.__name__ = "callproc" + + rowcount = "SomeRowCount" + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self): + return self + + 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 Exception("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") + + # pylint: disable=unused-argument, no-self-use + async def callproc(self, query, params=None, throw_exception=False): + if throw_exception: + raise Exception("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" + + rollback = mock.MagicMock(spec=types.MethodType) + rollback.__name__ = "rollback" + + def __init__(self, *args, **kwargs): + self.cursor_factory = kwargs.pop("cursor_factory", None) + + def cursor(self): + if self.cursor_factory: + return self.cursor_factory(self) + return MockCursor() + + def get_dsn_parameters(self): # pylint: disable=no-self-use + return {"dbname": "test"} + + +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 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 TestPostgresqlIntegration(TestBase): + 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() + + 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() + + # pylint: disable=unused-argument + def test_instrumentor(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.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.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) + + # 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) + + async def test_wrap_async_connection_class_with_cursor(self): + PsycopgInstrumentor().instrument() + + async def test_async_connection(): + acnx = await psycopg.AsyncConnection.connect(database="test") + async with acnx as cnx: + async with cnx.cursor() as cursor: + await cursor.execute("SELECT * FROM test") + + asyncio.run(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() + + asyncio.run(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(database="test") + async with acnx as cnx: + await cnx.execute("SELECT * FROM test") + + asyncio.run(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() + asyncio.run(test_async_connection()) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + def test_span_name(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database="test") + + cursor = cnx.cursor() + + cursor.execute("Test query", ("param1Value", False)) + cursor.execute( + """multi + line + query""" + ) + cursor.execute("tab\tseparated query") + cursor.execute("/* leading comment */ query") + cursor.execute("/* leading comment */ query /* trailing comment */") + 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") + + async def test_span_name_async(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.AsyncConnection.connect(database="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 + def test_not_recording(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 = psycopg.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + 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() + + # 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 = psycopg.AsyncConnection.connect(database="test") + async with cnx.cursor() as cursor: + query = "SELECT * FROM test" + 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() + + # pylint: disable=unused-argument + def test_custom_tracer_provider(self): + resource = resources.Resource.create({}) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + + PsycopgInstrumentor().instrument(tracer_provider=tracer_provider) + + cnx = psycopg.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertIs(span.resource, resource) + + # pylint: disable=unused-argument + def test_instrument_connection(self): + cnx = psycopg.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + cnx = PsycopgInstrumentor().instrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + # pylint: disable=unused-argument + def test_instrument_connection_with_instrument(self): + cnx = psycopg.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + PsycopgInstrumentor().instrument() + cnx = PsycopgInstrumentor().instrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + # pylint: disable=unused-argument + def test_uninstrument_connection_with_instrument(self): + PsycopgInstrumentor().instrument() + cnx = psycopg.connect(database="test") + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = PsycopgInstrumentor().uninstrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + # pylint: disable=unused-argument + def test_uninstrument_connection_with_instrument_connection(self): + cnx = psycopg.connect(database="test") + PsycopgInstrumentor().instrument_connection(cnx) + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + cnx = PsycopgInstrumentor().uninstrument_connection(cnx) + cursor = cnx.cursor() + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") + def test_sqlcommenter_enabled(self, event_mocked): + cnx = psycopg.connect(database="test") + PsycopgInstrumentor().instrument(enable_commenter=True) + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + kwargs = event_mocked.call_args[1] + self.assertEqual(kwargs["enable_commenter"], True) + + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") + def test_sqlcommenter_disabled(self, event_mocked): + cnx = psycopg.connect(database="test") + PsycopgInstrumentor().instrument() + query = "SELECT * FROM test" + cursor = cnx.cursor() + cursor.execute(query) + kwargs = event_mocked.call_args[1] + self.assertEqual(kwargs["enable_commenter"], False) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt new file mode 100644 index 0000000000..ade3b5fd26 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +psycopg2==2.9.9 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-psycopg2/tests/test_psycopg2_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py index 8252929037..369d63d5cf 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_integration.py @@ -61,7 +61,7 @@ def cursor(self): return MockCursor() def get_dsn_parameters(self): # pylint: disable=no-self-use - return dict(dbname="test") + return {"dbname": "test"} class TestPostgresqlIntegration(TestBase): 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 573414c1c7..512ce9ea56 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/src/opentelemetry/instrumentation/pymemcache/__init__.py @@ -169,7 +169,7 @@ def _get_address_attributes(instance): address_attributes[SpanAttributes.NET_PEER_NAME] = instance.server address_attributes[ SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.UNIX.value + ] = NetTransportValues.OTHER.value return address_attributes @@ -182,7 +182,12 @@ def instrumentation_dependencies(self) -> Collection[str]: def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) for cmd in COMMANDS: _wrap( 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..2e7313ab6e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-0.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymemcache==1.3.5 +pytest==7.1.3 +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..a1a3cc4fb1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-1.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymemcache==2.2.2 +pytest==7.1.3 +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..cb28ea22d7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-2.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymemcache==3.4.1 +pytest==7.1.3 +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..40152664ac --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-3.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymemcache==3.4.2 +pytest==7.1.3 +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..9031276ce4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/test-requirements-4.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymemcache==4.0.0 +pytest==7.1.3 +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 4e29091217..35b672bac0 100644 --- a/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py +++ b/instrumentation/opentelemetry-instrumentation-pymemcache/tests/test_pymemcache.py @@ -24,14 +24,15 @@ 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/src/opentelemetry/instrumentation/pymongo/__init__.py b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py index 00e757edee..506669a5c6 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/src/opentelemetry/instrumentation/pymongo/__init__.py @@ -79,14 +79,13 @@ def failed_hook(span, event): from pymongo import monitoring -from opentelemetry import context from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.pymongo.package import _instruments from opentelemetry.instrumentation.pymongo.utils import ( COMMAND_TO_ATTRIBUTE_MAPPING, ) from opentelemetry.instrumentation.pymongo.version import __version__ -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes from opentelemetry.trace import SpanKind, get_tracer from opentelemetry.trace.span import Span @@ -122,9 +121,7 @@ def __init__( def started(self, event: monitoring.CommandStartedEvent): """Method to handle a pymongo CommandStartedEvent""" - if not self.is_enabled or context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ): + if not self.is_enabled or not is_instrumentation_enabled(): return command_name = event.command_name span_name = f"{event.database_name}.{command_name}" @@ -167,9 +164,7 @@ def started(self, event: monitoring.CommandStartedEvent): def succeeded(self, event: monitoring.CommandSucceededEvent): """Method to handle a pymongo CommandSucceededEvent""" - if not self.is_enabled or context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ): + if not self.is_enabled or not is_instrumentation_enabled(): return span = self._pop_span(event) if span is None: @@ -185,9 +180,7 @@ def succeeded(self, event: monitoring.CommandSucceededEvent): def failed(self, event: monitoring.CommandFailedEvent): """Method to handle a pymongo CommandFailedEvent""" - if not self.is_enabled or context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ): + if not (self.is_enabled and is_instrumentation_enabled()): return span = self._pop_span(event) if span is None: @@ -248,7 +241,12 @@ def _instrument(self, **kwargs): capture_statement = kwargs.get("capture_statement") # Create and register a CommandTracer only the first time if self._commandtracer_instance is None: - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) self._commandtracer_instance = CommandTracer( tracer, diff --git a/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt new file mode 100644 index 0000000000..01d48e8dc4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymongo/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +dnspython==2.6.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pymongo==4.6.2 +pytest==7.1.3 +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-pymongo/tests/test_pymongo.py b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py index 8eab3b701c..5a8acfda31 100644 --- a/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py +++ b/instrumentation/opentelemetry-instrumentation-pymongo/tests/test_pymongo.py @@ -14,13 +14,12 @@ from unittest import mock -from opentelemetry import context from opentelemetry import trace as trace_api from opentelemetry.instrumentation.pymongo import ( CommandTracer, PymongoInstrumentor, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase @@ -112,16 +111,10 @@ def test_suppression_key(self): mock_event.command.get = mock.Mock() mock_event.command.get.return_value = "dummy" - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - - try: + with suppress_instrumentation(): command_tracer = CommandTracer(mock_tracer) command_tracer.started(event=mock_event) command_tracer.succeeded(event=mock_event) - finally: - context.detach(token) # if suppression key is set, CommandTracer methods return immediately, so command.get is not invoked. self.assertFalse( diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt new file mode 100644 index 0000000000..cb6619c5de --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pymysql/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +PyMySQL==1.1.0 +pytest==7.1.3 +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-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index ce15f0cb24..4f17da3da5 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -84,7 +84,11 @@ def _before_traversal(event): return start_time = request_environ.get(_ENVIRON_STARTTIME_KEY) - tracer = trace.get_tracer(__name__, __version__) + tracer = trace.get_tracer( + __name__, + __version__, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) if request.matched_route: span_name = request.matched_route.pattern @@ -128,11 +132,15 @@ def trace_tween_factory(handler, registry): # pylint: disable=too-many-statements settings = registry.settings enabled = asbool(settings.get(SETTING_TRACE_ENABLED, True)) - meter = get_meter(__name__, __version__) + meter = get_meter( + __name__, + __version__, + 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", + description="Duration of HTTP client requests.", ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt new file mode 100644 index 0000000000..1362e7166e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-pyramid/test-requirements.txt @@ -0,0 +1,30 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +hupper==1.12.1 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +PasteDeploy==3.1.0 +plaster==1.1.2 +plaster-pastedeploy==1.0.1 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pyramid==2.0.2 +pytest==7.1.3 +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==0.16.1 +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-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py index ba4b8d529e..1d61e8cfd3 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py @@ -333,7 +333,10 @@ def _instrument(self, **kwargs): """ tracer_provider = kwargs.get("tracer_provider") tracer = trace.get_tracer( - __name__, __version__, tracer_provider=tracer_provider + __name__, + __version__, + tracer_provider=tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) _instrument( tracer, 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 b24f9b2655..3c274c8c43 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py @@ -43,7 +43,7 @@ def _extract_conn_attributes(conn_kwargs): attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get("path", "") attributes[ SpanAttributes.NET_TRANSPORT - ] = NetTransportValues.UNIX.value + ] = NetTransportValues.OTHER.value return attributes diff --git a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt new file mode 100644 index 0000000000..90617f72e6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +async-timeout==4.0.3 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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 11e56ad953..234d756ef3 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio from unittest import mock +from unittest.mock import AsyncMock import redis import redis.asyncio diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/__init__.py b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/__init__.py index 87a26585fc..56e544edcd 100644 --- a/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-remoulade/src/opentelemetry/instrumentation/remoulade/__init__.py @@ -176,7 +176,12 @@ def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") # pylint: disable=attribute-defined-outside-init - self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self._tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) instrumentation_middleware = _InstrumentationMiddleware(self._tracer) broker.add_extra_default_middleware(instrumentation_middleware) diff --git a/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt new file mode 100644 index 0000000000..c50dfde9b5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +prometheus_client==0.20.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt new file mode 100644 index 0000000000..be34594506 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-requests/test-requirements.txt @@ -0,0 +1,24 @@ +asgiref==3.7.2 +attrs==23.2.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +httpretty==1.1.4 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.1 +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_ip_support.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_ip_support.py index cf2e7fb4dd..8589ac0e26 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_ip_support.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_ip_support.py @@ -38,7 +38,7 @@ def tearDown(self): @staticmethod def perform_request(url: str) -> requests.Response: - return requests.get(url) + return requests.get(url, timeout=5) def test_basic_http_success(self): response = self.perform_request(self.http_url) diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/__init__.py b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/__init__.py index 26f7315d05..a67bfa6ef4 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sklearn/src/opentelemetry/instrumentation/sklearn/__init__.py @@ -35,7 +35,7 @@ ).instrument() -Model intrumentation example: +Model instrumentation example: .. code-block:: python @@ -82,6 +82,8 @@ from sklearn.utils.metaestimators import _IffHasAttrDescriptor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +# pylint: disable=no-name-in-module from opentelemetry.instrumentation.sklearn.package import _instruments from opentelemetry.instrumentation.sklearn.version import __version__ from opentelemetry.trace import get_tracer @@ -129,9 +131,11 @@ def implement_span_function(func: Callable, name: str, attributes: Attributes): @wraps(func) def wrapper(*args, **kwargs): - with get_tracer(__name__, __version__).start_as_current_span( - name=name - ) as span: + with get_tracer( + __name__, + __version__, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ).start_as_current_span(name=name) as span: if span.is_recording(): for key, val in attributes.items(): span.set_attribute(key, val) @@ -291,7 +295,7 @@ class descendent) is being instrumented with opentelemetry. Within a SklearnInstrumentor(packages=packages).instrument() - Model intrumentation example: + Model instrumentation example: .. code-block:: python diff --git a/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt new file mode 100644 index 0000000000..fc966b4d6a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sklearn/test-requirements.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +joblib==1.3.2 +numpy==1.24.4 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-sklearn/tests/test_sklearn.py b/instrumentation/opentelemetry-instrumentation-sklearn/tests/test_sklearn.py index ad4d032280..db69761ece 100644 --- a/instrumentation/opentelemetry-instrumentation-sklearn/tests/test_sklearn.py +++ b/instrumentation/opentelemetry-instrumentation-sklearn/tests/test_sklearn.py @@ -14,6 +14,7 @@ from sklearn.ensemble import RandomForestClassifier +# pylint: disable=no-name-in-module from opentelemetry.instrumentation.sklearn import ( DEFAULT_EXCLUDE_CLASSES, DEFAULT_METHODS, diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py index e14ac9600c..2107bc3e23 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py @@ -142,10 +142,20 @@ def _instrument(self, **kwargs): An instrumented engine if passed in as an argument or list of instrumented engines, None otherwise. """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter_provider = kwargs.get("meter_provider") - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) connections_usage = meter.create_up_down_counter( name=MetricInstruments.DB_CLIENT_CONNECTIONS_USAGE, 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 1cf980929b..0632d71faf 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -224,11 +224,11 @@ def _before_cur_exec( for key, value in attrs.items(): span.set_attribute(key, value) if self.enable_commenter: - commenter_data = dict( - db_driver=conn.engine.driver, + commenter_data = { + "db_driver": conn.engine.driver, # Driver/framework centric information. - db_framework=f"sqlalchemy:{__version__}", - ) + "db_framework": f"sqlalchemy:{__version__}", + } if self.commenter_options.get("opentelemetry_values", True): commenter_data.update(**_get_opentelemetry_values()) @@ -296,7 +296,9 @@ 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.UNIX.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( 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..26fd283824 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-0.txt @@ -0,0 +1,22 @@ +asgiref==3.7.2 +attrs==23.2.0 +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==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..bfb9dac972 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/test-requirements-1.txt @@ -0,0 +1,20 @@ +aiosqlite==0.20.0 +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +greenlet==3.0.3 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-sqlalchemy/tests/test_sqlalchemy_metrics.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy_metrics.py index 2d753c3c42..8d89959428 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy_metrics.py @@ -56,8 +56,7 @@ def test_metrics_one_connection(self): pool_logging_name=pool_name, ) - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 0) + self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) with engine.connect(): self.assert_pool_idle_used_expected( @@ -78,8 +77,7 @@ def test_metrics_without_pool_name(self): pool_logging_name=pool_name, ) - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 0) + self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) with engine.connect(): self.assert_pool_idle_used_expected( @@ -100,8 +98,7 @@ def test_metrics_two_connections(self): pool_logging_name=pool_name, ) - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 0) + self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) with engine.connect(): with engine.connect(): @@ -122,8 +119,7 @@ def test_metrics_connections(self): pool_logging_name=pool_name, ) - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 0) + self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) with engine.connect(): with engine.connect(): @@ -156,5 +152,4 @@ def test_metric_uninstrument(self): engine.connect() - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 0) + self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt new file mode 100644 index 0000000000..16cfb33801 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 2d123aa70e..1ebc3348d4 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -207,7 +207,12 @@ def instrument_app( tracer_provider=None, ): """Instrument an uninstrumented Starlette application.""" - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) if not getattr(app, "is_instrumented_by_opentelemetry", False): app.add_middleware( OpenTelemetryMiddleware, @@ -273,7 +278,10 @@ class _InstrumentedStarlette(applications.Starlette): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) meter = get_meter( - __name__, __version__, _InstrumentedStarlette._meter_provider + __name__, + __version__, + _InstrumentedStarlette._meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) self.add_middleware( OpenTelemetryMiddleware, diff --git a/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt new file mode 100644 index 0000000000..1cd21039a7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-starlette/test-requirements.txt @@ -0,0 +1,31 @@ +anyio==4.3.0 +asgiref==3.7.2 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +sniffio==1.3.0 +starlette==0.13.8 +tomli==2.0.1 +typing_extensions==4.9.0 +urllib3==2.2.1 +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 e1c77312a4..3784672fb5 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -467,15 +467,18 @@ async def _(websocket: WebSocket) -> None: return app -@patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders): + @patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, + ) + def setUp(self) -> None: + super().setUp() + def test_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( @@ -590,15 +593,18 @@ def test_custom_response_headers_not_in_span_attributes(self): self.assertNotIn(key, server_span.attributes) -@patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders): + @patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, + ) + def setUp(self) -> None: + super().setUp() + def test_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": ( 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 b7bd38907e..32766fa0c5 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 @@ -36,6 +36,10 @@ "system.thread_count": None "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], + "process.runtime.gc_count": None, + "process.runtime.thread_count": None, + "process.runtime.cpu.utilization": None, + "process.runtime.context_switches": ["involuntary", "voluntary"], } Usage @@ -63,6 +67,7 @@ "system.network.io": ["transmit", "receive"], "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], + "process.runtime.context_switches": ["involuntary", "voluntary"], } SystemMetricsInstrumentor(config=configuration).instrument() @@ -71,7 +76,9 @@ """ import gc +import logging import os +import sys import threading from platform import python_implementation from typing import Collection, Dict, Iterable, List, Optional @@ -86,6 +93,9 @@ from opentelemetry.metrics import CallbackOptions, Observation, get_meter from opentelemetry.sdk.util import get_dict_as_key +_logger = logging.getLogger(__name__) + + _DEFAULT_CONFIG = { "system.cpu.time": ["idle", "user", "system", "irq"], "system.cpu.utilization": ["idle", "user", "system", "irq"], @@ -105,8 +115,15 @@ "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.gc_count": None, + "process.runtime.thread_count": None, + "process.runtime.cpu.utilization": None, + "process.runtime.context_switches": ["involuntary", "voluntary"], } +if sys.platform == "darwin": + # see https://github.com/giampaolo/psutil/issues/1219 + _DEFAULT_CONFIG.pop("system.network.connections") + class SystemMetricsInstrumentor(BaseInstrumentor): def __init__( @@ -150,6 +167,9 @@ def __init__( self._runtime_memory_labels = self._labels.copy() self._runtime_cpu_time_labels = self._labels.copy() self._runtime_gc_count_labels = self._labels.copy() + self._runtime_thread_count_labels = self._labels.copy() + self._runtime_cpu_utilization_labels = self._labels.copy() + self._runtime_context_switches_labels = self._labels.copy() def instrumentation_dependencies(self) -> Collection[str]: return _instruments @@ -161,6 +181,7 @@ def _instrument(self, **kwargs): __name__, __version__, meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", ) if "system.cpu.time" in self._config: @@ -340,11 +361,39 @@ def _instrument(self, **kwargs): ) if "process.runtime.gc_count" in self._config: + if self._python_implementation == "pypy": + _logger.warning( + "The process.runtime.gc_count metric won't be collected because the interpreter is PyPy" + ) + else: + self._meter.create_observable_counter( + name=f"process.runtime.{self._python_implementation}.gc_count", + callbacks=[self._get_runtime_gc_count], + description=f"Runtime {self._python_implementation} GC count", + unit="bytes", + ) + + if "process.runtime.thread_count" in self._config: + self._meter.create_observable_up_down_counter( + name=f"process.runtime.{self._python_implementation}.thread_count", + callbacks=[self._get_runtime_thread_count], + description="Runtime active threads count", + ) + + if "process.runtime.cpu.utilization" in self._config: + self._meter.create_observable_gauge( + name=f"process.runtime.{self._python_implementation}.cpu.utilization", + callbacks=[self._get_runtime_cpu_utilization], + description="Runtime CPU utilization", + unit="1", + ) + + if "process.runtime.context_switches" in self._config: self._meter.create_observable_counter( - name=f"process.runtime.{self._python_implementation}.gc_count", - callbacks=[self._get_runtime_gc_count], - description=f"Runtime {self._python_implementation} GC count", - unit="bytes", + name=f"process.runtime.{self._python_implementation}.context_switches", + callbacks=[self._get_runtime_context_switches], + description="Runtime context switches", + unit="switches", ) def _uninstrument(self, **__): @@ -646,3 +695,34 @@ def _get_runtime_gc_count( for index, count in enumerate(gc.get_count()): self._runtime_gc_count_labels["count"] = str(index) yield Observation(count, self._runtime_gc_count_labels.copy()) + + def _get_runtime_thread_count( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for runtime active thread count""" + yield Observation( + self._proc.num_threads(), self._runtime_thread_count_labels.copy() + ) + + def _get_runtime_cpu_utilization( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for runtime CPU utilization""" + proc_cpu_percent = self._proc.cpu_percent() + yield Observation( + proc_cpu_percent, + self._runtime_cpu_utilization_labels.copy(), + ) + + def _get_runtime_context_switches( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for runtime context switches""" + ctx_switches = self._proc.num_ctx_switches() + for metric in self._config["process.runtime.context_switches"]: + if hasattr(ctx_switches, metric): + self._runtime_context_switches_labels["type"] = metric + yield Observation( + getattr(ctx_switches, metric), + self._runtime_context_switches_labels.copy(), + ) 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..ee56025d5f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +psutil==5.9.8 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index f6dbd6c9a1..3986a32c16 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -16,7 +16,7 @@ from collections import namedtuple from platform import python_implementation -from unittest import mock +from unittest import mock, skipIf from opentelemetry.instrumentation.system_metrics import ( SystemMetricsInstrumentor, @@ -96,7 +96,6 @@ def test_system_metrics_instrument(self): for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: metric_names.append(metric.name) - self.assertEqual(len(metric_names), 18) observer_names = [ "system.cpu.time", @@ -116,9 +115,19 @@ def test_system_metrics_instrument(self): "system.thread_count", f"process.runtime.{self.implementation}.memory", f"process.runtime.{self.implementation}.cpu_time", - f"process.runtime.{self.implementation}.gc_count", + f"process.runtime.{self.implementation}.thread_count", + f"process.runtime.{self.implementation}.context_switches", + f"process.runtime.{self.implementation}.cpu.utilization", ] + if self.implementation == "pypy": + self.assertEqual(len(metric_names), 20) + else: + self.assertEqual(len(metric_names), 21) + observer_names.append( + f"process.runtime.{self.implementation}.gc_count", + ) + for observer in metric_names: self.assertIn(observer, observer_names) observer_names.remove(observer) @@ -127,9 +136,14 @@ def test_runtime_metrics_instrument(self): runtime_config = { "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], - "process.runtime.gc_count": None, + "process.runtime.thread_count": None, + "process.runtime.cpu.utilization": None, + "process.runtime.context_switches": ["involuntary", "voluntary"], } + if self.implementation != "pypy": + runtime_config["process.runtime.gc_count"] = None + reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) runtime_metrics = SystemMetricsInstrumentor(config=runtime_config) @@ -140,14 +154,23 @@ def test_runtime_metrics_instrument(self): for scope_metrics in resource_metrics.scope_metrics: for metric in scope_metrics.metrics: metric_names.append(metric.name) - self.assertEqual(len(metric_names), 3) observer_names = [ f"process.runtime.{self.implementation}.memory", f"process.runtime.{self.implementation}.cpu_time", - f"process.runtime.{self.implementation}.gc_count", + f"process.runtime.{self.implementation}.thread_count", + f"process.runtime.{self.implementation}.context_switches", + f"process.runtime.{self.implementation}.cpu.utilization", ] + if self.implementation == "pypy": + self.assertEqual(len(metric_names), 5) + else: + self.assertEqual(len(metric_names), 6) + observer_names.append( + f"process.runtime.{self.implementation}.gc_count" + ) + for observer in metric_names: self.assertIn(observer, observer_names) observer_names.remove(observer) @@ -771,6 +794,9 @@ def test_runtime_cpu_time(self, mock_process_cpu_times): ) @mock.patch("gc.get_count") + @skipIf( + python_implementation().lower() == "pypy", "not supported for pypy" + ) def test_runtime_get_count(self, mock_gc_get_count): mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) @@ -782,3 +808,37 @@ def test_runtime_get_count(self, mock_gc_get_count): self._test_metrics( f"process.runtime.{self.implementation}.gc_count", expected ) + + @mock.patch("psutil.Process.num_ctx_switches") + def test_runtime_context_switches(self, mock_process_num_ctx_switches): + PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"]) + + mock_process_num_ctx_switches.configure_mock( + **{"return_value": PCtxSwitches(voluntary=1, involuntary=2)} + ) + + expected = [ + _SystemMetricsResult({"type": "voluntary"}, 1), + _SystemMetricsResult({"type": "involuntary"}, 2), + ] + self._test_metrics( + f"process.runtime.{self.implementation}.context_switches", expected + ) + + @mock.patch("psutil.Process.num_threads") + def test_runtime_thread_num(self, mock_process_thread_num): + mock_process_thread_num.configure_mock(**{"return_value": 42}) + + expected = [_SystemMetricsResult({}, 42)] + self._test_metrics( + f"process.runtime.{self.implementation}.thread_count", expected + ) + + @mock.patch("psutil.Process.cpu_percent") + def test_runtime_cpu_percent(self, mock_process_cpu_percent): + mock_process_cpu_percent.configure_mock(**{"return_value": 42}) + + expected = [_SystemMetricsResult({}, 42)] + self._test_metrics( + f"process.runtime.{self.implementation}.cpu.utilization", expected + ) diff --git a/instrumentation/opentelemetry-instrumentation-threading/LICENSE b/instrumentation/opentelemetry-instrumentation-threading/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /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 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/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..16874b13f9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/pyproject.toml @@ -0,0 +1,51 @@ +[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", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-instrumentation == 0.46b0.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..ff4933b20b --- /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.46b0.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..ffda21f234 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-threading/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +confluent-kafka==2.3.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index 1e2f0e5162..5a39538837 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -236,10 +236,20 @@ def _instrument(self, **kwargs): process lifetime. """ tracer_provider = kwargs.get("tracer_provider") - tracer = trace.get_tracer(__name__, __version__, tracer_provider) + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) meter_provider = kwargs.get("meter_provider") - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) client_histograms = _create_client_histograms(meter) server_histograms = _create_server_histograms(meter) @@ -286,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, diff --git a/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt new file mode 100644 index 0000000000..9f637278fd --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt @@ -0,0 +1,32 @@ +asgiref==3.7.2 +attrs==23.2.0 +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.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +tomli==2.0.1 +tornado==6.4 +typing_extensions==4.9.0 +urllib3==2.2.1 +Werkzeug==3.0.1 +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/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/src/opentelemetry/instrumentation/tortoiseorm/__init__.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py index 0b1ae4a29e..7988daf130 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py @@ -95,7 +95,12 @@ def _instrument(self, **kwargs): """ tracer_provider = kwargs.get("tracer_provider") # pylint: disable=attribute-defined-outside-init - self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) + self._tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) self.capture_parameters = kwargs.get("capture_parameters", False) if TORTOISE_SQLITE_SUPPORT: funcs = [ diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt new file mode 100644 index 0000000000..0fafc56253 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/test-requirements.txt @@ -0,0 +1,25 @@ +aiosqlite==0.17.0 +annotated-types==0.6.0 +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +iso8601==1.1.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pydantic==2.6.2 +pydantic_core==2.16.3 +pypika-tortoise==0.1.6 +pytest==7.1.3 +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/src/opentelemetry/instrumentation/urllib/__init__.py b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py index cdd35a0bad..3738c4d2c6 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/src/opentelemetry/instrumentation/urllib/__init__.py @@ -85,16 +85,13 @@ def response_hook(span, request_obj, response) Request, ) -from opentelemetry import context - -# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.urllib.package import _instruments from opentelemetry.instrumentation.urllib.version import __version__ from opentelemetry.instrumentation.utils import ( - _SUPPRESS_INSTRUMENTATION_KEY, http_status_to_status_code, + is_http_instrumentation_enabled, + suppress_http_instrumentation, ) from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject @@ -137,10 +134,20 @@ def _instrument(self, **kwargs): list of regexes used to exclude URLs from tracking """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) histograms = _create_client_histograms(meter) @@ -196,9 +203,7 @@ def call_wrapped(): def _instrumented_open_call( _, request, call_wrapped, get_or_create_headers ): # pylint: disable=too-many-locals - if context.get_value( - _SUPPRESS_INSTRUMENTATION_KEY - ) or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY): + if not is_http_instrumentation_enabled(): return call_wrapped() url = request.full_url @@ -226,18 +231,15 @@ def _instrumented_open_call( headers = get_or_create_headers() inject(headers) - token = context.attach( - context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_http_instrumentation(): start_time = default_timer() - result = call_wrapped() # *** PROCEED - except Exception as exc: # pylint: disable=W0703 - exception = exc - result = getattr(exc, "file", None) - finally: - elapsed_time = round((default_timer() - start_time) * 1000) - context.detach(token) + try: + result = call_wrapped() # *** PROCEED + except Exception as exc: # pylint: disable=W0703 + exception = exc + result = getattr(exc, "file", None) + finally: + elapsed_time = round((default_timer() - start_time) * 1000) if result is not None: code_ = result.getcode() @@ -297,17 +299,17 @@ def _create_client_histograms(meter) -> Dict[str, Histogram]: MetricInstruments.HTTP_CLIENT_DURATION: meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", - description="measures the duration outbound HTTP requests", + description="Measures the duration of outbound HTTP requests.", ), MetricInstruments.HTTP_CLIENT_REQUEST_SIZE: meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", - description="measures the size of HTTP request messages (compressed)", + description="Measures the size of HTTP request messages.", ), MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE: meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", - description="measures the size of HTTP response messages (compressed)", + description="Measures the size of HTTP response messages.", ), } diff --git a/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt new file mode 100644 index 0000000000..cdb10df7e9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib/test-requirements.txt @@ -0,0 +1,19 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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_metrics_instrumentation.py b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_metrics_instrumentation.py index f56aa4f97d..f79749dfd8 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_metrics_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_metrics_instrumentation.py @@ -13,11 +13,13 @@ # limitations under the License. +from platform import python_implementation from timeit import default_timer from urllib import request from urllib.parse import urlencode import httpretty +from pytest import mark from opentelemetry.instrumentation.urllib import ( # pylint: disable=no-name-in-module,import-error URLLibInstrumentor, @@ -185,16 +187,152 @@ def test_basic_metric_request_not_empty(self): ), ) + @mark.skipif( + python_implementation() == "PyPy", reason="Fails randomly in pypy" + ) def test_metric_uninstrument(self): with request.urlopen(self.URL): - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 3) - URLLibInstrumentor().uninstrument() - with request.urlopen(self.URL): - metrics = self.get_sorted_metrics() - self.assertEqual(len(metrics), 3) + self.assertEqual( + len( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ) + ), + 3, + ) + + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .bucket_counts[1] + ), + 1, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[0] + .bucket_counts[0] + ), + 1, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[2] + .data.data_points[0] + .bucket_counts[2] + ), + 1, + ) + + with request.urlopen(self.URL): + + self.assertEqual( + len( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ) + ), + 3, + ) + + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .bucket_counts[1] + ), + 2, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[0] + .bucket_counts[0] + ), + 2, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[2] + .data.data_points[0] + .bucket_counts[2] + ), + 2, + ) + + URLLibInstrumentor().uninstrument() + + with request.urlopen(self.URL): + + self.assertEqual( + len( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics + ) + ), + 3, + ) - for metric in metrics: - for point in list(metric.data.data_points): - self.assertEqual(point.count, 1) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[0] + .data.data_points[0] + .bucket_counts[1] + ), + 2, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[1] + .data.data_points[0] + .bucket_counts[0] + ), + 2, + ) + self.assertEqual( + ( + self.memory_metrics_reader.get_metrics_data() + .resource_metrics[0] + .scope_metrics[0] + .metrics[2] + .data.data_points[0] + .bucket_counts[2] + ), + 2, + ) diff --git a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py index f27f594a30..36189e12c1 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py +++ b/instrumentation/opentelemetry-instrumentation-urllib/tests/test_urllib_integration.py @@ -24,14 +24,14 @@ import httpretty import opentelemetry.instrumentation.urllib # pylint: disable=no-name-in-module,import-error -from opentelemetry import context, trace - -# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry import trace from opentelemetry.instrumentation.urllib import ( # pylint: disable=no-name-in-module,import-error URLLibInstrumentor, ) -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import ( + suppress_http_instrumentation, + suppress_instrumentation, +) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.sdk import resources from opentelemetry.semconv.trace import SpanAttributes @@ -255,26 +255,16 @@ def test_uninstrument_session(self): self.assert_span() def test_suppress_instrumentation(self): - token = context.attach( - context.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) def test_suppress_http_instrumentation(self): - token = context.attach( - context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) - ) - try: + with suppress_http_instrumentation(): result = self.perform_request(self.URL) self.assertEqual(result.read(), b"Hello!") - finally: - context.detach(token) self.assert_span(num_spans=0) 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 d3016ea5ee..985f291199 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/src/opentelemetry/instrumentation/urllib3/__init__.py @@ -79,7 +79,6 @@ def response_hook(span, request, response): """ import collections.abc -import contextlib import io import typing from timeit import default_timer @@ -88,16 +87,13 @@ def response_hook(span, request, response): import urllib3.connectionpool import wrapt -from opentelemetry import context - -# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.urllib3.package import _instruments from opentelemetry.instrumentation.urllib3.version import __version__ from opentelemetry.instrumentation.utils import ( - _SUPPRESS_INSTRUMENTATION_KEY, http_status_to_status_code, + is_http_instrumentation_enabled, + suppress_http_instrumentation, unwrap, ) from opentelemetry.metrics import Histogram, get_meter @@ -163,27 +159,37 @@ def _instrument(self, **kwargs): list of regexes used to exclude URLs from tracking """ tracer_provider = kwargs.get("tracer_provider") - tracer = get_tracer(__name__, __version__, tracer_provider) + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") - meter = get_meter(__name__, __version__, meter_provider) + meter = get_meter( + __name__, + __version__, + meter_provider, + schema_url="https://opentelemetry.io/schemas/1.11.0", + ) duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_DURATION, unit="ms", - description="measures the duration outbound HTTP requests", + description="Measures the duration of outbound HTTP requests.", ) request_size_histogram = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE, unit="By", - description="measures the size of HTTP request messages (compressed)", + description="Measures the size of HTTP request messages.", ) response_size_histogram = meter.create_histogram( name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE, unit="By", - description="measures the size of HTTP response messages (compressed)", + description="Measures the size of HTTP response messages.", ) _instrument( @@ -214,7 +220,7 @@ def _instrument( excluded_urls: ExcludeList = None, ): def instrumented_urlopen(wrapped, instance, args, kwargs): - if _is_instrumentation_suppressed(): + if not is_http_instrumentation_enabled(): return wrapped(*args, **kwargs) url = _get_url(instance, args, kwargs, url_filter) @@ -238,7 +244,7 @@ def instrumented_urlopen(wrapped, instance, args, kwargs): request_hook(span, instance, headers, body) inject(headers) - with _suppress_further_instrumentation(): + with suppress_http_instrumentation(): start_time = default_timer() response = wrapped(*args, **kwargs) elapsed_time = round((default_timer() - start_time) * 1000) @@ -342,13 +348,6 @@ def _apply_response(span: Span, response: urllib3.response.HTTPResponse): span.set_status(Status(http_status_to_status_code(response.status))) -def _is_instrumentation_suppressed() -> bool: - return bool( - context.get_value(_SUPPRESS_INSTRUMENTATION_KEY) - or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) - ) - - def _create_metric_attributes( instance: urllib3.connectionpool.HTTPConnectionPool, response: urllib3.response.HTTPResponse, @@ -372,16 +371,5 @@ def _create_metric_attributes( return metric_attributes -@contextlib.contextmanager -def _suppress_further_instrumentation(): - token = context.attach( - context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True) - ) - try: - yield - finally: - context.detach(token) - - def _uninstrument(): unwrap(urllib3.connectionpool.HTTPConnectionPool, "urlopen") 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..730ef16977 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-0.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==1.26.18 +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..1f10502f57 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-urllib3/test-requirements-1.txt @@ -0,0 +1,20 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +httpretty==1.1.4 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.1 +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/tests/test_urllib3_integration.py b/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_integration.py index 7ba7e2731b..23124ea590 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_integration.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_integration.py @@ -19,12 +19,12 @@ import urllib3 import urllib3.exceptions -from opentelemetry import context, trace - -# FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. -from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry import trace from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor -from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.instrumentation.utils import ( + suppress_http_instrumentation, + suppress_instrumentation, +) from opentelemetry.propagate import get_global_textmap, set_global_textmap from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.mock_textmap import MockTextMapPropagator @@ -138,6 +138,17 @@ def test_basic_https_success_using_connection_pool(self): self.assert_success_span(response, self.HTTPS_URL) + def test_schema_url(self): + pool = urllib3.HTTPSConnectionPool("mock") + response = pool.request("GET", "/status/200") + + self.assertEqual(b"Hello!", response.data) + span = self.assert_span() + self.assertEqual( + span.instrumentation_info.schema_url, + "https://opentelemetry.io/schemas/1.11.0", + ) + def test_basic_not_found(self): url_404 = "http://mock/status/404" httpretty.register_uri(httpretty.GET, url_404, status=404) @@ -214,20 +225,17 @@ def test_uninstrument(self): URLLib3Instrumentor().instrument() def test_suppress_instrumentation(self): - suppression_keys = ( - _SUPPRESS_HTTP_INSTRUMENTATION_KEY, - _SUPPRESS_INSTRUMENTATION_KEY, + suppression_cms = ( + suppress_instrumentation, + suppress_http_instrumentation, ) - for key in suppression_keys: + for cm in suppression_cms: self.memory_exporter.clear() - with self.subTest(key=key): - token = context.attach(context.set_value(key, True)) - try: + with self.subTest(cm=cm): + with cm(): response = self.perform_request(self.HTTP_URL) self.assertEqual(b"Hello!", response.data) - finally: - context.detach(token) self.assert_span(num_spans=0) diff --git a/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_metrics.py b/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_metrics.py index 2fd4cb2c5c..787b920d7c 100644 --- a/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-urllib3/tests/test_urllib3_metrics.py @@ -155,6 +155,20 @@ def test_str_request_body_size_metrics(self): ], ) + def test_schema_url(self): + self.pool.request("POST", self.HTTP_URL, body="foobar") + + resource_metrics = ( + self.memory_metrics_reader.get_metrics_data().resource_metrics + ) + + for metrics in resource_metrics: + for scope_metrics in metrics.scope_metrics: + self.assertEqual( + scope_metrics.scope.schema_url, + "https://opentelemetry.io/schemas/1.11.0", + ) + def test_bytes_request_body_size_metrics(self): self.pool.request("POST", self.HTTP_URL, body=b"foobar") diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt new file mode 100644 index 0000000000..a4910352ed --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-wsgi/test-requirements.txt @@ -0,0 +1,18 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/opentelemetry-distro/test-requirements.txt b/opentelemetry-distro/test-requirements.txt new file mode 100644 index 0000000000..978389dc9a --- /dev/null +++ b/opentelemetry-distro/test-requirements.txt @@ -0,0 +1,17 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index df21ce5b3d..56f5f289e3 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -14,7 +14,7 @@ Installation pip install opentelemetry-instrumentation -This package provides a couple of commands that help automatically instruments a program: +This package provides commands that help automatically instrument a program: .. note:: You need to install a distro package to get auto instrumentation working. The ``opentelemetry-distro`` @@ -22,7 +22,10 @@ This package provides a couple of commands that help automatically instruments a For more info about ``opentelemetry-distro`` check `here `__ :: - pip install opentelemetry-distro[otlp] + pip install "opentelemetry-distro[otlp]" + + When creating a custom distro and/or configurator, be sure to add entry points for each under `opentelemetry_distro` and `opentelemetry_configurator` respectfully. + If you have entry points for multiple distros or configurators present in your environment, you should specify the entry point name of the distro and configurator you want to be used via the `OTEL_PYTHON_DISTRO` and `OTEL_PYTHON_CONFIGURATOR` environment variables. When creating a custom distro and/or configurator, be sure to add entry points for each under `opentelemetry_distro` and `opentelemetry_configurator` respectfully. If you have entry points for multiple distros or configurators present in your environment, you should specify the entry point name of the distro and configurator you want to be used via the `OTEL_PYTHON_DISTRO` and `OTEL_PYTHON_CONFIGURATOR` environment variables. @@ -33,13 +36,14 @@ opentelemetry-bootstrap :: - opentelemetry-bootstrap --action=install|requirements + opentelemetry-bootstrap [-a |--action=][install|requirements] -This commands inspects the active Python site-packages and figures out which -instrumentation packages the user might want to install. By default it prints out -a list of the suggested instrumentation packages which can be added to a requirements.txt -file. It also supports installing the suggested packages when run with :code:`--action=install` -flag. +This command install default instrumentation packages and detects active Python site-packages +to figure out which instrumentation packages the user might want to install. By default, it +prints out a list of the default and detected instrumentation packages that can be added to a +requirements.txt file. It also supports installing the packages when run with +:code:`--action=install` or :code:`-a install` flag. All default and detectable +instrumentation packages are defined `here `. opentelemetry-instrument @@ -51,12 +55,12 @@ opentelemetry-instrument The instrument command will try to automatically detect packages used by your python program and when possible, apply automatic tracing instrumentation on them. This means your program -will get automatic distributed tracing for free without having to make any code changes -at all. This will also configure a global tracer and tracing exporter without you having to -make any code changes. By default, the instrument command will use the OTLP exporter but -this can be overridden when needed. +will get automatic distributed tracing without having to make any code changes. This will +also configure a global tracer and tracing exporter as well as a meter and meter exporter. +By default, the instrument command will use the OTLP exporter but this can be overridden. -The command supports the following configuration options as CLI arguments and environment vars: +The command supports the following configuration options as CLI arguments and environment +variables: * ``--traces_exporter`` or ``OTEL_TRACES_EXPORTER`` @@ -64,27 +68,32 @@ The command supports the following configuration options as CLI arguments and en * ``--distro`` or ``OTEL_PYTHON_DISTRO`` * ``--configurator`` or ``OTEL_PYTHON_CONFIGURATOR`` -Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter -names (see below). +The exporter options define what exporter destination to use and can be set to one or more +exporter names (see below). You can pass multiple values to configure multiple exporters +(e.g., ``zipkin_json,otlp``). - Defaults to `otlp`. - Can be set to `none` to disable automatic tracer initialization. + - Can be set to 'console` to display JSON results locally. -You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` - -Well known trace exporter names: +Trace exporter names: - jaeger_proto - jaeger_thrift - opencensus + - otlp + - otlp_proto_grpc (`deprecated`) + - otlp_proto_http (`deprecated`) - zipkin_json - zipkin_proto + +Metric exporter names: + - otlp - otlp_proto_grpc (`deprecated`) - - otlp_proto_http (`deprecated`) + - prometheus Note: The default transport protocol for ``otlp`` is gRPC. -HTTP is currently supported for traces only, and should be set using ``OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf`` * ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` @@ -106,9 +115,9 @@ Examples :: - opentelemetry-instrument --traces_exporter otlp flask run --port=3000 + opentelemetry-instrument --traces_exporter console flask run --port=3000 -The above command will pass ``--traces_exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. +The above command will pass ``--traces_exporter console`` to the instrument command and ``--port=3000`` to ``flask run``. :: diff --git a/opentelemetry-instrumentation/pyproject.toml b/opentelemetry-instrumentation/pyproject.toml index b781f51542..a20b005911 100644 --- a/opentelemetry-instrumentation/pyproject.toml +++ b/opentelemetry-instrumentation/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -30,9 +29,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 new file mode 100644 index 0000000000..fbfc92cf21 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -0,0 +1,217 @@ +# 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 os +import threading +from enum import Enum + +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" + +_client_duration_attrs_old = [ + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_HOST, + SpanAttributes.NET_PEER_PORT, + SpanAttributes.NET_PEER_NAME, + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SCHEME, +] + +_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, + # TODO: Support opt-in for scheme in new semconv + # SpanAttributes.URL_SCHEME, +] + + +def _filter_duration_attrs(attrs, sem_conv_opt_in_mode): + filtered_attrs = {} + allowed_attributes = ( + _client_duration_attrs_new + if sem_conv_opt_in_mode == _OpenTelemetryStabilityMode.HTTP + else _client_duration_attrs_old + ) + for key, val in attrs.items(): + if key in allowed_attributes: + filtered_attrs[key] = val + return filtered_attrs + + +def set_string_attribute(result, key, value): + if value: + result[key] = value + + +def set_int_attribute(result, key, value): + if value: + try: + result[key] = int(value) + except ValueError: + return + + +def _set_http_method(result, original, normalized, sem_conv_opt_in_mode): + original = original.strip() + normalized = normalized.strip() + # 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 + ) + + 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 + ) + + +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) + + +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) + + +def _set_http_hostname(result, hostname, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_HOST, hostname) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, hostname) + + +def _set_http_net_peer_name(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) + + +def _set_http_port(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) + + +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, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, code + ) + + +def _set_http_network_protocol_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 + ) + + +_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN" + + +class _OpenTelemetryStabilitySignalType: + HTTP = "http" + + +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 _report_new(mode): + return mode.name != _OpenTelemetryStabilityMode.DEFAULT.name + + +def _report_old(mode): + return mode.name != _OpenTelemetryStabilityMode.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_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 + ) + + +# Get schema version based off of opt-in mode +def _get_schema_url(mode: _OpenTelemetryStabilityMode) -> str: + if mode is _OpenTelemetryStabilityMode.DEFAULT: + return "https://opentelemetry.io/schemas/1.11.0" + return SpanAttributes.SCHEMA_URL diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 5758ef1834..a09334432d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 6fa36f0463..0c8f0aa3c4 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright The OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -77,7 +75,7 @@ def _pip_check(): ) as check_pipe: pip_check = check_pipe.communicate()[0].decode() pip_check_lower = pip_check.lower() - for package_tup in libraries.values(): + for package_tup in libraries: for package in package_tup: if package.lower() in pip_check_lower: raise RuntimeError(f"Dependency conflict found: {pip_check}") @@ -102,15 +100,12 @@ def _is_installed(req): def _find_installed_libraries(): - libs = default_instrumentations[:] - libs.extend( - [ - v["instrumentation"] - for _, v in libraries.items() - if _is_installed(v["library"]) - ] - ) - return libs + for lib in default_instrumentations: + yield lib + + for lib in libraries: + if _is_installed(lib["library"]): + yield lib["instrumentation"] def _run_requirements(): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 8d856abf65..455d08e41a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -15,173 +15,183 @@ # DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. # RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. -libraries = { - "aio_pika": { +libraries = [ + { "library": "aio_pika >= 7.2.0, < 10.0.0", - "instrumentation": "opentelemetry-instrumentation-aio-pika==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-aio-pika==0.46b0.dev", }, - "aiohttp": { + { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.46b0.dev", }, - "aiopg": { + { + "library": "aiohttp ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.46b0.dev", + }, + { "library": "aiopg >= 0.13.0, < 2.0.0", - "instrumentation": "opentelemetry-instrumentation-aiopg==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.46b0.dev", }, - "asgiref": { + { "library": "asgiref ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-asgi==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-asgi==0.46b0.dev", }, - "asyncpg": { + { "library": "asyncpg >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-asyncpg==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.46b0.dev", }, - "boto": { + { "library": "boto~=2.0", - "instrumentation": "opentelemetry-instrumentation-boto==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-boto==0.46b0.dev", }, - "boto3": { + { "library": "boto3 ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.46b0.dev", }, - "botocore": { + { "library": "botocore ~= 1.0", - "instrumentation": "opentelemetry-instrumentation-botocore==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-botocore==0.46b0.dev", }, - "cassandra-driver": { + { "library": "cassandra-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-cassandra==0.46b0.dev", }, - "scylla-driver": { + { "library": "scylla-driver ~= 3.25", - "instrumentation": "opentelemetry-instrumentation-cassandra==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-cassandra==0.46b0.dev", }, - "celery": { + { "library": "celery >= 4.0, < 6.0", - "instrumentation": "opentelemetry-instrumentation-celery==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-celery==0.46b0.dev", }, - "confluent-kafka": { - "library": "confluent-kafka >= 1.8.2, <= 2.2.0", - "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.42b0.dev", + { + "library": "confluent-kafka >= 1.8.2, <= 2.3.0", + "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.46b0.dev", }, - "django": { + { "library": "django >= 1.10", - "instrumentation": "opentelemetry-instrumentation-django==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-django==0.46b0.dev", }, - "elasticsearch": { + { "library": "elasticsearch >= 2.0", - "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.46b0.dev", }, - "falcon": { - "library": "falcon >= 1.4.1, < 4.0.0", - "instrumentation": "opentelemetry-instrumentation-falcon==0.42b0.dev", + { + "library": "falcon >= 1.4.1, < 3.1.2", + "instrumentation": "opentelemetry-instrumentation-falcon==0.46b0.dev", }, - "fastapi": { + { "library": "fastapi ~= 0.58", - "instrumentation": "opentelemetry-instrumentation-fastapi==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.46b0.dev", }, - "flask": { - "library": "flask >= 1.0, < 3.0", - "instrumentation": "opentelemetry-instrumentation-flask==0.42b0.dev", + { + "library": "flask >= 1.0", + "instrumentation": "opentelemetry-instrumentation-flask==0.46b0.dev", }, - "grpcio": { + { "library": "grpcio ~= 1.27", - "instrumentation": "opentelemetry-instrumentation-grpc==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-grpc==0.46b0.dev", }, - "httpx": { + { "library": "httpx >= 0.18.0", - "instrumentation": "opentelemetry-instrumentation-httpx==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-httpx==0.46b0.dev", }, - "jinja2": { + { "library": "jinja2 >= 2.7, < 4.0", - "instrumentation": "opentelemetry-instrumentation-jinja2==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.46b0.dev", }, - "kafka-python": { + { "library": "kafka-python >= 2.0", - "instrumentation": "opentelemetry-instrumentation-kafka-python==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-kafka-python==0.46b0.dev", }, - "mysql-connector-python": { + { "library": "mysql-connector-python ~= 8.0", - "instrumentation": "opentelemetry-instrumentation-mysql==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-mysql==0.46b0.dev", }, - "mysqlclient": { + { "library": "mysqlclient < 3", - "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.46b0.dev", }, - "pika": { + { "library": "pika >= 0.12.0", - "instrumentation": "opentelemetry-instrumentation-pika==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-pika==0.46b0.dev", }, - "psycopg2": { + { + "library": "psycopg >= 3.1.0", + "instrumentation": "opentelemetry-instrumentation-psycopg==0.46b0.dev", + }, + { "library": "psycopg2 >= 2.7.3.1", - "instrumentation": "opentelemetry-instrumentation-psycopg2==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.46b0.dev", }, - "pymemcache": { + { "library": "pymemcache >= 1.3.5, < 5", - "instrumentation": "opentelemetry-instrumentation-pymemcache==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.46b0.dev", }, - "pymongo": { + { "library": "pymongo >= 3.1, < 5.0", - "instrumentation": "opentelemetry-instrumentation-pymongo==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.46b0.dev", }, - "PyMySQL": { + { "library": "PyMySQL < 2", - "instrumentation": "opentelemetry-instrumentation-pymysql==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.46b0.dev", }, - "pyramid": { + { "library": "pyramid >= 1.7", - "instrumentation": "opentelemetry-instrumentation-pyramid==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.46b0.dev", }, - "redis": { + { "library": "redis >= 2.6", - "instrumentation": "opentelemetry-instrumentation-redis==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-redis==0.46b0.dev", }, - "remoulade": { + { "library": "remoulade >= 0.50", - "instrumentation": "opentelemetry-instrumentation-remoulade==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-remoulade==0.46b0.dev", }, - "requests": { + { "library": "requests ~= 2.0", - "instrumentation": "opentelemetry-instrumentation-requests==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-requests==0.46b0.dev", }, - "scikit-learn": { + { "library": "scikit-learn ~= 0.24.0", - "instrumentation": "opentelemetry-instrumentation-sklearn==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.46b0.dev", }, - "sqlalchemy": { + { "library": "sqlalchemy", - "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.46b0.dev", }, - "starlette": { + { "library": "starlette ~= 0.13.0", - "instrumentation": "opentelemetry-instrumentation-starlette==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-starlette==0.46b0.dev", }, - "psutil": { + { "library": "psutil >= 5", - "instrumentation": "opentelemetry-instrumentation-system-metrics==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-system-metrics==0.46b0.dev", }, - "tornado": { + { "library": "tornado >= 5.1.1", - "instrumentation": "opentelemetry-instrumentation-tornado==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-tornado==0.46b0.dev", }, - "tortoise-orm": { + { "library": "tortoise-orm >= 0.17.0", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.46b0.dev", }, - "pydantic": { + { "library": "pydantic >= 1.10.2", - "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.46b0.dev", }, - "urllib3": { + { "library": "urllib3 >= 1.0.0, < 3.0.0", - "instrumentation": "opentelemetry-instrumentation-urllib3==0.42b0.dev", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.46b0.dev", }, -} +] default_instrumentations = [ - "opentelemetry-instrumentation-aws-lambda==0.42b0.dev", - "opentelemetry-instrumentation-dbapi==0.42b0.dev", - "opentelemetry-instrumentation-logging==0.42b0.dev", - "opentelemetry-instrumentation-sqlite3==0.42b0.dev", - "opentelemetry-instrumentation-urllib==0.42b0.dev", - "opentelemetry-instrumentation-wsgi==0.42b0.dev", + "opentelemetry-instrumentation-asyncio==0.46b0.dev", + "opentelemetry-instrumentation-aws-lambda==0.46b0.dev", + "opentelemetry-instrumentation-dbapi==0.46b0.dev", + "opentelemetry-instrumentation-logging==0.46b0.dev", + "opentelemetry-instrumentation-sqlite3==0.46b0.dev", + "opentelemetry-instrumentation-threading==0.46b0.dev", + "opentelemetry-instrumentation-urllib==0.46b0.dev", + "opentelemetry-instrumentation-wsgi==0.46b0.dev", ] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py index 7f05e7f30a..c612bfeceb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -21,6 +21,9 @@ from logging import getLogger from typing import Collection, Optional +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.dependencies import ( DependencyConflict, get_dependency_conflicts, @@ -105,6 +108,9 @@ def instrument(self, **kwargs): _LOG.error(conflict) return None + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + result = self._instrument( # pylint: disable=assignment-from-no-return **kwargs ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py index bc40f7742c..018595996f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -59,12 +59,13 @@ def set(self, carrier, key, value): # pylint: disable=no-self-use class FuncSetter(Setter): - """FuncSetter coverts a function into a valid Setter. Any function that can - set values in a carrier can be converted into a Setter by using FuncSetter. - This is useful when injecting trace context into non-dict objects such - HTTP Response objects for different framework. + """FuncSetter converts a function into a valid Setter. Any function that + can set values in a carrier can be converted into a Setter by using + FuncSetter. This is useful when injecting trace context into non-dict + objects such HTTP Response objects for different framework. - For example, it can be used to create a setter for Falcon response object as: + For example, it can be used to create a setter for Falcon response object + as: setter = FuncSetter(falcon.api.Response.append_header) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 35a55a1279..318aaeaa74 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -13,16 +13,22 @@ # limitations under the License. import urllib.parse +from contextlib import contextmanager from re import escape, sub -from typing import Dict, Sequence +from typing import Dict, Iterable, Sequence from wrapt import ObjectProxy from opentelemetry import context, trace -# pylint: disable=unused-import # pylint: disable=E0611 -from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 +# FIXME: fix the importing of these private attributes when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.= +from opentelemetry.context import ( + _SUPPRESS_HTTP_INSTRUMENTATION_KEY, + _SUPPRESS_INSTRUMENTATION_KEY, +) + +# pylint: disable=E0611 from opentelemetry.propagate import extract from opentelemetry.trace import StatusCode from opentelemetry.trace.propagation.tracecontext import ( @@ -152,3 +158,42 @@ def _python_path_without_directory(python_path, directory, path_separator): "", python_path, ) + + +def is_instrumentation_enabled() -> bool: + if context.get_value(_SUPPRESS_INSTRUMENTATION_KEY): + return False + return True + + +def is_http_instrumentation_enabled() -> bool: + return is_instrumentation_enabled() and not context.get_value( + _SUPPRESS_HTTP_INSTRUMENTATION_KEY + ) + + +@contextmanager +def _suppress_instrumentation(*keys: str) -> Iterable[None]: + """Suppress instrumentation within the context.""" + ctx = context.get_current() + for key in keys: + ctx = context.set_value(key, True, ctx) + token = context.attach(ctx) + try: + yield + finally: + context.detach(token) + + +@contextmanager +def suppress_instrumentation() -> Iterable[None]: + """Suppress instrumentation within the context.""" + with _suppress_instrumentation(_SUPPRESS_INSTRUMENTATION_KEY): + yield + + +@contextmanager +def suppress_http_instrumentation() -> Iterable[None]: + """Suppress instrumentation within the context.""" + with _suppress_instrumentation(_SUPPRESS_HTTP_INSTRUMENTATION_KEY): + yield diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py index c2996671d6..ff4933b20b 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.42b0.dev" +__version__ = "0.46b0.dev" diff --git a/opentelemetry-instrumentation/test-requirements.txt b/opentelemetry-instrumentation/test-requirements.txt new file mode 100644 index 0000000000..473a423bda --- /dev/null +++ b/opentelemetry-instrumentation/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py index 416aad0667..bbe2f5623b 100644 --- a/opentelemetry-instrumentation/tests/test_bootstrap.py +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -36,7 +36,7 @@ class TestBootstrap(TestCase): @classmethod def setUpClass(cls): cls.installed_libraries = sample_packages( - [lib["instrumentation"] for lib in libraries.values()], 0.6 + [lib["instrumentation"] for lib in libraries], 0.6 ) # treat 50% of sampled packages as pre-installed diff --git a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml index ee74713310..6361de39a0 100644 --- a/propagator/opentelemetry-propagator-aws-xray/pyproject.toml +++ b/propagator/opentelemetry-propagator-aws-xray/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "AWS X-Ray Propagator for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -28,9 +27,6 @@ dependencies = [ "opentelemetry-api ~= 1.12", ] -[project.optional-dependencies] -test = [] - [project.entry-points.opentelemetry_propagator] xray = "opentelemetry.propagators.aws:AwsXRayPropagator" 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..b6b197bdcc --- /dev/null +++ b/propagator/opentelemetry-propagator-aws-xray/test-requirements.txt @@ -0,0 +1,21 @@ +asgiref==3.7.2 +attrs==23.2.0 +certifi==2024.2.2 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +idna==3.6 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +pytest-benchmark==4.0.0 +requests==2.31.0 +tomli==2.0.1 +typing_extensions==4.10.0 +urllib3==2.2.1 +wrapt==1.16.0 +zipp==3.17.0 +-e propagator/opentelemetry-propagator-aws-xray diff --git a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml index 77cb23b42b..41f374ee1b 100644 --- a/propagator/opentelemetry-propagator-ot-trace/pyproject.toml +++ b/propagator/opentelemetry-propagator-ot-trace/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "OT Trace Propagator for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -29,9 +28,6 @@ dependencies = [ "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/test-requirements.txt b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt new file mode 100644 index 0000000000..69c1829a5c --- /dev/null +++ b/propagator/opentelemetry-propagator-ot-trace/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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..f92a5db8b1 --- /dev/null +++ b/resource/opentelemetry-resource-detector-azure/CHANGELOG.md @@ -0,0 +1,15 @@ +# 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 + +- Change meta data service timeout to 200ms + ([#2387](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2387)) + +## Version 0.1.2 (2024-01-25) + +- Initial CHANGELOG.md entry 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..eee7aaa46d --- /dev/null +++ b/resource/opentelemetry-resource-detector-container/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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 dc3e237946..fa490a6a35 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -27,12 +27,21 @@ DISTDIR=dist fi ) done + ( cd $DISTDIR - for x in *.tar.gz ; do + for x in * ; do + # FIXME: Remove this logic once these packages are available in Pypi + if echo "$x" | grep -Eq "^opentelemetry_(instrumentation_aiohttp_server|resource_detector_container).*(\.tar\.gz|\.whl)$"; then + echo "Skipping $x because of erroneous uploads. See: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2053" + rm $x + # FIXME: Remove this once opentelemetry-resource-detector-azure package goes 1.X + elif echo "$x" | grep -Eq "^opentelemetry_resource_detector_azure.*(\.tar\.gz|\.whl)$"; then + echo "Skipping $x because of manual upload by Azure maintainers." + rm $x # NOTE: We filter beta vs 1.0 package at this point because we can read the - # version directly from the .tar.gz file. - if (echo "$x" | grep -Eq ^opentelemetry_.*-0\..*\.tar\.gz$); then + # version directly from the .tar.gz/whl file + elif echo "$x" | grep -Eq "^opentelemetry_.*-0\..*(\.tar\.gz|\.whl)$"; then : else echo "Skipping $x because it is not in pre-1.0 state and should be released using a tag." diff --git a/scripts/generate_instrumentation_bootstrap.py b/scripts/generate_instrumentation_bootstrap.py index 23841309ff..1c0cc30f7b 100755 --- a/scripts/generate_instrumentation_bootstrap.py +++ b/scripts/generate_instrumentation_bootstrap.py @@ -21,7 +21,6 @@ import sys import astor -import pkg_resources from otel_packaging import ( get_instrumentation_packages, root_path, @@ -58,14 +57,12 @@ def main(): # pylint: disable=no-member default_instrumentations = ast.List(elts=[]) - libraries = ast.Dict(keys=[], values=[]) + libraries = ast.List(elts=[]) for pkg in get_instrumentation_packages(): if not pkg["instruments"]: default_instrumentations.elts.append(ast.Str(pkg["requirement"])) for target_pkg in pkg["instruments"]: - parsed = pkg_resources.Requirement.parse(target_pkg) - libraries.keys.append(ast.Str(parsed.name)) - libraries.values.append( + libraries.elts.append( ast.Dict( keys=[ast.Str("library"), ast.Str("instrumentation")], values=[ast.Str(target_pkg), ast.Str(pkg["requirement"])], 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/scripts/update_sha.py b/scripts/update_sha.py index d74ccc12db..1c913249a2 100644 --- a/scripts/update_sha.py +++ b/scripts/update_sha.py @@ -27,7 +27,7 @@ def get_sha(branch): url = API_URL + branch - response = requests.get(url) + response = requests.get(url, timeout=15) response.raise_for_status() return response.json()["sha"] diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml index dbb777cae6..12b2b23ddc 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml +++ b/sdk-extension/opentelemetry-sdk-extension-aws/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "AWS SDK extension for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -28,12 +27,16 @@ 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/src/opentelemetry/sdk/extension/aws/resource/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/__init__.py index 550fde612b..81877eea58 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/__init__.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/__init__.py @@ -11,6 +11,7 @@ # 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=no-name-in-module from opentelemetry.sdk.extension.aws.resource._lambda import ( AwsLambdaResourceDetector, diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py index bb36ae45d5..671358dddb 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/__init__.py @@ -11,6 +11,7 @@ # 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=no-name-in-module from opentelemetry.sdk.extension.aws.trace.aws_xray_id_generator import ( AwsXRayIdGenerator, diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_id_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_id_generator.py index 562ec3ff55..6068511844 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_id_generator.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/trace/aws_xray_id_generator.py @@ -77,8 +77,7 @@ class AwsXRayIdGenerator(IdGenerator): def generate_span_id(self) -> int: return self.random_id_generator.generate_span_id() - @staticmethod - def generate_trace_id() -> int: + def generate_trace_id(self) -> int: trace_time = int(time.time()) trace_identifier = random.getrandbits(96) return (trace_time << 96) + trace_identifier 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..e569ade322 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/test-requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.7.2 +attrs==23.2.0 +Deprecated==1.2.14 +importlib-metadata==6.11.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.4.0 +py==1.11.0 +py-cpuinfo==9.0.0 +pytest==7.1.3 +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/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py index ed78c8f0e7..d2e05240e3 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/trace/test_aws_xray_ids_generator.py @@ -16,6 +16,7 @@ import time import unittest +# pylint: disable=no-name-in-module from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator from opentelemetry.trace.span import INVALID_TRACE_ID diff --git a/util/opentelemetry-util-http/pyproject.toml b/util/opentelemetry-util-http/pyproject.toml index 285697e868..88724caf1a 100644 --- a/util/opentelemetry-util-http/pyproject.toml +++ b/util/opentelemetry-util-http/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["version"] description = "Web util for OpenTelemetry" readme = "README.rst" license = "Apache-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" authors = [ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, ] @@ -18,7 +18,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/httplib.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/httplib.py index de95a0aa92..3d6b875752 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/httplib.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/httplib.py @@ -78,7 +78,7 @@ def trysetip(conn: http.client.HTTPConnection, loglevel=logging.DEBUG) -> bool: state = _getstate() if not state: return True - spanlist = state.get("need_ip") # type: typing.List[Span] + spanlist: typing.List[Span] = state.get("need_ip") if not spanlist: return True @@ -88,7 +88,7 @@ def trysetip(conn: http.client.HTTPConnection, loglevel=logging.DEBUG) -> bool: sock = "" try: - sock = conn.sock # type: typing.Optional[socket.socket] + sock: typing.Optional[socket.socket] = conn.sock logger.debug("Got socket: %s", sock) if sock is None: return False @@ -163,7 +163,7 @@ def set_ip_on_next_http_connection(span: Span): finally: context.detach(token) else: - spans = state["need_ip"] # type: typing.List[Span] + spans: typing.List[Span] = state["need_ip"] spans.append(span) try: yield