未验证 提交 36a4c53e 编写于 作者: C clay 提交者: GitHub

feature: add Tornado Plugin (#48)

上级 d3c13bf8
......@@ -78,6 +78,7 @@ Library | Plugin Name
| [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` |
| [tornado](https://www.tornadoweb.org/en/stable/) | `sw_tornado` |
## API
......
......@@ -47,6 +47,7 @@ setup(
"Werkzeug",
"pymysql",
"redis",
"tornado",
],
},
classifiers=[
......
......@@ -28,6 +28,7 @@ class Component(Enum):
Requests = 7002
PyMysql = 7003
Django = 7004
Tornado = 7005
Redis = 7
......
#
# 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 inspect import iscoroutinefunction, isawaitable
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():
try:
from tornado.web import RequestHandler
old_execute = RequestHandler._execute
old_log_exception = RequestHandler.log_exception
RequestHandler._execute = _gen_sw_get_response_func(old_execute)
def _sw_handler_uncaught_exception(self: RequestHandler, ty, value, tb, *args, **kwargs):
if value is not None:
entry_span = get_context().active_span()
if entry_span is not None:
entry_span.raised()
return old_log_exception(self, ty, value, tb, *args, **kwargs)
RequestHandler.log_exception = _sw_handler_uncaught_exception
except Exception:
logger.warning('failed to install plugin %s', __name__)
def _gen_sw_get_response_func(old_execute):
from tornado.gen import coroutine
awaitable = iscoroutinefunction(old_execute)
if awaitable:
# Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
# In that case our method should be a coroutine function too
async def _sw_get_response(self, *args, **kwargs):
request = self.request
context = get_context()
carrier = Carrier()
for item in carrier:
if item.key.capitalize() in request.headers:
item.val = request.headers[item.key.capitalize()]
with context.new_entry_span(op=request.path, carrier=carrier) as span:
span.layer = Layer.Http
span.component = Component.Tornado
peer = request.connection.stream.socket.getpeername()
span.peer = '{0}:{1}'.format(*peer)
span.tag(Tag(key=tags.HttpMethod, val=request.method))
span.tag(
Tag(key=tags.HttpUrl, val='{}://{}{}'.format(request.protocol, request.host, request.path)))
result = old_execute(self, *args, **kwargs)
span.tag(Tag(key=tags.HttpStatus, val=self._status_code))
if isawaitable(result):
result = await result
if self._status_code >= 400:
span.error_occurred = True
return result
else:
@coroutine
def _sw_get_response(self, *args, **kwargs):
request = self.request
context = get_context()
carrier = Carrier()
for item in carrier:
if item.key.capitalize() in request.headers:
item.val = request.headers[item.key.capitalize()]
with context.new_entry_span(op=request.path, carrier=carrier) as span:
span.layer = Layer.Http
span.component = Component.Tornado
peer = request.connection.stream.socket.getpeername()
span.peer = '{0}:{1}'.format(*peer)
span.tag(Tag(key=tags.HttpMethod, val=request.method))
span.tag(
Tag(key=tags.HttpUrl, val='{}://{}{}'.format(request.protocol, request.host, request.path)))
result = yield from old_execute(self, *args, **kwargs)
span.tag(Tag(key=tags.HttpStatus, val=self._status_code))
if self._status_code >= 400:
span.error_occurred = True
return result
return _sw_get_response
#
# 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.
#
#
# 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:/provider.py
command: ['bash', '-c', 'pip3 install tornado && python3 /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:/consumer.py
command: ['bash', '-c', 'pip3 install tornado && python3 /consumer.py']
depends_on:
collector:
condition: service_healthy
provider:
condition: service_healthy
networks:
beyond:
#
# 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: GET
- 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: 7005
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: GET
- 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: 7005
spanType: Entry
peer: not null
skipAnalysis: false
\ No newline at end of file
#
# 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.
#
#
# 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 skywalking import agent, config
if __name__ == "__main__":
config.service_name = 'consumer'
config.logging_level = 'DEBUG'
agent.start()
import requests
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
res = requests.get("http://provider:9091/users")
self.write(res.text)
def make_app():
return tornado.web.Application([
(r"/users", MainHandler),
])
app = make_app()
app.listen(9090, '0.0.0.0')
tornado.ioloop.IOLoop.current().start()
#
# 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 skywalking import agent, config
if __name__ == "__main__":
config.service_name = "provider"
config.logging_level = "DEBUG"
agent.start()
import json
import time
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
time.sleep(0.5)
self.write(json.dumps({'song': 'Despacito', 'artist': 'Luis Fonsi'}))
def make_app():
return tornado.web.Application([
(r"/users", MainHandler),
])
app = make_app()
app.listen(9091, '0.0.0.0')
tornado.ioloop.IOLoop.current().start()
#
# 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 inspect
import time
import unittest
from os.path import dirname
from testcontainers.compose import DockerCompose
from tests.plugin import BasePluginTest
class TestPlugin(BasePluginTest):
@classmethod
def setUpClass(cls):
cls.compose = DockerCompose(filepath=dirname(inspect.getfile(cls)))
cls.compose.start()
cls.compose.wait_for(cls.url(('consumer', '9090'), 'users'))
def test_plugin(self):
time.sleep(3)
self.validate()
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册