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

668 lines
22 KiB
Python

"""
tests for Tracer and utilities.
"""
import contextlib
import multiprocessing
from os import getpid
import sys
import warnings
from unittest.case import SkipTest
import mock
import pytest
import ddtrace
from ddtrace.ext import system
from ddtrace.context import Context
from .base import BaseTracerTestCase
from .util import override_global_tracer
from .utils.tracer import DummyTracer
from .utils.tracer import DummyWriter # noqa
def get_dummy_tracer():
return DummyTracer()
class TracerTestCase(BaseTracerTestCase):
def test_tracer_vars(self):
span = self.trace('a', service='s', resource='r', span_type='t')
span.assert_matches(name='a', service='s', resource='r', span_type='t')
# DEV: Finish to ensure we don't leak `service` between spans
span.finish()
span = self.trace('a')
span.assert_matches(name='a', service=None, resource='a', span_type=None)
def test_tracer(self):
def _mix():
with self.trace('cake.mix'):
pass
def _bake():
with self.trace('cake.bake'):
pass
def _make_cake():
with self.trace('cake.make') as span:
span.service = 'baker'
span.resource = 'cake'
_mix()
_bake()
# let's run it and make sure all is well.
self.assert_has_no_spans()
_make_cake()
# Capture root's trace id to assert later
root_trace_id = self.get_root_span().trace_id
# Assert structure of this trace
self.assert_structure(
# Root span with 2 children
dict(name='cake.make', resource='cake', service='baker', parent_id=None),
(
# Span with no children
dict(name='cake.mix', resource='cake.mix', service='baker'),
# Span with no children
dict(name='cake.bake', resource='cake.bake', service='baker'),
),
)
# do it again and make sure it has new trace ids
self.reset()
_make_cake()
self.assert_span_count(3)
for s in self.spans:
assert s.trace_id != root_trace_id
def test_tracer_wrap(self):
@self.tracer.wrap('decorated_function', service='s', resource='r', span_type='t')
def f(tag_name, tag_value):
# make sure we can still set tags
span = self.tracer.current_span()
span.set_tag(tag_name, tag_value)
f('a', 'b')
self.assert_span_count(1)
span = self.get_root_span()
span.assert_matches(
name='decorated_function', service='s', resource='r', span_type='t', meta=dict(a='b'),
)
def test_tracer_pid(self):
with self.trace('root') as root_span:
with self.trace('child') as child_span:
pass
# Root span should contain the pid of the current process
root_span.assert_metrics({system.PID: getpid()}, exact=False)
# Child span should not contain a pid tag
child_span.assert_metrics(dict(), exact=True)
def test_tracer_wrap_default_name(self):
@self.tracer.wrap()
def f():
pass
f()
self.assert_structure(dict(name='tests.test_tracer.f'))
def test_tracer_wrap_exception(self):
@self.tracer.wrap()
def f():
raise Exception('bim')
with self.assertRaises(Exception) as ex:
f()
self.assert_structure(
dict(
name='tests.test_tracer.f',
error=1,
meta={
'error.msg': ex.message,
'error.type': ex.__class__.__name__,
},
),
)
def test_tracer_wrap_multiple_calls(self):
@self.tracer.wrap()
def f():
pass
f()
f()
self.assert_span_count(2)
assert self.spans[0].span_id != self.spans[1].span_id
def test_tracer_wrap_span_nesting_current_root_span(self):
@self.tracer.wrap('inner')
def inner():
root_span = self.tracer.current_root_span()
self.assertEqual(root_span.name, 'outer')
@self.tracer.wrap('outer')
def outer():
root_span = self.tracer.current_root_span()
self.assertEqual(root_span.name, 'outer')
with self.trace('mid'):
root_span = self.tracer.current_root_span()
self.assertEqual(root_span.name, 'outer')
inner()
outer()
def test_tracer_wrap_span_nesting(self):
@self.tracer.wrap('inner')
def inner():
pass
@self.tracer.wrap('outer')
def outer():
with self.trace('mid'):
inner()
outer()
self.assert_span_count(3)
self.assert_structure(
dict(name='outer'),
(
(
dict(name='mid'),
(
dict(name='inner'),
)
),
),
)
def test_tracer_wrap_class(self):
class Foo(object):
@staticmethod
@self.tracer.wrap()
def s():
return 1
@classmethod
@self.tracer.wrap()
def c(cls):
return 2
@self.tracer.wrap()
def i(cls):
return 3
f = Foo()
self.assertEqual(f.s(), 1)
self.assertEqual(f.c(), 2)
self.assertEqual(f.i(), 3)
self.assert_span_count(3)
self.spans[0].assert_matches(name='tests.test_tracer.s')
self.spans[1].assert_matches(name='tests.test_tracer.c')
self.spans[2].assert_matches(name='tests.test_tracer.i')
def test_tracer_wrap_factory(self):
def wrap_executor(tracer, fn, args, kwargs, span_name=None, service=None, resource=None, span_type=None):
with tracer.trace('wrap.overwrite') as span:
span.set_tag('args', args)
span.set_tag('kwargs', kwargs)
return fn(*args, **kwargs)
@self.tracer.wrap()
def wrapped_function(param, kw_param=None):
self.assertEqual(42, param)
self.assertEqual(42, kw_param)
# set the custom wrap factory after the wrapper has been called
self.tracer.configure(wrap_executor=wrap_executor)
# call the function expecting that the custom tracing wrapper is used
wrapped_function(42, kw_param=42)
self.assert_span_count(1)
self.spans[0].assert_matches(
name='wrap.overwrite',
meta=dict(args='(42,)', kwargs='{\'kw_param\': 42}'),
)
def test_tracer_wrap_factory_nested(self):
def wrap_executor(tracer, fn, args, kwargs, span_name=None, service=None, resource=None, span_type=None):
with tracer.trace('wrap.overwrite') as span:
span.set_tag('args', args)
span.set_tag('kwargs', kwargs)
return fn(*args, **kwargs)
@self.tracer.wrap()
def wrapped_function(param, kw_param=None):
self.assertEqual(42, param)
self.assertEqual(42, kw_param)
# set the custom wrap factory after the wrapper has been called
self.tracer.configure(wrap_executor=wrap_executor)
# call the function expecting that the custom tracing wrapper is used
with self.trace('wrap.parent', service='webserver'):
wrapped_function(42, kw_param=42)
self.assert_structure(
dict(name='wrap.parent', service='webserver'),
(
dict(
name='wrap.overwrite',
service='webserver',
meta=dict(args='(42,)', kwargs='{\'kw_param\': 42}')
),
),
)
def test_tracer_disabled(self):
self.tracer.enabled = True
with self.trace('foo') as s:
s.set_tag('a', 'b')
self.assert_has_spans()
self.reset()
self.tracer.enabled = False
with self.trace('foo') as s:
s.set_tag('a', 'b')
self.assert_has_no_spans()
def test_unserializable_span_with_finish(self):
try:
import numpy as np
except ImportError:
raise SkipTest('numpy not installed')
# a weird case where manually calling finish with an unserializable
# span was causing an loop of serialization.
with self.trace('parent') as span:
span.metrics['as'] = np.int64(1) # circumvent the data checks
span.finish()
def test_tracer_disabled_mem_leak(self):
# ensure that if the tracer is disabled, we still remove things from the
# span buffer upon finishing.
self.tracer.enabled = False
s1 = self.trace('foo')
s1.finish()
p1 = self.tracer.current_span()
s2 = self.trace('bar')
self.assertIsNone(s2._parent)
s2.finish()
self.assertIsNone(p1)
def test_tracer_global_tags(self):
s1 = self.trace('brie')
s1.finish()
self.assertIsNone(s1.get_tag('env'))
self.assertIsNone(s1.get_tag('other'))
self.tracer.set_tags({'env': 'prod'})
s2 = self.trace('camembert')
s2.finish()
self.assertEqual(s2.get_tag('env'), 'prod')
self.assertIsNone(s2.get_tag('other'))
self.tracer.set_tags({'env': 'staging', 'other': 'tag'})
s3 = self.trace('gruyere')
s3.finish()
self.assertEqual(s3.get_tag('env'), 'staging')
self.assertEqual(s3.get_tag('other'), 'tag')
def test_global_context(self):
# the tracer uses a global thread-local Context
span = self.trace('fake_span')
ctx = self.tracer.get_call_context()
self.assertEqual(len(ctx._trace), 1)
self.assertEqual(ctx._trace[0], span)
def test_tracer_current_span(self):
# the current span is in the local Context()
span = self.trace('fake_span')
self.assertEqual(self.tracer.current_span(), span)
def test_tracer_current_span_missing_context(self):
self.assertIsNone(self.tracer.current_span())
def test_tracer_current_root_span_missing_context(self):
self.assertIsNone(self.tracer.current_root_span())
def test_default_provider_get(self):
# Tracer Context Provider must return a Context object
# even if empty
ctx = self.tracer.context_provider.active()
self.assertTrue(isinstance(ctx, Context))
self.assertEqual(len(ctx._trace), 0)
def test_default_provider_set(self):
# The Context Provider can set the current active Context;
# this could happen in distributed tracing
ctx = Context(trace_id=42, span_id=100)
self.tracer.context_provider.activate(ctx)
span = self.trace('web.request')
span.assert_matches(name='web.request', trace_id=42, parent_id=100)
def test_default_provider_trace(self):
# Context handled by a default provider must be used
# when creating a trace
span = self.trace('web.request')
ctx = self.tracer.context_provider.active()
self.assertEqual(len(ctx._trace), 1)
self.assertEqual(span._context, ctx)
def test_start_span(self):
# it should create a root Span
span = self.start_span('web.request')
span.assert_matches(
name='web.request',
tracer=self.tracer,
_parent=None,
parent_id=None,
)
self.assertIsNotNone(span._context)
self.assertEqual(span._context._current_span, span)
def test_start_span_optional(self):
# it should create a root Span with arguments
span = self.start_span('web.request', service='web', resource='/', span_type='http')
span.assert_matches(
name='web.request',
service='web',
resource='/',
span_type='http',
)
def test_start_child_span(self):
# it should create a child Span for the given parent
parent = self.start_span('web.request')
child = self.start_span('web.worker', child_of=parent)
parent.assert_matches(
name='web.request',
parent_id=None,
_context=child._context,
_parent=None,
tracer=self.tracer,
)
child.assert_matches(
name='web.worker',
parent_id=parent.span_id,
_context=parent._context,
_parent=parent,
tracer=self.tracer,
)
self.assertEqual(child._context._current_span, child)
def test_start_child_span_attributes(self):
# it should create a child Span with parent's attributes
parent = self.start_span('web.request', service='web', resource='/', span_type='http')
child = self.start_span('web.worker', child_of=parent)
child.assert_matches(name='web.worker', service='web')
def test_start_child_from_context(self):
# it should create a child span with a populated Context
root = self.start_span('web.request')
context = root.context
child = self.start_span('web.worker', child_of=context)
child.assert_matches(
name='web.worker',
parent_id=root.span_id,
trace_id=root.trace_id,
_context=root._context,
_parent=root,
tracer=self.tracer,
)
self.assertEqual(child._context._current_span, child)
def test_adding_services(self):
self.assertEqual(self.tracer._services, set())
root = self.start_span('root', service='one')
context = root.context
self.assertSetEqual(self.tracer._services, set(['one']))
self.start_span('child', service='two', child_of=context)
self.assertSetEqual(self.tracer._services, set(['one', 'two']))
def test_configure_runtime_worker(self):
# by default runtime worker not started though runtime id is set
self.assertIsNone(self.tracer._runtime_worker)
# configure tracer with runtime metrics collection
self.tracer.configure(collect_metrics=True)
self.assertIsNotNone(self.tracer._runtime_worker)
def test_configure_dogstatsd_host(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
self.tracer.configure(dogstatsd_host='foo')
assert self.tracer._dogstatsd_client.host == 'foo'
assert self.tracer._dogstatsd_client.port == 8125
# verify warnings triggered
assert len(w) == 1
assert issubclass(w[-1].category, ddtrace.utils.deprecation.RemovedInDDTrace10Warning)
assert 'Use `dogstatsd_url`' in str(w[-1].message)
def test_configure_dogstatsd_host_port(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
self.tracer.configure(dogstatsd_host='foo', dogstatsd_port='1234')
assert self.tracer._dogstatsd_client.host == 'foo'
assert self.tracer._dogstatsd_client.port == 1234
# verify warnings triggered
assert len(w) == 2
assert issubclass(w[0].category, ddtrace.utils.deprecation.RemovedInDDTrace10Warning)
assert 'Use `dogstatsd_url`' in str(w[0].message)
assert issubclass(w[1].category, ddtrace.utils.deprecation.RemovedInDDTrace10Warning)
assert 'Use `dogstatsd_url`' in str(w[1].message)
def test_configure_dogstatsd_url_host_port(self):
self.tracer.configure(dogstatsd_url='foo:1234')
assert self.tracer._dogstatsd_client.host == 'foo'
assert self.tracer._dogstatsd_client.port == 1234
def test_configure_dogstatsd_url_socket(self):
self.tracer.configure(dogstatsd_url='unix:///foo.sock')
assert self.tracer._dogstatsd_client.host is None
assert self.tracer._dogstatsd_client.port is None
assert self.tracer._dogstatsd_client.socket_path == '/foo.sock'
def test_span_no_runtime_tags(self):
self.tracer.configure(collect_metrics=False)
root = self.start_span('root')
context = root.context
child = self.start_span('child', child_of=context)
self.assertIsNone(root.get_tag('language'))
self.assertIsNone(child.get_tag('language'))
def test_only_root_span_runtime_internal_span_types(self):
self.tracer.configure(collect_metrics=True)
for span_type in ("custom", "template", "web", "worker"):
root = self.start_span('root', span_type=span_type)
context = root.context
child = self.start_span('child', child_of=context)
self.assertEqual(root.get_tag('language'), 'python')
self.assertIsNone(child.get_tag('language'))
def test_only_root_span_runtime_external_span_types(self):
self.tracer.configure(collect_metrics=True)
for span_type in ("algoliasearch.search", "boto", "cache", "cassandra", "elasticsearch",
"grpc", "kombu", "http", "memcached", "redis", "sql", "vertica"):
root = self.start_span('root', span_type=span_type)
context = root.context
child = self.start_span('child', child_of=context)
self.assertIsNone(root.get_tag('language'))
self.assertIsNone(child.get_tag('language'))
def test_installed_excepthook():
ddtrace.install_excepthook()
assert sys.excepthook is ddtrace._excepthook
ddtrace.uninstall_excepthook()
assert sys.excepthook is not ddtrace._excepthook
ddtrace.install_excepthook()
assert sys.excepthook is ddtrace._excepthook
def test_excepthook():
ddtrace.install_excepthook()
class Foobar(Exception):
pass
called = {}
def original(tp, value, traceback):
called['yes'] = True
sys.excepthook = original
ddtrace.install_excepthook()
e = Foobar()
tracer = ddtrace.Tracer()
tracer._dogstatsd_client = mock.Mock()
with override_global_tracer(tracer):
sys.excepthook(e.__class__, e, None)
tracer._dogstatsd_client.increment.assert_has_calls((
mock.call('datadog.tracer.uncaught_exceptions', 1, tags=['class:Foobar']),
))
assert called
def test_tracer_url():
t = ddtrace.Tracer()
assert t.writer.api.hostname == 'localhost'
assert t.writer.api.port == 8126
t = ddtrace.Tracer(url='http://foobar:12')
assert t.writer.api.hostname == 'foobar'
assert t.writer.api.port == 12
t = ddtrace.Tracer(url='unix:///foobar')
assert t.writer.api.uds_path == '/foobar'
t = ddtrace.Tracer(url='http://localhost')
assert t.writer.api.hostname == 'localhost'
assert t.writer.api.port == 80
assert not t.writer.api.https
t = ddtrace.Tracer(url='https://localhost')
assert t.writer.api.hostname == 'localhost'
assert t.writer.api.port == 443
assert t.writer.api.https
with pytest.raises(ValueError) as e:
t = ddtrace.Tracer(url='foo://foobar:12')
assert str(e) == 'Unknown scheme `https` for agent URL'
def test_tracer_dogstatsd_url():
t = ddtrace.Tracer()
assert t._dogstatsd_client.host == 'localhost'
assert t._dogstatsd_client.port == 8125
t = ddtrace.Tracer(dogstatsd_url='foobar:12')
assert t._dogstatsd_client.host == 'foobar'
assert t._dogstatsd_client.port == 12
t = ddtrace.Tracer(dogstatsd_url='udp://foobar:12')
assert t._dogstatsd_client.host == 'foobar'
assert t._dogstatsd_client.port == 12
t = ddtrace.Tracer(dogstatsd_url='/var/run/statsd.sock')
assert t._dogstatsd_client.socket_path == '/var/run/statsd.sock'
t = ddtrace.Tracer(dogstatsd_url='unix:///var/run/statsd.sock')
assert t._dogstatsd_client.socket_path == '/var/run/statsd.sock'
with pytest.raises(ValueError) as e:
t = ddtrace.Tracer(dogstatsd_url='foo://foobar:12')
assert str(e) == 'Unknown url format for `foo://foobar:12`'
def test_tracer_fork():
t = ddtrace.Tracer()
original_pid = t._pid
original_writer = t.writer
@contextlib.contextmanager
def capture_failures(errors):
try:
yield
except AssertionError as e:
errors.put(e)
def task(t, errors):
# Start a new span to trigger process checking
with t.trace('test', service='test') as span:
# Assert we recreated the writer and have a new queue
with capture_failures(errors):
assert t._pid != original_pid
assert t.writer != original_writer
assert t.writer._trace_queue != original_writer._trace_queue
# Stop the background worker so we don't accidetnally flush the
# queue before we can assert on it
t.writer.stop()
t.writer.join()
# Assert the trace got written into the correct queue
assert original_writer._trace_queue.qsize() == 0
assert t.writer._trace_queue.qsize() == 1
assert [[span]] == list(t.writer._trace_queue.get())
# Assert tracer in a new process correctly recreates the writer
errors = multiprocessing.Queue()
p = multiprocessing.Process(target=task, args=(t, errors))
try:
p.start()
finally:
p.join(timeout=2)
while errors.qsize() > 0:
raise errors.get()
# Ensure writing into the tracer in this process still works as expected
with t.trace('test', service='test') as span:
assert t._pid == original_pid
assert t.writer == original_writer
assert t.writer._trace_queue == original_writer._trace_queue
# Stop the background worker so we don't accidentally flush the
# queue before we can assert on it
t.writer.stop()
t.writer.join()
# Assert the trace got written into the correct queue
assert original_writer._trace_queue.qsize() == 1
assert t.writer._trace_queue.qsize() == 1
assert [[span]] == list(t.writer._trace_queue.get())