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/07/23 15:48:35 UTC

[skywalking-python] branch master updated: feature: add Tornado Plugin (#48)

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 36a4c53  feature: add Tornado Plugin (#48)
36a4c53 is described below

commit 36a4c53e1dbb47d4d2e59ae5c9ffca3b68562ab2
Author: clay <li...@gmail.com>
AuthorDate: Thu Jul 23 23:48:17 2020 +0800

    feature: add Tornado Plugin (#48)
---
 README.md                                    |   1 +
 setup.py                                     |   1 +
 skywalking/__init__.py                       |   1 +
 skywalking/plugins/sw_tornado/__init__.py    | 100 +++++++++++++++++++++++++++
 tests/plugin/sw_tornado/__init__.py          |  16 +++++
 tests/plugin/sw_tornado/docker-compose.yml   |  60 ++++++++++++++++
 tests/plugin/sw_tornado/expected.data.yml    |  91 ++++++++++++++++++++++++
 tests/plugin/sw_tornado/services/__init__.py |  16 +++++
 tests/plugin/sw_tornado/services/consumer.py |  40 +++++++++++
 tests/plugin/sw_tornado/services/provider.py |  42 +++++++++++
 tests/plugin/sw_tornado/test_tornado.py      |  42 +++++++++++
 11 files changed, 410 insertions(+)

diff --git a/README.md b/README.md
index b2fdc66..4a54e9a 100755
--- a/README.md
+++ b/README.md
@@ -78,6 +78,7 @@ Library | Plugin Name
 | [PyMySQL](https://pymysql.readthedocs.io/en/latest/) | `sw_pymysql` |
 | [Django](https://www.djangoproject.com/) | `sw_django` |
 | [redis-py](https://github.com/andymccurdy/redis-py/) | `sw_redis` |
+| [tornado](https://www.tornadoweb.org/en/stable/) | `sw_tornado` |
 
 ## API
 
diff --git a/setup.py b/setup.py
index 8ef00a0..92fdd1b 100644
--- a/setup.py
+++ b/setup.py
@@ -47,6 +47,7 @@ setup(
             "Werkzeug",
             "pymysql",
             "redis",
+            "tornado",
         ],
     },
     classifiers=[
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index c097107..174d816 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -28,6 +28,7 @@ class Component(Enum):
     Requests = 7002
     PyMysql = 7003
     Django = 7004
+    Tornado = 7005
     Redis = 7
 
 
diff --git a/skywalking/plugins/sw_tornado/__init__.py b/skywalking/plugins/sw_tornado/__init__.py
new file mode 100644
index 0000000..560773c
--- /dev/null
+++ b/skywalking/plugins/sw_tornado/__init__.py
@@ -0,0 +1,100 @@
+#
+# 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 inspect import iscoroutinefunction, isawaitable
+
+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():
+    try:
+        from tornado.web import RequestHandler
+        old_execute = RequestHandler._execute
+        old_log_exception = RequestHandler.log_exception
+        RequestHandler._execute = _gen_sw_get_response_func(old_execute)
+
+        def _sw_handler_uncaught_exception(self: RequestHandler, ty, value, tb, *args, **kwargs):
+            if value is not None:
+                entry_span = get_context().active_span()
+                if entry_span is not None:
+                    entry_span.raised()
+
+            return old_log_exception(self, ty, value, tb, *args, **kwargs)
+
+        RequestHandler.log_exception = _sw_handler_uncaught_exception
+    except Exception:
+        logger.warning('failed to install plugin %s', __name__)
+
+
+def _gen_sw_get_response_func(old_execute):
+    from tornado.gen import coroutine
+
+    awaitable = iscoroutinefunction(old_execute)
+    if awaitable:
+        # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
+        # In that case our method should be a coroutine function too
+        async def _sw_get_response(self, *args, **kwargs):
+            request = self.request
+            context = get_context()
+            carrier = Carrier()
+            for item in carrier:
+                if item.key.capitalize() in request.headers:
+                    item.val = request.headers[item.key.capitalize()]
+            with context.new_entry_span(op=request.path, carrier=carrier) as span:
+                span.layer = Layer.Http
+                span.component = Component.Tornado
+                peer = request.connection.stream.socket.getpeername()
+                span.peer = '{0}:{1}'.format(*peer)
+                span.tag(Tag(key=tags.HttpMethod, val=request.method))
+                span.tag(
+                    Tag(key=tags.HttpUrl, val='{}://{}{}'.format(request.protocol, request.host, request.path)))
+                result = old_execute(self, *args, **kwargs)
+                span.tag(Tag(key=tags.HttpStatus, val=self._status_code))
+                if isawaitable(result):
+                    result = await result
+                if self._status_code >= 400:
+                    span.error_occurred = True
+            return result
+    else:
+        @coroutine
+        def _sw_get_response(self, *args, **kwargs):
+            request = self.request
+            context = get_context()
+            carrier = Carrier()
+            for item in carrier:
+                if item.key.capitalize() in request.headers:
+                    item.val = request.headers[item.key.capitalize()]
+            with context.new_entry_span(op=request.path, carrier=carrier) as span:
+                span.layer = Layer.Http
+                span.component = Component.Tornado
+                peer = request.connection.stream.socket.getpeername()
+                span.peer = '{0}:{1}'.format(*peer)
+                span.tag(Tag(key=tags.HttpMethod, val=request.method))
+                span.tag(
+                    Tag(key=tags.HttpUrl, val='{}://{}{}'.format(request.protocol, request.host, request.path)))
+                result = yield from old_execute(self, *args, **kwargs)
+                span.tag(Tag(key=tags.HttpStatus, val=self._status_code))
+                if self._status_code >= 400:
+                    span.error_occurred = True
+            return result
+    return _sw_get_response
diff --git a/tests/plugin/sw_tornado/__init__.py b/tests/plugin/sw_tornado/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_tornado/__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_tornado/docker-compose.yml b/tests/plugin/sw_tornado/docker-compose.yml
new file mode 100644
index 0000000..98bb544
--- /dev/null
+++ b/tests/plugin/sw_tornado/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:
+      - ./services/provider.py:/provider.py
+    command: ['bash', '-c', 'pip3 install tornado && python3 /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:
+      - ./services/consumer.py:/consumer.py
+    command: ['bash', '-c', 'pip3 install tornado && python3 /consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+
+networks:
+  beyond:
diff --git a/tests/plugin/sw_tornado/expected.data.yml b/tests/plugin/sw_tornado/expected.data.yml
new file mode 100644
index 0000000..747c6c5
--- /dev/null
+++ b/tests/plugin/sw_tornado/expected.data.yml
@@ -0,0 +1,91 @@
+#
+# 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: 7005
+            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: status.code
+                value: '200'
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7005
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
\ No newline at end of file
diff --git a/tests/plugin/sw_tornado/services/__init__.py b/tests/plugin/sw_tornado/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_tornado/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_tornado/services/consumer.py b/tests/plugin/sw_tornado/services/consumer.py
new file mode 100644
index 0000000..8360f4c
--- /dev/null
+++ b/tests/plugin/sw_tornado/services/consumer.py
@@ -0,0 +1,40 @@
+#
+# 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 agent, config
+
+if __name__ == "__main__":
+    config.service_name = 'consumer'
+    config.logging_level = 'DEBUG'
+    agent.start()
+
+    import requests
+    import tornado.ioloop
+    import tornado.web
+
+    class MainHandler(tornado.web.RequestHandler):
+        def get(self):
+            res = requests.get("http://provider:9091/users")
+            self.write(res.text)
+
+    def make_app():
+        return tornado.web.Application([
+            (r"/users", MainHandler),
+        ])
+
+    app = make_app()
+    app.listen(9090, '0.0.0.0')
+    tornado.ioloop.IOLoop.current().start()
diff --git a/tests/plugin/sw_tornado/services/provider.py b/tests/plugin/sw_tornado/services/provider.py
new file mode 100644
index 0000000..19a760a
--- /dev/null
+++ b/tests/plugin/sw_tornado/services/provider.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 skywalking import agent, config
+
+if __name__ == "__main__":
+    config.service_name = "provider"
+    config.logging_level = "DEBUG"
+    agent.start()
+
+    import json
+    import time
+
+    import tornado.ioloop
+    import tornado.web
+
+    class MainHandler(tornado.web.RequestHandler):
+        def get(self):
+            time.sleep(0.5)
+            self.write(json.dumps({'song': 'Despacito', 'artist': 'Luis Fonsi'}))
+
+    def make_app():
+        return tornado.web.Application([
+            (r"/users", MainHandler),
+        ])
+
+    app = make_app()
+    app.listen(9091, '0.0.0.0')
+    tornado.ioloop.IOLoop.current().start()
diff --git a/tests/plugin/sw_tornado/test_tornado.py b/tests/plugin/sw_tornado/test_tornado.py
new file mode 100644
index 0000000..8187451
--- /dev/null
+++ b/tests/plugin/sw_tornado/test_tornado.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.
+#
+import inspect
+import time
+import unittest
+from os.path import dirname
+
+from testcontainers.compose import DockerCompose
+
+from tests.plugin import BasePluginTest
+
+
+class TestPlugin(BasePluginTest):
+    @classmethod
+    def setUpClass(cls):
+        cls.compose = DockerCompose(filepath=dirname(inspect.getfile(cls)))
+        cls.compose.start()
+
+        cls.compose.wait_for(cls.url(('consumer', '9090'), 'users'))
+
+    def test_plugin(self):
+        time.sleep(3)
+
+        self.validate()
+
+
+if __name__ == '__main__':
+    unittest.main()