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

880 lines
32 KiB
Python

from __future__ import division
import contextlib
import mock
import re
import unittest
import pytest
from ddtrace.compat import iteritems
from ddtrace.constants import SAMPLING_PRIORITY_KEY, SAMPLE_RATE_METRIC_KEY
from ddtrace.constants import SAMPLING_AGENT_DECISION, SAMPLING_RULE_DECISION, SAMPLING_LIMIT_DECISION
from ddtrace.ext.priority import AUTO_KEEP, AUTO_REJECT
from ddtrace.internal.rate_limiter import RateLimiter
from ddtrace.sampler import DatadogSampler, SamplingRule
from ddtrace.sampler import RateSampler, AllSampler, RateByServiceSampler
from ddtrace.span import Span
from .utils import override_env
from .test_tracer import get_dummy_tracer
@pytest.fixture
def dummy_tracer():
return get_dummy_tracer()
def assert_sampling_decision_tags(span, agent=None, limit=None, rule=None):
assert span.get_metric(SAMPLING_AGENT_DECISION) == agent
assert span.get_metric(SAMPLING_LIMIT_DECISION) == limit
assert span.get_metric(SAMPLING_RULE_DECISION) == rule
def create_span(tracer=None, name='test.span', meta=None, *args, **kwargs):
tracer = tracer or get_dummy_tracer()
if 'context' not in kwargs:
kwargs['context'] = tracer.get_call_context()
span = Span(tracer=tracer, name=name, *args, **kwargs)
if meta:
span.set_tags(meta)
return span
class RateSamplerTest(unittest.TestCase):
def test_set_sample_rate(self):
sampler = RateSampler()
assert sampler.sample_rate == 1.0
for rate in [0.001, 0.01, 0.1, 0.25, 0.5, 0.75, 0.99999999, 1.0, 1]:
sampler.set_sample_rate(rate)
assert sampler.sample_rate == float(rate)
sampler.set_sample_rate(str(rate))
assert sampler.sample_rate == float(rate)
def test_set_sample_rate_str(self):
sampler = RateSampler()
sampler.set_sample_rate('0.5')
assert sampler.sample_rate == 0.5
def test_sample_rate_deviation(self):
for sample_rate in [0.1, 0.25, 0.5, 1]:
tracer = get_dummy_tracer()
writer = tracer.writer
tracer.sampler = RateSampler(sample_rate)
iterations = int(1e4 / sample_rate)
for i in range(iterations):
span = tracer.trace(i)
span.finish()
samples = writer.pop()
# We must have at least 1 sample, check that it has its sample rate properly assigned
assert samples[0].get_metric(SAMPLE_RATE_METRIC_KEY) == sample_rate
# Less than 5% deviation when 'enough' iterations (arbitrary, just check if it converges)
deviation = abs(len(samples) - (iterations * sample_rate)) / (iterations * sample_rate)
assert deviation < 0.05, 'Deviation too high %f with sample_rate %f' % (deviation, sample_rate)
def test_deterministic_behavior(self):
""" Test that for a given trace ID, the result is always the same """
tracer = get_dummy_tracer()
writer = tracer.writer
tracer.sampler = RateSampler(0.5)
for i in range(10):
span = tracer.trace(i)
span.finish()
samples = writer.pop()
assert len(samples) <= 1, 'there should be 0 or 1 spans'
sampled = (1 == len(samples))
for j in range(10):
other_span = Span(tracer, i, trace_id=span.trace_id)
assert (
sampled == tracer.sampler.sample(other_span)
), 'sampling should give the same result for a given trace_id'
class RateByServiceSamplerTest(unittest.TestCase):
def test_default_key(self):
assert (
'service:,env:' == RateByServiceSampler._default_key
), 'default key should correspond to no service and no env'
def test_key(self):
assert RateByServiceSampler._default_key == RateByServiceSampler._key()
assert 'service:mcnulty,env:' == RateByServiceSampler._key(service='mcnulty')
assert 'service:,env:test' == RateByServiceSampler._key(env='test')
assert 'service:mcnulty,env:test' == RateByServiceSampler._key(service='mcnulty', env='test')
assert 'service:mcnulty,env:test' == RateByServiceSampler._key('mcnulty', 'test')
def test_sample_rate_deviation(self):
for sample_rate in [0.1, 0.25, 0.5, 1]:
tracer = get_dummy_tracer()
writer = tracer.writer
tracer.configure(sampler=AllSampler())
# We need to set the writer because tracer.configure overrides it,
# indeed, as we enable priority sampling, we must ensure the writer
# is priority sampling aware and pass it a reference on the
# priority sampler to send the feedback it gets from the agent
assert writer != tracer.writer, 'writer should have been updated by configure'
tracer.writer = writer
tracer.priority_sampler.set_sample_rate(sample_rate)
iterations = int(1e4 / sample_rate)
for i in range(iterations):
span = tracer.trace(i)
span.finish()
samples = writer.pop()
samples_with_high_priority = 0
for sample in samples:
if sample.get_metric(SAMPLING_PRIORITY_KEY) is not None:
if sample.get_metric(SAMPLING_PRIORITY_KEY) > 0:
samples_with_high_priority += 1
else:
assert (
0 == sample.get_metric(SAMPLING_PRIORITY_KEY)
), 'when priority sampling is on, priority should be 0 when trace is to be dropped'
assert_sampling_decision_tags(sample, agent=sample_rate)
# We must have at least 1 sample, check that it has its sample rate properly assigned
assert samples[0].get_metric(SAMPLE_RATE_METRIC_KEY) is None
# Less than 5% deviation when 'enough' iterations (arbitrary, just check if it converges)
deviation = abs(samples_with_high_priority - (iterations * sample_rate)) / (iterations * sample_rate)
assert deviation < 0.05, 'Deviation too high %f with sample_rate %f' % (deviation, sample_rate)
def test_update_rate_by_service_sample_rates(self):
cases = [
{
'service:,env:': 1,
},
{
'service:,env:': 1,
'service:mcnulty,env:dev': 0.33,
'service:postgres,env:dev': 0.7,
},
{
'service:,env:': 1,
'service:mcnulty,env:dev': 0.25,
'service:postgres,env:dev': 0.5,
'service:redis,env:prod': 0.75,
},
]
tracer = get_dummy_tracer()
tracer.configure(sampler=AllSampler())
priority_sampler = tracer.priority_sampler
for case in cases:
priority_sampler.update_rate_by_service_sample_rates(case)
rates = {}
for k, v in iteritems(priority_sampler._by_service_samplers):
rates[k] = v.sample_rate
assert case == rates, '%s != %s' % (case, rates)
# It's important to also test in reverse mode for we want to make sure key deletion
# works as well as key insertion (and doing this both ways ensures we trigger both cases)
cases.reverse()
for case in cases:
priority_sampler.update_rate_by_service_sample_rates(case)
rates = {}
for k, v in iteritems(priority_sampler._by_service_samplers):
rates[k] = v.sample_rate
assert case == rates, '%s != %s' % (case, rates)
@pytest.mark.parametrize(
'sample_rate,allowed',
[
# Min/max allowed values
(0.0, True),
(1.0, True),
# Accepted boundaries
(0.000001, True),
(0.999999, True),
# Outside the bounds
(-0.000000001, False),
(1.0000000001, False),
] + [
# Try a bunch of decimal values between 0 and 1
(1 / i, True) for i in range(1, 50)
] + [
# Try a bunch of decimal values less than 0
(-(1 / i), False) for i in range(1, 50)
] + [
# Try a bunch of decimal values greater than 1
(1 + (1 / i), False) for i in range(1, 50)
]
)
def test_sampling_rule_init_sample_rate(sample_rate, allowed):
if allowed:
rule = SamplingRule(sample_rate=sample_rate)
assert rule.sample_rate == sample_rate
else:
with pytest.raises(ValueError):
SamplingRule(sample_rate=sample_rate)
def test_sampling_rule_init_defaults():
rule = SamplingRule(sample_rate=1.0)
assert rule.sample_rate == 1.0
assert rule.service == SamplingRule.NO_RULE
assert rule.name == SamplingRule.NO_RULE
def test_sampling_rule_init():
name_regex = re.compile(r'\.request$')
def resource_check(resource):
return 'healthcheck' in resource
rule = SamplingRule(
sample_rate=0.0,
# Value
service='my-service',
# Regex
name=name_regex,
)
assert rule.sample_rate == 0.0
assert rule.service == 'my-service'
assert rule.name == name_regex
@pytest.mark.parametrize(
'span,rule,expected',
[
# DEV: Use sample_rate=1 to ensure SamplingRule._sample always returns True
(create_span(name=name), SamplingRule(
sample_rate=1, name=pattern), expected)
for name, pattern, expected in [
('test.span', SamplingRule.NO_RULE, True),
# DEV: `span.name` cannot be `None`
('test.span', None, False),
('test.span', 'test.span', True),
('test.span', 'test_span', False),
('test.span', re.compile(r'^test\.span$'), True),
('test_span', re.compile(r'^test.span$'), True),
('test.span', re.compile(r'^test_span$'), False),
('test.span', re.compile(r'test'), True),
('test.span', re.compile(r'test\.span|another\.span'), True),
('another.span', re.compile(r'test\.span|another\.span'), True),
('test.span', lambda name: 'span' in name, True),
('test.span', lambda name: 'span' not in name, False),
('test.span', lambda name: 1 / 0, False),
]
]
)
def test_sampling_rule_matches_name(span, rule, expected):
assert rule.matches(span) is expected, '{} -> {} -> {}'.format(rule, span, expected)
@pytest.mark.parametrize(
'span,rule,expected',
[
# DEV: Use sample_rate=1 to ensure SamplingRule._sample always returns True
(create_span(service=service), SamplingRule(sample_rate=1, service=pattern), expected)
for service, pattern, expected in [
('my-service', SamplingRule.NO_RULE, True),
('my-service', None, False),
(None, None, True),
(None, 'my-service', False),
(None, re.compile(r'my-service'), False),
(None, lambda service: 'service' in service, False),
('my-service', 'my-service', True),
('my-service', 'my_service', False),
('my-service', re.compile(r'^my-'), True),
('my_service', re.compile(r'^my[_-]'), True),
('my-service', re.compile(r'^my_'), False),
('my-service', re.compile(r'my-service'), True),
('my-service', re.compile(r'my'), True),
('my-service', re.compile(r'my-service|another-service'), True),
('another-service', re.compile(r'my-service|another-service'), True),
('my-service', lambda service: 'service' in service, True),
('my-service', lambda service: 'service' not in service, False),
('my-service', lambda service: 1 / 0, False),
]
]
)
def test_sampling_rule_matches_service(span, rule, expected):
assert rule.matches(span) is expected, '{} -> {} -> {}'.format(rule, span, expected)
@pytest.mark.parametrize(
'span,rule,expected',
[
# All match
(
create_span(
name='test.span',
service='my-service',
),
SamplingRule(
sample_rate=1,
name='test.span',
service=re.compile(r'^my-'),
),
True,
),
# All match, but sample rate of 0%
# DEV: We are checking if it is a match, not computing sampling rate, sample_rate=0 is not considered
(
create_span(
name='test.span',
service='my-service',
),
SamplingRule(
sample_rate=0,
name='test.span',
service=re.compile(r'^my-'),
),
True,
),
# Name doesn't match
(
create_span(
name='test.span',
service='my-service',
),
SamplingRule(
sample_rate=1,
name='test_span',
service=re.compile(r'^my-'),
),
False,
),
# Service doesn't match
(
create_span(
name='test.span',
service='my-service',
),
SamplingRule(
sample_rate=1,
name='test.span',
service=re.compile(r'^service-'),
),
False,
),
],
)
def test_sampling_rule_matches(span, rule, expected):
assert rule.matches(span) is expected, '{} -> {} -> {}'.format(rule, span, expected)
def test_sampling_rule_matches_exception():
e = Exception('an error occurred')
def pattern(prop):
raise e
rule = SamplingRule(sample_rate=1.0, name=pattern)
span = create_span(name='test.span')
with mock.patch('ddtrace.sampler.log') as mock_log:
assert rule.matches(span) is False
mock_log.warning.assert_called_once_with(
'%r pattern %r failed with %r',
rule,
pattern,
'test.span',
exc_info=True,
)
@pytest.mark.parametrize('sample_rate', [0.01, 0.1, 0.15, 0.25, 0.5, 0.75, 0.85, 0.9, 0.95, 0.991])
def test_sampling_rule_sample(sample_rate):
tracer = get_dummy_tracer()
rule = SamplingRule(sample_rate=sample_rate)
iterations = int(1e4 / sample_rate)
sampled = sum(
rule.sample(Span(tracer=tracer, name=i))
for i in range(iterations)
)
# Less than 5% deviation when 'enough' iterations (arbitrary, just check if it converges)
deviation = abs(sampled - (iterations * sample_rate)) / (iterations * sample_rate)
assert deviation < 0.05, (
'Deviation {!r} too high with sample_rate {!r} for {} sampled'.format(deviation, sample_rate, sampled)
)
def test_sampling_rule_sample_rate_1():
tracer = get_dummy_tracer()
rule = SamplingRule(sample_rate=1)
iterations = int(1e4)
assert all(
rule.sample(Span(tracer=tracer, name=i))
for i in range(iterations)
)
def test_sampling_rule_sample_rate_0():
tracer = get_dummy_tracer()
rule = SamplingRule(sample_rate=0)
iterations = int(1e4)
assert sum(
rule.sample(Span(tracer=tracer, name=i))
for i in range(iterations)
) == 0
def test_datadog_sampler_init():
# No args
sampler = DatadogSampler()
assert sampler.rules == []
assert isinstance(sampler.limiter, RateLimiter)
assert sampler.limiter.rate_limit == DatadogSampler.DEFAULT_RATE_LIMIT
assert isinstance(sampler.default_sampler, RateByServiceSampler)
# With rules
rule = SamplingRule(sample_rate=1)
sampler = DatadogSampler(rules=[rule])
assert sampler.rules == [rule]
assert sampler.limiter.rate_limit == DatadogSampler.DEFAULT_RATE_LIMIT
assert isinstance(sampler.default_sampler, RateByServiceSampler)
# With rate limit
sampler = DatadogSampler(rate_limit=10)
assert sampler.limiter.rate_limit == 10
assert isinstance(sampler.default_sampler, RateByServiceSampler)
# With default_sample_rate
sampler = DatadogSampler(default_sample_rate=0.5)
assert sampler.limiter.rate_limit == DatadogSampler.DEFAULT_RATE_LIMIT
assert isinstance(sampler.default_sampler, SamplingRule)
assert sampler.default_sampler.sample_rate == 0.5
# From env variables
with override_env(dict(DD_TRACE_SAMPLE_RATE='0.5', DD_TRACE_RATE_LIMIT='10')):
sampler = DatadogSampler()
assert sampler.limiter.rate_limit == 10
assert isinstance(sampler.default_sampler, SamplingRule)
assert sampler.default_sampler.sample_rate == 0.5
# Invalid rules
for val in (None, True, False, object(), 1, Exception()):
with pytest.raises(TypeError):
DatadogSampler(rules=[val])
# Ensure rule order
rule_1 = SamplingRule(sample_rate=1)
rule_2 = SamplingRule(sample_rate=0.5, service='test')
rule_3 = SamplingRule(sample_rate=0.25, name='flask.request')
sampler = DatadogSampler(rules=[rule_1, rule_2, rule_3])
assert sampler.rules == [rule_1, rule_2, rule_3]
@mock.patch('ddtrace.sampler.RateByServiceSampler.sample')
def test_datadog_sampler_sample_no_rules(mock_sample, dummy_tracer):
sampler = DatadogSampler()
span = create_span(tracer=dummy_tracer)
# Default RateByServiceSampler() is applied
# No rules configured
# No global rate limit
# No rate limit configured
# RateByServiceSampler.sample(span) returns True
mock_sample.return_value = True
assert sampler.sample(span) is True
assert span._context.sampling_priority is AUTO_KEEP
assert span.sampled is True
span = create_span(tracer=dummy_tracer)
# Default RateByServiceSampler() is applied
# No rules configured
# No global rate limit
# No rate limit configured
# RateByServiceSampler.sample(span) returns False
mock_sample.return_value = False
assert sampler.sample(span) is False
assert span._context.sampling_priority is AUTO_REJECT
assert span.sampled is False
@mock.patch('ddtrace.internal.rate_limiter.RateLimiter.is_allowed')
def test_datadog_sampler_sample_rules(mock_is_allowed, dummy_tracer):
# Do not let the limiter get in the way of our test
mock_is_allowed.return_value = True
rules = [
mock.Mock(spec=SamplingRule),
mock.Mock(spec=SamplingRule),
mock.Mock(spec=SamplingRule),
]
sampler = DatadogSampler(rules=rules)
# Reset all of our mocks
@contextlib.contextmanager
def reset_mocks():
def reset():
mock_is_allowed.reset_mock()
for rule in rules:
rule.reset_mock()
rule.sample_rate = 0.5
default_rule = SamplingRule(sample_rate=1.0)
sampler.default_sampler = mock.Mock(spec=SamplingRule, wraps=default_rule)
# Mock has lots of problems with mocking/wrapping over class properties
sampler.default_sampler.sample_rate = default_rule.sample_rate
reset() # Reset before, just in case
try:
yield
finally:
reset() # Must reset after
# No rules want to sample
# It is allowed because of default rate sampler
# All rules SamplingRule.matches are called
# No calls to SamplingRule.sample happen
with reset_mocks():
span = create_span(tracer=dummy_tracer)
for rule in rules:
rule.matches.return_value = False
assert sampler.sample(span) is True
assert span._context.sampling_priority is AUTO_KEEP
assert span.sampled is True
mock_is_allowed.assert_called_once_with()
for rule in rules:
rule.matches.assert_called_once_with(span)
rule.sample.assert_not_called()
sampler.default_sampler.matches.assert_not_called()
sampler.default_sampler.sample.assert_called_once_with(span)
assert_sampling_decision_tags(span, rule=1.0, limit=1.0)
# One rule thinks it should be sampled
# All following rule's SamplingRule.matches are not called
# It goes through limiter
# It is allowed
with reset_mocks():
span = create_span(tracer=dummy_tracer)
rules[1].matches.return_value = True
rules[1].sample.return_value = True
assert sampler.sample(span) is True
assert span._context.sampling_priority is AUTO_KEEP
assert span.sampled is True
mock_is_allowed.assert_called_once_with()
sampler.default_sampler.sample.assert_not_called()
assert_sampling_decision_tags(span, rule=0.5, limit=1.0)
rules[0].matches.assert_called_once_with(span)
rules[0].sample.assert_not_called()
rules[1].matches.assert_called_once_with(span)
rules[1].sample.assert_called_once_with(span)
rules[2].matches.assert_not_called()
rules[2].sample.assert_not_called()
# All rules think it should be sampled
# The first rule's SamplingRule.matches is called
# It goes through limiter
# It is allowed
with reset_mocks():
span = create_span(tracer=dummy_tracer)
for rule in rules:
rule.matches.return_value = True
rules[0].sample.return_value = True
assert sampler.sample(span) is True
assert span._context.sampling_priority is AUTO_KEEP
assert span.sampled is True
mock_is_allowed.assert_called_once_with()
sampler.default_sampler.sample.assert_not_called()
assert_sampling_decision_tags(span, rule=0.5, limit=1.0)
rules[0].matches.assert_called_once_with(span)
rules[0].sample.assert_called_once_with(span)
for rule in rules[1:]:
rule.matches.assert_not_called()
rule.sample.assert_not_called()
# Rule matches but does not think it should be sampled
# The rule's SamplingRule.matches is called
# The rule's SamplingRule.sample is called
# Rate limiter is not called
# The span is rejected
with reset_mocks():
span = create_span(tracer=dummy_tracer)
rules[0].matches.return_value = False
rules[2].matches.return_value = False
rules[1].matches.return_value = True
rules[1].sample.return_value = False
assert sampler.sample(span) is False
assert span._context.sampling_priority is AUTO_REJECT
assert span.sampled is False
mock_is_allowed.assert_not_called()
sampler.default_sampler.sample.assert_not_called()
assert_sampling_decision_tags(span, rule=0.5)
rules[0].matches.assert_called_once_with(span)
rules[0].sample.assert_not_called()
rules[1].matches.assert_called_once_with(span)
rules[1].sample.assert_called_once_with(span)
rules[2].matches.assert_not_called()
rules[2].sample.assert_not_called()
# No rules match and RateByServiceSampler is used
# All rules SamplingRule.matches are called
# Priority sampler's `sample` method is called
# Result of priority sampler is returned
# Rate limiter is not called
with reset_mocks():
span = create_span(tracer=dummy_tracer)
# Configure mock priority sampler
priority_sampler = RateByServiceSampler()
sampler.default_sampler = mock.Mock(spec=RateByServiceSampler, wraps=priority_sampler)
for rule in rules:
rule.matches.return_value = False
rule.sample.return_value = False
assert sampler.sample(span) is True
assert span._context.sampling_priority is AUTO_KEEP
assert span.sampled is True
mock_is_allowed.assert_not_called()
sampler.default_sampler.sample.assert_called_once_with(span)
assert_sampling_decision_tags(span, agent=1)
[r.matches.assert_called_once_with(span) for r in rules]
[r.sample.assert_not_called() for r in rules]
# No rules match and priority sampler is defined
# All rules SamplingRule.matches are called
# Priority sampler's `sample` method is called
# Result of priority sampler is returned
# Rate limiter is not called
with reset_mocks():
span = create_span(tracer=dummy_tracer)
# Configure mock priority sampler
priority_sampler = RateByServiceSampler()
for rate_sampler in priority_sampler._by_service_samplers.values():
rate_sampler.set_sample_rate(0)
sampler.default_sampler = mock.Mock(spec=RateByServiceSampler, wraps=priority_sampler)
for rule in rules:
rule.matches.return_value = False
rule.sample.return_value = False
assert sampler.sample(span) is False
assert span._context.sampling_priority is AUTO_REJECT
assert span.sampled is False
mock_is_allowed.assert_not_called()
sampler.default_sampler.sample.assert_called_once_with(span)
assert_sampling_decision_tags(span, agent=0)
[r.matches.assert_called_once_with(span) for r in rules]
[r.sample.assert_not_called() for r in rules]
def test_datadog_sampler_tracer(dummy_tracer):
rule = SamplingRule(sample_rate=1.0, name='test.span')
rule_spy = mock.Mock(spec=rule, wraps=rule)
rule_spy.sample_rate = rule.sample_rate
sampler = DatadogSampler(rules=[rule_spy])
limiter_spy = mock.Mock(spec=sampler.limiter, wraps=sampler.limiter)
sampler.limiter = limiter_spy
sampler_spy = mock.Mock(spec=sampler, wraps=sampler)
dummy_tracer.configure(sampler=sampler_spy)
assert dummy_tracer.sampler is sampler_spy
with dummy_tracer.trace('test.span') as span:
# Assert all of our expected functions were called
sampler_spy.sample.assert_called_once_with(span)
rule_spy.matches.assert_called_once_with(span)
rule_spy.sample.assert_called_once_with(span)
limiter_spy.is_allowed.assert_called_once_with()
# It must always mark it as sampled
assert span.sampled is True
# We know it was sampled because we have a sample rate of 1.0
assert span._context.sampling_priority is AUTO_KEEP
assert_sampling_decision_tags(span, rule=1.0)
def test_datadog_sampler_tracer_rate_limited(dummy_tracer):
rule = SamplingRule(sample_rate=1.0, name='test.span')
rule_spy = mock.Mock(spec=rule, wraps=rule)
rule_spy.sample_rate = rule.sample_rate
sampler = DatadogSampler(rules=[rule_spy])
limiter_spy = mock.Mock(spec=sampler.limiter, wraps=sampler.limiter)
limiter_spy.is_allowed.return_value = False # Have the limiter deny the span
sampler.limiter = limiter_spy
sampler_spy = mock.Mock(spec=sampler, wraps=sampler)
dummy_tracer.configure(sampler=sampler_spy)
assert dummy_tracer.sampler is sampler_spy
with dummy_tracer.trace('test.span') as span:
# Assert all of our expected functions were called
sampler_spy.sample.assert_called_once_with(span)
rule_spy.matches.assert_called_once_with(span)
rule_spy.sample.assert_called_once_with(span)
limiter_spy.is_allowed.assert_called_once_with()
# We must always mark the span as sampled
assert span.sampled is True
assert span._context.sampling_priority is AUTO_REJECT
assert_sampling_decision_tags(span, rule=1.0, limit=None)
def test_datadog_sampler_tracer_rate_0(dummy_tracer):
rule = SamplingRule(sample_rate=0, name='test.span') # Sample rate of 0 means never sample
rule_spy = mock.Mock(spec=rule, wraps=rule)
rule_spy.sample_rate = rule.sample_rate
sampler = DatadogSampler(rules=[rule_spy])
limiter_spy = mock.Mock(spec=sampler.limiter, wraps=sampler.limiter)
sampler.limiter = limiter_spy
sampler_spy = mock.Mock(spec=sampler, wraps=sampler)
dummy_tracer.configure(sampler=sampler_spy)
assert dummy_tracer.sampler is sampler_spy
with dummy_tracer.trace('test.span') as span:
# Assert all of our expected functions were called
sampler_spy.sample.assert_called_once_with(span)
rule_spy.matches.assert_called_once_with(span)
rule_spy.sample.assert_called_once_with(span)
limiter_spy.is_allowed.assert_not_called()
# It must always mark it as sampled
assert span.sampled is True
# We know it was not sampled because we have a sample rate of 0.0
assert span._context.sampling_priority is AUTO_REJECT
assert_sampling_decision_tags(span, rule=0)
def test_datadog_sampler_tracer_child(dummy_tracer):
rule = SamplingRule(sample_rate=1.0) # No rules means it gets applied to every span
rule_spy = mock.Mock(spec=rule, wraps=rule)
rule_spy.sample_rate = rule.sample_rate
sampler = DatadogSampler(rules=[rule_spy])
limiter_spy = mock.Mock(spec=sampler.limiter, wraps=sampler.limiter)
sampler.limiter = limiter_spy
sampler_spy = mock.Mock(spec=sampler, wraps=sampler)
dummy_tracer.configure(sampler=sampler_spy)
assert dummy_tracer.sampler is sampler_spy
with dummy_tracer.trace('parent.span') as parent:
with dummy_tracer.trace('child.span') as child:
# Assert all of our expected functions were called
# DEV: `assert_called_once_with` ensures we didn't also call with the child span
sampler_spy.sample.assert_called_once_with(parent)
rule_spy.matches.assert_called_once_with(parent)
rule_spy.sample.assert_called_once_with(parent)
limiter_spy.is_allowed.assert_called_once_with()
# We know it was sampled because we have a sample rate of 1.0
assert parent.sampled is True
assert parent._context.sampling_priority is AUTO_KEEP
assert_sampling_decision_tags(parent, rule=1.0)
assert child.sampled is True
assert child._parent is parent
assert child._context.sampling_priority is AUTO_KEEP
def test_datadog_sampler_tracer_start_span(dummy_tracer):
rule = SamplingRule(sample_rate=1.0) # No rules means it gets applied to every span
rule_spy = mock.Mock(spec=rule, wraps=rule)
rule_spy.sample_rate = rule.sample_rate
sampler = DatadogSampler(rules=[rule_spy])
limiter_spy = mock.Mock(spec=sampler.limiter, wraps=sampler.limiter)
sampler.limiter = limiter_spy
sampler_spy = mock.Mock(spec=sampler, wraps=sampler)
dummy_tracer.configure(sampler=sampler_spy)
assert dummy_tracer.sampler is sampler_spy
span = dummy_tracer.start_span('test.span')
# Assert all of our expected functions were called
sampler_spy.sample.assert_called_once_with(span)
rule_spy.matches.assert_called_once_with(span)
rule_spy.sample.assert_called_once_with(span)
limiter_spy.is_allowed.assert_called_once_with()
# It must always mark it as sampled
assert span.sampled is True
# We know it was sampled because we have a sample rate of 1.0
assert span._context.sampling_priority is AUTO_KEEP
assert_sampling_decision_tags(span, rule=1.0)
def test_datadog_sampler_update_rate_by_service_sample_rates(dummy_tracer):
cases = [
{
'service:,env:': 1,
},
{
'service:,env:': 1,
'service:mcnulty,env:dev': 0.33,
'service:postgres,env:dev': 0.7,
},
{
'service:,env:': 1,
'service:mcnulty,env:dev': 0.25,
'service:postgres,env:dev': 0.5,
'service:redis,env:prod': 0.75,
},
]
# By default sampler sets it's default sampler to RateByServiceSampler
sampler = DatadogSampler()
for case in cases:
sampler.update_rate_by_service_sample_rates(case)
rates = {}
for k, v in iteritems(sampler.default_sampler._by_service_samplers):
rates[k] = v.sample_rate
assert case == rates, '%s != %s' % (case, rates)
# It's important to also test in reverse mode for we want to make sure key deletion
# works as well as key insertion (and doing this both ways ensures we trigger both cases)
cases.reverse()
for case in cases:
sampler.update_rate_by_service_sample_rates(case)
rates = {}
for k, v in iteritems(sampler.default_sampler._by_service_samplers):
rates[k] = v.sample_rate
assert case == rates, '%s != %s' % (case, rates)