Repository: open-telemetry/opentelemetry-python-contrib
Pull request #3319: Add asyncclick instrumentation
Author: login: jomcgi
Created: 2025-03-02T21:59:19Z | Merged: 2025-03-28T11:20:30Z

# Description

* 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 <anuraaga@gmail.com>
- Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>

Fixes #3318

## Type of change

- [x] New feature (non-breaking change which adds functionality)


# How Has This Been Tested?

- [x] tox -e test-instrumentation-asyncclick

# Does This PR Require a Core Repo Change?

- [x] No.

# Checklist:

See [contributing.md](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md) for styleguide, changelog guidelines, and more.

- [x] Followed the style guidelines of this project
- [x] Changelogs have been updated
- [x] Unit tests have been added
- [x] Documentation has been updated


Linked issue #3318: Feature Request: automatic instrumentation for asyncclick
### What problem do you want to solve?

[asyncclick](https://github.com/python-trio/asyncclick) is an async fork of click.
The existing click autoinstrumentation is compatible if you patch the import:
`import click` -> `import asyncclick as click`

### Describe the solution you'd like

A separate autoinstrumentation package for the async fork of click.

### Describe alternatives you've considered

Coupling this to the existing click autoinstrumentation adds an unnecessary dependency for most existing users and could become more complex as the libraries diverge.
A little duplication is better than a little coupling.

### Additional Context

Related issue for a project using asyncclick:
https://github.com/tahnok/colmi_r02_client/issues/38

### Would you like to implement a fix?

Yes

Diff:
diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index 81a9647788..1d1e2b00a6 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 c003de5050..2c91497268 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 b9e14f65dc..c9880fc7dd 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 2639d50e1c..8402fdbf9e 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 39850dd74f..f6009bd39a 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 4d6c81fe51..ef0a1b7128 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 0c41a54fba..d92c56e3d1 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 0e36426fe3..e190f302a4 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 0000000000..8288bfa1fe
--- /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 d74edf8ea2..4c89015b78 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 0000000000..0d21a737c3
--- /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 <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/asyncclick/asyncclick.html>`_
+* `OpenTelemetry Project <https://opentelemetry.io/>`_
diff --git a/instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml b/instrumentation/opentelemetry-instrumentation-asyncclick/pyproject.toml
new file mode 100644
index 0000000000..c8a98b29bb
--- /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 0000000000..db2c815853
--- /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 0000000000..c908a22f34
--- /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 0000000000..e69de29bb2
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 0000000000..dac6ee53c1
--- /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 0000000000..8e2deb26b6
--- /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 0000000000..d7104d5059
--- /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 04b7f0776d..9f9b62c0d4 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 6c54525a31..4cca90f1cc 100644
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -48,6 +48,10 @@
         "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 3af5cdff1b..f2e9cdf98d 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 afcc054e0f..68c59b4b12 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 c2b160252f..e5ba8062b7 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" }


Reviews:
user: login: xrmx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:35:38Z
user: login: jomcgi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:43:58Z
user: login: emdneto
body: wdyt add that to the pyright strict list in pyproject.toml?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T19:20:37Z
user: login: jomcgi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:17:04Z
user: login: jomcgi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:17:39Z
user: login: jomcgi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T09:18:04Z
user: login: emdneto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T23:48:52Z
user: login: emdneto
body: Thanks for the work so far on this one. I'll share this in SIG tomorrow and see if we can at least create the package in PyPI while this doesn't get merged.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T23:55:28Z
user: login: xrmx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:43:24Z
user: login: xrmx
body: Other than the small nit in the example LGTM. Hopefully this is not used by long running servers and so we may never need to introduce code to disable the instrumentation in these cases.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:45:08Z
user: login: jomcgi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:40:02Z
user: login: emdneto
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T21:10:08Z

