Skip to content

Commit 4519fed

Browse files
authored
New Tornado instrumentation (#146)
* Tornado Server instrumentation, tests and support * Don't log HTTPError as a general exception * Tornado server tests * Tornado client instrumentation and tests * Updated 3xx handling * Init AsyncHTTPClient after loop * Limit tornado version to <6.0.0 * pylint improvements * Better tracing of Tornado internal 301 handling
1 parent b575cf2 commit 4519fed

File tree

12 files changed

+1361
-27
lines changed

12 files changed

+1361
-27
lines changed

instana/__init__.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
from __future__ import absolute_import
2-
3-
import os
4-
import sys
5-
import pkg_resources
6-
from threading import Timer
7-
8-
91
"""
102
The Instana package has two core components: the agent and the tracer.
113
@@ -25,6 +17,14 @@
2517
Recorder
2618
"""
2719

20+
from __future__ import absolute_import
21+
22+
import os
23+
import sys
24+
from threading import Timer
25+
import pkg_resources
26+
27+
2828
if "INSTANA_MAGIC" in os.environ:
2929
pkg_resources.working_set.add_entry("/tmp/instana/python")
3030

@@ -41,7 +41,7 @@
4141
__version__ = 'unknown'
4242

4343

44-
def load(module):
44+
def load(_):
4545
"""
4646
Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP
4747
environment variable.
@@ -53,26 +53,33 @@ def load(module):
5353

5454

5555
# User configurable EUM API key for instana.helpers.eum_snippet()
56+
# pylint: disable=invalid-name
5657
eum_api_key = ''
5758

5859

5960
def boot_agent():
60-
import instana.singletons # noqa
61+
"""Initialize the Instana agent and conditionally load auto-instrumentation."""
62+
# Disable all the unused-import violations in this function
63+
# pylint: disable=unused-import
64+
65+
import instana.singletons
6166

6267
if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ:
6368
# Import & initialize instrumentation
6469
if sys.version_info >= (3, 5, 3):
65-
from .instrumentation import asyncio # noqa
66-
from .instrumentation.aiohttp import client # noqa
67-
from .instrumentation.aiohttp import server # noqa
68-
from .instrumentation import asynqp # noqa
69-
from .instrumentation import logging # noqa
70-
from .instrumentation import mysqlpython # noqa
71-
from .instrumentation import redis # noqa
72-
from .instrumentation import sqlalchemy # noqa
73-
from .instrumentation import sudsjurko # noqa
74-
from .instrumentation import urllib3 # noqa
75-
from .instrumentation.django import middleware # noqa
70+
from .instrumentation import asyncio
71+
from .instrumentation.aiohttp import client
72+
from .instrumentation.aiohttp import server
73+
from .instrumentation import asynqp
74+
from .instrumentation.tornado import client
75+
from .instrumentation.tornado import server
76+
from .instrumentation import logging
77+
from .instrumentation import mysqlpython
78+
from .instrumentation import redis
79+
from .instrumentation import sqlalchemy
80+
from .instrumentation import sudsjurko
81+
from .instrumentation import urllib3
82+
from .instrumentation.django import middleware
7683

7784

7885
if "INSTANA_MAGIC" in os.environ:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from __future__ import absolute_import
2+
3+
import opentracing
4+
import wrapt
5+
import functools
6+
import basictracer
7+
import sys
8+
9+
from ...log import logger
10+
from ...singletons import agent, tornado_tracer
11+
from ...util import strip_secrets
12+
13+
from distutils.version import LooseVersion
14+
15+
16+
# Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing
17+
# scope managers which we will tackle soon.
18+
# Limit Tornado version for the time being.
19+
if (('tornado' in sys.modules) and
20+
hasattr(sys.modules['tornado'], 'version') and
21+
(LooseVersion(sys.modules['tornado'].version) < LooseVersion('6.0.0'))):
22+
try:
23+
import tornado
24+
25+
@wrapt.patch_function_wrapper('tornado.httpclient', 'AsyncHTTPClient.fetch')
26+
def fetch_with_instana(wrapped, instance, argv, kwargs):
27+
try:
28+
parent_span = tornado_tracer.active_span
29+
30+
# If we're not tracing, just return
31+
if (parent_span is None) or (parent_span.operation_name == "tornado-client"):
32+
return wrapped(*argv, **kwargs)
33+
34+
request = argv[0]
35+
36+
# To modify request headers, we have to preemptively create an HTTPRequest object if a
37+
# URL string was passed.
38+
if not isinstance(request, tornado.httpclient.HTTPRequest):
39+
request = tornado.httpclient.HTTPRequest(url=request, **kwargs)
40+
41+
new_kwargs = {}
42+
for param in ('callback', 'raise_error'):
43+
# if not in instead and pop
44+
if param in kwargs:
45+
new_kwargs[param] = kwargs.pop(param)
46+
kwargs = new_kwargs
47+
48+
scope = tornado_tracer.start_active_span('tornado-client', child_of=parent_span)
49+
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, request.headers)
50+
51+
# Query param scrubbing
52+
parts = request.url.split('?')
53+
if len(parts) > 1:
54+
cleaned_qp = strip_secrets(parts[1], agent.secrets_matcher, agent.secrets_list)
55+
scope.span.set_tag("http.params", cleaned_qp)
56+
57+
scope.span.set_tag("http.url", parts[0])
58+
scope.span.set_tag("http.method", request.method)
59+
60+
future = wrapped(request, **kwargs)
61+
62+
if future is not None:
63+
cb = functools.partial(finish_tracing, scope=scope)
64+
future.add_done_callback(cb)
65+
66+
return future
67+
except Exception:
68+
logger.debug("tornado fetch", exc_info=True)
69+
raise
70+
71+
def finish_tracing(future, scope):
72+
try:
73+
response = future.result()
74+
scope.span.set_tag("http.status_code", response.code)
75+
except tornado.httpclient.HTTPClientError as e:
76+
scope.span.set_tag("http.status_code", e.code)
77+
scope.span.log_exception(e)
78+
raise
79+
finally:
80+
scope.close()
81+
82+
83+
logger.debug("Instrumenting tornado client")
84+
except ImportError:
85+
pass
86+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import absolute_import
2+
3+
import opentracing
4+
from opentracing.scope_managers.tornado import tracer_stack_context
5+
import wrapt
6+
import sys
7+
8+
from ...log import logger
9+
from ...singletons import agent, tornado_tracer
10+
from ...util import strip_secrets
11+
12+
from distutils.version import LooseVersion
13+
14+
# Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing
15+
# scope managers which we will tackle soon.
16+
# Limit Tornado version for the time being.
17+
if (('tornado' in sys.modules) and
18+
hasattr(sys.modules['tornado'], 'version') and
19+
(LooseVersion(sys.modules['tornado'].version) < LooseVersion('6.0.0'))):
20+
21+
try:
22+
import tornado
23+
24+
@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler._execute')
25+
def execute_with_instana(wrapped, instance, argv, kwargs):
26+
try:
27+
ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, instance.request.headers)
28+
scope = tornado_tracer.start_active_span('tornado-server', child_of=ctx)
29+
30+
# Query param scrubbing
31+
if instance.request.query is not None and len(instance.request.query) > 0:
32+
cleaned_qp = strip_secrets(instance.request.query, agent.secrets_matcher, agent.secrets_list)
33+
scope.span.set_tag("http.params", cleaned_qp)
34+
35+
scope.span.set_tag("http.host", instance.request.host)
36+
scope.span.set_tag("http.method", instance.request.method)
37+
scope.span.set_tag("http.path", instance.request.path)
38+
39+
# Custom header tracking support
40+
if agent.extra_headers is not None:
41+
for custom_header in agent.extra_headers:
42+
if custom_header in instance.request.headers:
43+
scope.span.set_tag("http.%s" % custom_header, instance.request.headers[custom_header])
44+
45+
with tracer_stack_context():
46+
setattr(instance.request, "_instana", scope)
47+
48+
# Set the context response headers now because tornado doesn't give us a better option to do so
49+
# later for this request.
50+
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers)
51+
instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id)
52+
53+
return wrapped(*argv, **kwargs)
54+
except Exception:
55+
logger.debug("tornado execute", exc_info=True)
56+
57+
58+
@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.set_default_headers')
59+
def set_default_headers_with_instana(wrapped, instance, argv, kwargs):
60+
if not hasattr(instance.request, '_instana'):
61+
return wrapped(*argv, **kwargs)
62+
63+
scope = instance.request._instana
64+
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers)
65+
instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id)
66+
67+
68+
@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.on_finish')
69+
def on_finish_with_instana(wrapped, instance, argv, kwargs):
70+
try:
71+
if not hasattr(instance.request, '_instana'):
72+
return wrapped(*argv, **kwargs)
73+
74+
scope = instance.request._instana
75+
status_code = instance.get_status()
76+
77+
# Mark 500 responses as errored
78+
if 500 <= status_code <= 511:
79+
scope.span.set_tag("error", True)
80+
ec = scope.span.tags.get('ec', 0)
81+
if ec is 0:
82+
scope.span.set_tag("ec", ec + 1)
83+
84+
scope.span.set_tag("http.status_code", status_code)
85+
scope.span.finish()
86+
scope.close()
87+
88+
return wrapped(*argv, **kwargs)
89+
except Exception:
90+
logger.debug("tornado on_finish", exc_info=True)
91+
92+
@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.log_exception')
93+
def log_exception_with_instana(wrapped, instance, argv, kwargs):
94+
try:
95+
if not hasattr(instance.request, '_instana'):
96+
return wrapped(*argv, **kwargs)
97+
98+
if not isinstance(argv[1], tornado.web.HTTPError):
99+
scope = instance.request._instana
100+
scope.span.log_exception(argv[0])
101+
102+
return wrapped(*argv, **kwargs)
103+
except Exception:
104+
logger.debug("tornado log_exception", exc_info=True)
105+
106+
logger.debug("Instrumenting tornado server")
107+
except ImportError:
108+
pass
109+

instana/json_span.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class HttpData(BaseSpan):
5151
params = None
5252
status = 0
5353
method = None
54+
path = None
5455
path_tpl = None
5556
error = None
5657

instana/recorder.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import absolute_import
22

33
import os
4-
import socket
54
import sys
65
import threading as t
76

@@ -25,12 +24,13 @@
2524
class InstanaRecorder(SpanRecorder):
2625
registered_spans = ("aiohttp-client", "aiohttp-server", "django", "log", "memcache", "mysql",
2726
"rabbitmq", "redis", "rpc-client", "rpc-server", "sqlalchemy", "soap",
28-
"urllib3", "wsgi")
29-
http_spans = ("aiohttp-client", "aiohttp-server", "django", "wsgi", "urllib3", "soap")
27+
"tornado-server", "tornado-client", "urllib3", "wsgi")
28+
http_spans = ("aiohttp-client", "aiohttp-server", "django", "http", "soap", "tornado-server",
29+
"tornado-client", "urllib3", "wsgi")
3030

3131
exit_spans = ("aiohttp-client", "log", "memcache", "mysql", "rabbitmq", "redis", "rpc-client",
32-
"sqlalchemy", "soap", "urllib3")
33-
entry_spans = ("aiohttp-server", "django", "wsgi", "rabbitmq", "rpc-server")
32+
"sqlalchemy", "soap", "tornado-client", "urllib3")
33+
entry_spans = ("aiohttp-server", "django", "wsgi", "rabbitmq", "rpc-server", "tornado-server")
3434

3535
entry_kind = ["entry", "server", "consumer"]
3636
exit_kind = ["exit", "client", "producer"]
@@ -118,6 +118,7 @@ def build_registered_span(self, span):
118118
if span.operation_name in self.http_spans:
119119
data.http = HttpData(host=span.tags.pop("http.host", None),
120120
url=span.tags.pop(ext.HTTP_URL, None),
121+
path=span.tags.pop("http.path", None),
121122
params=span.tags.pop('http.params', None),
122123
method=span.tags.pop(ext.HTTP_METHOD, None),
123124
status=span.tags.pop(ext.HTTP_STATUS_CODE, None),

instana/singletons.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
if sys.version_info >= (3,4):
2222
from opentracing.scope_managers.asyncio import AsyncioScopeManager
23+
from opentracing.scope_managers.tornado import TornadoScopeManager
24+
2325
async_tracer = InstanaTracer(scope_manager=AsyncioScopeManager())
26+
tornado_tracer = InstanaTracer(scope_manager=TornadoScopeManager())
2427

2528
# Set ourselves as the tracer.
2629
opentracing.tracer = tracer

runtests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
command_line = [__file__, '--verbose']
66

77
if (LooseVersion(sys.version) < LooseVersion('3.5.3')):
8-
command_line.extend(['-e', 'asynqp', '-e', 'aiohttp', '-e', 'async'])
8+
command_line.extend(['-e', 'asynqp', '-e', 'aiohttp', '-e', 'async', '-e', 'tornado'])
99

1010
if (LooseVersion(sys.version) >= LooseVersion('3.7.0')):
1111
command_line.extend(['-e', 'sudsjurko'])

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def check_setuptools():
6969
'sqlalchemy>=1.1.15',
7070
'spyne>=2.9,<=2.12.14',
7171
'suds-jurko>=0.6',
72+
'tornado>=4.5.3,<6.0',
7273
'urllib3[secure]>=1.15'
7374
],
7475
},

tests/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,17 @@
4545
print("Starting background aiohttp server...")
4646
aio_server.start()
4747

48+
49+
if sys.version_info >= (3, 5, 3):
50+
# Background Tornado application
51+
from .apps.tornado import run_server
52+
53+
# Spawn our background Tornado app that the tests will throw
54+
# requests at.
55+
tornado_server = threading.Thread(target=run_server)
56+
tornado_server.daemon = True
57+
tornado_server.name = "Background Tornado server"
58+
print("Starting background Tornado server...")
59+
tornado_server.start()
60+
4861
time.sleep(1)

0 commit comments

Comments
 (0)