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 5f04fcd Add werkzeug support to sw_http_server plugin (#20) 5f04fcd is described below commit 5f04fcd3101522c1d88f9d27882fe7cb41b780c1 Author: Lei <leix....@qq.com> AuthorDate: Mon Jun 22 23:26:05 2020 +0800 Add werkzeug support to sw_http_server plugin (#20) --- Makefile | 6 +- setup.py | 1 + skywalking/plugins/sw_http_server/__init__.py | 79 +++++++++++++------- tests/plugin/docker/Dockerfile.agent | 2 +- .../Dockerfile.agent => sw_http_wsgi/__init__.py} | 11 +-- tests/plugin/sw_http_wsgi/docker-compose.yml | 78 ++++++++++++++++++++ tests/plugin/sw_http_wsgi/expected.data.yml | 84 ++++++++++++++++++++++ .../services/__init__.py} | 12 +--- tests/plugin/sw_http_wsgi/services/consumer.py | 48 +++++++++++++ .../services/provider.py} | 25 +++++-- .../test_http_wsgi.py} | 32 +++++++-- 11 files changed, 322 insertions(+), 56 deletions(-) diff --git a/Makefile b/Makefile index 3714710..92f13c8 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ setup: python3 -m pip install --upgrade pip python3 -m pip install grpcio --ignore-installed +setup-test: setup + pip3 install -e .[test] + gen: python3 -m grpc_tools.protoc --version || python3 -m pip install grpcio-tools python3 -m grpc_tools.protoc -I protocol --python_out=. --grpc_python_out=. protocol/**/*.proto @@ -38,8 +41,7 @@ lint: clean license: clean python3 tools/check-license-header.py skywalking tests tools -test: gen - pip3 install -e .[test] +test: gen setup-test python3 -m unittest -v install: gen diff --git a/setup.py b/setup.py index effb262..720c5b0 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setup( extras_require={ "test": [ "testcontainers", + "Werkzeug" ], }, classifiers=[ diff --git a/skywalking/plugins/sw_http_server/__init__.py b/skywalking/plugins/sw_http_server/__init__.py index d790975..04b58ca 100644 --- a/skywalking/plugins/sw_http_server/__init__.py +++ b/skywalking/plugins/sw_http_server/__init__.py @@ -34,32 +34,63 @@ def install(): _handle = BaseHTTPRequestHandler.handle - def _sw_handle(this: BaseHTTPRequestHandler): - http_methods = ('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH') - for method in http_methods: - _wrap_do_method(this, method) - _handle(this) - - def _wrap_do_method(this, method): - if hasattr(this, 'do_' + method) and inspect.ismethod(getattr(this, 'do_' + method)): - _do_method = getattr(this, 'do_' + method) - - def _sw_do_method(): - context = get_context() - carrier = Carrier() - for item in carrier: - item.val = this.headers[item.key.capitalize()] - with context.new_entry_span(op=this.path, carrier=carrier) as span: - span.layer = Layer.Http - span.component = Component.General - span.peer = '%s:%s' % this.client_address - span.tag(Tag(key=tags.HttpMethod, val=method)) - - _do_method() - - setattr(this, 'do_' + method, _sw_do_method) + def _sw_handle(handler: BaseHTTPRequestHandler): + clazz = handler.__class__ + if 'werkzeug.serving.WSGIRequestHandler' == ".".join([clazz.__module__, clazz.__name__]): + wrap_werkzeug_request_handler(handler) + else: + wrap_default_request_handler(handler) + _handle(handler) BaseHTTPRequestHandler.handle = _sw_handle + except Exception: logger.warning('failed to install plugin %s', __name__) traceback.print_exc() + + +def wrap_werkzeug_request_handler(handler): + """ + Wrap run_wsgi of werkzeug.serving.WSGIRequestHandler to add skywalking instrument code. + """ + _run_wsgi = handler.run_wsgi + + def _wrap_run_wsgi(): + context = get_context() + carrier = Carrier() + for item in carrier: + item.val = handler.headers[item.key.capitalize()] + with context.new_entry_span(op=handler.path, carrier=carrier) as span: + span.layer = Layer.Http + span.component = Component.General + span.peer = '%s:%s' % handler.client_address + span.tag(Tag(key=tags.HttpMethod, val=handler.command)) + return _run_wsgi() + + handler.run_wsgi = _wrap_run_wsgi + + +def wrap_default_request_handler(handler): + http_methods = ('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH') + for method in http_methods: + _wrap_do_method(handler, method) + + +def _wrap_do_method(handler, method): + if hasattr(handler, 'do_' + method) and inspect.ismethod(getattr(handler, 'do_' + method)): + _do_method = getattr(handler, 'do_' + method) + + def _sw_do_method(): + context = get_context() + carrier = Carrier() + for item in carrier: + item.val = handler.headers[item.key.capitalize()] + with context.new_entry_span(op=handler.path, carrier=carrier) as span: + span.layer = Layer.Http + span.component = Component.General + span.peer = '%s:%s' % handler.client_address + span.tag(Tag(key=tags.HttpMethod, val=method)) + + _do_method() + + setattr(handler, 'do_' + method, _sw_do_method) diff --git a/tests/plugin/docker/Dockerfile.agent b/tests/plugin/docker/Dockerfile.agent index 7672618..81ea394 100644 --- a/tests/plugin/docker/Dockerfile.agent +++ b/tests/plugin/docker/Dockerfile.agent @@ -21,4 +21,4 @@ WORKDIR /agent ADD $ROOT /agent -RUN make setup install +RUN make setup-test install diff --git a/tests/plugin/docker/Dockerfile.agent b/tests/plugin/sw_http_wsgi/__init__.py similarity index 90% copy from tests/plugin/docker/Dockerfile.agent copy to tests/plugin/sw_http_wsgi/__init__.py index 7672618..6222972 100644 --- a/tests/plugin/docker/Dockerfile.agent +++ b/tests/plugin/sw_http_wsgi/__init__.py @@ -1,3 +1,4 @@ +# # 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. @@ -12,13 +13,5 @@ # 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 python:3.7 - -ARG ROOT=. - -WORKDIR /agent - -ADD $ROOT /agent - -RUN make setup install diff --git a/tests/plugin/sw_http_wsgi/docker-compose.yml b/tests/plugin/sw_http_wsgi/docker-compose.yml new file mode 100644 index 0000000..3f0d305 --- /dev/null +++ b/tests/plugin/sw_http_wsgi/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: + build: + context: ../docker + dockerfile: Dockerfile.tool + ports: + - 19876:19876 + - 12800:12800 + networks: + - beyond + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/12800"] + interval: 5s + timeout: 60s + retries: 120 + + provider: + build: + context: ../../../ + dockerfile: tests/plugin/docker/Dockerfile.agent + networks: + - beyond + ports: + - 9091:9091 + volumes: + - ./services/provider.py:/app/provider.py + environment: + SW_AGENT_COLLECTOR_BACKEND_SERVICES: collector:19876 + command: ['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: + build: + context: ../../../ + dockerfile: tests/plugin/docker/Dockerfile.agent + networks: + - beyond + ports: + - 9090:9090 + volumes: + - ./services/consumer.py:/app/consumer.py + environment: + SW_AGENT_COLLECTOR_BACKEND_SERVICES: collector:19876 + command: ['python3', '/app/consumer.py'] + depends_on: + collector: + condition: service_healthy + provider: + condition: service_healthy + +networks: + beyond: diff --git a/tests/plugin/sw_http_wsgi/expected.data.yml b/tests/plugin/sw_http_wsgi/expected.data.yml new file mode 100644 index 0000000..509127e --- /dev/null +++ b/tests/plugin/sw_http_wsgi/expected.data.yml @@ -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. +# + +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 + 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: 7000 + 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: / + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + tags: + - key: http.method + value: POST + startTime: gt 0 + endTime: gt 0 + componentId: 7000 + spanType: Entry + peer: not null + skipAnalysis: false + diff --git a/tests/plugin/docker/Dockerfile.agent b/tests/plugin/sw_http_wsgi/services/__init__.py similarity index 89% copy from tests/plugin/docker/Dockerfile.agent copy to tests/plugin/sw_http_wsgi/services/__init__.py index 7672618..b1312a0 100644 --- a/tests/plugin/docker/Dockerfile.agent +++ b/tests/plugin/sw_http_wsgi/services/__init__.py @@ -1,3 +1,4 @@ +# # 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. @@ -12,13 +13,4 @@ # 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 python:3.7 - -ARG ROOT=. - -WORKDIR /agent - -ADD $ROOT /agent - -RUN make setup install +# diff --git a/tests/plugin/sw_http_wsgi/services/consumer.py b/tests/plugin/sw_http_wsgi/services/consumer.py new file mode 100644 index 0000000..43f60bd --- /dev/null +++ b/tests/plugin/sw_http_wsgi/services/consumer.py @@ -0,0 +1,48 @@ +# +# 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 urllib import request + +from skywalking import agent, config + +if __name__ == '__main__': + config.service_name = 'consumer' + config.logging_level = 'DEBUG' + agent.start() + + import socketserver + from http.server import BaseHTTPRequestHandler + + class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_POST(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.end_headers() + + data = '{"name": "whatever"}'.encode('utf8') + req = request.Request('http://provider:9091/users') + req.add_header('Content-Type', 'application/json; charset=utf-8') + req.add_header('Content-Length', str(len(data))) + with request.urlopen(req, data): + self.wfile.write(data) + + PORT = 9090 + Handler = SimpleHTTPRequestHandler + + with socketserver.TCPServer(("", PORT), Handler) as httpd: + print("serving at port", PORT) + httpd.serve_forever() diff --git a/tests/plugin/docker/Dockerfile.agent b/tests/plugin/sw_http_wsgi/services/provider.py similarity index 62% copy from tests/plugin/docker/Dockerfile.agent copy to tests/plugin/sw_http_wsgi/services/provider.py index 7672618..e15f33b 100644 --- a/tests/plugin/docker/Dockerfile.agent +++ b/tests/plugin/sw_http_wsgi/services/provider.py @@ -1,3 +1,4 @@ +# # 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. @@ -12,13 +13,27 @@ # 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 werkzeug import Request, Response -FROM python:3.7 -ARG ROOT=. + @Request.application + def application(request): + time.sleep(0.5) + return Response('{"song": "Despacito", "artist": "Luis Fonsi"}') -WORKDIR /agent -ADD $ROOT /agent + from werkzeug.serving import run_simple -RUN make setup install + PORT = 9091 + run_simple("", PORT, application) diff --git a/tests/plugin/docker/Dockerfile.agent b/tests/plugin/sw_http_wsgi/test_http_wsgi.py similarity index 52% copy from tests/plugin/docker/Dockerfile.agent copy to tests/plugin/sw_http_wsgi/test_http_wsgi.py index 7672618..eacaa98 100644 --- a/tests/plugin/docker/Dockerfile.agent +++ b/tests/plugin/sw_http_wsgi/test_http_wsgi.py @@ -1,3 +1,4 @@ +# # 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. @@ -12,13 +13,34 @@ # 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 + +import requests +from testcontainers.compose import DockerCompose + +from tests.plugin import BasePluginTest + + +class TestRequestPlugin(BasePluginTest): + @classmethod + def setUpClass(cls): + cls.compose = DockerCompose(filepath=dirname(abspath(__file__))) + cls.compose.start() + + cls.compose.wait_for(cls.url(cls.collector_address())) -FROM python:3.7 + def test_request_plugin(self): + print('traffic: ', requests.post(url=self.url(('consumer', '9090')))) -ARG ROOT=. + time.sleep(3) -WORKDIR /agent + self.validate(expected_file_name=os.path.join(dirname(abspath(__file__)), 'expected.data.yml')) -ADD $ROOT /agent -RUN make setup install +if __name__ == '__main__': + unittest.main()