mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-30 05:32:30 +08:00
880 lines
32 KiB
Python
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)
|