diff --git a/README.md b/README.md index 694f04c7f..b2fdc66f2 100755 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Library | Plugin Name | [Flask](https://flask.palletsprojects.com/en/1.1.x/) | `sw_flask` | | [PyMySQL](https://pymysql.readthedocs.io/en/latest/) | `sw_pymysql` | | [Django](https://www.djangoproject.com/) | `sw_django` | +| [redis-py](https://github.com/andymccurdy/redis-py/) | `sw_redis` | ## API diff --git a/setup.py b/setup.py index c83df15fd..651107830 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ "testcontainers", "Werkzeug", "pymysql", + "redis", ], }, classifiers=[ diff --git a/skywalking/__init__.py b/skywalking/__init__.py index 3619b89f4..0892f53bd 100644 --- a/skywalking/__init__.py +++ b/skywalking/__init__.py @@ -28,6 +28,7 @@ class Component(Enum): Requests = 7002 PyMysql = 7003 Django = 7004 + Redis = 7005 class Layer(Enum): diff --git a/skywalking/plugins/sw_redis/__init__.py b/skywalking/plugins/sw_redis/__init__.py new file mode 100644 index 000000000..c140476af --- /dev/null +++ b/skywalking/plugins/sw_redis/__init__.py @@ -0,0 +1,54 @@ +# +# 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.context import get_context +from skywalking.trace.tags import Tag + +logger = logging.getLogger(__name__) + + +def install(): + # noinspection PyBroadException + try: + from redis.connection import Connection + + _send_command = Connection.send_command + + def _sw_send_command(this: Connection, *args, **kwargs): + peer = "%s:%s" % (this.host, this.port) + op = args[0] + context = get_context() + with context.new_exit_span(op="Redis/"+op or "/", peer=peer) as span: + span.layer = Layer.Cache + span.component = Component.Redis + + try: + res = _send_command(this, *args, **kwargs) + span.tag(Tag(key=tags.DbType, val="Redis")) + span.tag(Tag(key=tags.DbInstance, val=this.db)) + span.tag(Tag(key=tags.DbStatement, val=op)) + except BaseException as e: + span.raised() + raise e + return res + + Connection.send_command = _sw_send_command + except Exception: + logger.warning('failed to install plugin %s', __name__) diff --git a/tests/plugin/sw_redis/__init__.py b/tests/plugin/sw_redis/__init__.py new file mode 100644 index 000000000..b1312a090 --- /dev/null +++ b/tests/plugin/sw_redis/__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_redis/docker-compose.yml b/tests/plugin/sw_redis/docker-compose.yml new file mode 100644 index 000000000..ba9fc19da --- /dev/null +++ b/tests/plugin/sw_redis/docker-compose.yml @@ -0,0 +1,74 @@ +# +# 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 + + redis: + image: redis:3.2.9-alpine + hostname: redis + ports: + - 6379:6379 + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/6379"] + 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 redis && 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_redis/expected.data.yml b/tests/plugin/sw_redis/expected.data.yml new file mode 100644 index 000000000..12686fd21 --- /dev/null +++ b/tests/plugin/sw_redis/expected.data.yml @@ -0,0 +1,127 @@ +# +# 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: Redis/SET + operationId: 0 + parentSpanId: 0 + spanId: 1 + spanLayer: Cache + tags: + - key: db.type + value: Redis + - key: db.instance + value: '0' + - key: db.statement + value: 'SET' + startTime: gt 0 + endTime: gt 0 + componentId: 7005 + spanType: Exit + peer: redis:6379 + skipAnalysis: false + - operationName: Redis/GET + operationId: 0 + parentSpanId: 0 + spanId: 2 + spanLayer: Cache + tags: + - key: db.type + value: Redis + - key: db.instance + value: '0' + - key: db.statement + value: 'GET' + startTime: gt 0 + endTime: gt 0 + componentId: 7005 + spanType: Exit + peer: redis:6379 + 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/tests/plugin/sw_redis/services/__init__.py b/tests/plugin/sw_redis/services/__init__.py new file mode 100644 index 000000000..b1312a090 --- /dev/null +++ b/tests/plugin/sw_redis/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_redis/services/consumer.py b/tests/plugin/sw_redis/services/consumer.py new file mode 100644 index 000000000..c942f91ce --- /dev/null +++ b/tests/plugin/sw_redis/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_redis/services/provider.py b/tests/plugin/sw_redis/services/provider.py new file mode 100644 index 000000000..0b1fb0f9e --- /dev/null +++ b/tests/plugin/sw_redis/services/provider.py @@ -0,0 +1,44 @@ +# +# 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 + import redis + + app = Flask(__name__) + + @app.route("/users", methods=["POST", "GET"]) + def application(): + time.sleep(0.5) + + r = redis.StrictRedis(host='redis', port=6379, db=0) + + r.set('foo', 'bar') + r.get('foo') + + 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_redis/test_redispy.py b/tests/plugin/sw_redis/test_redispy.py new file mode 100644 index 000000000..dfeaaa6ce --- /dev/null +++ b/tests/plugin/sw_redis/test_redispy.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()