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)
 |