You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2020/11/27 01:35:18 UTC

[skywalking-python] branch master updated: Add sanic plugin (#91)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 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 b5740d5  Add sanic plugin (#91)
b5740d5 is described below

commit b5740d5a2baeb5c133deb2e07af6ccc996ce0537
Author: fuhuo <bl...@qq.com>
AuthorDate: Thu Nov 26 19:35:07 2020 -0600

    Add sanic plugin (#91)
---
 docs/Plugins.md                            |  1 +
 requirements.txt                           |  9 +++
 skywalking/__init__.py                     |  1 +
 skywalking/config.py                       |  2 +
 skywalking/plugins/sw_sanic.py             | 92 +++++++++++++++++++++++++++++
 tests/plugin/sw_sanic/__init__.py          | 16 +++++
 tests/plugin/sw_sanic/docker-compose.yml   | 60 +++++++++++++++++++
 tests/plugin/sw_sanic/expected.data.yml    | 93 ++++++++++++++++++++++++++++++
 tests/plugin/sw_sanic/services/__init__.py | 16 +++++
 tests/plugin/sw_sanic/services/consumer.py | 38 ++++++++++++
 tests/plugin/sw_sanic/services/provider.py | 37 ++++++++++++
 tests/plugin/sw_sanic/test_sanic.py        | 42 ++++++++++++++
 12 files changed, 407 insertions(+)

diff --git a/docs/Plugins.md b/docs/Plugins.md
index dafeb8f..4deeb33 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -15,5 +15,6 @@ Library | Versions | Plugin Name
 | [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` |
+| [sanic](https://sanic.readthedocs.io/en/latest/) | >= 20.3.0 <= 20.9.1 | `sw_sanic` |
 
 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/requirements.txt b/requirements.txt
index 1c3889f..f9687d4 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 asgiref==3.2.10
+aiofiles==0.6.0
 attrs==19.3.0
 blindspin==2.0.1
 certifi==2020.6.20
@@ -15,6 +16,9 @@ gevent==20.6.2
 greenlet==0.4.16
 grpcio==1.31.0
 grpcio-tools==1.31.0
+httpcore==0.11.1
+httptools==0.1.1
+httpx==0.15.4
 idna==2.10
 importlib-metadata==1.7.0
 iniconfig==1.0.1
@@ -23,6 +27,7 @@ Jinja2==2.11.2
 kafka-python==2.0.1
 MarkupSafe==1.1.1
 more-itertools==8.4.0
+multidict==5.0.0
 packaging==20.4
 parameterized==0.7.4
 pika==1.1.0
@@ -37,12 +42,16 @@ pytz==2020.1
 PyYAML==5.3.1
 redis==3.5.3
 requests==2.24.0
+rfc3986==1.4.0
+sanic==20.9.1
 six==1.15.0
+sniffio==1.2.0
 sqlparse==0.3.1
 testcontainers==3.0.3
 toml==0.10.1
 tornado==6.0.4
 urllib3==1.25.10
+websockets==8.1
 websocket-client==0.57.0
 Werkzeug==1.0.1
 wrapt==1.12.1
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 6df8ecb..06c737b 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -37,6 +37,7 @@ class Component(Enum):
     RabbitmqConsumer = 53
     Elasticsearch = 47
     Urllib3 = 7006
+    Sanic = 7007
 
 
 class Layer(Enum):
diff --git a/skywalking/config.py b/skywalking/config.py
index 339747b..204d277 100644
--- a/skywalking/config.py
+++ b/skywalking/config.py
@@ -39,6 +39,8 @@ ignore_suffix = os.getenv('SW_IGNORE_SUFFIX') or '.jpg,.jpeg,.js,.css,.png,.bmp,
                                                  '.mp4,.html,.svg '  # type: str
 flask_collect_http_params = True if os.getenv('SW_FLASK_COLLECT_HTTP_PARAMS') and \
                                     os.getenv('SW_FLASK_COLLECT_HTTP_PARAMS') == 'True' else False  # type: bool
+sanic_collect_http_params = True if os.getenv('SW_SANIC_COLLECT_HTTP_PARAMS') and \
+                                    os.getenv('SW_SANIC_COLLECT_HTTP_PARAMS') == 'True' else False  # type: bool
 http_params_length_threshold = int(os.getenv('SW_HTTP_PARAMS_LENGTH_THRESHOLD') or '1024')  # type: int
 django_collect_http_params = True if os.getenv('SW_DJANGO_COLLECT_HTTP_PARAMS') and \
                                      os.getenv('SW_DJANGO_COLLECT_HTTP_PARAMS') == 'True' else False  # type: bool
diff --git a/skywalking/plugins/sw_sanic.py b/skywalking/plugins/sw_sanic.py
new file mode 100644
index 0000000..1ea6299
--- /dev/null
+++ b/skywalking/plugins/sw_sanic.py
@@ -0,0 +1,92 @@
+#
+# 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, config
+from skywalking.trace import tags
+from skywalking.trace.carrier import Carrier
+from skywalking.trace.context import get_context
+from skywalking.trace.span import NoopSpan
+from skywalking.trace.tags import Tag
+
+logger = logging.getLogger(__name__)
+
+version_rule = {
+    "name": "sanic",
+    "rules": [">=20.3.0"]
+}
+
+
+def install():
+    from sanic import Sanic, handlers, response
+
+    _format_http1_response = response.format_http1_response
+    _handle_request = Sanic.handle_request
+    _handlers_ErrorHandler_reponse = handlers.ErrorHandler.response
+
+    def _sw_format_http1_reponse(status: int, headers, body=b""):
+        if status is not None:
+            entry_span = get_context().active_span()
+            if entry_span is not None and type(entry_span) is not NoopSpan:
+                if status >= 400:
+                    entry_span.error_occurred = True
+                entry_span.tag(Tag(key=tags.HttpStatus, val=status))
+
+        return _format_http1_response(status, headers, body)
+
+    def _sw_handlers_ErrorHandler_reponse(self: handlers.ErrorHandler, req, e):
+        if e is not None:
+            entry_span = get_context().active_span()
+            if entry_span is not None and type(entry_span) is not NoopSpan:
+                entry_span.raised()
+
+        return _handlers_ErrorHandler_reponse(self, req, e)
+
+    response.format_http1_response = _sw_format_http1_reponse
+    Sanic.handle_request = _gen_sw_handle_request(_handle_request)
+    handlers.ErrorHandler.response = _sw_handlers_ErrorHandler_reponse
+
+
+def _gen_sw_handle_request(_handle_request):
+    from inspect import isawaitable
+
+    def params_tostring(params):
+        return "\n".join([k + '=[' + ",".join(params.getlist(k)) + ']' for k, _ in params.items()])
+
+    async def _sw_handle_request(self, request, write_callback, stream_callback):
+        req = request
+        context = get_context()
+        carrier = Carrier()
+
+        for item in carrier:
+            if item.key.capitalize() in req.headers:
+                item.val = req.headers[item.key.capitalize()]
+        with context.new_entry_span(op=req.path, carrier=carrier) as span:
+            span.layer = Layer.Http
+            span.component = Component.Sanic
+            span.peer = '%s:%s' % (req.remote_addr or req.ip, req.port)
+            span.tag(Tag(key=tags.HttpMethod, val=req.method))
+            span.tag(Tag(key=tags.HttpUrl, val=req.url.split("?")[0]))
+            if config.sanic_collect_http_params and req.args:
+                span.tag(Tag(key=tags.HttpParams,
+                             val=params_tostring(req.args)[0:config.http_params_length_threshold]))
+            resp = _handle_request(self, request, write_callback, stream_callback)
+            if isawaitable(resp):
+                result = await resp
+
+        return result
+    return _sw_handle_request
diff --git a/tests/plugin/sw_sanic/__init__.py b/tests/plugin/sw_sanic/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_sanic/__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_sanic/docker-compose.yml b/tests/plugin/sw_sanic/docker-compose.yml
new file mode 100644
index 0000000..6082079
--- /dev/null
+++ b/tests/plugin/sw_sanic/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 sanic && pip install -r /app/requirements.txt && 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 sanic && 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_sanic/expected.data.yml b/tests/plugin/sw_sanic/expected.data.yml
new file mode 100644
index 0000000..e1bd345
--- /dev/null
+++ b/tests/plugin/sw_sanic/expected.data.yml
@@ -0,0 +1,93 @@
+#
+# 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: GET
+              - 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: 7007
+            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
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/users
+              - key: status.code
+                value: '200'
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7002
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://0.0.0.0:9090/users
+              - key: http.params
+                value: "test=[test1,test2]\ntest2=[test2]"
+              - key: status.code
+                value: '200'
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7007
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
\ No newline at end of file
diff --git a/tests/plugin/sw_sanic/services/__init__.py b/tests/plugin/sw_sanic/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_sanic/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_sanic/services/consumer.py b/tests/plugin/sw_sanic/services/consumer.py
new file mode 100644
index 0000000..49a7301
--- /dev/null
+++ b/tests/plugin/sw_sanic/services/consumer.py
@@ -0,0 +1,38 @@
+#
+# 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 requests
+
+from skywalking import agent, config
+
+if __name__ == '__main__':
+    config.service_name = 'consumer'
+    config.logging_level = 'DEBUG'
+    config.sanic_collect_http_params = True
+    agent.start()
+
+    from sanic import Sanic, response
+
+    app = Sanic(__name__)
+
+    @app.route("/users", methods=["GET"])
+    async def application(req):
+        res = requests.get("http://provider:9091/users")
+        return response.json(res.json())
+
+    PORT = 9090
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_sanic/services/provider.py b/tests/plugin/sw_sanic/services/provider.py
new file mode 100644
index 0000000..18b24a9
--- /dev/null
+++ b/tests/plugin/sw_sanic/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 sanic import Sanic, response
+
+    app = Sanic(__name__)
+
+    @app.route("/users", methods=["GET"])
+    async def application(req):
+        time.sleep(0.5)
+        return response.json({"song": "Despacito", "artist": "Luis Fonsi"})
+
+    PORT = 9091
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_sanic/test_sanic.py b/tests/plugin/sw_sanic/test_sanic.py
new file mode 100644
index 0000000..2473a52
--- /dev/null
+++ b/tests/plugin/sw_sanic/test_sanic.py
@@ -0,0 +1,42 @@
+#
+# 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?test=test1&test=test2&test2=test2')
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', [
+        'sanic==20.3.0',
+        'sanic==20.6.0',
+        'sanic==20.6.1',
+        'sanic==20.6.2',
+        'sanic==20.6.3',
+        'sanic==20.9.0',
+        'sanic==20.9.1',
+    ])
+    def test_plugin(self, docker_compose, version):
+        self.validate()