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()