diff --git a/docs/en/setup/Plugins.md b/docs/en/setup/Plugins.md index 0aa38a31..baea2144 100644 --- a/docs/en/setup/Plugins.md +++ b/docs/en/setup/Plugins.md @@ -47,6 +47,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!) | [sanic](https://sanic.readthedocs.io/en/latest) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.7 - ['20.12']; | `sw_sanic` | | [tornado](https://www.tornadoweb.org) | Python >=3.14 - ['6.4']; Python >=3.10 - ['6.0', '6.1']; | `sw_tornado` | | [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - NOT SUPPORTED YET; Python >=3.10 - ['1.26', '1.25']; | `sw_urllib3` | +| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - ['2.3', '2.0']; | `sw_urllib3_v2` | | [urllib_request](https://docs.python.org/3/library/urllib.request.html) | Python >=3.7 - ['*']; | `sw_urllib_request` | | [websockets](https://websockets.readthedocs.io) | Python >=3.7 - ['10.3', '10.4']; | `sw_websockets` | ### Notes @@ -58,6 +59,8 @@ Hug is believed to be abandoned project, use this plugin with a bit more caution Instead of Hug, plugin test should move to test actual Falcon. - The Neo4j plugin integrates neo4j python driver 5.x.x versions which support both Neo4j 5 and 4.4 DBMS. +- urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2. +- urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3. - The websocket instrumentation only traces client side connection handshake, the actual message exchange (send/recv) is not traced since injecting headers to socket message body is the only way to propagate the trace context, which requires customization of message structure diff --git a/skywalking/plugins/sw_urllib3.py b/skywalking/plugins/sw_urllib3.py index ae57b9c9..56f7f737 100644 --- a/skywalking/plugins/sw_urllib3.py +++ b/skywalking/plugins/sw_urllib3.py @@ -23,11 +23,11 @@ link_vector = ['https://urllib3.readthedocs.io/en/latest/'] support_matrix = { 'urllib3': { - '>=3.12': [], # urllib3 2.x removed urllib3.request.RequestMethods, plugin needs adaptation + '>=3.12': [], # urllib3 1.x can't install on 3.12+, see sw_urllib3_v2 for 2.x '>=3.10': ['1.26', '1.25'], } } -note = """""" +note = """urllib3 1.x plugin. For urllib3 2.x, see sw_urllib3_v2.""" def install(): diff --git a/skywalking/plugins/sw_urllib3_v2.py b/skywalking/plugins/sw_urllib3_v2.py new file mode 100644 index 00000000..1e8e3e07 --- /dev/null +++ b/skywalking/plugins/sw_urllib3_v2.py @@ -0,0 +1,77 @@ +# +# 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://urllib3.readthedocs.io/en/latest/'] +support_matrix = { + 'urllib3': { + '>=3.12': ['2.3', '2.0'], + } +} +note = """urllib3 2.x plugin. For urllib3 1.x, see sw_urllib3.""" + + +def install(): + from urllib3 import PoolManager + + # urllib3 2.x removed RequestMethods base class; + # PoolManager.request is the direct entry point. + # Guard: if RequestMethods still exists, this is urllib3 1.x — let sw_urllib3 handle it. + try: + from urllib3.request import RequestMethods # noqa: F401 + return # urllib3 1.x detected, skip — sw_urllib3 handles it + except ImportError: + pass # urllib3 2.x, proceed + + _request = PoolManager.request + + def _sw_request(this: PoolManager, method, url, body=None, fields=None, headers=None, json=None, **urlopen_kw): + from skywalking.utils.filter import sw_urlparse + + url_param = sw_urlparse(url) + + span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \ + else get_context().new_exit_span(op=url_param.path or '/', peer=url_param.netloc, + component=Component.Urllib3) + + with span: + carrier = span.inject() + span.layer = Layer.Http + + if headers is None: + headers = {} + else: + headers = dict(headers) + for item in carrier: + headers[item.key] = item.val + + span.tag(TagHttpMethod(method.upper())) + span.tag(TagHttpURL(url_param.geturl())) + + res = _request(this, method, url, body=body, fields=fields, headers=headers, json=json, **urlopen_kw) + + span.tag(TagHttpStatusCode(res.status)) + if res.status >= 400: + span.error_occurred = True + + return res + + PoolManager.request = _sw_request diff --git a/tests/plugin/http/sw_urllib3/test_urllib3.py b/tests/plugin/http/sw_urllib3/test_urllib3.py index 082cd33b..2fff31df 100644 --- a/tests/plugin/http/sw_urllib3/test_urllib3.py +++ b/tests/plugin/http/sw_urllib3/test_urllib3.py @@ -19,10 +19,15 @@ import pytest import requests -from skywalking.plugins.sw_urllib3 import support_matrix +from skywalking.plugins.sw_urllib3 import support_matrix as v1_matrix +from skywalking.plugins.sw_urllib3_v2 import support_matrix as v2_matrix from tests.orchestrator import get_test_vector from tests.plugin.base import TestPluginBase +# Merge v1 and v2 test vectors — get_test_vector returns versions for the current Python +_versions = (get_test_vector(lib_name='urllib3', support_matrix=v1_matrix) + + get_test_vector(lib_name='urllib3', support_matrix=v2_matrix)) + @pytest.fixture def prepare(): @@ -31,6 +36,6 @@ def prepare(): class TestPlugin(TestPluginBase): - @pytest.mark.parametrize('version', get_test_vector(lib_name='urllib3', support_matrix=support_matrix)) + @pytest.mark.parametrize('version', _versions) def test_plugin(self, docker_compose, version): self.validate()