diff --git a/skywalking/plugins/sw_flask/__init__.py b/skywalking/plugins/sw_flask/__init__.py new file mode 100644 index 00000000..b62ceb09 --- /dev/null +++ b/skywalking/plugins/sw_flask/__init__.py @@ -0,0 +1,72 @@ +# +# 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.span import NoopSpan +from skywalking.trace.tags import Tag + +logger = logging.getLogger(__name__) + + +def install(): + # noinspection PyBroadException + try: + from flask import Flask + _full_dispatch_request = Flask.full_dispatch_request + + _handle_user_exception = Flask.handle_user_exception + + def _sw_full_dispatch_request(this: Flask): + import flask + req = flask.request + context = get_context() + carrier = Carrier() + + for item in carrier: + if item.key.capitalize() in req.headers: + item.val = req.headers[item.key.capitalize()] + with context.new_entry_span(op=req.path, carrier=carrier) as span: + span.layer = Layer.Http + span.component = Component.Flask + span.peer = '%s:%s' % (req.environ["REMOTE_ADDR"], req.environ["REMOTE_PORT"]) + span.tag(Tag(key=tags.HttpMethod, val=req.method)) + span.tag(Tag(key=tags.HttpUrl, val=req.url)) + resp = _full_dispatch_request(this) + + if resp.status_code >= 400: + span.error_occurred = True + + span.tag(Tag(key=tags.HttpStatus, val=resp.status_code)) + return resp + + def _sw_handle_user_exception(this: Flask, e): + if e 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 _handle_user_exception(this, e) + + Flask.full_dispatch_request = _sw_full_dispatch_request + Flask.handle_user_exception = _sw_handle_user_exception + + except Exception: + logger.warning('failed to install plugin %s', __name__) diff --git a/skywalking/trace/context/__init__.py b/skywalking/trace/context/__init__.py index 190121f1..bd458db3 100644 --- a/skywalking/trace/context/__init__.py +++ b/skywalking/trace/context/__init__.py @@ -91,6 +91,12 @@ def stop(self, span: Span) -> bool: return len(self.spans) == 0 + def active_span(self): + if self.spans: + return self.spans[len(self.spans) - 1] + + return None + class NoopContext(SpanContext): def __init__(self): @@ -114,6 +120,9 @@ def stop(self, span: Span) -> bool: self._depth -= 1 return self._depth == 0 + def active_span(self): + return self._noop_span + _thread_local = threading.local() _thread_local.context = None diff --git a/tests/plugin/sw_flask/__init__.py b/tests/plugin/sw_flask/__init__.py new file mode 100644 index 00000000..b1312a09 --- /dev/null +++ b/tests/plugin/sw_flask/__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_flask/docker-compose.yml b/tests/plugin/sw_flask/docker-compose.yml new file mode 100644 index 00000000..61f3be7d --- /dev/null +++ b/tests/plugin/sw_flask/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: + - ./services/provider.py:/app/provider.py + command: ['bash', '-c', 'pip install flask && 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_flask/expected.data.yml b/tests/plugin/sw_flask/expected.data.yml new file mode 100644 index 00000000..aa3d2dcb --- /dev/null +++ b/tests/plugin/sw_flask/expected.data.yml @@ -0,0 +1,91 @@ +# +# 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 + 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: 7000 + 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_flask/services/__init__.py b/tests/plugin/sw_flask/services/__init__.py new file mode 100644 index 00000000..b1312a09 --- /dev/null +++ b/tests/plugin/sw_flask/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_flask/services/consumer.py b/tests/plugin/sw_flask/services/consumer.py new file mode 100644 index 00000000..c942f91c --- /dev/null +++ b/tests/plugin/sw_flask/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_flask/services/provider.py b/tests/plugin/sw_flask/services/provider.py new file mode 100644 index 00000000..11f2c0be --- /dev/null +++ b/tests/plugin/sw_flask/services/provider.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 time + +from skywalking import agent, config + +if __name__ == '__main__': + config.service_name = 'provider' + config.logging_level = 'DEBUG' + agent.start() + + from flask import Flask, jsonify + + app = Flask(__name__) + + @app.route("/users", methods=["POST", "GET"]) + def application(): + time.sleep(0.5) + 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_flask/test_flask.py b/tests/plugin/sw_flask/test_flask.py new file mode 100644 index 00000000..dfeaaa6c --- /dev/null +++ b/tests/plugin/sw_flask/test_flask.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() diff --git a/tests/plugin/sw_requests/docker-compose.yml b/tests/plugin/sw_requests/docker-compose.yml index 4233f03c..b875de60 100644 --- a/tests/plugin/sw_requests/docker-compose.yml +++ b/tests/plugin/sw_requests/docker-compose.yml @@ -55,4 +55,4 @@ services: condition: service_healthy networks: - beyond: + beyond: \ No newline at end of file diff --git a/tests/plugin/sw_requests/expected.data.yml b/tests/plugin/sw_requests/expected.data.yml index 509127e9..e85480f4 100644 --- a/tests/plugin/sw_requests/expected.data.yml +++ b/tests/plugin/sw_requests/expected.data.yml @@ -80,5 +80,4 @@ segmentItems: componentId: 7000 spanType: Entry peer: not null - skipAnalysis: false - + skipAnalysis: false \ No newline at end of file