You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by yi...@apache.org on 2023/02/13 02:15:03 UTC
[skywalking-python] branch master updated: Add Httpx plugin (#283)
This is an automated email from the ASF dual-hosted git repository.
yihaochen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-python.git
The following commit(s) were added to refs/heads/master by this push:
new f9f3dcd Add Httpx plugin (#283)
f9f3dcd is described below
commit f9f3dcd3e818e305c7f7cc971a293d7de050d831
Author: XinweiLyu <lx...@gmail.com>
AuthorDate: Sun Feb 12 21:14:56 2023 -0500
Add Httpx plugin (#283)
---
CHANGELOG.md | 1 +
docs/en/contribution/Developer.md | 2 +
docs/en/contribution/How-to-develop-plugin.md | 29 ++---
docs/en/contribution/How-to-test-locally.md | 2 +
docs/en/setup/Plugins.md | 1 +
poetry.lock | 106 ++++++++++++++----
pyproject.toml | 1 +
skywalking/__init__.py | 1 +
skywalking/plugins/sw_httpx.py | 102 ++++++++++++++++++
tests/plugin/http/sw_httpx/__init__.py | 16 +++
tests/plugin/http/sw_httpx/docker-compose.yml | 63 +++++++++++
tests/plugin/http/sw_httpx/expected.data.yml | 136 ++++++++++++++++++++++++
tests/plugin/http/sw_httpx/services/__init__.py | 16 +++
tests/plugin/http/sw_httpx/services/consumer.py | 39 +++++++
tests/plugin/http/sw_httpx/services/provider.py | 32 ++++++
tests/plugin/http/sw_httpx/test_httpx.py | 36 +++++++
16 files changed, 548 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36e9c4c..8d7f963 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
- Add HBase plugin Python HappyBase model (#266)
- Add FastAPI plugin websocket protocol support (#269)
- Add Websockets (client) plugin (#269)
+ - Add HTTPX plugin (#283)
- Fixes:
- Allow RabbitMQ BlockingChannel.basic_consume() to link with outgoing spans (#224)
diff --git a/docs/en/contribution/Developer.md b/docs/en/contribution/Developer.md
index 132177d..3bf4781 100644
--- a/docs/en/contribution/Developer.md
+++ b/docs/en/contribution/Developer.md
@@ -25,6 +25,8 @@ We have migrated from basic pip to [Poetry](https://python-poetry.org/) to manag
Once you have `make` ready, run `make env`, this will automatically install the right Poetry release, and create
(plus manage) a `.venv` virtual environment for us based on the currently activated Python 3 version. Enjoy coding!
+Note: Make sure you have `python3` aliased to `python` available on Windows computers instead of pointing to the Microsoft app store.
+
### Switching between Multiple Python Versions
Do not develop/test on Python < 3.7, since Poetry and some other functionalities we implement rely on Python 3.7+
diff --git a/docs/en/contribution/How-to-develop-plugin.md b/docs/en/contribution/How-to-develop-plugin.md
index 4d78720..c1f52b5 100644
--- a/docs/en/contribution/How-to-develop-plugin.md
+++ b/docs/en/contribution/How-to-develop-plugin.md
@@ -3,24 +3,25 @@
You can always take [the existing plugins](../setup/Plugins.md) as examples, while there are some general ideas for all plugins.
1. A plugin is a module under the directory `skywalking/plugins` with an `install` method;
2. Inside the `install` method, you find out the relevant method(s) of the libraries that you plan to instrument, and create/close spans before/after those method(s).
-3. You should also provide version rules in the plugin module, which means the version of package your plugin support. You should init a dict with keys `name` and `rules`. the `name` is your plugin's corresponding package's name, the `rules` is the version rules this package should follow.
-
- You can use >, >=, ==, <=, <, and != operators in rules.
-
- The relation between rules element in the rules array is **OR**, which means the version of the package should follow at least one rule in rules array.
-
- You can set many version rules in one element of rules array, separate each other with a space character, the relation of rules in one rule element is **AND**, which means the version of package should follow all rules in this rule element.
-
- For example, below `version_rule` indicates that the package version of `django` should `>=2.0 AND <=2.3 AND !=2.2.1` OR `>3.0`.
+3. You should also provide version rules in the plugin module, which means the version of package your plugin aim to test.
+
+ All below variables will be used by the tools/plugin_doc_gen.py to produce a latest [Plugin Doc](../setup/Plugins.md).
+
```python
- version_rule = {
- "name": "django",
- "rules": [">=2.0 <=2.3 !=2.2.1", ">3.0"]
+ link_vector = ['https://www.python-httpx.org/'] # This should link to the official website/doc of this lib
+ # The support matrix is for scenarios where some libraries don't work for certain Python versions
+ # Therefore, we use the matrix to instruct the CI testing pipeline to skip over plugin test for such Python version
+ # The right side versions, should almost always use A.B.* to test the latest minor version of two recent major versions.
+ support_matrix = {
+ 'httpx': {
+ '>=3.7': ['0.23.*', '0.22.*']
+ }
}
+ # The note will be used when generating the plugin documentation for users.
+ note = """"""
```
4. Every plugin requires a corresponding test under `tests/plugin` before it can be merged, refer to the [Plugin Test Guide](How-to-test-plugin.md) when writing a plugin test.
-5. Update the [Supported Plugin List](../setup/Plugins.md).
-6. Add the environment variables to [Environment Variable list](../setup/Configuration.md) if any.
+5. Add the corresponding configuration options added/modified by the new plugin to the config.py and add new comments for each, then regenerate the `configuration.md` by `make doc-gen`.
## Steps after coding
diff --git a/docs/en/contribution/How-to-test-locally.md b/docs/en/contribution/How-to-test-locally.md
index 7ab9fb4..43d0291 100644
--- a/docs/en/contribution/How-to-test-locally.md
+++ b/docs/en/contribution/How-to-test-locally.md
@@ -11,6 +11,8 @@ Please first refer to the [Developer Guide](Developer.md) to set up a developmen
TL;DR: run ``make env``. This will create virtual environments for python and generate the protocol folder needed for the agent.
+Note: Make sure you have `python3` aliased to `python` available on Windows computers instead of pointing to the Microsoft app store.
+
By now, you can do what you want. Let's get to the topic of how to test.
The test process requires `docker` and `docker-compose` throughout. If you haven't installed them, please install them first.
diff --git a/docs/en/setup/Plugins.md b/docs/en/setup/Plugins.md
index 23df62e..4afa888 100644
--- a/docs/en/setup/Plugins.md
+++ b/docs/en/setup/Plugins.md
@@ -29,6 +29,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
| [happybase](https://happybase.readthedocs.io) | Python >=3.7 - ['1.2.0']; | `sw_happybase` |
| [http_server](https://docs.python.org/3/library/http.server.html) | Python >=3.7 - ['*']; | `sw_http_server` |
| [werkzeug](https://werkzeug.palletsprojects.com/) | Python >=3.7 - ['1.0.1', '2.0']; | `sw_http_server` |
+| [httpx](https://www.python-httpx.org/) | Python >=3.7 - ['0.23.*', '0.22.*']; | `sw_httpx` |
| [kafka-python](https://kafka-python.readthedocs.io) | Python >=3.7 - ['2.0']; | `sw_kafka` |
| [loguru](https://pypi.org/project/loguru/) | Python >=3.7 - ['0.6.0']; | `sw_loguru` |
| [mysqlclient](https://mysqlclient.readthedocs.io/) | Python >=3.7 - ['2.1.*']; | `sw_mysqlclient` |
diff --git a/poetry.lock b/poetry.lock
index 114c102..88afb64 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,14 +2,14 @@
[[package]]
name = "aiofiles"
-version = "22.1.0"
+version = "23.1.0"
description = "File support for asyncio."
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
files = [
- {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"},
- {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"},
+ {file = "aiofiles-23.1.0-py3-none-any.whl", hash = "sha256:9312414ae06472eb6f1d163f555e466a23aed1c8f60c30cccf7121dba2e53eb2"},
+ {file = "aiofiles-23.1.0.tar.gz", hash = "sha256:edd247df9a19e0db16534d4baaf536d6609a43e1de5401d7a4c1c148753a1635"},
]
[[package]]
@@ -91,14 +91,14 @@ hiredis = ["hiredis (>=1.0)"]
[[package]]
name = "aiormq"
-version = "6.6.4"
+version = "6.7.1"
description = "Pure python AMQP asynchronous client library"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
files = [
- {file = "aiormq-6.6.4-py3-none-any.whl", hash = "sha256:9fb93dc871eb9c45a410e29669624790c01b70d27f20d512a22b146c411440ea"},
- {file = "aiormq-6.6.4.tar.gz", hash = "sha256:95835a4db6117263305d450f838ccdc3eabd427c0deb32fd617769ad4c989e98"},
+ {file = "aiormq-6.7.1-py3-none-any.whl", hash = "sha256:c6de232c34c0be051a0251684fa480cb5ee498e9f536f244fba0668d06a7c8ed"},
+ {file = "aiormq-6.7.1.tar.gz", hash = "sha256:f0328da19ba47b9f8bcdb3eb80faa20a6acc195ba721f1ccc008754ddd0abaee"},
]
[package.dependencies]
@@ -901,14 +901,14 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "flake8-docstrings"
-version = "1.6.0"
+version = "1.7.0"
description = "Extension for flake8 which uses pydocstyle to check docstrings"
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
files = [
- {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"},
- {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"},
+ {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"},
+ {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"},
]
[package.dependencies]
@@ -1302,6 +1302,28 @@ files = [
six = "*"
thriftpy2 = ">=0.4"
+[[package]]
+name = "httpcore"
+version = "0.16.3"
+description = "A minimal low-level HTTP client."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
+ {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+]
+
+[package.dependencies]
+anyio = ">=3.0,<5.0"
+certifi = "*"
+h11 = ">=0.13,<0.15"
+sniffio = ">=1.0.0,<2.0.0"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
[[package]]
name = "httptools"
version = "0.5.0"
@@ -1356,6 +1378,30 @@ files = [
[package.extras]
test = ["Cython (>=0.29.24,<0.30.0)"]
+[[package]]
+name = "httpx"
+version = "0.23.3"
+description = "The next generation HTTP client."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
+ {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
+]
+
+[package.dependencies]
+certifi = "*"
+httpcore = ">=0.15.0,<0.17.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
+
[[package]]
name = "hug"
version = "2.6.1"
@@ -2477,6 +2523,24 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
+ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+]
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
[[package]]
name = "sanic"
version = "21.9.1"
@@ -2518,14 +2582,14 @@ files = [
[[package]]
name = "setuptools"
-version = "66.0.0"
+version = "67.2.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"},
- {file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"},
+ {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"},
+ {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"},
]
[package.extras]
@@ -2996,14 +3060,14 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"]
[[package]]
name = "websocket-client"
-version = "1.4.2"
+version = "1.5.1"
description = "WebSocket client for Python with low level API options"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"},
- {file = "websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"},
+ {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"},
+ {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"},
]
[package.extras]
@@ -3285,18 +3349,18 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]]
name = "zipp"
-version = "3.11.0"
+version = "3.13.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
- {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
+ {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"},
+ {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"},
]
[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[[package]]
@@ -3399,4 +3463,4 @@ kafka = ["kafka-python"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.7, <3.11"
-content-hash = "2b84493cca5e5189414dc6ded1c4a2e881a3bb20a01a8da4e28b5b1cac2d47c1"
+content-hash = "e5d6730b56dadeffa7355b287be1264c2925a142e958cff02ed3d8067ec9261c"
diff --git a/pyproject.toml b/pyproject.toml
index 1f21338..899f042 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -127,6 +127,7 @@ asyncpg = "^0.27.0"
happybase = "1.2.0"
websockets = "^10.4"
loguru = "^0.6.0"
+httpx = "^0.23.3"
[tool.poetry.group.lint.dependencies]
flake8 = "^5.0.4"
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 8cacb4f..75b5e27 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -51,6 +51,7 @@ class Component(Enum):
AsyncPG = 7016
AIORedis = 7017
Websockets = 7018
+ HTTPX = 7019
class Layer(Enum):
diff --git a/skywalking/plugins/sw_httpx.py b/skywalking/plugins/sw_httpx.py
new file mode 100644
index 0000000..2131b8f
--- /dev/null
+++ b/skywalking/plugins/sw_httpx.py
@@ -0,0 +1,102 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 skywalking import Layer, Component, config
+from skywalking.trace.context import get_context, NoopContext
+from skywalking.trace.span import NoopSpan
+from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode
+
+link_vector = ['https://www.python-httpx.org/']
+support_matrix = {
+ 'httpx': {
+ '>=3.7': ['0.23.*', '0.22.*']
+ }
+}
+note = """"""
+
+
+def install():
+ from httpx import _client
+ from httpx import USE_CLIENT_DEFAULT
+
+ _async_send = _client.AsyncClient.send
+ _send = _client.Client.send
+
+ async def _sw_async_send(self, request, *, stream=False, auth=USE_CLIENT_DEFAULT,
+ follow_redirects=USE_CLIENT_DEFAULT, ):
+ url_object = request.url
+
+ span = NoopSpan(NoopContext()) if config.ignore_http_method_check(
+ request.method) else get_context().new_exit_span(op=url_object.path or '/', peer=url_object.netloc.decode(),
+ component=Component.HTTPX)
+ with span:
+ carrier = span.inject()
+ span.layer = Layer.Http
+
+ if not request.headers:
+ request.headers = {}
+ for item in carrier:
+ request.headers[item.key] = item.val
+
+ span.tag(TagHttpMethod(request.method.upper()))
+ url_safe = str(url_object).replace(url_object.username, '').replace(url_object.password, '')
+
+ span.tag(TagHttpURL(url_safe))
+
+ res = await _async_send(self, request, stream=stream, auth=auth, follow_redirects=follow_redirects)
+
+ status_code = res.status_code
+ span.tag(TagHttpStatusCode(status_code))
+
+ if status_code >= 400:
+ span.error_occurred = True
+
+ return res
+
+ _client.AsyncClient.send = _sw_async_send
+
+ def _sw_send(self, request, *, stream=False, auth=USE_CLIENT_DEFAULT, follow_redirects=USE_CLIENT_DEFAULT, ):
+ url_object = request.url
+
+ span = NoopSpan(NoopContext()) if config.ignore_http_method_check(
+ request.method) else get_context().new_exit_span(op=url_object.path or '/', peer=url_object.netloc.decode(),
+ component=Component.HTTPX)
+ with span:
+ carrier = span.inject()
+ span.layer = Layer.Http
+
+ if not request.headers:
+ request.headers = {}
+ for item in carrier:
+ request.headers[item.key] = item.val
+
+ span.tag(TagHttpMethod(request.method.upper()))
+ url_safe = str(url_object).replace(url_object.username, '').replace(url_object.password, '')
+
+ span.tag(TagHttpURL(url_safe))
+
+ res = _send(self, request, stream=stream, auth=auth, follow_redirects=follow_redirects)
+
+ status_code = res.status_code
+ span.tag(TagHttpStatusCode(status_code))
+
+ if status_code >= 400:
+ span.error_occurred = True
+
+ return res
+
+ _client.Client.send = _sw_send
diff --git a/tests/plugin/http/sw_httpx/__init__.py b/tests/plugin/http/sw_httpx/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/tests/plugin/http/sw_httpx/docker-compose.yml b/tests/plugin/http/sw_httpx/docker-compose.yml
new file mode 100644
index 0000000..d5ac43b
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/docker-compose.yml
@@ -0,0 +1,63 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+services:
+ collector:
+ extends:
+ service: collector
+ file: ../../docker-compose.base.yml
+
+ provider:
+ extends:
+ service: agent
+ file: ../../docker-compose.base.yml
+ ports:
+ - 9091:9091
+ volumes:
+ - .:/app
+ depends_on:
+ collector:
+ condition: service_healthy
+ command: ['bash', '-c', 'pip install fastapi uvicorn && pip install -r /app/requirements.txt && sw-python run python3 /app/services/provider.py']
+ healthcheck:
+ test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9091"]
+ interval: 5s
+ timeout: 60s
+ retries: 120
+ environment:
+ SW_AGENT_SERVICE_NAME: provider
+ SW_AGENT_LOGGING_LEVEL: DEBUG
+
+ consumer:
+ extends:
+ service: agent
+ file: ../../docker-compose.base.yml
+ ports:
+ - 9090:9090
+ volumes:
+ - .:/app
+ command: ['bash', '-c', 'pip install fastapi uvicorn && pip install -r /app/requirements.txt && sw-python run python3 /app/services/consumer.py']
+ depends_on:
+ collector:
+ condition: service_healthy
+ provider:
+ condition: service_healthy
+ environment:
+ SW_AGENT_SERVICE_NAME: consumer
+ SW_AGENT_LOGGING_LEVEL: DEBUG
+
+networks:
+ beyond:
diff --git a/tests/plugin/http/sw_httpx/expected.data.yml b/tests/plugin/http/sw_httpx/expected.data.yml
new file mode 100644
index 0000000..7550471
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/expected.data.yml
@@ -0,0 +1,136 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+segmentItems:
+ - serviceName: provider
+ segmentSize: 2
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: /users
+ operationId: 0
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ tags:
+ - key: http.method
+ value: POST
+ - key: http.url
+ value: http://provider:9091/users
+ - key: http.status_code
+ value: '200'
+ refs:
+ - parentEndpoint: /users
+ networkAddress: provider:9091
+ refType: CrossProcess
+ parentSpanId: 1
+ parentTraceSegmentId: not null
+ parentServiceInstance: not null
+ parentService: consumer
+ traceId: not null
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 7014
+ spanType: Entry
+ peer: not null
+ skipAnalysis: false
+ - segmentId: not null
+ spans:
+ - operationName: /users
+ operationId: 0
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ tags:
+ - key: http.method
+ value: POST
+ - key: http.url
+ value: http://provider:9091/users
+ - key: http.status_code
+ value: '200'
+ refs:
+ - parentEndpoint: /users
+ networkAddress: provider:9091
+ refType: CrossProcess
+ parentSpanId: 2
+ parentTraceSegmentId: not null
+ parentServiceInstance: not null
+ parentService: consumer
+ traceId: not null
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 7014
+ spanType: Entry
+ peer: not null
+ skipAnalysis: false
+ - serviceName: consumer
+ segmentSize: 1
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: /users
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Http
+ tags:
+ - key: http.method
+ value: POST
+ - key: http.url
+ value: http://provider:9091/users
+ - key: http.status_code
+ value: '200'
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 7019
+ spanType: Exit
+ peer: provider:9091
+ skipAnalysis: false
+ - operationName: /users
+ parentSpanId: 0
+ spanId: 2
+ spanLayer: Http
+ tags:
+ - key: http.method
+ value: POST
+ - key: http.url
+ value: http://provider:9091/users
+ - key: http.status_code
+ value: '200'
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 7019
+ spanType: Exit
+ peer: provider:9091
+ skipAnalysis: false
+ - operationName: /users
+ operationId: 0
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ tags:
+ - key: http.method
+ value: POST
+ - key: http.url
+ value: http://0.0.0.0:9090/users
+ - key: http.status_code
+ value: '200'
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 7014
+ spanType: Entry
+ peer: not null
+ skipAnalysis: false
diff --git a/tests/plugin/http/sw_httpx/services/__init__.py b/tests/plugin/http/sw_httpx/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/services/__init__.py
@@ -0,0 +1,16 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
diff --git a/tests/plugin/http/sw_httpx/services/consumer.py b/tests/plugin/http/sw_httpx/services/consumer.py
new file mode 100644
index 0000000..b096ca1
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/services/consumer.py
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import uvicorn
+from fastapi import FastAPI
+import httpx
+
+async_client = httpx.AsyncClient()
+client = httpx.Client()
+app = FastAPI()
+
+
+@app.post('/users')
+async def application():
+ try:
+ await async_client.post('http://provider:9091/users')
+ res = client.post('http://provider:9091/users')
+
+ return res.json()
+ except Exception: # noqa
+ return {'message': 'Error'}
+
+
+if __name__ == '__main__':
+ uvicorn.run(app, host='0.0.0.0', port=9090)
diff --git a/tests/plugin/http/sw_httpx/services/provider.py b/tests/plugin/http/sw_httpx/services/provider.py
new file mode 100644
index 0000000..8b08399
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/services/provider.py
@@ -0,0 +1,32 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the 'License'); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import uvicorn
+from fastapi import FastAPI
+
+app = FastAPI()
+
+
+@app.post('/users')
+async def application():
+ try:
+ return {'song': 'Despacito', 'artist': 'Luis Fonsi'}
+ except Exception: # noqa
+ return {'message': 'Error'}
+
+
+if __name__ == '__main__':
+ uvicorn.run(app, host='0.0.0.0', port=9091)
diff --git a/tests/plugin/http/sw_httpx/test_httpx.py b/tests/plugin/http/sw_httpx/test_httpx.py
new file mode 100644
index 0000000..3dd78c3
--- /dev/null
+++ b/tests/plugin/http/sw_httpx/test_httpx.py
@@ -0,0 +1,36 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from typing import Callable
+
+import pytest
+import requests
+
+from skywalking.plugins.sw_httpx import support_matrix
+from tests.orchestrator import get_test_vector
+from tests.plugin.base import TestPluginBase
+
+
+@pytest.fixture
+def prepare():
+ # type: () -> Callable
+ return lambda *_: requests.post('http://0.0.0.0:9090/users', timeout=5)
+
+
+class TestPlugin(TestPluginBase):
+ @pytest.mark.parametrize('version', get_test_vector(lib_name='httpx', support_matrix=support_matrix))
+ def test_plugin(self, docker_compose, version):
+ self.validate()