Skip to content

Commit 78bed98

Browse files
authored
feat(plugin): add urllib3 2.x support for Python 3.12+ (#387)
urllib3 2.x removed `urllib3.request.RequestMethods` which the existing plugin hooks. Add a new plugin `sw_urllib3_v2` that hooks `PoolManager.request` directly (the 2.x entry point). Auto-detection logic: - sw_urllib3: tries `from urllib3.request import RequestMethods`. Succeeds on 1.x, fails on 2.x (skipped). - sw_urllib3_v2: checks if `RequestMethods` exists. If yes (1.x), returns early. If no (2.x), hooks PoolManager. Both plugins share the same test directory. The test merges version vectors from both plugins' support_matrix. Verified locally on Python 3.13: - urllib3==2.3 PASSED (span validation) - urllib3==2.0 PASSED (span validation)
1 parent a70b2cc commit 78bed98

File tree

4 files changed

+89
-4
lines changed

4 files changed

+89
-4
lines changed

docs/en/setup/Plugins.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
4747
| [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['20.12']; | `sw_sanic` |
4848
| [tornado](https://www.tornadoweb.org) | Python >=3.14 - ['6.4']; Python >=3.10 - ['6.0', '6.1']; | `sw_tornado` |
4949
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - NOT SUPPORTED YET; Python >=3.10 - ['1.26', '1.25']; | `sw_urllib3` |
50+
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - ['2.3', '2.0']; | `sw_urllib3_v2` |
5051
| [urllib_request](https://docs.python.org/3/library/urllib.request.html) | Python >=3.7 - ['*']; | `sw_urllib_request` |
5152
| [websockets](https://websockets.readthedocs.io) | Python >=3.7 - ['10.3', '10.4']; | `sw_websockets` |
5253
### Notes
@@ -58,6 +59,8 @@ Hug is believed to be abandoned project, use this plugin with a bit more caution
5859
Instead of Hug, plugin test should move to test actual Falcon.
5960
- The Neo4j plugin integrates neo4j python driver 5.x.x versions which
6061
support both Neo4j 5 and 4.4 DBMS.
62+
- urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2.
63+
- urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3.
6164
- The websocket instrumentation only traces client side connection handshake,
6265
the actual message exchange (send/recv) is not traced since injecting headers to socket message
6366
body is the only way to propagate the trace context, which requires customization of message structure

skywalking/plugins/sw_urllib3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
link_vector = ['https://urllib3.readthedocs.io/en/latest/']
2424
support_matrix = {
2525
'urllib3': {
26-
'>=3.12': [], # urllib3 2.x removed urllib3.request.RequestMethods, plugin needs adaptation
26+
'>=3.12': [], # urllib3 1.x can't install on 3.12+, see sw_urllib3_v2 for 2.x
2727
'>=3.10': ['1.26', '1.25'],
2828
}
2929
}
30-
note = """"""
30+
note = """urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2."""
3131

3232

3333
def install():
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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://urllib3.readthedocs.io/en/latest/']
24+
support_matrix = {
25+
'urllib3': {
26+
'>=3.12': ['2.3', '2.0'],
27+
}
28+
}
29+
note = """urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3."""
30+
31+
32+
def install():
33+
from urllib3 import PoolManager
34+
35+
# urllib3 2.x removed RequestMethods base class;
36+
# PoolManager.request is the direct entry point.
37+
# Guard: if RequestMethods still exists, this is urllib3 1.x — let sw_urllib3 handle it.
38+
try:
39+
from urllib3.request import RequestMethods # noqa: F401
40+
return # urllib3 1.x detected, skip — sw_urllib3 handles it
41+
except ImportError:
42+
pass # urllib3 2.x, proceed
43+
44+
_request = PoolManager.request
45+
46+
def _sw_request(this: PoolManager, method, url, body=None, fields=None, headers=None, json=None, **urlopen_kw):
47+
from skywalking.utils.filter import sw_urlparse
48+
49+
url_param = sw_urlparse(url)
50+
51+
span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
52+
else get_context().new_exit_span(op=url_param.path or '/', peer=url_param.netloc,
53+
component=Component.Urllib3)
54+
55+
with span:
56+
carrier = span.inject()
57+
span.layer = Layer.Http
58+
59+
if headers is None:
60+
headers = {}
61+
else:
62+
headers = dict(headers)
63+
for item in carrier:
64+
headers[item.key] = item.val
65+
66+
span.tag(TagHttpMethod(method.upper()))
67+
span.tag(TagHttpURL(url_param.geturl()))
68+
69+
res = _request(this, method, url, body=body, fields=fields, headers=headers, json=json, **urlopen_kw)
70+
71+
span.tag(TagHttpStatusCode(res.status))
72+
if res.status >= 400:
73+
span.error_occurred = True
74+
75+
return res
76+
77+
PoolManager.request = _sw_request

tests/plugin/http/sw_urllib3/test_urllib3.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
import pytest
2020
import requests
2121

22-
from skywalking.plugins.sw_urllib3 import support_matrix
22+
from skywalking.plugins.sw_urllib3 import support_matrix as v1_matrix
23+
from skywalking.plugins.sw_urllib3_v2 import support_matrix as v2_matrix
2324
from tests.orchestrator import get_test_vector
2425
from tests.plugin.base import TestPluginBase
2526

27+
# Merge v1 and v2 test vectors — get_test_vector returns versions for the current Python
28+
_versions = (get_test_vector(lib_name='urllib3', support_matrix=v1_matrix)
29+
+ get_test_vector(lib_name='urllib3', support_matrix=v2_matrix))
30+
2631

2732
@pytest.fixture
2833
def prepare():
@@ -31,6 +36,6 @@ def prepare():
3136

3237

3338
class TestPlugin(TestPluginBase):
34-
@pytest.mark.parametrize('version', get_test_vector(lib_name='urllib3', support_matrix=support_matrix))
39+
@pytest.mark.parametrize('version', _versions)
3540
def test_plugin(self, docker_compose, version):
3641
self.validate()

0 commit comments

Comments
 (0)