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/15 05:13:11 UTC
[skywalking-python] branch master updated: Add PyMsql Plugin (#35)
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 <al...@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()