Review comments:
diff_hunk: @@ -0,0 +1,185 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        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
+
+try:
+    from flask.cli import ScriptInfo as FlaskScriptInfo
+except ImportError:
+    FlaskScriptInfo = None
+
+
+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__)
+
+
+def _skip_servers(ctx: asyncclick.Context) -> bool:
+    # flask run
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: xrmx
body: Since flask is not using asyncclick this is dead code, no? Do you know of any server using this for their cli?
created_at: 2025-03-07T08:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,185 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        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
+
+try:
+    from flask.cli import ScriptInfo as FlaskScriptInfo
+except ImportError:
+    FlaskScriptInfo = None
+
+
+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__)
+
+
+def _skip_servers(ctx: asyncclick.Context) -> bool:
+    # flask run
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: jomcgi
body: No - I can remove it from this PR. 
It's not required for the project that I'm supporting.
created_at: 2025-03-07T08:43:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+OpenTelemetry asyncclick Instrumentation
+====================================
path: instrumentation/opentelemetry-instrumentation-asyncclick/README.rst
user: login: emdneto
body: ```suggestion
OpenTelemetry asyncclick Instrumentation
========================================
```
created_at: 2025-03-11T19:18:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,185 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        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
+
+try:
+    from flask.cli import ScriptInfo as FlaskScriptInfo
+except ImportError:
+    FlaskScriptInfo = None
+
+
+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__)
+
+
+def _skip_servers(ctx: asyncclick.Context) -> bool:
+    # flask run
+    if (
+        ctx.info_name == "run"
+        and FlaskScriptInfo
+        and isinstance(ctx.obj, FlaskScriptInfo)
+    ):
+        return True
+    # uvicorn
+    if ctx.info_name == "uvicorn":
+        return True
+    return False
+
+
+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]
+
+    # we don't want to create a root span for long running processes like servers
+    # otherwise all requests would have the same trace id
+    if _skip_servers(ctx):
+        return await wrapped(*args, **kwargs)
+
+    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, exc.__class__.__qualname__)
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: emdneto
body: ```suggestion
                span.set_attribute(ERROR_TYPE, type(exc).__qualname__)
```
created_at: 2025-03-11T19:19:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,185 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        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
+
+try:
+    from flask.cli import ScriptInfo as FlaskScriptInfo
+except ImportError:
+    FlaskScriptInfo = None
+
+
+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__)
+
+
+def _skip_servers(ctx: asyncclick.Context) -> bool:
+    # flask run
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: jomcgi
body: References to flask have been removed.
created_at: 2025-03-12T09:17:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+OpenTelemetry asyncclick Instrumentation
+====================================
path: instrumentation/opentelemetry-instrumentation-asyncclick/README.rst
user: login: jomcgi
body: Thanks for catching - this has been updated.
created_at: 2025-03-12T09:17:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,185 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        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
+
+try:
+    from flask.cli import ScriptInfo as FlaskScriptInfo
+except ImportError:
+    FlaskScriptInfo = None
+
+
+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__)
+
+
+def _skip_servers(ctx: asyncclick.Context) -> bool:
+    # flask run
+    if (
+        ctx.info_name == "run"
+        and FlaskScriptInfo
+        and isinstance(ctx.obj, FlaskScriptInfo)
+    ):
+        return True
+    # uvicorn
+    if ctx.info_name == "uvicorn":
+        return True
+    return False
+
+
+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]
+
+    # we don't want to create a root span for long running processes like servers
+    # otherwise all requests would have the same trace id
+    if _skip_servers(ctx):
+        return await wrapped(*args, **kwargs)
+
+    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, exc.__class__.__qualname__)
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: jomcgi
body: I've pushed a commit that includes this suggestion.
created_at: 2025-03-12T09:18:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,7 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([#3307](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3307))
 - Loosen `opentelemetry-instrumentation-starlette[instruments]` specifier
   ([#3304](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3304))
-
+- `opentelemetry-instrumentation-asyncclick`: new instrumentation to trace asyncclick commands
+  ([#3318](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3318))
path: CHANGELOG.md
user: login: emdneto
body: ```suggestion
  ([#3319](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3319))
```
created_at: 2025-03-26T23:48:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,162 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        hello()
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: xrmx
body: Is this missing an asyncio.run() wrapper?
created_at: 2025-03-27T16:43:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,162 @@
+# 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 asyncclick
+    from opentelemetry.instrumentation.asyncclick import AsyncClickInstrumentor
+
+    AsyncClickInstrumentor().instrument()
+
+    @asyncclick.command()
+    async def hello():
+       click.echo(f'Hello world!')
+
+    if __name__ == "__main__":
+        hello()
path: instrumentation/opentelemetry-instrumentation-asyncclick/src/opentelemetry/instrumentation/asyncclick/__init__.py
user: login: jomcgi
body: Thanks for catching that - I've updated and tested the example.
created_at: 2025-03-27T17:40:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: jomcgi
created_at: 2025-03-12T09:17:20Z
body: > wdyt add that to the pyright strict list in pyproject.toml?

Done!
author_association: CONTRIBUTOR
user: login: jomcgi
created_at: 2025-03-27T09:02:53Z
body: Thanks! I'll try to attend too.
I didn't know the best place to advocate for reviews / feedback so I really appreciate the help.

author_association: CONTRIBUTOR