Skip to content

Commit f9f3dcd

Browse files
authored
Add Httpx plugin (#283)
1 parent 8feb3d0 commit f9f3dcd

File tree

16 files changed

+548
-35
lines changed

16 files changed

+548
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- Add HBase plugin Python HappyBase model (#266)
2727
- Add FastAPI plugin websocket protocol support (#269)
2828
- Add Websockets (client) plugin (#269)
29+
- Add HTTPX plugin (#283)
2930

3031
- Fixes:
3132
- Allow RabbitMQ BlockingChannel.basic_consume() to link with outgoing spans (#224)

docs/en/contribution/Developer.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ We have migrated from basic pip to [Poetry](https://python-poetry.org/) to manag
2525
Once you have `make` ready, run `make env`, this will automatically install the right Poetry release, and create
2626
(plus manage) a `.venv` virtual environment for us based on the currently activated Python 3 version. Enjoy coding!
2727

28+
Note: Make sure you have `python3` aliased to `python` available on Windows computers instead of pointing to the Microsoft app store.
29+
2830
### Switching between Multiple Python Versions
2931
Do not develop/test on Python < 3.7, since Poetry and some other functionalities we implement rely on Python 3.7+
3032

docs/en/contribution/How-to-develop-plugin.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@
33
You can always take [the existing plugins](../setup/Plugins.md) as examples, while there are some general ideas for all plugins.
44
1. A plugin is a module under the directory `skywalking/plugins` with an `install` method;
55
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).
6-
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.
7-
8-
You can use >, >=, ==, <=, <, and != operators in rules.
9-
10-
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.
11-
12-
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.
13-
14-
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`.
6+
3. You should also provide version rules in the plugin module, which means the version of package your plugin aim to test.
7+
8+
All below variables will be used by the tools/plugin_doc_gen.py to produce a latest [Plugin Doc](../setup/Plugins.md).
9+
1510
```python
16-
version_rule = {
17-
"name": "django",
18-
"rules": [">=2.0 <=2.3 !=2.2.1", ">3.0"]
11+
link_vector = ['https://www.python-httpx.org/'] # This should link to the official website/doc of this lib
12+
# The support matrix is for scenarios where some libraries don't work for certain Python versions
13+
# Therefore, we use the matrix to instruct the CI testing pipeline to skip over plugin test for such Python version
14+
# The right side versions, should almost always use A.B.* to test the latest minor version of two recent major versions.
15+
support_matrix = {
16+
'httpx': {
17+
'>=3.7': ['0.23.*', '0.22.*']
18+
}
1919
}
20+
# The note will be used when generating the plugin documentation for users.
21+
note = """"""
2022
```
2123
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.
22-
5. Update the [Supported Plugin List](../setup/Plugins.md).
23-
6. Add the environment variables to [Environment Variable list](../setup/Configuration.md) if any.
24+
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`.
2425

2526
## Steps after coding
2627

docs/en/contribution/How-to-test-locally.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Please first refer to the [Developer Guide](Developer.md) to set up a developmen
1111

1212
TL;DR: run ``make env``. This will create virtual environments for python and generate the protocol folder needed for the agent.
1313

14+
Note: Make sure you have `python3` aliased to `python` available on Windows computers instead of pointing to the Microsoft app store.
15+
1416
By now, you can do what you want. Let's get to the topic of how to test.
1517

1618
The test process requires `docker` and `docker-compose` throughout. If you haven't installed them, please install them first.

docs/en/setup/Plugins.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
2929
| [happybase](https://happybase.readthedocs.io) | Python >=3.7 - ['1.2.0']; | `sw_happybase` |
3030
| [http_server](https://docs.python.org/3/library/http.server.html) | Python >=3.7 - ['*']; | `sw_http_server` |
3131
| [werkzeug](https://werkzeug.palletsprojects.com/) | Python >=3.7 - ['1.0.1', '2.0']; | `sw_http_server` |
32+
| [httpx](https://www.python-httpx.org/) | Python >=3.7 - ['0.23.*', '0.22.*']; | `sw_httpx` |
3233
| [kafka-python](https://kafka-python.readthedocs.io) | Python >=3.7 - ['2.0']; | `sw_kafka` |
3334
| [loguru](https://pypi.org/project/loguru/) | Python >=3.7 - ['0.6.0']; | `sw_loguru` |
3435
| [mysqlclient](https://mysqlclient.readthedocs.io/) | Python >=3.7 - ['2.1.*']; | `sw_mysqlclient` |

poetry.lock

Lines changed: 85 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ asyncpg = "^0.27.0"
127127
happybase = "1.2.0"
128128
websockets = "^10.4"
129129
loguru = "^0.6.0"
130+
httpx = "^0.23.3"
130131

131132
[tool.poetry.group.lint.dependencies]
132133
flake8 = "^5.0.4"

skywalking/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Component(Enum):
5151
AsyncPG = 7016
5252
AIORedis = 7017
5353
Websockets = 7018
54+
HTTPX = 7019
5455

5556

5657
class Layer(Enum):

skywalking/plugins/sw_httpx.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
from skywalking import Layer, Component, config
19+
from skywalking.trace.context import get_context, NoopContext
20+
from skywalking.trace.span import NoopSpan
21+
from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpStatusCode
22+
23+
link_vector = ['https://www.python-httpx.org/']
24+
support_matrix = {
25+
'httpx': {
26+
'>=3.7': ['0.23.*', '0.22.*']
27+
}
28+
}
29+
note = """"""
30+
31+
32+
def install():
33+
from httpx import _client
34+
from httpx import USE_CLIENT_DEFAULT
35+
36+
_async_send = _client.AsyncClient.send
37+
_send = _client.Client.send
38+
39+
async def _sw_async_send(self, request, *, stream=False, auth=USE_CLIENT_DEFAULT,
40+
follow_redirects=USE_CLIENT_DEFAULT, ):
41+
url_object = request.url
42+
43+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(
44+
request.method) else get_context().new_exit_span(op=url_object.path or '/', peer=url_object.netloc.decode(),
45+
component=Component.HTTPX)
46+
with span:
47+
carrier = span.inject()
48+
span.layer = Layer.Http
49+
50+
if not request.headers:
51+
request.headers = {}
52+
for item in carrier:
53+
request.headers[item.key] = item.val
54+
55+
span.tag(TagHttpMethod(request.method.upper()))
56+
url_safe = str(url_object).replace(url_object.username, '').replace(url_object.password, '')
57+
58+
span.tag(TagHttpURL(url_safe))
59+
60+
res = await _async_send(self, request, stream=stream, auth=auth, follow_redirects=follow_redirects)
61+
62+
status_code = res.status_code
63+
span.tag(TagHttpStatusCode(status_code))
64+
65+
if status_code >= 400:
66+
span.error_occurred = True
67+
68+
return res
69+
70+
_client.AsyncClient.send = _sw_async_send
71+
72+
def _sw_send(self, request, *, stream=False, auth=USE_CLIENT_DEFAULT, follow_redirects=USE_CLIENT_DEFAULT, ):
73+
url_object = request.url
74+
75+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(
76+
request.method) else get_context().new_exit_span(op=url_object.path or '/', peer=url_object.netloc.decode(),
77+
component=Component.HTTPX)
78+
with span:
79+
carrier = span.inject()
80+
span.layer = Layer.Http
81+
82+
if not request.headers:
83+
request.headers = {}
84+
for item in carrier:
85+
request.headers[item.key] = item.val
86+
87+
span.tag(TagHttpMethod(request.method.upper()))
88+
url_safe = str(url_object).replace(url_object.username, '').replace(url_object.password, '')
89+
90+
span.tag(TagHttpURL(url_safe))
91+
92+
res = _send(self, request, stream=stream, auth=auth, follow_redirects=follow_redirects)
93+
94+
status_code = res.status_code
95+
span.tag(TagHttpStatusCode(status_code))
96+
97+
if status_code >= 400:
98+
span.error_occurred = True
99+
100+
return res
101+
102+
_client.Client.send = _sw_send

0 commit comments

Comments
 (0)