[Plugin] Add plugin for urllib3 library (#69)

Co-authored-by: kezhenxu94 <kezhenxu94@163.com>
diff --git a/docs/Plugins.md b/docs/Plugins.md
index a660d88..dafeb8f 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -14,5 +14,6 @@
 | [pika](https://pika.readthedocs.io/en/stable/) | 1.1.0 | `sw_rabbitmq` |
 | [pymongo](https://pymongo.readthedocs.io/en/stable/) | 3.11.0 | `sw_pymongo` |
 | [elasticsearch](https://github.com/elastic/elasticsearch-py) | 7.9.0 | `sw_elasticsearch` |
+| [urllib3](https://urllib3.readthedocs.io/en/latest/) | >= 1.25.9 <= 1.25.10 | `sw_urllib3` |
 
 The column `Versions` only indicates that the versions are tested, if you found the newer versions are also supported, welcome to add the newer version into the table.
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 3616a32..6df8ecb 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -36,6 +36,7 @@
     RabbitmqProducer = 52
     RabbitmqConsumer = 53
     Elasticsearch = 47
+    Urllib3 = 7006
 
 
 class Layer(Enum):
diff --git a/skywalking/plugins/sw_urllib3.py b/skywalking/plugins/sw_urllib3.py
new file mode 100644
index 0000000..a8fc984
--- /dev/null
+++ b/skywalking/plugins/sw_urllib3.py
@@ -0,0 +1,69 @@
+#
+# 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 logging
+
+from skywalking import Layer, Component
+from skywalking.trace import tags
+from skywalking.trace.carrier import Carrier
+from skywalking.trace.context import get_context
+from skywalking.trace.tags import Tag
+
+logger = logging.getLogger(__name__)
+
+
+def install():
+    # noinspection PyBroadException
+    try:
+        from urllib3.request import RequestMethods
+
+        _request = RequestMethods.request
+
+        def _sw_request(this: RequestMethods, method, url, fields=None, headers=None, **urlopen_kw):
+
+            from urllib.parse import urlparse
+            url_param = urlparse(url)
+            carrier = Carrier()
+            context = get_context()
+            with context.new_exit_span(op=url_param.path or "/", peer=url_param.netloc, carrier=carrier) as span:
+                span.layer = Layer.Http
+                span.component = Component.Urllib3
+
+                if headers is None:
+                    headers = {}
+                    for item in carrier:
+                        headers[item.key] = item.val
+                else:
+                    for item in carrier:
+                        headers[item.key] = item.val
+
+                try:
+                    res = _request(this, method, url, fields=fields, headers=headers, **urlopen_kw)
+
+                    span.tag(Tag(key=tags.HttpMethod, val=method.upper()))
+                    span.tag(Tag(key=tags.HttpUrl, val=url))
+                    span.tag(Tag(key=tags.HttpStatus, val=res.status))
+                    if res.status >= 400:
+                        span.error_occurred = True
+                except BaseException as e:
+                    span.raised()
+                    raise e
+                return res
+
+        RequestMethods.request = _sw_request
+    except Exception:
+        logger.warning('failed to install plugin %s', __name__)
diff --git a/tests/plugin/sw_urllib3/__init__.py b/tests/plugin/sw_urllib3/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_urllib3/__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/sw_urllib3/docker-compose.yml b/tests/plugin/sw_urllib3/docker-compose.yml
new file mode 100644
index 0000000..56c6498
--- /dev/null
+++ b/tests/plugin/sw_urllib3/docker-compose.yml
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+version: '2.1'
+
+services:
+  collector:
+    extends:
+      service: collector
+      file: ../docker/docker-compose.base.yml
+
+  provider:
+    extends:
+      service: agent
+      file: ../docker/docker-compose.base.yml
+    ports:
+      - 9091:9091
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip install flask && python3 /app/services/provider.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9091"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+
+  consumer:
+    extends:
+      service: agent
+      file: ../docker/docker-compose.base.yml
+    ports:
+      - 9090:9090
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip install flask && pip install -r /app/requirements.txt && python3 /app/services/consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+
+networks:
+  beyond:
diff --git a/tests/plugin/sw_urllib3/expected.data.yml b/tests/plugin/sw_urllib3/expected.data.yml
new file mode 100644
index 0000000..42f9c3c
--- /dev/null
+++ b/tests/plugin/sw_urllib3/expected.data.yml
@@ -0,0 +1,94 @@
+#
+# 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: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            tags:
+              - key: http.method
+                value: POST
+              - key: url
+                value: http://provider:9091/users
+              - key: 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: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+  - serviceName: consumer
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /users
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7006
+            isError: false
+            spanType: Exit
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: POST
+              - key: url
+                value: 'http://provider:9091/users'
+              - key: status.code
+                value: '200'
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            isError: false
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: 'http://0.0.0.0:9090/users'
+              - key: status.code
+                value: '200'
\ No newline at end of file
diff --git a/tests/plugin/sw_urllib3/services/__init__.py b/tests/plugin/sw_urllib3/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_urllib3/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/sw_urllib3/services/consumer.py b/tests/plugin/sw_urllib3/services/consumer.py
new file mode 100644
index 0000000..98210b2
--- /dev/null
+++ b/tests/plugin/sw_urllib3/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 json
+
+from skywalking import agent, config
+
+if __name__ == '__main__':
+    config.service_name = 'consumer'
+    config.logging_level = 'DEBUG'
+    agent.start()
+
+    from flask import Flask, jsonify
+
+    app = Flask(__name__)
+    import urllib3
+
+    @app.route("/users", methods=["POST", "GET"])
+    def application():
+        http = urllib3.PoolManager()
+        res = http.request("POST", "http://provider:9091/users")
+
+        return jsonify(json.loads(res.data.decode('utf-8')))
+
+    PORT = 9090
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_urllib3/services/provider.py b/tests/plugin/sw_urllib3/services/provider.py
new file mode 100644
index 0000000..11f2c0b
--- /dev/null
+++ b/tests/plugin/sw_urllib3/services/provider.py
@@ -0,0 +1,37 @@
+#
+# 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 time
+
+from skywalking import agent, config
+
+if __name__ == '__main__':
+    config.service_name = 'provider'
+    config.logging_level = 'DEBUG'
+    agent.start()
+
+    from flask import Flask, jsonify
+
+    app = Flask(__name__)
+
+    @app.route("/users", methods=["POST", "GET"])
+    def application():
+        time.sleep(0.5)
+        return jsonify({"song": "Despacito", "artist": "Luis Fonsi"})
+
+    PORT = 9091
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_urllib3/test_urllib3.py b/tests/plugin/sw_urllib3/test_urllib3.py
new file mode 100644
index 0000000..e56a82b
--- /dev/null
+++ b/tests/plugin/sw_urllib3/test_urllib3.py
@@ -0,0 +1,37 @@
+#
+# 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 tests.plugin.base import TestPluginBase
+
+
+@pytest.fixture
+def prepare():
+    # type: () -> Callable
+    return lambda *_: requests.get('http://0.0.0.0:9090/users')
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', [
+        'urllib3==1.25.10',
+        'urllib3==1.25.9'
+    ])
+    def test_plugin(self, docker_compose, version):
+        self.validate()