From 0ea9998c4c46c86b849312bc94a226ca696c56b8 Mon Sep 17 00:00:00 2001 From: Joe McGinley <116890464+jomcgi@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:20:29 +0000 Subject: [PATCH] Add asyncclick instrumentation (#3319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add asyncclick instrumentation * Add instrumentation for asyncclick based CLI apps * Add tox * Add Changelog * Update workflows This implementation is based on the original click instrumentation work by: - Emídio Neto <9735060+emdneto@users.noreply.github.com> - Anuraag (Rag) Agrawal - Riccardo Magliocchetti * Update CHANGELOG.md Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> * Fix async refs in code-block example --------- Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- .github/component_owners.yml | 3 + .github/workflows/core_contrib_test_0.yml | 22 ++ .github/workflows/lint_0.yml | 18 + .github/workflows/test_0.yml | 252 ++++++------ .github/workflows/test_1.yml | 252 ++++++------ .github/workflows/test_2.yml | 126 ++++++ CHANGELOG.md | 3 + docs-requirements.txt | 1 + .../instrumentation/asyncclick/asyncclick.rst | 7 + instrumentation/README.md | 1 + .../README.rst | 24 ++ .../pyproject.toml | 57 +++ .../instrumentation/asyncclick/__init__.py | 163 ++++++++ .../instrumentation/asyncclick/package.py | 16 + .../instrumentation/asyncclick/py.typed | 0 .../instrumentation/asyncclick/version.py | 15 + .../test-requirements.txt | 10 + .../tests/test_asyncclick.py | 371 ++++++++++++++++++ .../pyproject.toml | 1 + .../instrumentation/bootstrap_gen.py | 4 + pyproject.toml | 2 + tox.ini | 13 + uv.lock | 63 +++ 23 files changed, 1172 insertions(+), 252 deletions(-) create mode 100644 docs/instrumentation/asyncclick/asyncclick.rst create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/py.typed create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt create mode 100644 instrumentation/opentelemetry-instrumentation-asyncclick/tests/test_asyncclick.py diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 81a964778..1d1e2b00a 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -11,6 +11,9 @@ components: - oxeye-nikolay - nikosokolik + instrumentation/opentelemetry-instrumentation-asyncclick: + - jomcgi + instrumentation/opentelemetry-instrumentation-kafka-python: - nozik diff --git a/.github/workflows/core_contrib_test_0.yml b/.github/workflows/core_contrib_test_0.yml index c003de505..2c9149726 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -525,6 +525,28 @@ jobs: - name: Run tests run: tox -e py38-test-instrumentation-boto -- -ra + py38-test-instrumentation-asyncclick: + name: instrumentation-asyncclick + runs-on: ubuntu-latest + steps: + - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + architecture: "x64" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-asyncclick -- -ra + py38-test-instrumentation-click: name: instrumentation-click runs-on: ubuntu-latest diff --git a/.github/workflows/lint_0.yml b/.github/workflows/lint_0.yml index b9e14f65d..c9880fc7d 100644 --- a/.github/workflows/lint_0.yml +++ b/.github/workflows/lint_0.yml @@ -322,6 +322,24 @@ jobs: - name: Run tests run: tox -e lint-instrumentation-boto + lint-instrumentation-asyncclick: + name: instrumentation-asyncclick + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e lint-instrumentation-asyncclick + lint-instrumentation-click: name: instrumentation-click runs-on: ubuntu-latest diff --git a/.github/workflows/test_0.yml b/.github/workflows/test_0.yml index 2639d50e1..8402fdbf9 100644 --- a/.github/workflows/test_0.yml +++ b/.github/workflows/test_0.yml @@ -2842,6 +2842,132 @@ jobs: - name: Run tests run: tox -e py311-test-instrumentation-boto -- -ra + py38-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-asyncclick -- -ra + + py39-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-asyncclick -- -ra + + py310-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-instrumentation-asyncclick -- -ra + + py311-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-instrumentation-asyncclick -- -ra + + py312-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-instrumentation-asyncclick -- -ra + + py313-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick 3.13 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-instrumentation-asyncclick -- -ra + + pypy3-test-instrumentation-asyncclick_ubuntu-latest: + name: instrumentation-asyncclick pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e pypy3-test-instrumentation-asyncclick -- -ra + py38-test-instrumentation-click_ubuntu-latest: name: instrumentation-click 3.8 Ubuntu runs-on: ubuntu-latest @@ -4389,129 +4515,3 @@ jobs: - name: Run tests run: tox -e py312-test-instrumentation-urllib -- -ra - - py313-test-instrumentation-urllib_ubuntu-latest: - name: instrumentation-urllib 3.13 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py313-test-instrumentation-urllib -- -ra - - pypy3-test-instrumentation-urllib_ubuntu-latest: - name: instrumentation-urllib pypy-3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e pypy3-test-instrumentation-urllib -- -ra - - py38-test-instrumentation-urllib3-0_ubuntu-latest: - name: instrumentation-urllib3-0 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-instrumentation-urllib3-0 -- -ra - - py38-test-instrumentation-urllib3-1_ubuntu-latest: - name: instrumentation-urllib3-1 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-instrumentation-urllib3-1 -- -ra - - py39-test-instrumentation-urllib3-0_ubuntu-latest: - name: instrumentation-urllib3-0 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-instrumentation-urllib3-0 -- -ra - - py39-test-instrumentation-urllib3-1_ubuntu-latest: - name: instrumentation-urllib3-1 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-instrumentation-urllib3-1 -- -ra - - py310-test-instrumentation-urllib3-0_ubuntu-latest: - name: instrumentation-urllib3-0 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py310-test-instrumentation-urllib3-0 -- -ra diff --git a/.github/workflows/test_1.yml b/.github/workflows/test_1.yml index 39850dd74..f6009bd39 100644 --- a/.github/workflows/test_1.yml +++ b/.github/workflows/test_1.yml @@ -16,6 +16,132 @@ env: jobs: + py313-test-instrumentation-urllib_ubuntu-latest: + name: instrumentation-urllib 3.13 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-instrumentation-urllib -- -ra + + pypy3-test-instrumentation-urllib_ubuntu-latest: + name: instrumentation-urllib pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e pypy3-test-instrumentation-urllib -- -ra + + py38-test-instrumentation-urllib3-0_ubuntu-latest: + name: instrumentation-urllib3-0 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-urllib3-0 -- -ra + + py38-test-instrumentation-urllib3-1_ubuntu-latest: + name: instrumentation-urllib3-1 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-urllib3-1 -- -ra + + py39-test-instrumentation-urllib3-0_ubuntu-latest: + name: instrumentation-urllib3-0 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-urllib3-0 -- -ra + + py39-test-instrumentation-urllib3-1_ubuntu-latest: + name: instrumentation-urllib3-1 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-urllib3-1 -- -ra + + py310-test-instrumentation-urllib3-0_ubuntu-latest: + name: instrumentation-urllib3-0 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-instrumentation-urllib3-0 -- -ra + py310-test-instrumentation-urllib3-1_ubuntu-latest: name: instrumentation-urllib3-1 3.10 Ubuntu runs-on: ubuntu-latest @@ -4389,129 +4515,3 @@ jobs: - name: Run tests run: tox -e py312-test-instrumentation-threading -- -ra - - py313-test-instrumentation-threading_ubuntu-latest: - name: instrumentation-threading 3.13 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.13 - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py313-test-instrumentation-threading -- -ra - - pypy3-test-instrumentation-threading_ubuntu-latest: - name: instrumentation-threading pypy-3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python pypy-3.8 - uses: actions/setup-python@v5 - with: - python-version: "pypy-3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e pypy3-test-instrumentation-threading -- -ra - - py38-test-instrumentation-tornado_ubuntu-latest: - name: instrumentation-tornado 3.8 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: "3.8" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py38-test-instrumentation-tornado -- -ra - - py39-test-instrumentation-tornado_ubuntu-latest: - name: instrumentation-tornado 3.9 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py39-test-instrumentation-tornado -- -ra - - py310-test-instrumentation-tornado_ubuntu-latest: - name: instrumentation-tornado 3.10 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py310-test-instrumentation-tornado -- -ra - - py311-test-instrumentation-tornado_ubuntu-latest: - name: instrumentation-tornado 3.11 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.11 - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py311-test-instrumentation-tornado -- -ra - - py312-test-instrumentation-tornado_ubuntu-latest: - name: instrumentation-tornado 3.12 Ubuntu - runs-on: ubuntu-latest - steps: - - name: Checkout repo @ SHA - ${{ github.sha }} - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install tox - run: pip install tox-uv - - - name: Run tests - run: tox -e py312-test-instrumentation-tornado -- -ra diff --git a/.github/workflows/test_2.yml b/.github/workflows/test_2.yml index 4d6c81fe5..ef0a1b712 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -16,6 +16,132 @@ env: jobs: + py313-test-instrumentation-threading_ubuntu-latest: + name: instrumentation-threading 3.13 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-instrumentation-threading -- -ra + + pypy3-test-instrumentation-threading_ubuntu-latest: + name: instrumentation-threading pypy-3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python pypy-3.8 + uses: actions/setup-python@v5 + with: + python-version: "pypy-3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e pypy3-test-instrumentation-threading -- -ra + + py38-test-instrumentation-tornado_ubuntu-latest: + name: instrumentation-tornado 3.8 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py38-test-instrumentation-tornado -- -ra + + py39-test-instrumentation-tornado_ubuntu-latest: + name: instrumentation-tornado 3.9 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-instrumentation-tornado -- -ra + + py310-test-instrumentation-tornado_ubuntu-latest: + name: instrumentation-tornado 3.10 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-instrumentation-tornado -- -ra + + py311-test-instrumentation-tornado_ubuntu-latest: + name: instrumentation-tornado 3.11 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-instrumentation-tornado -- -ra + + py312-test-instrumentation-tornado_ubuntu-latest: + name: instrumentation-tornado 3.12 Ubuntu + runs-on: ubuntu-latest + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-instrumentation-tornado -- -ra + py313-test-instrumentation-tornado_ubuntu-latest: name: instrumentation-tornado 3.13 Ubuntu runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c41a54fb..d92c56e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-instrumentation-asyncclick`: new instrumentation to trace asyncclick commands + ([#3319](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3319)) + ### Fixed - `opentelemetry-instrumentation` Fix client address is set to server address in new semconv diff --git a/docs-requirements.txt b/docs-requirements.txt index 0e36426fe..e190f302a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -15,6 +15,7 @@ aiohttp~=3.0 aiokafka~=0.11.0 aiopg>=0.13.0,<1.3.0 asyncpg>=0.12.0 +asyncclick~=8.0 boto~=2.0 botocore~=1.0 boto3~=1.0 diff --git a/docs/instrumentation/asyncclick/asyncclick.rst b/docs/instrumentation/asyncclick/asyncclick.rst new file mode 100644 index 000000000..8288bfa1f --- /dev/null +++ b/docs/instrumentation/asyncclick/asyncclick.rst @@ -0,0 +1,7 @@ +.. include:: ../../../instrumentation/opentelemetry-instrumentation-asyncclick/README.rst + :end-before: References + +.. automodule:: opentelemetry.instrumentation.asyncclick + :members: + :undoc-members: + :show-inheritance: diff --git a/instrumentation/README.md b/instrumentation/README.md index d74edf8ea..4c89015b7 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -7,6 +7,7 @@ | [opentelemetry-instrumentation-aiokafka](./opentelemetry-instrumentation-aiokafka) | aiokafka >= 0.8, < 1.0 | No | development | [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No | development | [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | Yes | migration +| [opentelemetry-instrumentation-asyncclick](./opentelemetry-instrumentation-asyncclick) | asyncclick ~= 8.0 | No | development | [opentelemetry-instrumentation-asyncio](./opentelemetry-instrumentation-asyncio) | asyncio | No | development | [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No | development | [opentelemetry-instrumentation-aws-lambda](./opentelemetry-instrumentation-aws-lambda) | aws_lambda | No | development diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/README.rst b/instrumentation/opentelemetry-instrumentation-asyncclick/README.rst new file mode 100644 index 000000000..0d21a737c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry asyncclick Instrumentation +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-asyncclick.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-asyncclick/ + +This library allows tracing requests made by the asyncclick fork of the click library. + +Installation +------------ + + +:: + + pip install opentelemetry-instrumentation-asyncclick + + +References +---------- + +* `OpenTelemetry asyncclick/ Tracing `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml new file mode 100644 index 000000000..c8a98b29b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-asyncclick" +dynamic = ["version"] +description = "Async Click 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", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-semantic-conventions == 0.53b0.dev", + "wrapt ~= 1.0", + "typing_extensions ~= 4.12", +] + +[project.optional-dependencies] +instruments = [ + "asyncclick ~= 8.0", +] + +[project.entry-points.opentelemetry_instrumentor] +asyncclick = "opentelemetry.instrumentation.asyncclick:AsyncClickInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-asyncclick" +Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/asyncclick/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py new file mode 100644 index 000000000..db2c81585 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py @@ -0,0 +1,163 @@ +# 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 `asyncclick`_ CLI applications. The instrumentor will avoid instrumenting +well-known servers (e.g. *flask run* and *uvicorn*) to avoid unexpected effects +like every request having the same Trace ID. + + + +.. _asyncclick: https://pypi.org/project/asyncclick/ + +Usage +----- + +.. code-block:: python + + import asyncio + import asyncclick + from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor + + AsyncClickInstrumentor().instrument() + + @asyncclick.command() + async def hello(): + asyncclick.echo(f'Hello world!') + + if __name__ == "__main__": + asyncio.run(hello()) + +API +--- +""" + +from __future__ import annotations + +import os +import sys +from functools import partial +from logging import getLogger +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Collection, + TypeVar, +) + +import asyncclick +from typing_extensions import ParamSpec, Unpack +from wrapt import ( + wrap_function_wrapper, # type: ignore[reportUnknownVariableType] +) + +from opentelemetry import trace +from opentelemetry.instrumentation.asyncclick.package import _instruments +from opentelemetry.instrumentation.asyncclick.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import ( + unwrap, +) +from opentelemetry.semconv._incubating.attributes.process_attributes import ( + PROCESS_COMMAND_ARGS, + PROCESS_EXECUTABLE_NAME, + PROCESS_EXIT_CODE, + PROCESS_PID, +) +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from opentelemetry.trace.status import StatusCode + +if TYPE_CHECKING: + from typing import TypedDict + + class InstrumentKwargs(TypedDict, total=False): + tracer_provider: trace.TracerProvider + + class UninstrumentKwargs(TypedDict, total=False): + pass + + +_logger = getLogger(__name__) + + +T = TypeVar("T") +P = ParamSpec("P") + + +async def _command_invoke_wrapper( + wrapped: Callable[P, Awaitable[T]], + instance: asyncclick.core.Command, + args: tuple[Any, ...], + kwargs: dict[str, Any], + tracer: trace.Tracer, +) -> T: + # Subclasses of Command include groups and CLI runners, but + # we only want to instrument the actual commands which are + # instances of Command itself. + if instance.__class__ != asyncclick.Command: + return await wrapped(*args, **kwargs) + + ctx = args[0] + + span_name = ctx.info_name + span_attributes = { + PROCESS_COMMAND_ARGS: sys.argv, + PROCESS_EXECUTABLE_NAME: sys.argv[0], + PROCESS_EXIT_CODE: 0, + PROCESS_PID: os.getpid(), + } + + with tracer.start_as_current_span( + name=span_name, + kind=trace.SpanKind.INTERNAL, + attributes=span_attributes, + ) as span: + try: + result = await wrapped(*args, **kwargs) + return result + except Exception as exc: + span.set_status(StatusCode.ERROR, str(exc)) + if span.is_recording(): + span.set_attribute(ERROR_TYPE, type(exc).__qualname__) + span.set_attribute( + PROCESS_EXIT_CODE, getattr(exc, "exit_code", 1) + ) + raise + + +# pylint: disable=no-self-use +class AsyncClickInstrumentor(BaseInstrumentor): + """An instrumentor for asyncclick""" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Unpack[InstrumentKwargs]) -> None: + tracer_provider = kwargs.get("tracer_provider") + tracer = trace.get_tracer( + __name__, + __version__, + tracer_provider, + ) + + wrap_function_wrapper( + asyncclick.core.Command, + "invoke", + partial(_command_invoke_wrapper, tracer=tracer), + ) + + def _uninstrument(self, **kwargs: Unpack["UninstrumentKwargs"]) -> None: + unwrap(asyncclick.core.Command, "invoke") diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/package.py b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/package.py new file mode 100644 index 000000000..c908a22f3 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/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 = ("asyncclick ~= 8.0",) diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/py.typed b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/version.py b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/version.py new file mode 100644 index 000000000..dac6ee53c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/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.53b0.dev" diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt new file mode 100644 index 000000000..8e2deb26b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt @@ -0,0 +1,10 @@ + +asyncclick~=8.0 +Flask~=3.0 +pytest==7.4.4 +anyio~=4.5 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-asyncclick +-e instrumentation/opentelemetry-instrumentation-flask +-e instrumentation/opentelemetry-instrumentation-wsgi +-e util/opentelemetry-util-http diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/tests/test_asyncclick.py b/instrumentation/opentelemetry-instrumentation-asyncclick/tests/test_asyncclick.py new file mode 100644 index 000000000..d7104d505 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-asyncclick/tests/test_asyncclick.py @@ -0,0 +1,371 @@ +# 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 __future__ import annotations + +import asyncio +import os +from typing import Any +from unittest import IsolatedAsyncioTestCase, mock + +import asyncclick +import asyncclick.testing +from asyncclick.testing import CliRunner + +from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import StatusCode + + +def run_asyncclick_command_test( + command: asyncclick.core.Command, args: tuple[Any, ...] = (), **kwargs: Any +) -> asyncclick.testing.Result: + """ + Run an asyncclick command and return the result. + + Args: + command: The AsyncClick command to run + args: Command-line arguments + **kwargs: Additional arguments for CliRunner.invoke + + Returns: + The result of invoking the command + """ + + async def _run() -> asyncclick.testing.Result: + runner = CliRunner() + return await runner.invoke(command, args, **kwargs) + + return asyncio.run(_run()) + + +class ClickTestCase(TestBase, IsolatedAsyncioTestCase): + # pylint: disable=unbalanced-tuple-unpacking + def setUp(self): # pylint: disable=invalid-name + super().setUp() + + AsyncClickInstrumentor().instrument() + + def tearDown(self): # pylint: disable=invalid-name + super().tearDown() + AsyncClickInstrumentor().uninstrument() + + @mock.patch("sys.argv", ["command.py"]) + def test_cli_command_wrapping(self): + @asyncclick.command() + async def command() -> None: + pass + + result = run_asyncclick_command_test(command) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": ("command.py",), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command.py"]) + def test_cli_command_wrapping_with_name(self): + @asyncclick.command("mycommand") + async def renamedcommand() -> None: + pass + + result = run_asyncclick_command_test(renamedcommand) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "mycommand") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": ("command.py",), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command.py", "--opt", "argument"]) + def test_cli_command_wrapping_with_options(self): + @asyncclick.command() + @asyncclick.argument("argument") + @asyncclick.option("--opt/--no-opt", default=False) + async def command(argument: str, opt: str) -> None: + pass + + argv = ["command.py", "--opt", "argument"] + result = run_asyncclick_command_test(command, argv[1:]) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": tuple(argv), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command-raises.py"]) + def test_cli_command_raises_error(self): + @asyncclick.command() + async def command_raises() -> None: + raise ValueError() + + result = run_asyncclick_command_test(command_raises) + self.assertEqual(result.exit_code, 1) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.kind, SpanKind.INTERNAL) + self.assertEqual(span.name, "command-raises") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command-raises.py", + "process.command_args": ("command-raises.py",), + "process.exit.code": 1, + "process.pid": os.getpid(), + "error.type": "ValueError", + }, + ) + + def test_uninstrument(self): + AsyncClickInstrumentor().uninstrument() + + @asyncclick.command() + async def notracecommand() -> None: + pass + + result = run_asyncclick_command_test(notracecommand) + self.assertEqual(result.exit_code, 0) + + self.assertFalse(self.memory_exporter.get_finished_spans()) + AsyncClickInstrumentor().instrument() + + @mock.patch("sys.argv", ["command.py", "sub1", "sub2"]) + def test_nested_command_groups(self): + """Test instrumentation of nested command groups.""" + + @asyncclick.group() + async def cli() -> None: + pass + + @cli.group() + async def sub1() -> None: + pass + + @sub1.command() + async def sub2() -> None: + pass + + result = run_asyncclick_command_test(cli, ["sub1", "sub2"]) + self.assertEqual(result.exit_code, 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual( + len(spans), 1 + ) # Only the leaf command should be instrumented + span = spans[0] + self.assertEqual(span.name, "sub2") + self.assertEqual( + dict(span.attributes), + { + "process.executable.name": "command.py", + "process.command_args": ("command.py", "sub1", "sub2"), + "process.exit.code": 0, + "process.pid": os.getpid(), + }, + ) + + @mock.patch("sys.argv", ["command.py"]) + def test_command_with_callback(self): + """Test instrumentation of commands with callbacks.""" + callback_called = False + + def callback_func( + ctx: asyncclick.Context, param: asyncclick.Parameter, value: bool + ) -> bool: + nonlocal callback_called + callback_called = True + return value + + @asyncclick.command() + @asyncclick.option( + "--option", callback=callback_func, default="default" + ) + async def command(option: str) -> None: + pass + + result = run_asyncclick_command_test(command) + self.assertEqual(result.exit_code, 0) + self.assertTrue(callback_called) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.name, "command") + self.assertEqual(span.status.status_code, StatusCode.UNSET) + + @mock.patch("sys.argv", ["command.py"]) + def test_command_with_result_callback(self): + """Test instrumentation with result callbacks.""" + callback_called = False + + @asyncclick.group(chain=True) + async def cli() -> None: + pass + + @cli.result_callback() + async def process_result(result: asyncclick.testing.Result) -> None: + nonlocal callback_called + callback_called = True + + @cli.command() + async def command() -> None: + pass + + result = run_asyncclick_command_test(cli, ["command"]) + self.assertEqual(result.exit_code, 0) + self.assertTrue(callback_called) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "command") + + @mock.patch("sys.argv", ["command.py"]) + def test_command_chaining(self): + """Test instrumentation with command chaining.""" + + @asyncclick.group(chain=True) + async def cli() -> None: + pass + + @cli.command() + async def cmd1() -> None: + return "result1" + + @cli.command() + async def cmd2() -> None: + return "result2" + + result = run_asyncclick_command_test(cli, ["cmd1", "cmd2"]) + self.assertEqual(result.exit_code, 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + # Spans should be ordered by execution + self.assertEqual(spans[0].name, "cmd1") + self.assertEqual(spans[1].name, "cmd2") + + @mock.patch("sys.argv", ["command.py"]) + def test_custom_exit_codes(self) -> None: + """Test instrumentation with custom exit codes.""" + + @asyncclick.command() + async def command() -> None: + raise asyncclick.exceptions.Exit(code=42) + + result = run_asyncclick_command_test(command) + self.assertEqual(result.exit_code, 42) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.name, "command") + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.attributes["process.exit.code"], 42) + self.assertEqual(span.attributes["error.type"], "Exit") + + @mock.patch("sys.argv", ["command.py"]) + def test_context_object_passing(self): + """Test that instrumentation preserves context object passing.""" + + @asyncclick.group() + @asyncclick.option("--debug/--no-debug", default=False) + @asyncclick.pass_context + async def cli(ctx: asyncclick.Context, debug: bool) -> None: + ctx.ensure_object(dict) + ctx.obj["DEBUG"] = debug + + @cli.command() + @asyncclick.pass_context + async def command(ctx: asyncclick.Context) -> None: + assert isinstance(ctx.obj, dict) + assert "DEBUG" in ctx.obj + + result = run_asyncclick_command_test(cli, ["--debug", "command"]) + self.assertEqual(result.exit_code, 0) + + (span,) = self.memory_exporter.get_finished_spans() + self.assertEqual(span.name, "command") + + def test_multiple_instrumentation(self): + """Test that instrumenting multiple times only applies once.""" + # Already instrumented in setUp, instrument again + AsyncClickInstrumentor().instrument() + + @asyncclick.command() + async def command() -> None: + pass + + result = run_asyncclick_command_test(command) + self.assertEqual(result.exit_code, 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + def test_concurrency(self): + """Test instrumentation with concurrent command execution.""" + + @asyncclick.command() + async def command1() -> None: + pass + + @asyncclick.command() + async def command2() -> None: + pass + + async def run_both() -> ( + tuple[asyncclick.testing.Result, asyncclick.testing.Result] + ): + runner = CliRunner() + task1 = asyncio.create_task(runner.invoke(command1)) + task2 = asyncio.create_task(runner.invoke(command2)) + results = await asyncio.gather(task1, task2) + return results + + results = asyncio.run(run_both()) + self.assertEqual(results[0].exit_code, 0) + self.assertEqual(results[1].exit_code, 0) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + span_names = [span.name for span in spans] + self.assertIn("command1", span_names) + self.assertIn("command2", span_names) diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml index 04b7f0776..9f9b62c0d 100644 --- a/opentelemetry-contrib-instrumentations/pyproject.toml +++ b/opentelemetry-contrib-instrumentations/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "opentelemetry-instrumentation-aiokafka==0.53b0.dev", "opentelemetry-instrumentation-aiopg==0.53b0.dev", "opentelemetry-instrumentation-asgi==0.53b0.dev", + "opentelemetry-instrumentation-asyncclick==0.53b0.dev", "opentelemetry-instrumentation-asyncio==0.53b0.dev", "opentelemetry-instrumentation-asyncpg==0.53b0.dev", "opentelemetry-instrumentation-aws-lambda==0.53b0.dev", diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 6c54525a3..4cca90f1c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -48,6 +48,10 @@ libraries = [ "library": "asgiref ~= 3.0", "instrumentation": "opentelemetry-instrumentation-asgi==0.53b0.dev", }, + { + "library": "asyncclick ~= 8.0", + "instrumentation": "opentelemetry-instrumentation-asyncclick==0.53b0.dev", + }, { "library": "asyncpg >= 0.12.0", "instrumentation": "opentelemetry-instrumentation-asyncpg==0.53b0.dev", diff --git a/pyproject.toml b/pyproject.toml index 3af5cdff1..f2e9cdf98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,12 +196,14 @@ pythonVersion = "3.8" reportPrivateUsage = false # Ignore private attributes added by instrumentation packages. # Add progressively instrumentation packages here. include = [ + "instrumentation/opentelemetry-instrumentation-asyncclick", "instrumentation/opentelemetry-instrumentation-threading", "instrumentation-genai/opentelemetry-instrumentation-vertexai", ] # We should also add type hints to the test suite - It helps on finding bugs. # We are excluding for now because it's easier, and more important to add to the instrumentation packages. exclude = [ + "instrumentation/opentelemetry-instrumentation-asyncclick/tests/**/*.py", "instrumentation/opentelemetry-instrumentation-threading/tests/**", "instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/**/*.py", "instrumentation-genai/opentelemetry-instrumentation-vertexai/examples/**/*.py", diff --git a/tox.ini b/tox.ini index afcc054e0..68c59b4b1 100644 --- a/tox.ini +++ b/tox.ini @@ -109,6 +109,11 @@ envlist = ; pypy3-test-instrumentation-boto lint-instrumentation-boto + ; opentelemetry-instrumentation-asyncclick + py3{8,9,10,11,12,13}-test-instrumentation-asyncclick + pypy3-test-instrumentation-asyncclick + lint-instrumentation-asyncclick + ; opentelemetry-instrumentation-click py3{8,9,10,11,12,13}-test-instrumentation-click pypy3-test-instrumentation-click @@ -466,6 +471,9 @@ deps = pypy3-test-instrumentation-celery: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt lint-instrumentation-celery: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-celery/test-requirements-1.txt + asyncclick: {[testenv]test_deps} + asyncclick: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick/test-requirements.txt + click: {[testenv]test_deps} click: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-click/test-requirements.txt @@ -741,6 +749,9 @@ commands = test-instrumentation-asgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi/tests {posargs} lint-instrumentation-asgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asgi" + test-instrumentation-asyncclick: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick/tests {posargs} + lint-instrumentation-asyncclick: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncclick" + test-instrumentation-asyncpg: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg/tests {posargs} lint-instrumentation-asyncpg: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-asyncpg" @@ -1036,5 +1047,7 @@ deps = {toxinidir}/util/opentelemetry-util-http {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-vertexai[instruments] {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-google-genai[instruments] + {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick[instruments] + commands = pyright diff --git a/uv.lock b/uv.lock index c2b160252..e5ba8062b 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,7 @@ members = [ "opentelemetry-instrumentation-aiokafka", "opentelemetry-instrumentation-aiopg", "opentelemetry-instrumentation-asgi", + "opentelemetry-instrumentation-asyncclick", "opentelemetry-instrumentation-asyncio", "opentelemetry-instrumentation-asyncpg", "opentelemetry-instrumentation-aws-lambda", @@ -590,6 +591,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, ] +[[package]] +name = "asyncclick" +version = "8.1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/bf/59d836c3433d7aa07f76c2b95c4eb763195ea8a5d7f9ad3311ed30c2af61/asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0", size = 349073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/6e/9acdbb25733e1de411663b59abe521bec738e72fe4e85843f6ff8b212832/asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02", size = 99191 }, +] + +[[package]] +name = "asyncclick" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.10.*'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "anyio", version = "4.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/e1e5fdf1c1bb7e6e614987c120a98d9324bf8edfaa5f5cd16a6235c9d91b/asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c", size = 232900 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/cc/a436f0fc2d04e57a0697e0f87a03b9eaed03ad043d2d5f887f8eebcec95f/asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6", size = 99093 }, + { url = "https://files.pythonhosted.org/packages/92/c4/ae9e9d25522c6dc96ff167903880a0fe94d7bd31ed999198ee5017d977ed/asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678", size = 99115 }, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -2861,6 +2898,32 @@ requires-dist = [ ] provides-extras = ["instruments"] +[[package]] +name = "opentelemetry-instrumentation-asyncclick" +source = { editable = "instrumentation/opentelemetry-instrumentation-asyncclick" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] + +[package.optional-dependencies] +instruments = [ + { name = "asyncclick", version = "8.1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "asyncclick", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "asyncclick", marker = "extra == 'instruments'", specifier = "~=8.0" }, + { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, + { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, + { name = "typing-extensions", specifier = "~=4.12" }, + { name = "wrapt", specifier = "~=1.0" }, +] +provides-extras = ["instruments"] + [[package]] name = "opentelemetry-instrumentation-asyncio" source = { editable = "instrumentation/opentelemetry-instrumentation-asyncio" }