Files
2020-04-08 10:39:44 -07:00

492 lines
20 KiB
Python

from .web.app import CustomDefaultHandler
from .utils import TornadoTestCase
from ddtrace import config
from ddtrace.constants import SAMPLING_PRIORITY_KEY, ORIGIN_KEY, ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.ext import http
import pytest
import tornado
from tests.opentracer.utils import init_tracer
from ...utils import assert_span_http_status_code
class TestTornadoWeb(TornadoTestCase):
"""
Ensure that Tornado web handlers are properly traced.
"""
def test_success_handler(self, query_string=''):
# it should trace a handler that returns 200
if query_string:
fqs = '?' + query_string
else:
fqs = ''
response = self.fetch('/success/' + fqs)
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.SuccessHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 200)
assert self.get_url('/success/') == request_span.get_tag(http.URL)
if config.tornado.trace_query_string:
assert query_string == request_span.get_tag(http.QUERY_STRING)
else:
assert http.QUERY_STRING not in request_span.meta
assert 0 == request_span.error
def test_success_handler_query_string(self):
self.test_success_handler('foo=bar')
def test_success_handler_query_string_trace(self):
with self.override_http_config('tornado', dict(trace_query_string=True)):
self.test_success_handler('foo=bar')
def test_nested_handler(self):
# it should trace a handler that calls the tracer.trace() method
# using the automatic Context retrieval
response = self.fetch('/nested/')
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 2 == len(traces[0])
# check request span
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.NestedHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 200)
assert self.get_url('/nested/') == request_span.get_tag(http.URL)
assert 0 == request_span.error
# check nested span
nested_span = traces[0][1]
assert 'tornado-web' == nested_span.service
assert 'tornado.sleep' == nested_span.name
assert 0 == nested_span.error
# check durations because of the yield sleep
assert request_span.duration >= 0.05
assert nested_span.duration >= 0.05
def test_exception_handler(self):
# it should trace a handler that raises an exception
response = self.fetch('/exception/')
assert 500 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.ExceptionHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 500)
assert self.get_url('/exception/') == request_span.get_tag(http.URL)
assert 1 == request_span.error
assert 'Ouch!' == request_span.get_tag('error.msg')
assert 'Exception: Ouch!' in request_span.get_tag('error.stack')
def test_http_exception_handler(self):
# it should trace a handler that raises a Tornado HTTPError
response = self.fetch('/http_exception/')
assert 501 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.HTTPExceptionHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 501)
assert self.get_url('/http_exception/') == request_span.get_tag(http.URL)
assert 1 == request_span.error
assert 'HTTP 501: Not Implemented (unavailable)' == request_span.get_tag('error.msg')
assert 'HTTP 501: Not Implemented (unavailable)' in request_span.get_tag('error.stack')
def test_http_exception_500_handler(self):
# it should trace a handler that raises a Tornado HTTPError
response = self.fetch('/http_exception_500/')
assert 500 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.HTTPException500Handler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 500)
assert self.get_url('/http_exception_500/') == request_span.get_tag(http.URL)
assert 1 == request_span.error
assert 'HTTP 500: Server Error (server error)' == request_span.get_tag('error.msg')
assert 'HTTP 500: Server Error (server error)' in request_span.get_tag('error.stack')
def test_sync_success_handler(self):
# it should trace a synchronous handler that returns 200
response = self.fetch('/sync_success/')
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.SyncSuccessHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 200)
assert self.get_url('/sync_success/') == request_span.get_tag(http.URL)
assert 0 == request_span.error
def test_sync_exception_handler(self):
# it should trace a handler that raises an exception
response = self.fetch('/sync_exception/')
assert 500 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.SyncExceptionHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 500)
assert self.get_url('/sync_exception/') == request_span.get_tag(http.URL)
assert 1 == request_span.error
assert 'Ouch!' == request_span.get_tag('error.msg')
assert 'Exception: Ouch!' in request_span.get_tag('error.stack')
def test_404_handler(self):
# it should trace 404
response = self.fetch('/does_not_exist/')
assert 404 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tornado.web.ErrorHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 404)
assert self.get_url('/does_not_exist/') == request_span.get_tag(http.URL)
assert 0 == request_span.error
def test_redirect_handler(self):
# it should trace the built-in RedirectHandler
response = self.fetch('/redirect/')
assert 200 == response.code
# we trace two different calls: the RedirectHandler and the SuccessHandler
traces = self.tracer.writer.pop_traces()
assert 2 == len(traces)
assert 1 == len(traces[0])
assert 1 == len(traces[1])
redirect_span = traces[0][0]
assert 'tornado-web' == redirect_span.service
assert 'tornado.request' == redirect_span.name
assert 'web' == redirect_span.span_type
assert 'tornado.web.RedirectHandler' == redirect_span.resource
assert 'GET' == redirect_span.get_tag('http.method')
assert_span_http_status_code(redirect_span, 301)
assert self.get_url('/redirect/') == redirect_span.get_tag(http.URL)
assert 0 == redirect_span.error
success_span = traces[1][0]
assert 'tornado-web' == success_span.service
assert 'tornado.request' == success_span.name
assert 'web' == success_span.span_type
assert 'tests.contrib.tornado.web.app.SuccessHandler' == success_span.resource
assert 'GET' == success_span.get_tag('http.method')
assert_span_http_status_code(success_span, 200)
assert self.get_url('/success/') == success_span.get_tag(http.URL)
assert 0 == success_span.error
def test_static_handler(self):
# it should trace the access to static files
response = self.fetch('/statics/empty.txt')
assert 200 == response.code
assert 'Static file\n' == response.body.decode('utf-8')
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tornado.web.StaticFileHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 200)
assert self.get_url('/statics/empty.txt') == request_span.get_tag(http.URL)
assert 0 == request_span.error
def test_propagation(self):
# it should trace a handler that returns 200 with a propagated context
headers = {
'x-datadog-trace-id': '1234',
'x-datadog-parent-id': '4567',
'x-datadog-sampling-priority': '2'
}
response = self.fetch('/success/', headers=headers)
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
# simple sanity check on the span
assert 'tornado.request' == request_span.name
assert_span_http_status_code(request_span, 200)
assert self.get_url('/success/') == request_span.get_tag(http.URL)
assert 0 == request_span.error
# check propagation
assert 1234 == request_span.trace_id
assert 4567 == request_span.parent_id
assert 2 == request_span.get_metric(SAMPLING_PRIORITY_KEY)
# Opentracing support depends on new AsyncioScopeManager
# See: https://github.com/opentracing/opentracing-python/pull/118
@pytest.mark.skipif(tornado.version_info >= (5, 0),
reason='Opentracing ScopeManager not available for Tornado >= 5')
def test_success_handler_ot(self):
"""OpenTracing version of test_success_handler."""
from opentracing.scope_managers.tornado import TornadoScopeManager
ot_tracer = init_tracer('tornado_svc', self.tracer, scope_manager=TornadoScopeManager())
with ot_tracer.start_active_span('tornado_op'):
response = self.fetch('/success/')
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 2 == len(traces[0])
# dd_span will start and stop before the ot_span finishes
ot_span, dd_span = traces[0]
# confirm the parenting
assert ot_span.parent_id is None
assert dd_span.parent_id == ot_span.span_id
assert ot_span.name == 'tornado_op'
assert ot_span.service == 'tornado_svc'
assert 'tornado-web' == dd_span.service
assert 'tornado.request' == dd_span.name
assert 'web' == dd_span.span_type
assert 'tests.contrib.tornado.web.app.SuccessHandler' == dd_span.resource
assert 'GET' == dd_span.get_tag('http.method')
assert_span_http_status_code(dd_span, 200)
assert self.get_url('/success/') == dd_span.get_tag(http.URL)
assert 0 == dd_span.error
class TestTornadoWebAnalyticsDefault(TornadoTestCase):
"""
Ensure that Tornado web handlers generate APM events with default settings
"""
def test_analytics_global_on_integration_default(self):
"""
When making a request
When an integration trace search is not event sample rate is not set and globally trace search is enabled
We expect the root span to have the appropriate tag
"""
with self.override_global_config(dict(analytics_enabled=True)):
# it should trace a handler that returns 200
response = self.fetch('/success/')
self.assertEqual(200, response.code)
self.assert_structure(
dict(name='tornado.request', metrics={ANALYTICS_SAMPLE_RATE_KEY: 1.0}),
)
def test_analytics_global_off_integration_default(self):
"""
When making a request
When an integration trace search is not set and sample rate is set and globally trace search is disabled
We expect the root span to not include tag
"""
with self.override_global_config(dict(analytics_enabled=False)):
# it should trace a handler that returns 200
response = self.fetch('/success/')
self.assertEqual(200, response.code)
root = self.get_root_span()
self.assertIsNone(root.get_metric(ANALYTICS_SAMPLE_RATE_KEY))
class TestTornadoWebAnalyticsOn(TornadoTestCase):
"""
Ensure that Tornado web handlers generate APM events with default settings
"""
def get_settings(self):
# distributed_tracing needs to be disabled manually
return {
'datadog_trace': {
'analytics_enabled': True,
'analytics_sample_rate': 0.5,
},
}
def test_analytics_global_on_integration_on(self):
"""
When making a request
When an integration trace search is enabled and sample rate is set and globally trace search is enabled
We expect the root span to have the appropriate tag
"""
with self.override_global_config(dict(analytics_enabled=True)):
# it should trace a handler that returns 200
response = self.fetch('/success/')
self.assertEqual(200, response.code)
self.assert_structure(
dict(name='tornado.request', metrics={ANALYTICS_SAMPLE_RATE_KEY: 0.5}),
)
def test_analytics_global_off_integration_on(self):
"""
When making a request
When an integration trace search is enabled and sample rate is set and globally trace search is disabled
We expect the root span to have the appropriate tag
"""
with self.override_global_config(dict(analytics_enabled=False)):
# it should trace a handler that returns 200
response = self.fetch('/success/')
self.assertEqual(200, response.code)
self.assert_structure(
dict(name='tornado.request', metrics={ANALYTICS_SAMPLE_RATE_KEY: 0.5}),
)
class TestTornadoWebAnalyticsNoRate(TornadoTestCase):
"""
Ensure that Tornado web handlers generate APM events with default settings
"""
def get_settings(self):
# distributed_tracing needs to be disabled manually
return {
'datadog_trace': {
'analytics_enabled': True,
},
}
def test_analytics_global_on_integration_on(self):
"""
When making a request
When an integration trace search is enabled and sample rate is set and globally trace search is enabled
We expect the root span to have the appropriate tag
"""
with self.override_global_config(dict(analytics_enabled=True)):
# it should trace a handler that returns 200
response = self.fetch('/success/')
self.assertEqual(200, response.code)
self.assert_structure(
dict(name='tornado.request', metrics={ANALYTICS_SAMPLE_RATE_KEY: 1.0}),
)
class TestNoPropagationTornadoWeb(TornadoTestCase):
"""
Ensure that Tornado web handlers are properly traced and are ignoring propagated HTTP headers when disabled.
"""
def get_settings(self):
# distributed_tracing needs to be disabled manually
return {
'datadog_trace': {
'distributed_tracing': False,
},
}
def test_no_propagation(self):
# it should not propagate the HTTP context
headers = {
'x-datadog-trace-id': '1234',
'x-datadog-parent-id': '4567',
'x-datadog-sampling-priority': '2',
'x-datadog-origin': 'synthetics',
}
response = self.fetch('/success/', headers=headers)
assert 200 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
# simple sanity check on the span
assert 'tornado.request' == request_span.name
assert_span_http_status_code(request_span, 200)
assert self.get_url('/success/') == request_span.get_tag(http.URL)
assert 0 == request_span.error
# check non-propagation
assert request_span.trace_id != 1234
assert request_span.parent_id != 4567
assert request_span.get_metric(SAMPLING_PRIORITY_KEY) != 2
assert request_span.get_tag(ORIGIN_KEY) != 'synthetics'
class TestCustomTornadoWeb(TornadoTestCase):
"""
Ensure that Tornado web handlers are properly traced when using
a custom default handler.
"""
def get_settings(self):
return {
'default_handler_class': CustomDefaultHandler,
'default_handler_args': dict(status_code=400),
}
def test_custom_default_handler(self):
# it should trace any call that uses a custom default handler
response = self.fetch('/custom_handler/')
assert 400 == response.code
traces = self.tracer.writer.pop_traces()
assert 1 == len(traces)
assert 1 == len(traces[0])
request_span = traces[0][0]
assert 'tornado-web' == request_span.service
assert 'tornado.request' == request_span.name
assert 'web' == request_span.span_type
assert 'tests.contrib.tornado.web.app.CustomDefaultHandler' == request_span.resource
assert 'GET' == request_span.get_tag('http.method')
assert_span_http_status_code(request_span, 400)
assert self.get_url('/custom_handler/') == request_span.get_tag(http.URL)
assert 0 == request_span.error