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/02 15:00:25 UTC

[skywalking-python] branch master updated: Flask plugin (#31)

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 d4e663a  Flask plugin (#31)
d4e663a is described below

commit d4e663a8894d5dc3a6976363cb165795c7af6e94
Author: huawei <al...@gmail.com>
AuthorDate: Thu Jul 2 23:00:16 2020 +0800

    Flask plugin (#31)
    
    Co-authored-by: kezhenxu94 <ke...@163.com>
---
 skywalking/plugins/sw_flask/__init__.py            | 72 ++++++++++++++++++++++
 skywalking/trace/context/__init__.py               |  9 +++
 tests/plugin/sw_flask/__init__.py                  | 16 +++++
 .../{sw_requests => sw_flask}/docker-compose.yml   |  6 +-
 .../{sw_requests => sw_flask}/expected.data.yml    | 19 ++++--
 tests/plugin/sw_flask/services/__init__.py         | 16 +++++
 tests/plugin/sw_flask/services/consumer.py         | 37 +++++++++++
 tests/plugin/sw_flask/services/provider.py         | 37 +++++++++++
 tests/plugin/sw_flask/test_flask.py                | 43 +++++++++++++
 tests/plugin/sw_requests/docker-compose.yml        |  2 +-
 tests/plugin/sw_requests/expected.data.yml         |  3 +-
 11 files changed, 249 insertions(+), 11 deletions(-)

diff --git a/skywalking/plugins/sw_flask/__init__.py b/skywalking/plugins/sw_flask/__init__.py
new file mode 100644
index 0000000..b62ceb0
--- /dev/null
+++ b/skywalking/plugins/sw_flask/__init__.py
@@ -0,0 +1,72 @@
+#
+# 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.span import NoopSpan
+from skywalking.trace.tags import Tag
+
+logger = logging.getLogger(__name__)
+
+
+def install():
+    # noinspection PyBroadException
+    try:
+        from flask import Flask
+        _full_dispatch_request = Flask.full_dispatch_request
+
+        _handle_user_exception = Flask.handle_user_exception
+
+        def _sw_full_dispatch_request(this: Flask):
+            import flask
+            req = flask.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.Flask
+                span.peer = '%s:%s' % (req.environ["REMOTE_ADDR"], req.environ["REMOTE_PORT"])
+                span.tag(Tag(key=tags.HttpMethod, val=req.method))
+                span.tag(Tag(key=tags.HttpUrl, val=req.url))
+                resp = _full_dispatch_request(this)
+
+                if resp.status_code >= 400:
+                    span.error_occurred = True
+
+                span.tag(Tag(key=tags.HttpStatus, val=resp.status_code))
+                return resp
+
+        def _sw_handle_user_exception(this: Flask, 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 _handle_user_exception(this, e)
+
+        Flask.full_dispatch_request = _sw_full_dispatch_request
+        Flask.handle_user_exception = _sw_handle_user_exception
+
+    except Exception:
+        logger.warning('failed to install plugin %s', __name__)
diff --git a/skywalking/trace/context/__init__.py b/skywalking/trace/context/__init__.py
index 190121f..bd458db 100644
--- a/skywalking/trace/context/__init__.py
+++ b/skywalking/trace/context/__init__.py
@@ -91,6 +91,12 @@ class SpanContext(object):
 
         return len(self.spans) == 0
 
+    def active_span(self):
+        if self.spans:
+            return self.spans[len(self.spans) - 1]
+
+        return None
+
 
 class NoopContext(SpanContext):
     def __init__(self):
@@ -114,6 +120,9 @@ class NoopContext(SpanContext):
         self._depth -= 1
         return self._depth == 0
 
+    def active_span(self):
+        return self._noop_span
+
 
 _thread_local = threading.local()
 _thread_local.context = None
diff --git a/tests/plugin/sw_flask/__init__.py b/tests/plugin/sw_flask/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_flask/__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_requests/docker-compose.yml b/tests/plugin/sw_flask/docker-compose.yml
similarity index 86%
copy from tests/plugin/sw_requests/docker-compose.yml
copy to tests/plugin/sw_flask/docker-compose.yml
index 4233f03..61f3be7 100644
--- a/tests/plugin/sw_requests/docker-compose.yml
+++ b/tests/plugin/sw_flask/docker-compose.yml
@@ -30,7 +30,8 @@ services:
     ports:
       - 9091:9091
     volumes:
-      - ./services/provider.py:/entrypoint.py
+      - ./services/provider.py:/app/provider.py
+    command: ['bash', '-c', 'pip install flask && python3 /app/provider.py']
     depends_on:
       collector:
         condition: service_healthy
@@ -47,7 +48,8 @@ services:
     ports:
       - 9090:9090
     volumes:
-      - ./services/consumer.py:/entrypoint.py
+      - ./services/consumer.py:/app/consumer.py
+    command: ['bash', '-c', 'pip install flask && python3 /app/consumer.py']
     depends_on:
       collector:
         condition: service_healthy
diff --git a/tests/plugin/sw_requests/expected.data.yml b/tests/plugin/sw_flask/expected.data.yml
similarity index 85%
copy from tests/plugin/sw_requests/expected.data.yml
copy to tests/plugin/sw_flask/expected.data.yml
index 509127e..aa3d2dc 100644
--- a/tests/plugin/sw_requests/expected.data.yml
+++ b/tests/plugin/sw_flask/expected.data.yml
@@ -29,6 +29,10 @@ segmentItems:
             tags:
               - key: http.method
                 value: POST
+              - key: url
+                value: http://provider:9091/users
+              - key: status.code
+                value: '200'
             refs:
               - parentEndpoint: /users
                 networkAddress: provider:9091
@@ -40,7 +44,7 @@ segmentItems:
                 traceId: not null
             startTime: gt 0
             endTime: gt 0
-            componentId: 7000
+            componentId: 7001
             spanType: Entry
             peer: not null
             skipAnalysis: false
@@ -67,18 +71,21 @@ segmentItems:
             spanType: Exit
             peer: provider:9091
             skipAnalysis: false
-          - operationName: /
+          - operationName: /users
             operationId: 0
             parentSpanId: -1
             spanId: 0
             spanLayer: Http
             tags:
               - key: http.method
-                value: POST
+                value: GET
+              - key: url
+                value: http://0.0.0.0:9090/users
+              - key: status.code
+                value: '200'
             startTime: gt 0
             endTime: gt 0
-            componentId: 7000
+            componentId: 7001
             spanType: Entry
             peer: not null
-            skipAnalysis: false
-
+            skipAnalysis: false
\ No newline at end of file
diff --git a/tests/plugin/sw_flask/services/__init__.py b/tests/plugin/sw_flask/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_flask/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_flask/services/consumer.py b/tests/plugin/sw_flask/services/consumer.py
new file mode 100644
index 0000000..c942f91
--- /dev/null
+++ b/tests/plugin/sw_flask/services/consumer.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 requests
+
+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__)
+
+    @app.route("/users", methods=["POST", "GET"])
+    def application():
+        res = requests.post("http://provider:9091/users")
+        return jsonify(res.json())
+
+    PORT = 9090
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_flask/services/provider.py b/tests/plugin/sw_flask/services/provider.py
new file mode 100644
index 0000000..11f2c0b
--- /dev/null
+++ b/tests/plugin/sw_flask/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_flask/test_flask.py b/tests/plugin/sw_flask/test_flask.py
new file mode 100644
index 0000000..dfeaaa6
--- /dev/null
+++ b/tests/plugin/sw_flask/test_flask.py
@@ -0,0 +1,43 @@
+#
+# 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 os
+import time
+import unittest
+from os.path import abspath, dirname
+
+from testcontainers.compose import DockerCompose
+
+from tests.plugin import BasePluginTest
+
+
+class TestPlugin(BasePluginTest):
+    @classmethod
+    def setUpClass(cls):
+        cls.compose = DockerCompose(filepath=dirname(abspath(__file__)))
+        cls.compose.start()
+
+        cls.compose.wait_for(cls.url(('consumer', '9090'), 'users'))
+
+    def test_request_plugin(self):
+        time.sleep(3)
+
+        self.validate(expected_file_name=os.path.join(dirname(abspath(__file__)), 'expected.data.yml'))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/plugin/sw_requests/docker-compose.yml b/tests/plugin/sw_requests/docker-compose.yml
index 4233f03..b875de6 100644
--- a/tests/plugin/sw_requests/docker-compose.yml
+++ b/tests/plugin/sw_requests/docker-compose.yml
@@ -55,4 +55,4 @@ services:
         condition: service_healthy
 
 networks:
-  beyond:
+  beyond:
\ No newline at end of file
diff --git a/tests/plugin/sw_requests/expected.data.yml b/tests/plugin/sw_requests/expected.data.yml
index 509127e..e85480f 100644
--- a/tests/plugin/sw_requests/expected.data.yml
+++ b/tests/plugin/sw_requests/expected.data.yml
@@ -80,5 +80,4 @@ segmentItems:
             componentId: 7000
             spanType: Entry
             peer: not null
-            skipAnalysis: false
-
+            skipAnalysis: false
\ No newline at end of file