You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2022/07/05 00:55:28 UTC

[skywalking-python] branch master updated: Add plugin for bottle (#214)

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

wusheng 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 4090933  Add plugin for bottle (#214)
4090933 is described below

commit 4090933f627add88618335754a7d9e17ed1efa18
Author: jiang1997 <ji...@live.cn>
AuthorDate: Tue Jul 5 08:55:23 2022 +0800

    Add plugin for bottle (#214)
---
 CHANGELOG.md                                    |  1 +
 docs/en/setup/EnvVars.md                        |  1 +
 docs/en/setup/Plugins.md                        |  1 +
 requirements.txt                                |  3 +-
 skywalking/__init__.py                          |  1 +
 skywalking/config.py                            |  1 +
 skywalking/plugins/sw_bottle.py                 | 84 ++++++++++++++++++++++
 tests/plugin/web/sw_bottle/__init__.py          | 16 +++++
 tests/plugin/web/sw_bottle/docker-compose.yml   | 67 ++++++++++++++++++
 tests/plugin/web/sw_bottle/expected.data.yml    | 93 +++++++++++++++++++++++++
 tests/plugin/web/sw_bottle/services/__init__.py | 16 +++++
 tests/plugin/web/sw_bottle/services/consumer.py | 29 ++++++++
 tests/plugin/web/sw_bottle/services/provider.py | 30 ++++++++
 tests/plugin/web/sw_bottle/test_bottle.py       | 36 ++++++++++
 14 files changed, 378 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab07e11..87d28dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
   - Add MySQL support (#178)
   - Add FastAPI support (#181)
   - Drop support for flask 1.x due to dependency issue in Jinja2 and EOL (#195)
+  - Add Bottle support (#214)
 
 - Fixes:
   - Spans now correctly reference finished parents (#161)
diff --git a/docs/en/setup/EnvVars.md b/docs/en/setup/EnvVars.md
index e3cd6c6..2735713 100644
--- a/docs/en/setup/EnvVars.md
+++ b/docs/en/setup/EnvVars.md
@@ -50,3 +50,4 @@ customize the agent behavior, please read the descriptions for what they can ach
 | `SW_AGENT_CAUSE_EXCEPTION_DEPTH`                    | This config limits agent to report up to `limit` stacktrace, please refer to [Python traceback](https://docs.python.org/3/library/traceback.html#traceback.print_tb) for more explanations.                                                                                                             | `10` | 
 | `SW_PYTHON_BOOTSTRAP_PROPAGATE`                     | This config controls the child process agent bootstrap behavior in `sw-python` CLI, if set to `False`, a valid child process will not boot up a SkyWalking Agent. Please refer to the [CLI Guide](CLI.md) for details.                                                                                  | unset |
 | `SW_FASTAPI_COLLECT_HTTP_PARAMS`                    | This config item controls that whether the FastAPI plugin should collect the parameters of the request.                                                                                                                                                                                                 | `false` |
+| `SW_BOTTLE_COLLECT_HTTP_PARAMS`                    | This config item controls that whether the Bottle plugin should collect the parameters of the request.                                                                                                                                                                                                 | `false` |
diff --git a/docs/en/setup/Plugins.md b/docs/en/setup/Plugins.md
index 2655c9a..f032c15 100644
--- a/docs/en/setup/Plugins.md
+++ b/docs/en/setup/Plugins.md
@@ -14,6 +14,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
 Library | Python Version - Lib Version | Plugin Name
 | :--- | :--- | :--- |
 | [aiohttp](https://docs.aiohttp.org) | Python >=3.10 - NOT SUPPORTED YET; Python >=3.6 - ['3.7.4'];  | `sw_aiohttp` |
+| [bottle](http://bottlepy.org/docs/dev/) | Python >=3.6 - ['0.12.21'];  | `sw_bottle` |
 | [celery](https://docs.celeryq.dev) | Python >=3.6 - ['5.1'];  | `sw_celery` |
 | [django](https://www.djangoproject.com/) | Python >=3.6 - ['3.2'];  | `sw_django` |
 | [elasticsearch](https://github.com/elastic/elasticsearch-py) | Python >=3.6 - ['7.13', '7.14', '7.15'];  | `sw_elasticsearch` |
diff --git a/requirements.txt b/requirements.txt
index 3f5bd35..efdd11e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,4 +31,5 @@ yarl==1.7.0
 mysqlclient==2.1.0
 fastapi==0.70.1
 uvicorn==0.16.0
-gunicorn==20.1.0
\ No newline at end of file
+gunicorn==20.1.0
+bottle==0.12.21
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 31b218b..9621fdb 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -46,6 +46,7 @@ class Component(Enum):
     Falcon = 7012
     MysqlClient = 7013
     FastAPI = 7014
+    Bottle = 7015
 
 
 class Layer(Enum):
diff --git a/skywalking/config.py b/skywalking/config.py
index 61baf82..b3c33fd 100644
--- a/skywalking/config.py
+++ b/skywalking/config.py
@@ -62,6 +62,7 @@ flask_collect_http_params: bool = os.getenv('SW_FLASK_COLLECT_HTTP_PARAMS') == '
 sanic_collect_http_params: bool = os.getenv('SW_SANIC_COLLECT_HTTP_PARAMS') == 'True'
 django_collect_http_params: bool = os.getenv('SW_DJANGO_COLLECT_HTTP_PARAMS') == 'True'
 fastapi_collect_http_params: bool = os.getenv('SW_FASTAPI_COLLECT_HTTP_PARAMS') == 'True'
+bottle_collect_http_params: bool = os.getenv('SW_BOTTLE_COLLECT_HTTP_PARAMS') == 'True'
 
 celery_parameters_length: int = int(os.getenv('SW_CELERY_PARAMETERS_LENGTH') or '512')
 
diff --git a/skywalking/plugins/sw_bottle.py b/skywalking/plugins/sw_bottle.py
new file mode 100644
index 0000000..326d671
--- /dev/null
+++ b/skywalking/plugins/sw_bottle.py
@@ -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.
+#
+
+from skywalking import Layer, Component, config
+from skywalking.trace.carrier import Carrier
+from skywalking.trace.context import get_context, NoopContext
+from skywalking.trace.span import NoopSpan
+from skywalking.trace.tags import TagHttpMethod, TagHttpParams, TagHttpStatusCode, TagHttpURL
+
+link_vector = ['http://bottlepy.org/docs/dev/']
+support_matrix = {
+    'bottle': {
+        '>=3.6': ['0.12.21']
+    }
+}
+note = """"""
+
+
+def install():
+    from bottle import Bottle
+    from wsgiref.simple_server import WSGIRequestHandler
+
+    _app_call = Bottle.__call__
+    _get_environ = WSGIRequestHandler.get_environ
+
+    def params_tostring(params):
+        return '\n'.join([f"{k}=[{','.join(params.getlist(k))}]" for k, _ in params.items()])
+
+    def sw_get_environ(self):
+        env = _get_environ(self)
+        env['REMOTE_PORT'] = self.client_address[1]
+        return env
+
+    def sw_app_call(self, environ, start_response):
+        from bottle import response
+        from bottle import LocalRequest
+
+        request = LocalRequest()
+        request.bind(environ)
+        carrier = Carrier()
+        method = request.method
+
+        for item in carrier:
+            if item.key.capitalize() in request.headers:
+                item.val = request.headers[item.key.capitalize()]
+
+        span = NoopSpan(NoopContext()) if config.ignore_http_method_check(method) \
+            else get_context().new_entry_span(op=request.path, carrier=carrier, inherit=Component.General)
+
+        with span:
+            span.layer = Layer.Http
+            span.component = Component.Bottle
+            if all(environ_key in environ for environ_key in ('REMOTE_ADDR', 'REMOTE_PORT')):
+                span.peer = f"{environ['REMOTE_ADDR']}:{environ['REMOTE_PORT']}"
+            span.tag(TagHttpMethod(method))
+            span.tag(TagHttpURL(request.url.split('?')[0]))
+
+            if config.bottle_collect_http_params and request.query:
+                span.tag(TagHttpParams(params_tostring(request.query)[0:config.http_params_length_threshold]))
+
+            res = _app_call(self, environ, start_response)
+
+            span.tag(TagHttpStatusCode(response.status_code))
+            if response.status_code >= 400:
+                span.error_occurred = True
+
+            return res
+
+    Bottle.__call__ = sw_app_call
+    WSGIRequestHandler.get_environ = sw_get_environ
diff --git a/tests/plugin/web/sw_bottle/__init__.py b/tests/plugin/web/sw_bottle/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/__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/web/sw_bottle/docker-compose.yml b/tests/plugin/web/sw_bottle/docker-compose.yml
new file mode 100644
index 0000000..c8e0979
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/docker-compose.yml
@@ -0,0 +1,67 @@
+#
+# 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-compose.base.yml
+
+  provider:
+    extends:
+      service: agent
+      file: ../../docker-compose.base.yml
+    ports:
+      - 9091:9091
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip install -r /app/requirements.txt && sw-python run 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
+    environment:
+      SW_AGENT_NAME: provider
+      SW_AGENT_LOGGING_LEVEL: DEBUG
+
+  consumer:
+    extends:
+      service: agent
+      file: ../../docker-compose.base.yml
+    ports:
+      - 9090:9090
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip install -r /app/requirements.txt && sw-python run python3 /app/services/consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+    environment:
+      SW_AGENT_NAME: consumer
+      SW_AGENT_LOGGING_LEVEL: DEBUG
+      SW_BOTTLE_COLLECT_HTTP_PARAMS: 'True'
+
+networks:
+  beyond:
diff --git a/tests/plugin/web/sw_bottle/expected.data.yml b/tests/plugin/web/sw_bottle/expected.data.yml
new file mode 100644
index 0000000..b5081fd
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/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
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            isError: false
+            componentId: 7015
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: POST
+              - key: http.url
+                value: http://provider:9091/users
+              - key: http.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
+  - serviceName: consumer
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /users
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            isError: false
+            componentId: 7002
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: POST
+              - key: http.url
+                value: http://provider:9091/users
+              - key: http.status_code
+                value: '200'
+          - operationName: /users
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            isError: false
+            componentId: 7015
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: http.url
+                value: http://0.0.0.0:9090/users
+              - key: http.params
+                value: "test=[test1,test2]\ntest2=[test2]"
+              - key: http.status_code
+                value: '200'
diff --git a/tests/plugin/web/sw_bottle/services/__init__.py b/tests/plugin/web/sw_bottle/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/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/web/sw_bottle/services/consumer.py b/tests/plugin/web/sw_bottle/services/consumer.py
new file mode 100644
index 0000000..27970fb
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/services/consumer.py
@@ -0,0 +1,29 @@
+#
+# 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 bottle import route, run
+
+
+if __name__ == '__main__':
+    @route('/users', method='GET')
+    @route('/users', method='POST')
+    def hello():
+        res = requests.post('http://provider:9091/users', timeout=5)
+        return res.json()
+
+    run(host='0.0.0.0', port=9090, debug=True)
diff --git a/tests/plugin/web/sw_bottle/services/provider.py b/tests/plugin/web/sw_bottle/services/provider.py
new file mode 100644
index 0000000..047b5cb
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/services/provider.py
@@ -0,0 +1,30 @@
+#
+# 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
+import json
+from bottle import route, run
+
+
+if __name__ == '__main__':
+    @route('/users', method='GET')
+    @route('/users', method='POST')
+    def hello():
+        time.sleep(0.5)
+        return json.dumps({'song': 'Despacito', 'artist': 'Luis Fonsi'})
+
+    run(host='0.0.0.0', port=9091, debug=True)
diff --git a/tests/plugin/web/sw_bottle/test_bottle.py b/tests/plugin/web/sw_bottle/test_bottle.py
new file mode 100644
index 0000000..c67ba1c
--- /dev/null
+++ b/tests/plugin/web/sw_bottle/test_bottle.py
@@ -0,0 +1,36 @@
+#
+# 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 skywalking.plugins.sw_bottle import support_matrix
+from tests.orchestrator import get_test_vector
+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', timeout=5)
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', get_test_vector(lib_name='bottle', support_matrix=support_matrix))
+    def test_plugin(self, docker_compose, version):
+        self.validate()