mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-30 13:43:03 +08:00
Move DD code into its own directory (#6)
This commit is contained in:
879
reference/tests/test_sampler.py
Normal file
879
reference/tests/test_sampler.py
Normal file
@ -0,0 +1,879 @@
|
||||
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)
|
Reference in New Issue
Block a user