diff --git a/.changelog/4730.added b/.changelog/4730.added
new file mode 100644
index 0000000000..7b0b6602bd
--- /dev/null
+++ b/.changelog/4730.added
@@ -0,0 +1 @@
+`opentelemetry-instrumentation-httpx`: add support for httpx2
diff --git a/.github/workflows/core_contrib_test.yml b/.github/workflows/core_contrib_test.yml
index 21d1a088ca..be80dd5e94 100644
--- a/.github/workflows/core_contrib_test.yml
+++ b/.github/workflows/core_contrib_test.yml
@@ -3087,6 +3087,126 @@ jobs:
- name: Run tests
run: tox -e py310-test-instrumentation-httpx-1-wrapt2 -- -ra
+ py310-test-instrumentation-httpx-2-wrapt1:
+ name: instrumentation-httpx-2-wrapt1
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }}
+ uses: actions/checkout@v4
+ with:
+ repository: open-telemetry/opentelemetry-python
+ ref: ${{ env.CORE_REPO_SHA }}
+ path: opentelemetry-python
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: "x64"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py310-test-instrumentation-httpx-2-wrapt1 -- -ra
+
+ py310-test-instrumentation-httpx-2-wrapt2:
+ name: instrumentation-httpx-2-wrapt2
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }}
+ uses: actions/checkout@v4
+ with:
+ repository: open-telemetry/opentelemetry-python
+ ref: ${{ env.CORE_REPO_SHA }}
+ path: opentelemetry-python
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: "x64"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py310-test-instrumentation-httpx-2-wrapt2 -- -ra
+
+ py310-test-instrumentation-httpx-3-wrapt1:
+ name: instrumentation-httpx-3-wrapt1
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }}
+ uses: actions/checkout@v4
+ with:
+ repository: open-telemetry/opentelemetry-python
+ ref: ${{ env.CORE_REPO_SHA }}
+ path: opentelemetry-python
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: "x64"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py310-test-instrumentation-httpx-3-wrapt1 -- -ra
+
+ py310-test-instrumentation-httpx-3-wrapt2:
+ name: instrumentation-httpx-3-wrapt2
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }}
+ uses: actions/checkout@v4
+ with:
+ repository: open-telemetry/opentelemetry-python
+ ref: ${{ env.CORE_REPO_SHA }}
+ path: opentelemetry-python
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: "x64"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py310-test-instrumentation-httpx-3-wrapt2 -- -ra
+
py310-test-util-http:
name: util-http
runs-on: ubuntu-latest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 09ca381581..f27399a863 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10436,6 +10436,386 @@ jobs:
- name: Run tests
run: tox -e py312-test-instrumentation-httpx-1-wrapt2 -- -ra
+ py310-test-instrumentation-httpx-2-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt1 3.10 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt1 -- -ra
+
+ py310-test-instrumentation-httpx-2-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt2 3.10 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt2 -- -ra
+
+ py311-test-instrumentation-httpx-2-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt1 3.11 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt1 -- -ra
+
+ py311-test-instrumentation-httpx-2-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt2 3.11 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt2 -- -ra
+
+ py312-test-instrumentation-httpx-2-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt1 3.12 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt1 -- -ra
+
+ py312-test-instrumentation-httpx-2-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt2 3.12 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt2 -- -ra
+
+ py313-test-instrumentation-httpx-2-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt1 3.13 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt1 -- -ra
+
+ py313-test-instrumentation-httpx-2-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt2 3.13 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-2-wrapt2 -- -ra
+
+ py314-test-instrumentation-httpx-2-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt1 3.14 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repo @ SHA - ${{ github.sha }}
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.14
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.14"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py314-test-instrumentation-httpx-2-wrapt1 -- -ra
+
+ py314-test-instrumentation-httpx-2-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-2-wrapt2 3.14 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repo @ SHA - ${{ github.sha }}
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.14
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.14"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py314-test-instrumentation-httpx-2-wrapt2 -- -ra
+
+ py310-test-instrumentation-httpx-3-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt1 3.10 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt1 -- -ra
+
+ py310-test-instrumentation-httpx-3-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt2 3.10 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt2 -- -ra
+
+ py311-test-instrumentation-httpx-3-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt1 3.11 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt1 -- -ra
+
+ py311-test-instrumentation-httpx-3-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt2 3.11 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt2 -- -ra
+
+ py312-test-instrumentation-httpx-3-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt1 3.12 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt1 -- -ra
+
+ py312-test-instrumentation-httpx-3-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt2 3.12 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt2 -- -ra
+
+ py313-test-instrumentation-httpx-3-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt1 3.13 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt1 -- -ra
+
+ py313-test-instrumentation-httpx-3-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt2 3.13 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ 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-httpx-3-wrapt2 -- -ra
+
+ py314-test-instrumentation-httpx-3-wrapt1_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt1 3.14 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repo @ SHA - ${{ github.sha }}
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.14
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.14"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py314-test-instrumentation-httpx-3-wrapt1 -- -ra
+
+ py314-test-instrumentation-httpx-3-wrapt2_ubuntu-latest:
+ name: instrumentation-httpx-3-wrapt2 3.14 Ubuntu
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repo @ SHA - ${{ github.sha }}
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.14
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.14"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py314-test-instrumentation-httpx-3-wrapt2 -- -ra
+
py313-test-instrumentation-httpx-1-wrapt1_ubuntu-latest:
name: instrumentation-httpx-1-wrapt1 3.13 Ubuntu
runs-on: ubuntu-latest
diff --git a/docs/instrumentation/httpx/httpx.rst b/docs/instrumentation/httpx/httpx.rst
index f816539f5b..964156ab08 100644
--- a/docs/instrumentation/httpx/httpx.rst
+++ b/docs/instrumentation/httpx/httpx.rst
@@ -8,3 +8,4 @@ API
:members:
:undoc-members:
:show-inheritance:
+ :inherited-members:
diff --git a/docs/nitpick-exceptions.ini b/docs/nitpick-exceptions.ini
index 5c05f44b53..2c069ab847 100644
--- a/docs/nitpick-exceptions.ini
+++ b/docs/nitpick-exceptions.ini
@@ -50,6 +50,9 @@ py-class=
fastapi.applications.FastAPI
starlette.applications.Starlette
_contextvars.Token
+ opentelemetry.instrumentation.httpx._SyncOpenTelemetryTransportBase
+ opentelemetry.instrumentation.httpx._AsyncOpenTelemetryTransportBase
+ opentelemetry.instrumentation.httpx._BaseHTTPXClientInstrumentor
any=
; API
diff --git a/instrumentation/README.md b/instrumentation/README.md
index 64766ac5c2..4721484fe7 100644
--- a/instrumentation/README.md
+++ b/instrumentation/README.md
@@ -24,7 +24,7 @@
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.92 | Yes | migration
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | development
-| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | Yes | migration
+| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0,httpx2 >= 2.0.0 | Yes | migration
| [opentelemetry-instrumentation-jinja2](./opentelemetry-instrumentation-jinja2) | jinja2 >= 2.7, < 4.0 | No | development
| [opentelemetry-instrumentation-kafka-python](./opentelemetry-instrumentation-kafka-python) | kafka-python >= 2.0, < 3.0,kafka-python-ng >= 2.0, < 3.0 | No | development
| [opentelemetry-instrumentation-logging](./opentelemetry-instrumentation-logging) | logging | No | development
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/README.rst b/instrumentation/opentelemetry-instrumentation-httpx/README.rst
index 1a999c2a9d..e3a9591aa3 100644
--- a/instrumentation/opentelemetry-instrumentation-httpx/README.rst
+++ b/instrumentation/opentelemetry-instrumentation-httpx/README.rst
@@ -7,7 +7,12 @@ OpenTelemetry HTTPX Instrumentation
:target: https://pypi.org/project/opentelemetry-instrumentation-httpx/
This library allows tracing HTTP requests made by the
-`httpx `_ library.
+`httpx `_ and
+`httpx2 `_ libraries.
+
+If both libraries are installed, use ``HTTPXClientInstrumentor`` for
+``httpx`` clients and ``HTTPX2ClientInstrumentor`` for ``httpx2`` clients.
+The instrumentors can be enabled independently.
Installation
------------
@@ -43,6 +48,27 @@ When using the instrumentor, all clients will automatically trace requests.
asyncio.run(get(url))
+The same package also supports `httpx2 `_ using
+the ``HTTPX2ClientInstrumentor``:
+
+.. code-block:: python
+
+ import httpx2
+ import asyncio
+ from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor
+
+ url = "https://example.com"
+ HTTPX2ClientInstrumentor().instrument()
+
+ with httpx2.Client() as client:
+ response = client.get(url)
+
+ async def get(url):
+ async with httpx2.AsyncClient() as client:
+ response = await client.get(url)
+
+ asyncio.run(get(url))
+
Instrumenting single clients
****************************
@@ -70,6 +96,17 @@ use the `HTTPXClientInstrumentor.instrument_client` method.
asyncio.run(get(url))
+For ``httpx2`` clients, use ``HTTPX2ClientInstrumentor.instrument_client``:
+
+.. code-block:: python
+
+ import httpx2
+ from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor
+
+ with httpx2.Client() as client:
+ HTTPX2ClientInstrumentor.instrument_client(client)
+ response = client.get("https://example.com")
+
Uninstrument
************
@@ -121,6 +158,20 @@ If you don't want to use the instrumentor class, you can use the transport class
asyncio.run(get(url))
+For ``httpx2`` transports, use ``SyncOpenTelemetryTransportHttpx2`` and
+``AsyncOpenTelemetryTransportHttpx2``:
+
+.. code-block:: python
+
+ import httpx2
+ from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransportHttpx2
+
+ transport = httpx2.HTTPTransport()
+ telemetry_transport = SyncOpenTelemetryTransportHttpx2(transport)
+
+ with httpx2.Client(transport=telemetry_transport) as client:
+ response = client.get("https://example.com")
+
Request and response hooks
***************************
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml
index 3e3074c489..b8cee9ef2d 100644
--- a/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml
+++ b/instrumentation/opentelemetry-instrumentation-httpx/pyproject.toml
@@ -33,12 +33,15 @@ dependencies = [
]
[project.optional-dependencies]
-instruments = [
+instruments = []
+instruments-any = [
"httpx >= 0.18.0",
+ "httpx2 >= 2.0.0",
]
[project.entry-points.opentelemetry_instrumentor]
httpx = "opentelemetry.instrumentation.httpx:HTTPXClientInstrumentor"
+httpx2 = "opentelemetry.instrumentation.httpx:HTTPX2ClientInstrumentor"
[project.urls]
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-httpx"
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
index 1ac10ff1ec..a18be5b607 100644
--- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
@@ -28,6 +28,26 @@ async def get(url):
asyncio.run(get(url))
+When instrumenting ``httpx2`` clients, use ``HTTPX2ClientInstrumentor``:
+
+.. code-block:: python
+
+ import httpx2
+ import asyncio
+ from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor
+
+ url = "https://example.com"
+ HTTPX2ClientInstrumentor().instrument()
+
+ with httpx2.Client() as client:
+ response = client.get(url)
+
+ async def get(url):
+ async with httpx2.AsyncClient() as client:
+ response = await client.get(url)
+
+ asyncio.run(get(url))
+
Instrumenting single clients
****************************
@@ -54,6 +74,17 @@ async def get(url):
asyncio.run(get(url))
+For ``httpx2`` clients, use ``HTTPX2ClientInstrumentor.instrument_client``:
+
+.. code-block:: python
+
+ import httpx2
+ from opentelemetry.instrumentation.httpx import HTTPX2ClientInstrumentor
+
+ with httpx2.Client() as client:
+ HTTPX2ClientInstrumentor.instrument_client(client)
+ response = client.get("https://example.com")
+
Uninstrument
************
@@ -105,6 +136,20 @@ async def get(url):
asyncio.run(get(url))
+For ``httpx2`` transports, use ``SyncOpenTelemetryTransportHttpx2`` and
+``AsyncOpenTelemetryTransportHttpx2``:
+
+.. code-block:: python
+
+ import httpx2
+ from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransportHttpx2
+
+ transport = httpx2.HTTPTransport()
+ telemetry_transport = SyncOpenTelemetryTransportHttpx2(transport)
+
+ with httpx2.Client(transport=telemetry_transport) as client:
+ response = client.get("https://example.com")
+
Request and response hooks
***************************
@@ -307,11 +352,11 @@ async def async_response_hook(span, request, response):
import typing
from collections import defaultdict
from functools import partial
+from importlib import import_module
from inspect import iscoroutinefunction
from timeit import default_timer
from types import TracebackType
-import httpx
from wrapt import wrap_function_wrapper
from opentelemetry.instrumentation._semconv import (
@@ -336,7 +381,10 @@ async def async_response_hook(span, request, response):
_set_status,
_StabilityMode,
)
-from opentelemetry.instrumentation.httpx.package import _instruments
+from opentelemetry.instrumentation.httpx.package import (
+ _instruments_httpx,
+ _instruments_httpx2,
+)
from opentelemetry.instrumentation.httpx.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.utils import (
@@ -372,8 +420,36 @@ async def async_response_hook(span, request, response):
sanitize_method,
)
+if typing.TYPE_CHECKING:
+ try:
+ import httpx
+ except ImportError:
+ import httpx2 as httpx
+
+ class _HTTPXModule(typing.Protocol):
+ Request: type[httpx.Request]
+ Response: type[httpx.Response]
+ Headers: type[httpx.Headers]
+ URL: type[httpx.URL]
+ BaseTransport: type[httpx.BaseTransport]
+ AsyncBaseTransport: type[httpx.AsyncBaseTransport]
+ HTTPTransport: type[httpx.HTTPTransport]
+ AsyncHTTPTransport: type[httpx.AsyncHTTPTransport]
+
+
_logger = logging.getLogger(__name__)
+
+def _try_import(name: str) -> _HTTPXModule | None:
+ try:
+ return typing.cast("_HTTPXModule", import_module(name))
+ except ImportError:
+ return None
+
+
+_httpx_module = _try_import("httpx")
+_httpx2_module = _try_import("httpx2")
+
RequestHook = typing.Callable[[Span, "RequestInfo"], None]
ResponseHook = typing.Callable[[Span, "RequestInfo", "ResponseInfo"], None]
AsyncRequestHook = typing.Callable[
@@ -407,12 +483,16 @@ def _get_default_span_name(method: str) -> str:
return method
-def _prepare_headers(headers: httpx.Headers | None) -> httpx.Headers:
- return httpx.Headers(headers)
+def _prepare_headers(
+ headers: httpx.Headers | None, module: _HTTPXModule
+) -> httpx.Headers:
+ return typing.cast("httpx.Headers", module.Headers(headers))
def _extract_parameters(
- args: tuple[typing.Any, ...], kwargs: dict[str, typing.Any]
+ args: tuple[typing.Any, ...],
+ kwargs: dict[str, typing.Any],
+ module: _HTTPXModule,
) -> tuple[
bytes,
httpx.URL | tuple[bytes, bytes, int | None, bytes],
@@ -420,11 +500,11 @@ def _extract_parameters(
httpx.SyncByteStream | httpx.AsyncByteStream | None,
dict[str, typing.Any],
]:
- if isinstance(args[0], httpx.Request):
+ if isinstance(args[0], module.Request):
# In httpx >= 0.20.0, handle_request receives a Request object
- request: httpx.Request = args[0]
+ request = typing.cast("httpx.Request", args[0])
method = request.method.encode()
- url = httpx.URL(str(request.url))
+ url = typing.cast("httpx.URL", module.URL(str(request.url)))
headers = request.headers
stream = request.stream
extensions = request.extensions
@@ -457,11 +537,11 @@ def _normalize_url(
return str(url)
-def _inject_propagation_headers(headers, args, kwargs):
- _headers = _prepare_headers(headers)
+def _inject_propagation_headers(headers, args, kwargs, module: _HTTPXModule):
+ _headers = _prepare_headers(headers, module)
inject(_headers)
- if isinstance(args[0], httpx.Request):
- request: httpx.Request = args[0]
+ if isinstance(args[0], module.Request):
+ request = typing.cast("httpx.Request", args[0])
request.headers = _headers
else:
kwargs["headers"] = _headers.raw
@@ -472,9 +552,10 @@ def _normalize_headers(
| dict[str, list[str] | str]
| list[tuple[bytes, bytes]]
| None,
+ module: _HTTPXModule,
) -> dict[str, list[str]]:
normalized_headers: defaultdict[str, list[str]] = defaultdict(list)
- if isinstance(headers, httpx.Headers):
+ if isinstance(headers, module.Headers):
for key in headers.keys():
normalized_headers[key.lower()].extend(
headers.get_list(key, split_commas=True)
@@ -496,6 +577,7 @@ def _normalize_headers(
def _extract_response(
response: httpx.Response
| tuple[int, httpx.Headers, httpx.SyncByteStream, dict[str, typing.Any]],
+ module: _HTTPXModule,
) -> tuple[
int,
httpx.Headers,
@@ -503,12 +585,13 @@ def _extract_response(
dict[str, typing.Any],
str,
]:
- if isinstance(response, httpx.Response):
- status_code = response.status_code
- headers = response.headers
- stream = response.stream
- extensions = response.extensions
- http_version = response.http_version
+ if isinstance(response, module.Response):
+ http_response = typing.cast("httpx.Response", response)
+ status_code = http_response.status_code
+ headers = http_response.headers
+ stream = http_response.stream
+ extensions = http_response.extensions
+ http_version = http_response.http_version
else:
status_code, headers, stream, extensions = response
http_version = extensions.get("http_version", b"HTTP/1.1").decode(
@@ -524,11 +607,12 @@ def _apply_request_client_attributes_to_span(
url: str | httpx.URL,
method_original: str,
semconv: _StabilityMode,
+ module: _HTTPXModule,
headers: httpx.Headers | dict[str, list[str] | str] | None = None,
captured_headers: list[str] | None = None,
sensitive_headers: list[str] | None = None,
):
- url = httpx.URL(url)
+ url = typing.cast("httpx.URL", module.URL(url))
# http semconv transition: http.method -> http.request.method
_set_http_method(
span_attributes,
@@ -550,7 +634,7 @@ def _apply_request_client_attributes_to_span(
span_attributes.update(
get_custom_header_attributes(
- _normalize_headers(headers),
+ _normalize_headers(headers, module),
captured_headers,
sensitive_headers,
normalise_request_header_name,
@@ -585,6 +669,7 @@ def _apply_response_client_attributes_to_span(
status_code: int,
http_version: str,
semconv: _StabilityMode,
+ module: _HTTPXModule,
headers: httpx.Headers | dict[str, list[str] | str] | None = None,
captured_headers: list[str] | None = None,
sensitive_headers: list[str] | None = None,
@@ -602,7 +687,7 @@ def _apply_response_client_attributes_to_span(
span.set_attributes(
get_custom_header_attributes(
- _normalize_headers(headers),
+ _normalize_headers(headers, module),
captured_headers,
sensitive_headers,
normalise_response_header_name,
@@ -651,7 +736,7 @@ def _apply_response_client_attributes_to_metrics(
)
-class SyncOpenTelemetryTransport(httpx.BaseTransport):
+class _SyncOpenTelemetryTransportBase:
"""Sync transport class that will trace all requests made with a client.
Args:
@@ -664,6 +749,8 @@ class SyncOpenTelemetryTransport(httpx.BaseTransport):
that is called right before the span ends
"""
+ _module: _HTTPXModule
+
def __init__(
self,
transport: httpx.BaseTransport,
@@ -721,7 +808,7 @@ def __init__(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
- def __enter__(self) -> SyncOpenTelemetryTransport:
+ def __enter__(self) -> _SyncOpenTelemetryTransportBase:
self._transport.__enter__()
return self
@@ -747,7 +834,7 @@ def handle_request(
return self._transport.handle_request(*args, **kwargs)
method, url, headers, stream, extensions = _extract_parameters(
- args, kwargs
+ args, kwargs, self._module
)
if self._excluded_urls and self._excluded_urls.url_disabled(
@@ -766,6 +853,7 @@ def handle_request(
url,
method_original,
self._sem_conv_opt_in_mode,
+ self._module,
headers,
self._captured_request_headers,
self._sensitive_headers,
@@ -780,7 +868,7 @@ def handle_request(
if callable(self._request_hook):
self._request_hook(span, request_info)
- _inject_propagation_headers(headers, args, kwargs)
+ _inject_propagation_headers(headers, args, kwargs, self._module)
start_time = default_timer()
@@ -792,9 +880,9 @@ def handle_request(
finally:
elapsed_time = max(default_timer() - start_time, 0)
- if isinstance(response, (httpx.Response, tuple)):
+ if isinstance(response, (self._module.Response, tuple)):
status_code, headers, stream, extensions, http_version = (
- _extract_response(response)
+ _extract_response(response, self._module)
)
# Always apply response attributes to metrics
@@ -813,6 +901,7 @@ def handle_request(
status_code,
http_version,
self._sem_conv_opt_in_mode,
+ self._module,
headers,
self._captured_response_headers,
self._sensitive_headers,
@@ -864,7 +953,7 @@ def close(self) -> None:
self._transport.close()
-class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
+class _AsyncOpenTelemetryTransportBase:
"""Async transport class that will trace all requests made with a client.
Args:
@@ -877,6 +966,8 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
that is called right before the span ends
"""
+ _module: _HTTPXModule
+
def __init__(
self,
transport: httpx.AsyncBaseTransport,
@@ -936,7 +1027,7 @@ def __init__(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
)
- async def __aenter__(self) -> "AsyncOpenTelemetryTransport":
+ async def __aenter__(self) -> _AsyncOpenTelemetryTransportBase:
await self._transport.__aenter__()
return self
@@ -960,7 +1051,7 @@ async def handle_async_request(
return await self._transport.handle_async_request(*args, **kwargs)
method, url, headers, stream, extensions = _extract_parameters(
- args, kwargs
+ args, kwargs, self._module
)
if self._excluded_urls and self._excluded_urls.url_disabled(
@@ -979,6 +1070,7 @@ async def handle_async_request(
url,
method_original,
self._sem_conv_opt_in_mode,
+ self._module,
headers,
self._captured_request_headers,
self._sensitive_headers,
@@ -993,7 +1085,7 @@ async def handle_async_request(
if callable(self._request_hook):
await self._request_hook(span, request_info)
- _inject_propagation_headers(headers, args, kwargs)
+ _inject_propagation_headers(headers, args, kwargs, self._module)
start_time = default_timer()
@@ -1007,9 +1099,9 @@ async def handle_async_request(
finally:
elapsed_time = max(default_timer() - start_time, 0)
- if isinstance(response, (httpx.Response, tuple)):
+ if isinstance(response, (self._module.Response, tuple)):
status_code, headers, stream, extensions, http_version = (
- _extract_response(response)
+ _extract_response(response, self._module)
)
# Always apply response attributes to metrics
@@ -1028,6 +1120,7 @@ async def handle_async_request(
status_code,
http_version,
self._sem_conv_opt_in_mode,
+ self._module,
headers,
self._captured_response_headers,
self._sensitive_headers,
@@ -1081,31 +1174,43 @@ async def aclose(self) -> None:
await self._transport.aclose()
-class HTTPXClientInstrumentor(BaseInstrumentor):
+class _BaseHTTPXClientInstrumentor(BaseInstrumentor):
# pylint: disable=protected-access
- """An instrumentor for httpx Client and AsyncClient
+ """Base instrumentor for httpx API-compatible clients.
See `BaseInstrumentor`
"""
+ _module: typing.ClassVar[_HTTPXModule | None]
+ _module_name: typing.ClassVar[str]
+ _instrumentation_dependencies: typing.ClassVar[tuple[str, ...]]
+
def instrumentation_dependencies(self) -> typing.Collection[str]:
- return _instruments
+ return self._instrumentation_dependencies
# pylint: disable=too-many-locals
def _instrument(self, **kwargs: typing.Any):
- """Instruments httpx Client and AsyncClient
+ """Instrument the configured httpx API-compatible clients.
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global
``meter_provider``: a MeterProvider, defaults to global
- ``request_hook``: A ``httpx.Client`` hook that receives the span and request
- that is called right after the span is created
- ``response_hook``: A ``httpx.Client`` hook that receives the span, request,
- and response that is called right before the span ends
- ``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient``
- ``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
+ ``request_hook``: A ``httpx.Client`` or ``httpx2.Client`` hook
+ that receives the span and request that is called right
+ after the span is created.
+ ``response_hook``: A ``httpx.Client`` or ``httpx2.Client`` hook
+ that receives the span, request, and response that is
+ called right before the span ends.
+ ``async_request_hook``: Async request hook for
+ ``httpx.AsyncClient`` or ``httpx2.AsyncClient``.
+ ``async_response_hook``: Async response hook for
+ ``httpx.AsyncClient`` or ``httpx2.AsyncClient``.
"""
+ module = self._module
+ if module is None:
+ raise ModuleNotFoundError(f"{self._module_name} must be installed")
+
tracer_provider = kwargs.get("tracer_provider")
meter_provider = kwargs.get("meter_provider")
request_hook = kwargs.get("request_hook")
@@ -1170,10 +1275,11 @@ def _instrument(self, **kwargs: typing.Any):
)
wrap_function_wrapper(
- "httpx",
+ self._module_name,
"HTTPTransport.handle_request",
partial(
self._handle_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1187,10 +1293,11 @@ def _instrument(self, **kwargs: typing.Any):
),
)
wrap_function_wrapper(
- "httpx",
+ self._module_name,
"AsyncHTTPTransport.handle_async_request",
partial(
self._handle_async_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1205,8 +1312,11 @@ def _instrument(self, **kwargs: typing.Any):
)
def _uninstrument(self, **kwargs: typing.Any):
- unwrap(httpx.HTTPTransport, "handle_request")
- unwrap(httpx.AsyncHTTPTransport, "handle_async_request")
+ module = self._module
+ if module is None:
+ raise ModuleNotFoundError(f"{self._module_name} must be installed")
+ unwrap(module.HTTPTransport, "handle_request")
+ unwrap(module.AsyncHTTPTransport, "handle_async_request")
@staticmethod
def _handle_request_wrapper( # pylint: disable=too-many-locals
@@ -1214,6 +1324,8 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
instance: httpx.HTTPTransport,
args: tuple[typing.Any, ...],
kwargs: dict[str, typing.Any],
+ *,
+ module: _HTTPXModule,
tracer: Tracer,
duration_histogram_old: Histogram,
duration_histogram_new: Histogram,
@@ -1229,7 +1341,7 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
return wrapped(*args, **kwargs)
method, url, headers, stream, extensions = _extract_parameters(
- args, kwargs
+ args, kwargs, module
)
if excluded_urls and excluded_urls.url_disabled(_normalize_url(url)):
@@ -1246,6 +1358,7 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
url,
method_original,
sem_conv_opt_in_mode,
+ module,
headers,
captured_request_headers,
sensitive_headers,
@@ -1260,7 +1373,7 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
if callable(request_hook):
request_hook(span, request_info)
- _inject_propagation_headers(headers, args, kwargs)
+ _inject_propagation_headers(headers, args, kwargs, module)
start_time = default_timer()
@@ -1272,9 +1385,9 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
finally:
elapsed_time = max(default_timer() - start_time, 0)
- if isinstance(response, (httpx.Response, tuple)):
+ if isinstance(response, (module.Response, tuple)):
status_code, headers, stream, extensions, http_version = (
- _extract_response(response)
+ _extract_response(response, module)
)
# Always apply response attributes to metrics
@@ -1293,6 +1406,7 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
status_code,
http_version,
sem_conv_opt_in_mode,
+ module,
headers,
captured_response_headers,
sensitive_headers,
@@ -1345,6 +1459,8 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
instance: httpx.AsyncHTTPTransport,
args: tuple[typing.Any, ...],
kwargs: dict[str, typing.Any],
+ *,
+ module: _HTTPXModule,
tracer: Tracer,
duration_histogram_old: Histogram,
duration_histogram_new: Histogram,
@@ -1360,7 +1476,7 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
return await wrapped(*args, **kwargs)
method, url, headers, stream, extensions = _extract_parameters(
- args, kwargs
+ args, kwargs, module
)
if excluded_urls and excluded_urls.url_disabled(_normalize_url(url)):
@@ -1377,6 +1493,7 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
url,
method_original,
sem_conv_opt_in_mode,
+ module,
headers,
captured_request_headers,
sensitive_headers,
@@ -1391,7 +1508,7 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
if callable(async_request_hook):
await async_request_hook(span, request_info)
- _inject_propagation_headers(headers, args, kwargs)
+ _inject_propagation_headers(headers, args, kwargs, module)
start_time = default_timer()
@@ -1403,9 +1520,9 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
finally:
elapsed_time = max(default_timer() - start_time, 0)
- if isinstance(response, (httpx.Response, tuple)):
+ if isinstance(response, (module.Response, tuple)):
status_code, headers, stream, extensions, http_version = (
- _extract_response(response)
+ _extract_response(response, module)
)
# Always apply response attributes to metrics
@@ -1424,6 +1541,7 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
status_code,
http_version,
sem_conv_opt_in_mode,
+ module,
headers,
captured_response_headers,
sensitive_headers,
@@ -1477,10 +1595,10 @@ def instrument_client(
request_hook: RequestHook | AsyncRequestHook | None = None,
response_hook: ResponseHook | AsyncResponseHook | None = None,
) -> None:
- """Instrument httpx Client or AsyncClient
+ """Instrument an httpx API-compatible Client or AsyncClient.
Args:
- client: The httpx Client or AsyncClient instance
+ client: The Client or AsyncClient instance
tracer_provider: A TracerProvider, defaults to global
meter_provider: A MeterProvider, defaults to global
request_hook: A hook that receives the span and request that is called
@@ -1489,6 +1607,10 @@ def instrument_client(
that is called right before the span ends
"""
+ module = cls._module
+ if module is None:
+ raise ModuleNotFoundError(f"{cls._module_name} must be installed")
+
if getattr(client, "_is_instrumented_by_opentelemetry", False):
_logger.warning(
"Attempting to instrument Httpx client while already instrumented"
@@ -1560,6 +1682,7 @@ def instrument_client(
"handle_request",
partial(
cls._handle_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1579,6 +1702,7 @@ def instrument_client(
"handle_request",
partial(
cls._handle_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1598,6 +1722,7 @@ def instrument_client(
"handle_async_request",
partial(
cls._handle_async_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1617,6 +1742,7 @@ def instrument_client(
"handle_async_request",
partial(
cls._handle_async_request_wrapper,
+ module=module,
tracer=tracer,
duration_histogram_old=duration_histogram_old,
duration_histogram_new=duration_histogram_new,
@@ -1636,7 +1762,7 @@ def uninstrument_client(client: httpx.Client | httpx.AsyncClient) -> None:
"""Disables instrumentation for the given client instance
Args:
- client: The httpx Client or AsyncClient instance
+ client: The Client or AsyncClient instance
"""
if hasattr(client._transport, "handle_request"):
unwrap(client._transport, "handle_request")
@@ -1648,3 +1774,53 @@ def uninstrument_client(client: httpx.Client | httpx.AsyncClient) -> None:
for transport in client._mounts.values():
unwrap(transport, "handle_async_request")
client._is_instrumented_by_opentelemetry = False
+
+
+if _httpx_module is not None:
+
+ class SyncOpenTelemetryTransport(
+ _SyncOpenTelemetryTransportBase, _httpx_module.BaseTransport
+ ):
+ """Sync transport class that traces requests made with httpx."""
+
+ _module = _httpx_module
+
+ class AsyncOpenTelemetryTransport(
+ _AsyncOpenTelemetryTransportBase, _httpx_module.AsyncBaseTransport
+ ):
+ """Async transport class that traces requests made with httpx."""
+
+ _module = _httpx_module
+
+
+class HTTPXClientInstrumentor(_BaseHTTPXClientInstrumentor):
+ """An instrumentor for httpx Client and AsyncClient."""
+
+ _module = _httpx_module
+ _module_name = "httpx"
+ _instrumentation_dependencies = _instruments_httpx
+
+
+if _httpx2_module is not None:
+
+ class SyncOpenTelemetryTransportHttpx2(
+ _SyncOpenTelemetryTransportBase, _httpx2_module.BaseTransport
+ ):
+ """Sync transport class that traces requests made with httpx2."""
+
+ _module = _httpx2_module
+
+ class AsyncOpenTelemetryTransportHttpx2(
+ _AsyncOpenTelemetryTransportBase, _httpx2_module.AsyncBaseTransport
+ ):
+ """Async transport class that traces requests made with httpx2."""
+
+ _module = _httpx2_module
+
+
+class HTTPX2ClientInstrumentor(_BaseHTTPXClientInstrumentor):
+ """An instrumentor for httpx2 Client and AsyncClient."""
+
+ _module = _httpx2_module
+ _module_name = "httpx2"
+ _instrumentation_dependencies = _instruments_httpx2
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py
index 60edd1c0b1..c135bbc1f8 100644
--- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py
@@ -1,8 +1,11 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
+_instruments_httpx = ("httpx >= 0.18.0",)
+_instruments_httpx2 = ("httpx2 >= 2.0.0",)
-_instruments = ("httpx >= 0.18.0",)
+_instruments = ()
+_instruments_any = (*_instruments_httpx, *_instruments_httpx2)
_supports_metrics = True
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt1.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt1.txt
new file mode 100644
index 0000000000..43ade166cc
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt1.txt
@@ -0,0 +1,2 @@
+-r test-requirements-2.txt
+wrapt==1.17.3
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt2.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt2.txt
new file mode 100644
index 0000000000..e1147df891
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt2.txt
@@ -0,0 +1,2 @@
+-r test-requirements-2.txt
+wrapt==2.1.2
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2.txt
new file mode 100644
index 0000000000..3d8b8fa290
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2.txt
@@ -0,0 +1,5 @@
+httpx2==2.3.0
+pytest==7.4.4
+-e opentelemetry-instrumentation
+-e util/opentelemetry-util-http
+-e instrumentation/opentelemetry-instrumentation-httpx
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt1.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt1.txt
new file mode 100644
index 0000000000..de8d56b394
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt1.txt
@@ -0,0 +1,2 @@
+-r test-requirements-3.txt
+wrapt==1.17.3
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt2.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt2.txt
new file mode 100644
index 0000000000..3a175da179
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt2.txt
@@ -0,0 +1,2 @@
+-r test-requirements-3.txt
+wrapt==2.1.2
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3.txt b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3.txt
new file mode 100644
index 0000000000..a02b9558c5
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3.txt
@@ -0,0 +1,7 @@
+httpx==0.28.1
+httpx2==2.3.0
+pytest==7.4.4
+respx==0.22.0
+-e opentelemetry-instrumentation
+-e util/opentelemetry-util-http
+-e instrumentation/opentelemetry-instrumentation-httpx
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx2_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx2_integration.py
new file mode 100644
index 0000000000..f16fed054c
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx2_integration.py
@@ -0,0 +1,279 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# pylint: disable=arguments-differ,invalid-name,no-self-use,possibly-used-before-assignment
+
+import asyncio
+import importlib.util
+import threading
+import unittest
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from unittest import mock
+
+from opentelemetry import trace
+from opentelemetry.instrumentation._semconv import (
+ OTEL_SEMCONV_STABILITY_OPT_IN,
+ _OpenTelemetrySemanticConventionStability,
+)
+from opentelemetry.semconv._incubating.attributes.http_attributes import (
+ HTTP_METHOD,
+ HTTP_STATUS_CODE,
+ HTTP_URL,
+)
+from opentelemetry.test.test_base import TestBase
+
+httpx2_installed = importlib.util.find_spec("httpx2") is not None
+
+if httpx2_installed:
+ import httpx2
+
+ from opentelemetry.instrumentation.httpx import (
+ AsyncOpenTelemetryTransportHttpx2,
+ HTTPX2ClientInstrumentor,
+ SyncOpenTelemetryTransportHttpx2,
+ )
+
+
+class _TestHandler(BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+
+ def do_GET(self):
+ body = b"Hello!"
+ self.send_response(200)
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def log_message(self, fmt, *args):
+ pass
+
+
+@unittest.skipIf(not httpx2_installed, "httpx2 is not installed")
+class TestHTTPX2Instrumentor(TestBase):
+ # NOTE: Unlike the httpx tests which use respx for mocking, these tests use
+ # a local server for global instrumentation and httpx2.MockTransport for
+ # client-level instrumentation. At the time of writing, respx does not yet
+ # have stable httpx2 support (see
+ # https://github.com/lundberg/respx/issues/316 and PR #317).
+ # pytest-httpx2 v1.0.0 exists but uses a fixture-based API that does not fit
+ # this unittest-style suite. MockTransport is built in to httpx2 and has zero
+ # external dependencies; the local server is used where we need to exercise
+ # HTTPTransport.handle_request directly.
+ #
+ # These tests are intentionally focused on proving that httpx2 reaches each
+ # public instrumentation path. The detailed span attributes, metrics,
+ # semantic convention modes, header capture, error paths, and suppression
+ # behavior are still covered by the original httpx suite because both
+ # instrumentors share the same wrapper and transport implementation.
+
+ def setUp(self):
+ super().setUp()
+ self.env_patch = mock.patch.dict(
+ "os.environ",
+ {OTEL_SEMCONV_STABILITY_OPT_IN: "default"},
+ )
+ self.env_patch.start()
+ _OpenTelemetrySemanticConventionStability._initialized = False
+ self.server = ThreadingHTTPServer(("127.0.0.1", 0), _TestHandler)
+ self.server_thread = threading.Thread(
+ target=self.server.serve_forever,
+ daemon=True,
+ )
+ self.server_thread.start()
+ host, port = self.server.server_address
+ self.url = f"http://{host}:{port}/status/200"
+
+ def tearDown(self):
+ HTTPX2ClientInstrumentor().uninstrument()
+ self.server.shutdown()
+ self.server.server_close()
+ self.server_thread.join(timeout=5)
+ self.env_patch.stop()
+ super().tearDown()
+
+ def _mock_transport(self, status_code=200):
+ def handler(request):
+ return httpx2.Response(
+ status_code,
+ text="Hello!",
+ extensions={"http_version": b"HTTP/1.1"},
+ )
+
+ return httpx2.MockTransport(handler)
+
+ def _assert_basic_span(self, url=None):
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertIs(span.kind, trace.SpanKind.CLIENT)
+ self.assertEqual(span.name, "GET")
+ span_attributes = dict(span.attributes)
+ self.assertEqual(span_attributes[HTTP_METHOD], "GET")
+ self.assertEqual(span_attributes[HTTP_URL], url or self.url)
+ self.assertEqual(span_attributes[HTTP_STATUS_CODE], 200)
+ return span
+
+ def test_sync_global_instrumentation(self):
+ HTTPX2ClientInstrumentor().instrument(
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+
+ with httpx2.Client() as client:
+ response = client.get(self.url)
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span()
+
+ def test_async_global_instrumentation(self):
+ async def do_request():
+ HTTPX2ClientInstrumentor().instrument(
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+ async with httpx2.AsyncClient() as client:
+ return await client.get(self.url)
+
+ response = asyncio.run(do_request())
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span()
+
+ def test_sync_instrument_client(self):
+ url = "http://mock/status/200"
+ client = httpx2.Client(transport=self._mock_transport())
+ HTTPX2ClientInstrumentor.instrument_client(
+ client,
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+
+ response = client.get(url)
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span(url)
+ HTTPX2ClientInstrumentor.uninstrument_client(client)
+ client.close()
+
+ def test_async_instrument_client(self):
+ async def do_request():
+ url = "http://mock/status/200"
+ client = httpx2.AsyncClient(
+ transport=httpx2.MockTransport(
+ lambda request: httpx2.Response(
+ 200,
+ text="Hello!",
+ extensions={"http_version": b"HTTP/1.1"},
+ )
+ )
+ )
+ HTTPX2ClientInstrumentor.instrument_client(
+ client,
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+ response = await client.get(url)
+ HTTPX2ClientInstrumentor.uninstrument_client(client)
+ await client.aclose()
+ return url, response
+
+ url, response = asyncio.run(do_request())
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span(url)
+
+ def test_uninstrument_client(self):
+ url = "http://mock/status/200"
+ client = httpx2.Client(transport=self._mock_transport())
+ HTTPX2ClientInstrumentor.instrument_client(
+ client,
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+ HTTPX2ClientInstrumentor.uninstrument_client(client)
+
+ response = client.get(url)
+
+ self.assertEqual(response.text, "Hello!")
+ self.assertEqual(self.memory_exporter.get_finished_spans(), ())
+ client.close()
+
+ def test_sync_transport_wrapper(self):
+ url = "http://mock/status/200"
+ transport = SyncOpenTelemetryTransportHttpx2(
+ self._mock_transport(),
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+
+ with httpx2.Client(transport=transport) as client:
+ response = client.get(url)
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span(url)
+
+ def test_async_transport_wrapper(self):
+ async def do_request():
+ url = "http://mock/status/200"
+ transport = AsyncOpenTelemetryTransportHttpx2(
+ httpx2.MockTransport(
+ lambda request: httpx2.Response(
+ 200,
+ text="Hello!",
+ extensions={"http_version": b"HTTP/1.1"},
+ )
+ ),
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+ async with httpx2.AsyncClient(transport=transport) as client:
+ return url, await client.get(url)
+
+ url, response = asyncio.run(do_request())
+
+ self.assertEqual(response.text, "Hello!")
+ self._assert_basic_span(url)
+
+ def test_hooks(self):
+ url = "http://mock/status/200"
+
+ def request_hook(span, request):
+ span.set_attribute("request_hook", request.method.decode())
+
+ def response_hook(span, request, response):
+ span.set_attribute("response_hook", response.status_code)
+
+ client = httpx2.Client(transport=self._mock_transport())
+ HTTPX2ClientInstrumentor.instrument_client(
+ client,
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ request_hook=request_hook,
+ response_hook=response_hook,
+ )
+
+ response = client.get(url)
+
+ self.assertEqual(response.text, "Hello!")
+ span = self._assert_basic_span(url)
+ self.assertEqual(span.attributes["request_hook"], "GET")
+ self.assertEqual(span.attributes["response_hook"], 200)
+ HTTPX2ClientInstrumentor.uninstrument_client(client)
+ client.close()
+
+ def test_excluded_urls(self):
+ self.env_patch.stop()
+ with mock.patch.dict(
+ "os.environ",
+ {"OTEL_PYTHON_HTTPX_EXCLUDED_URLS": self.url},
+ ):
+ HTTPX2ClientInstrumentor().instrument(
+ tracer_provider=self.tracer_provider,
+ meter_provider=self.meter_provider,
+ )
+ with httpx2.Client() as client:
+ response = client.get(self.url)
+ self.env_patch.start()
+
+ self.assertEqual(response.text, "Hello!")
+ self.assertEqual(self.memory_exporter.get_finished_spans(), ())
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
index 8fd65b4c61..ac9adc84a3 100644
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -101,6 +101,10 @@
"library": "httpx >= 0.18.0",
"instrumentation": "opentelemetry-instrumentation-httpx==0.65b0.dev",
},
+ {
+ "library": "httpx2 >= 2.0.0",
+ "instrumentation": "opentelemetry-instrumentation-httpx==0.65b0.dev",
+ },
{
"library": "jinja2 >= 2.7, < 4.0",
"instrumentation": "opentelemetry-instrumentation-jinja2==0.65b0.dev",
diff --git a/pyproject.toml b/pyproject.toml
index acccc8e2d7..4c90657e1c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ dependencies = [
"opentelemetry-instrumentation-fastapi[instruments]",
"opentelemetry-instrumentation-flask[instruments]",
"opentelemetry-instrumentation-grpc[instruments]",
- "opentelemetry-instrumentation-httpx[instruments]",
+ "opentelemetry-instrumentation-httpx[instruments-any]",
"opentelemetry-instrumentation-jinja2[instruments]",
"opentelemetry-instrumentation-kafka-python[instruments]",
"opentelemetry-instrumentation-kafka-python[instruments-any]",
diff --git a/tox.ini b/tox.ini
index 4acd85337e..81ba52d1f5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -364,7 +364,11 @@ envlist =
; below mean these dependencies are being used:
; 0: httpx>=0.18.0,<0.19.0 respx~=0.17.0
; 1: httpx>=0.19.0 respx~=0.20.1
+ ; 2: httpx2>=2.0.0
+ ; 3: httpx>=0.19.0 respx~=0.20.1 httpx2>=2.0.0
py3{10,11,12}-test-instrumentation-httpx-{0,1}-{wrapt1,wrapt2}
+ py3{10,11,12,13,14}-test-instrumentation-httpx-2-{wrapt1,wrapt2}
+ py3{10,11,12,13,14}-test-instrumentation-httpx-3-{wrapt1,wrapt2}
py3{13,14}-test-instrumentation-httpx-1-{wrapt1,wrapt2}
pypy3-test-instrumentation-httpx-{0,1}-wrapt2
lint-instrumentation-httpx
@@ -741,6 +745,10 @@ deps =
httpx-0-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-0-wrapt2.txt
httpx-1-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt1.txt
httpx-1-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt2.txt
+ httpx-2-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt1.txt
+ httpx-2-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-2-wrapt2.txt
+ httpx-3-wrapt1: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt1.txt
+ httpx-3-wrapt2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-3-wrapt2.txt
lint-instrumentation-httpx: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/test-requirements-1-wrapt2.txt
# packages that are released individually should provide a test-requirements.txt with the lowest version of OTel API
@@ -1005,7 +1013,9 @@ commands =
test-instrumentation-wsgi: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi/tests {posargs}
lint-instrumentation-wsgi: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-wsgi"
- test-instrumentation-httpx: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs}
+ test-instrumentation-httpx-{0,1}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs}
+ test-instrumentation-httpx-2: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx2_integration.py {posargs}
+ test-instrumentation-httpx-3: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx/tests {posargs}
lint-instrumentation-httpx: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-httpx"
test-instrumentation-asyncio: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncio/tests {posargs}
diff --git a/uv.lock b/uv.lock
index e6d89beb73..adc4215353 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1893,6 +1893,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
+[[package]]
+name = "httpcore2"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "h11" },
+ { name = "truststore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/9b/2b1d1833a58236d1f6ee755e027a3917da0db59cc9708554cefc440ee8b6/httpcore2-2.4.0.tar.gz", hash = "sha256:3093a8ab8980d9f910b9cb4351df9186a0ad2350a6284a9107ac9a362a584422", size = 64618, upload-time = "2026-06-11T06:35:53.425Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/72/4fdf2306143a92a471fad9f3655aa542d43aa9188a7c9534e82c9aecf837/httpcore2-2.4.0-py3-none-any.whl", hash = "sha256:5218779da5d6e3c2013ac706121abfb3815d450e0613495c0de50264dce58242", size = 80151, upload-time = "2026-06-11T06:35:50.89Z" },
+]
+
[[package]]
name = "httpx"
version = "0.28.1"
@@ -1917,6 +1930,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
]
+[[package]]
+name = "httpx2"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpcore2" },
+ { name = "idna" },
+ { name = "truststore" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/60/b43ced4ccf26e95b396dbf67051d3e5042b645917d4da0469dd82a3bdd4f/httpx2-2.4.0.tar.gz", hash = "sha256:32e0734b61eb0824b3f56a9e98d6d92d381a3ef12c0045aa917ee63df6c411ef", size = 81691, upload-time = "2026-06-11T06:35:54.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/45/82bc57c3d9c3314f663b67cc057f1c017a6450685dde513f4f8db5cf431f/httpx2-2.4.0-py3-none-any.whl", hash = "sha256:425acd99297829599decf6701386dd84db3542597d36d3e2e4def930ecd57fd9", size = 74941, upload-time = "2026-06-11T06:35:52.235Z" },
+]
+
[[package]]
name = "hupper"
version = "1.12.1"
@@ -3496,20 +3525,22 @@ dependencies = [
]
[package.optional-dependencies]
-instruments = [
+instruments-any = [
{ name = "httpx" },
+ { name = "httpx2" },
]
[package.metadata]
requires-dist = [
- { name = "httpx", marker = "extra == 'instruments'", specifier = ">=0.18.0" },
+ { name = "httpx", marker = "extra == 'instruments-any'", specifier = ">=0.18.0" },
+ { name = "httpx2", marker = "extra == 'instruments-any'", specifier = ">=2.0.0" },
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
{ name = "opentelemetry-util-http", editable = "util/opentelemetry-util-http" },
{ name = "wrapt", specifier = ">=1.0.0,<3.0.0" },
]
-provides-extras = ["instruments"]
+provides-extras = ["instruments", "instruments-any"]
[[package]]
name = "opentelemetry-instrumentation-jinja2"
@@ -4325,7 +4356,7 @@ dependencies = [
{ name = "opentelemetry-instrumentation-fastapi", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-flask", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-grpc", extra = ["instruments"] },
- { name = "opentelemetry-instrumentation-httpx", extra = ["instruments"] },
+ { name = "opentelemetry-instrumentation-httpx", extra = ["instruments-any"] },
{ name = "opentelemetry-instrumentation-jinja2", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-kafka-python", extra = ["instruments-any"] },
{ name = "opentelemetry-instrumentation-logging" },
@@ -4398,7 +4429,7 @@ requires-dist = [
{ name = "opentelemetry-instrumentation-fastapi", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-flask", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-flask" },
{ name = "opentelemetry-instrumentation-grpc", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-grpc" },
- { name = "opentelemetry-instrumentation-httpx", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-httpx" },
+ { name = "opentelemetry-instrumentation-httpx", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-instrumentation-jinja2", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-jinja2" },
{ name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" },
{ name = "opentelemetry-instrumentation-kafka-python", extras = ["instruments-any"], editable = "instrumentation/opentelemetry-instrumentation-kafka-python" },
@@ -6332,6 +6363,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/98/36187601a15e3d37e9bfcf0e0e1055532b39d044353b06861c3a519737a9/translationstring-1.4-py2.py3-none-any.whl", hash = "sha256:5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262", size = 15028, upload-time = "2020-07-09T11:58:30.995Z" },
]
+[[package]]
+name = "truststore"
+version = "0.10.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"