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 e118f26  Add falcon plugin (#146)
e118f26 is described below

commit e118f266dc10a3689dbce199d512d8e8a081a6bb
Author: probeyang <[email protected]>
AuthorDate: Wed Aug 11 20:11:10 2021 +0800

    Add falcon plugin (#146)
---
 CHANGELOG.md                                |  5 ++
 docs/Plugins.md                             |  1 +
 requirements.txt                            |  1 +
 skywalking/__init__.py                      |  1 +
 skywalking/plugins/sw_falcon.py             | 98 +++++++++++++++++++++++++++++
 tests/plugin/sw_falcon/__init__.py          | 16 +++++
 tests/plugin/sw_falcon/docker-compose.yml   | 60 ++++++++++++++++++
 tests/plugin/sw_falcon/expected.data.yml    | 84 +++++++++++++++++++++++++
 tests/plugin/sw_falcon/services/__init__.py | 16 +++++
 tests/plugin/sw_falcon/services/consumer.py | 33 ++++++++++
 tests/plugin/sw_falcon/services/provider.py | 35 +++++++++++
 tests/plugin/sw_falcon/test_falcon.py       | 39 ++++++++++++
 12 files changed, 389 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4747ec6..f7e2b27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 ## Change Logs
 
+### 0.7.0
+
+- New plugins
+    - Falcon Plugin (#146)
+
 ### 0.6.0
 
 - Fixes:
diff --git a/docs/Plugins.md b/docs/Plugins.md
index 23f7473..30fd83a 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -20,6 +20,7 @@ Library | Versions | Plugin Name
 | [pyramid](https://trypyramid.com) | >= 1.9 | `sw_pyramid` |
 | [psycopg2](https://www.psycopg.org/) | >= 2.8.6 | `sw_psycopg2` |
 | [celery](https://docs.celeryproject.org/) | >= 4.2.1 | `sw_celery` |
+| [falcon](https://falcon.readthedocs.io/en/stable/) | >= 1.4.1 | `sw_falcon` |
 
 * Note: The celery server running with "celery -A ..." should be run with the 
http protocol as it uses multiprocessing by default which is not compatible 
with the grpc protocol implementation in skywalking currently. Celery clients 
can use whatever protocol they want.
 
diff --git a/requirements.txt b/requirements.txt
index 71d5e80..e97bddc 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -54,6 +54,7 @@ sqlparse==0.3.1
 testcontainers==3.0.3
 toml==0.10.1
 tornado==6.0.4
+hug==2.4.1
 urllib3==1.25.10
 websockets==8.1
 websocket-client==0.57.0
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 95461ce..9e57f36 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -43,6 +43,7 @@ class Component(Enum):
     Pyramid = 7009
     Psycopg = 7010
     Celery = 7011
+    Falcon = 7012
 
 
 class Layer(Enum):
diff --git a/skywalking/plugins/sw_falcon.py b/skywalking/plugins/sw_falcon.py
new file mode 100644
index 0000000..ce2c9f8
--- /dev/null
+++ b/skywalking/plugins/sw_falcon.py
@@ -0,0 +1,98 @@
+#
+# 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 Layer, Component
+from skywalking.trace.carrier import Carrier
+from skywalking.trace.context import get_context
+from skywalking.trace.span import NoopSpan
+from skywalking.trace.tags import TagHttpMethod, TagHttpURL, TagHttpParams, 
TagHttpStatusCode
+
+
+def install():
+    from falcon import API, request, response
+
+    _original_falcon_api = API.__call__
+    _original_falcon_handle_exception = API._handle_exception
+
+    def params_tostring(params):
+        return "\n".join([k + "=" + v for k, v in params.items()])
+
+    def _sw_falcon_api(this: API, env, start_response):
+        context = get_context()
+        carrier = Carrier()
+        headers = get_headers(env)
+        for item in carrier:
+            key = item.key.replace("_", "-") if "_" in item.key else item.key
+            if key.capitalize() in headers:
+                item.val = headers[key.capitalize()]
+        with context.new_entry_span(op="/", carrier=carrier) as span:
+            span.layer = Layer.Http
+            span.component = Component.Falcon
+
+            from falcon import RequestOptions
+
+            req = request.Request(env, RequestOptions())
+            span.op = str(req.url).split("?")[0]
+            span.peer = "%s:%s" % (req.remote_addr, req.port)
+
+            span.tag(TagHttpMethod(req.method))
+            span.tag(TagHttpURL(str(req.url)))
+            if req.params:
+                span.tag(TagHttpParams(params_tostring(req.params)[0:]))
+
+            resp = _original_falcon_api(this, env, start_response)
+
+            from falcon import ResponseOptions
+
+            resp_obj = response.Response(ResponseOptions())
+
+            resp_status = parse_status(resp_obj.status)
+            if int(resp_status[0]) >= 400:
+                span.error_occurred = True
+
+            span.tag(TagHttpStatusCode(int(resp_status[0])))
+
+            return resp
+
+    def _sw_handle_exception(this: API, req, resp, ex, params):
+        if ex 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 _original_falcon_handle_exception(this, req, resp, ex, params)
+
+    API.__call__ = _sw_falcon_api
+    API._handle_exception = _sw_handle_exception
+
+
+def get_headers(env):
+    headers = {}
+    wsgi_content_headers = frozenset(["CONTENT_TYPE", "CONTENT_LENGTH"])
+
+    for name, value in env.items():
+        if name.startswith("HTTP_"):
+            headers[name[5:].replace("_", "-")] = value
+
+        elif name in wsgi_content_headers:
+            headers[name.replace("_", "-")] = value
+
+    return headers
+
+
+def parse_status(status_str):
+    return status_str.split(" ") if status_str else [404, "status is empty"]
diff --git a/tests/plugin/sw_falcon/__init__.py 
b/tests/plugin/sw_falcon/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_falcon/__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_falcon/docker-compose.yml 
b/tests/plugin/sw_falcon/docker-compose.yml
new file mode 100644
index 0000000..05dbd10
--- /dev/null
+++ b/tests/plugin/sw_falcon/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 hug && pip install -r 
/app/requirements.txt && hug -f /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 hug && pip install -r 
/app/requirements.txt && hug -f /app/services/consumer.py' ]
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+
+networks:
+  beyond:
diff --git a/tests/plugin/sw_falcon/expected.data.yml 
b/tests/plugin/sw_falcon/expected.data.yml
new file mode 100644
index 0000000..c6dab1e
--- /dev/null
+++ b/tests/plugin/sw_falcon/expected.data.yml
@@ -0,0 +1,84 @@
+#
+# 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:
+- segmentSize: 1
+  segments:
+  - segmentId: not null
+    spans:
+    - componentId: 7012
+      endTime: gt 0
+      isError: false
+      operationId: 0
+      operationName: http://provider:9091/users
+      parentSpanId: -1
+      peer: not null
+      skipAnalysis: false
+      spanId: 0
+      spanLayer: Http
+      spanType: Entry
+      startTime: gt 0
+      tags:
+      - key: http.method
+        value: GET
+      - key: http.url
+        value: http://provider:9091/users
+      - key: http.status.code
+        value: '200'
+  serviceName: provider
+- segmentSize: 1
+  segments:
+  - segmentId: not null
+    spans:
+    - componentId: 7002
+      endTime: gt 0
+      isError: false
+      operationId: 0
+      operationName: /users
+      parentSpanId: 0
+      peer: provider:9091
+      skipAnalysis: false
+      spanId: 1
+      spanLayer: Http
+      spanType: Exit
+      startTime: gt 0
+      tags:
+      - key: http.method
+        value: GET
+      - key: http.url
+        value: http://provider:9091/users
+      - key: http.status.code
+        value: '200'
+    - componentId: 7012
+      endTime: gt 0
+      isError: false
+      operationId: 0
+      operationName: http://0.0.0.0:9090/users
+      parentSpanId: -1
+      peer: not null
+      skipAnalysis: false
+      spanId: 0
+      spanLayer: Http
+      spanType: Entry
+      startTime: gt 0
+      tags:
+      - key: http.method
+        value: GET
+      - key: http.url
+        value: http://0.0.0.0:9090/users
+      - key: http.status.code
+        value: '200'
+  serviceName: consumer
\ No newline at end of file
diff --git a/tests/plugin/sw_falcon/services/__init__.py 
b/tests/plugin/sw_falcon/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_falcon/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_falcon/services/consumer.py 
b/tests/plugin/sw_falcon/services/consumer.py
new file mode 100644
index 0000000..c3c1651
--- /dev/null
+++ b/tests/plugin/sw_falcon/services/consumer.py
@@ -0,0 +1,33 @@
+#
+# 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
+
+import requests
+import hug
+
+config.service_name = 'consumer'
+config.logging_level = 'DEBUG'
+agent.start()
+
+
[email protected]('/users')
+def get():
+    res = requests.get("http://provider:9091/users";)
+    return res.json()
+
+
+hug.API(__name__).http.serve(port=9090)
diff --git a/tests/plugin/sw_falcon/services/provider.py 
b/tests/plugin/sw_falcon/services/provider.py
new file mode 100644
index 0000000..7c04f54
--- /dev/null
+++ b/tests/plugin/sw_falcon/services/provider.py
@@ -0,0 +1,35 @@
+#
+# 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
+
+import hug
+import time
+import json
+
+config.service_name = 'provider'
+config.logging_level = 'DEBUG'
+agent.start()
+
+
[email protected]('/users')
+def get():
+    time.sleep(0.5)
+    return json.dumps({'song': 'Despacito', 'artist': 'Luis Fonsi'})
+
+
+hug.API(__name__).http.serve(port=9091)
diff --git a/tests/plugin/sw_falcon/test_falcon.py 
b/tests/plugin/sw_falcon/test_falcon.py
new file mode 100644
index 0000000..bb6dde8
--- /dev/null
+++ b/tests/plugin/sw_falcon/test_falcon.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.
+#
+
+from typing import Callable
+
+import pytest
+import requests
+
+from tests.plugin.base import TestPluginBase
+
+
[email protected]
+def prepare():
+    # type: () -> Callable
+    return lambda *_: requests.get('http://0.0.0.0:9090/users')
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', [
+        'hug==2.4.1',
+        'hug==2.5.0',
+        'hug==2.6.0',
+    ])
+    def test_plugin(self, docker_compose, version):
+        self.validate()

Reply via email to