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 c30b10b Add PyMsql Plugin (#35) c30b10b is described below commit c30b10bc87e2e8c58d9c882e1af04e081b5bb032 Author: huawei <alonela...@gmail.com> AuthorDate: Wed Jul 15 13:13:03 2020 +0800 Add PyMsql Plugin (#35) --- README.md | 1 + setup.py | 3 +- skywalking/__init__.py | 1 + skywalking/plugins/sw_pymysql/__init__.py | 57 +++++++++++ skywalking/trace/tags/__init__.py | 3 + .../tags => tests/plugin/sw_pymysql}/__init__.py | 9 -- tests/plugin/sw_pymysql/docker-compose.yml | 78 +++++++++++++++ tests/plugin/sw_pymysql/expected.data.yml | 109 +++++++++++++++++++++ .../plugin/sw_pymysql/services}/__init__.py | 9 -- .../plugin/sw_pymysql/services/consumer.py | 24 +++-- .../plugin/sw_pymysql/services/provider.py | 32 ++++-- .../plugin/sw_pymysql/test_pymysql.py | 30 ++++-- 12 files changed, 319 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 397ad84..55bad83 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Library | Plugin Name | [urllib.request](https://docs.python.org/3/library/urllib.request.html) | `sw_urllib_request` | | [requests](https://requests.readthedocs.io/en/master/) | `sw_requests` | | [Flask](https://flask.palletsprojects.com/en/1.1.x/) | `sw_flask` | +| [PyMySQL](https://pymysql.readthedocs.io/en/latest/) | `sw_pymysql` | ## API diff --git a/setup.py b/setup.py index 39c495f..c83df15 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,8 @@ setup( extras_require={ "test": [ "testcontainers", - "Werkzeug" + "Werkzeug", + "pymysql", ], }, classifiers=[ diff --git a/skywalking/__init__.py b/skywalking/__init__.py index f826ad6..ae9f4c4 100644 --- a/skywalking/__init__.py +++ b/skywalking/__init__.py @@ -26,6 +26,7 @@ class Component(Enum): General = 7000 # built-in modules that may not have a logo to display Flask = 7001 Requests = 7002 + PyMysql = 7003 class Layer(Enum): diff --git a/skywalking/plugins/sw_pymysql/__init__.py b/skywalking/plugins/sw_pymysql/__init__.py new file mode 100644 index 0000000..cc512f5 --- /dev/null +++ b/skywalking/plugins/sw_pymysql/__init__.py @@ -0,0 +1,57 @@ +# +# 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.tags import Tag + +logger = logging.getLogger(__name__) + + +def install(): + # noinspection PyBroadException + try: + from pymysql.cursors import Cursor + + _execute = Cursor.execute + + def _sw_execute(this: Cursor, query, args=None): + peer = "%s:%s" % (this.connection.host, this.connection.port) + + context = get_context() + carrier = Carrier() + with context.new_exit_span(op="Mysql/PyMsql/execute", peer=peer, carrier=carrier) as span: + span.layer = Layer.Database + span.component = Component.PyMysql + try: + res = _execute(this, query, args) + + span.tag(Tag(key=tags.DbType, val="mysql")) + span.tag(Tag(key=tags.DbInstance, val=this.connection.db.decode("utf-8"))) + span.tag(Tag(key=tags.DbStatement, val=query)) + + except BaseException as e: + span.raised() + raise e + return res + + Cursor.execute = _sw_execute + except Exception: + logger.warning('failed to install plugin %s', __name__) diff --git a/skywalking/trace/tags/__init__.py b/skywalking/trace/tags/__init__.py index 483e71d..0c2ec18 100644 --- a/skywalking/trace/tags/__init__.py +++ b/skywalking/trace/tags/__init__.py @@ -23,3 +23,6 @@ Tag.__new__.__defaults__ = (None, None, False) HttpUrl = 'url' HttpMethod = 'http.method' HttpStatus = 'status.code' +DbType = 'db.type' +DbInstance = 'db.instance' +DbStatement = 'db.statement' diff --git a/skywalking/trace/tags/__init__.py b/tests/plugin/sw_pymysql/__init__.py similarity index 79% copy from skywalking/trace/tags/__init__.py copy to tests/plugin/sw_pymysql/__init__.py index 483e71d..b1312a0 100644 --- a/skywalking/trace/tags/__init__.py +++ b/tests/plugin/sw_pymysql/__init__.py @@ -14,12 +14,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -from collections import namedtuple - -Tag = namedtuple('Tag', 'key val overridable') -Tag.__new__.__defaults__ = (None, None, False) - -HttpUrl = 'url' -HttpMethod = 'http.method' -HttpStatus = 'status.code' diff --git a/tests/plugin/sw_pymysql/docker-compose.yml b/tests/plugin/sw_pymysql/docker-compose.yml new file mode 100644 index 0000000..9685624 --- /dev/null +++ b/tests/plugin/sw_pymysql/docker-compose.yml @@ -0,0 +1,78 @@ +# +# 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 + + mysql: + image: mysql:5.7 + hostname: mysql + ports: + - 3306:3306 + - 33060:33060 + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_DATABASE=test + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/3306"] + interval: 5s + timeout: 60s + retries: 120 + networks: + - beyond + + provider: + extends: + service: agent + file: ../docker/docker-compose.base.yml + ports: + - 9091:9091 + volumes: + - ./services/provider.py:/app/provider.py + command: ['bash', '-c', 'pip install flask && pip install PyMySQL && python3 /app/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:/app/consumer.py + command: ['bash', '-c', 'pip install flask && python3 /app/consumer.py'] + depends_on: + collector: + condition: service_healthy + provider: + condition: service_healthy + +networks: + beyond: diff --git a/tests/plugin/sw_pymysql/expected.data.yml b/tests/plugin/sw_pymysql/expected.data.yml new file mode 100644 index 0000000..127b893 --- /dev/null +++ b/tests/plugin/sw_pymysql/expected.data.yml @@ -0,0 +1,109 @@ +# +# 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: Mysql/PyMsql/execute + operationId: 0 + parentSpanId: 0 + spanId: 1 + spanLayer: Database + tags: + - key: db.type + value: mysql + - key: db.instance + value: test + - key: db.statement + value: select 1 + startTime: gt 0 + endTime: gt 0 + componentId: 7003 + spanType: Exit + peer: mysql:3306 + skipAnalysis: false + - operationName: /users + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + tags: + - key: http.method + value: POST + - 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: 7001 + 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: POST + - 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: 7001 + spanType: Entry + peer: not null + skipAnalysis: false \ No newline at end of file diff --git a/skywalking/trace/tags/__init__.py b/tests/plugin/sw_pymysql/services/__init__.py similarity index 79% copy from skywalking/trace/tags/__init__.py copy to tests/plugin/sw_pymysql/services/__init__.py index 483e71d..b1312a0 100644 --- a/skywalking/trace/tags/__init__.py +++ b/tests/plugin/sw_pymysql/services/__init__.py @@ -14,12 +14,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -from collections import namedtuple - -Tag = namedtuple('Tag', 'key val overridable') -Tag.__new__.__defaults__ = (None, None, False) - -HttpUrl = 'url' -HttpMethod = 'http.method' -HttpStatus = 'status.code' diff --git a/skywalking/trace/tags/__init__.py b/tests/plugin/sw_pymysql/services/consumer.py similarity index 62% copy from skywalking/trace/tags/__init__.py copy to tests/plugin/sw_pymysql/services/consumer.py index 483e71d..c942f91 100644 --- a/skywalking/trace/tags/__init__.py +++ b/tests/plugin/sw_pymysql/services/consumer.py @@ -15,11 +15,23 @@ # limitations under the License. # -from collections import namedtuple +import requests -Tag = namedtuple('Tag', 'key val overridable') -Tag.__new__.__defaults__ = (None, None, False) +from skywalking import agent, config -HttpUrl = 'url' -HttpMethod = 'http.method' -HttpStatus = 'status.code' +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/skywalking/trace/tags/__init__.py b/tests/plugin/sw_pymysql/services/provider.py similarity index 51% copy from skywalking/trace/tags/__init__.py copy to tests/plugin/sw_pymysql/services/provider.py index 483e71d..a0a305a 100644 --- a/skywalking/trace/tags/__init__.py +++ b/tests/plugin/sw_pymysql/services/provider.py @@ -15,11 +15,31 @@ # limitations under the License. # -from collections import namedtuple +import time -Tag = namedtuple('Tag', 'key val overridable') -Tag.__new__.__defaults__ = (None, None, False) +from skywalking import agent, config -HttpUrl = 'url' -HttpMethod = 'http.method' -HttpStatus = 'status.code' +if __name__ == '__main__': + config.service_name = 'provider' + config.logging_level = 'DEBUG' + agent.start() + + from flask import Flask, jsonify + import pymysql.cursors + + app = Flask(__name__) + + @app.route("/users", methods=["POST", "GET"]) + def application(): + time.sleep(0.5) + connection = pymysql.connect(host='mysql', user='root', password='root', db='test', charset='utf8mb4') + with connection.cursor() as cursor: + sql = "select 1" + cursor.execute(sql) + + connection.close() + + return jsonify({"song": "Despacito", "artist": "Luis Fonsi"}) + + PORT = 9091 + app.run(host='0.0.0.0', port=PORT, debug=True) diff --git a/skywalking/trace/tags/__init__.py b/tests/plugin/sw_pymysql/test_pymysql.py similarity index 55% copy from skywalking/trace/tags/__init__.py copy to tests/plugin/sw_pymysql/test_pymysql.py index 483e71d..dfeaaa6 100644 --- a/skywalking/trace/tags/__init__.py +++ b/tests/plugin/sw_pymysql/test_pymysql.py @@ -15,11 +15,29 @@ # limitations under the License. # -from collections import namedtuple +import os +import time +import unittest +from os.path import abspath, dirname -Tag = namedtuple('Tag', 'key val overridable') -Tag.__new__.__defaults__ = (None, None, False) +from testcontainers.compose import DockerCompose -HttpUrl = 'url' -HttpMethod = 'http.method' -HttpStatus = 'status.code' +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()