Skip to content

Commit 46cd1c9

Browse files
committed
refactor(tracing): break span-sampling cycle
We break the cycle between the span object and the sampling logic by moving some methods and functions around
1 parent c463d6a commit 46cd1c9

File tree

11 files changed

+93
-112
lines changed

11 files changed

+93
-112
lines changed

ddtrace/_trace/processor/__init__.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
from ddtrace._trace.span import _is_top_level
1515
from ddtrace.constants import _APM_ENABLED_METRIC_KEY as MK_APM_ENABLED
1616
from ddtrace.constants import _SAMPLING_PRIORITY_KEY
17+
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM
1718
from ddtrace.constants import USER_KEEP
1819
from ddtrace.internal import gitmetadata
1920
from ddtrace.internal import telemetry
2021
from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS
2122
from ddtrace.internal.constants import LAST_DD_PARENT_ID_KEY
2223
from ddtrace.internal.constants import MAX_UINT_64BITS
24+
from ddtrace.internal.constants import SamplingMechanism
2325
from ddtrace.internal.logger import get_logger
2426
from ddtrace.internal.sampling import SpanSamplingRule
25-
from ddtrace.internal.sampling import is_single_span_sampled
2627
from ddtrace.internal.service import ServiceStatusError
2728
from ddtrace.internal.telemetry.constants import TELEMETRY_LOG_LEVEL
2829
from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE
@@ -150,7 +151,12 @@ def process_trace(self, trace: List[Span]) -> Optional[List[Span]]:
150151
if priority is not None and priority <= 0:
151152
# When any span is marked as keep by a single span sampling
152153
# decision then we still send all and only those spans.
153-
single_spans = [_ for _ in trace if is_single_span_sampled(_)]
154+
155+
single_spans = [
156+
_
157+
for _ in trace
158+
if _.get_metric(_SINGLE_SPAN_SAMPLING_MECHANISM) == SamplingMechanism.SPAN_SAMPLING_RULE
159+
]
154160

155161
return single_spans or None
156162

@@ -159,8 +165,8 @@ def process_trace(self, trace: List[Span]) -> Optional[List[Span]]:
159165
for span in trace:
160166
if span.context.sampling_priority is not None and span.context.sampling_priority <= 0:
161167
for rule in self.single_span_rules:
162-
if rule.match(span):
163-
rule.sample(span)
168+
if rule.match(span.name, span.service):
169+
span.sample(rule)
164170
# If stats computation is enabled, we won't send all spans to the agent.
165171
# In order to ensure that the agent does not update priority sampling rates
166172
# due to single spans sampling, we set all of these spans to manual keep.

ddtrace/_trace/sampler.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
from ..internal.constants import SamplingMechanism
1818
from ..internal.logger import get_logger
1919
from ..internal.rate_limiter import RateLimiter
20-
from ..internal.sampling import _get_highest_precedence_rule_matching
21-
from ..internal.sampling import _set_sampling_tags
2220
from .sampling_rule import SamplingRule
2321

2422

@@ -169,9 +167,18 @@ def set_sampling_rules(self, rules: str) -> None:
169167
# Sort the sampling_rules list using a lambda function as the key
170168
self.rules = sorted(sampling_rules, key=lambda rule: PROVENANCE_ORDER.index(rule.provenance))
171169

170+
def _get_highest_precedence_rule_matching(self, span: Span) -> Optional[SamplingRule]:
171+
if not (rules := self.rules):
172+
return None
173+
174+
for rule in rules:
175+
if span.matches(rule):
176+
return rule
177+
return None
178+
172179
def sample(self, span: Span) -> bool:
173180
span._update_tags_from_context()
174-
matched_rule = _get_highest_precedence_rule_matching(span, self.rules)
181+
matched_rule = self._get_highest_precedence_rule_matching(span)
175182
# Default sampling
176183
agent_service_based = False
177184
sampled = True
@@ -197,8 +204,7 @@ def sample(self, span: Span) -> bool:
197204
span.set_metric(_SAMPLING_LIMIT_DECISION, self.limiter.effective_rate)
198205

199206
sampling_mechanism = self._get_sampling_mechanism(matched_rule, agent_service_based)
200-
_set_sampling_tags(
201-
span,
207+
span._set_sampling_tags(
202208
sampled,
203209
sample_rate,
204210
sampling_mechanism,

ddtrace/_trace/sampling_rule.py

-23
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
from ddtrace.internal.utils.cache import cachedmethod
1111

1212

13-
if TYPE_CHECKING: # pragma: no cover
14-
from ddtrace._trace.span import Span # noqa:F401
15-
1613
log = get_logger(__name__)
1714
KNUTH_FACTOR = 1111111111111111111
1815

@@ -125,26 +122,6 @@ def _matches(self, key: Tuple[Optional[str], str, Optional[str]]) -> bool:
125122
else:
126123
return True
127124

128-
def matches(self, span):
129-
# type: (Span) -> bool
130-
"""
131-
Return if this span matches this rule
132-
133-
:param span: The span to match against
134-
:type span: :class:`ddtrace._trace.span.Span`
135-
:returns: Whether this span matches or not
136-
:rtype: :obj:`bool`
137-
"""
138-
tags_match = self.tags_match(span)
139-
return tags_match and self._matches((span.service, span.name, span.resource))
140-
141-
def tags_match(self, span):
142-
# type: (Span) -> bool
143-
tag_match = True
144-
if self._tag_value_matchers:
145-
tag_match = self.check_tags(span.get_tags(), span.get_metrics())
146-
return tag_match
147-
148125
def check_tags(self, meta, metrics):
149126
if meta is None and metrics is None:
150127
return False

ddtrace/_trace/span.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ddtrace._trace._span_pointer import _SpanPointer
2222
from ddtrace._trace._span_pointer import _SpanPointerDirection
2323
from ddtrace._trace.context import Context
24+
from ddtrace._trace.sampling_rule import SamplingRule
2425
from ddtrace._trace.types import _AttributeValueType
2526
from ddtrace._trace.types import _MetaDictType
2627
from ddtrace._trace.types import _MetricDictType
@@ -29,6 +30,10 @@
2930
from ddtrace.constants import _SAMPLING_AGENT_DECISION
3031
from ddtrace.constants import _SAMPLING_LIMIT_DECISION
3132
from ddtrace.constants import _SAMPLING_RULE_DECISION
33+
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MAX_PER_SEC
34+
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MAX_PER_SEC_NO_LIMIT
35+
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM
36+
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_RATE
3237
from ddtrace.constants import _SPAN_MEASURED_KEY
3338
from ddtrace.constants import ERROR_MSG
3439
from ddtrace.constants import ERROR_STACK
@@ -48,10 +53,14 @@
4853
from ddtrace.internal.compat import NumericType
4954
from ddtrace.internal.compat import ensure_text
5055
from ddtrace.internal.compat import is_integer
56+
from ddtrace.internal.constants import _KEEP_PRIORITY_INDEX
57+
from ddtrace.internal.constants import _REJECT_PRIORITY_INDEX
5158
from ddtrace.internal.constants import MAX_UINT_64BITS as _MAX_UINT_64BITS
59+
from ddtrace.internal.constants import SAMPLING_MECHANISM_TO_PRIORITIES
5260
from ddtrace.internal.constants import SPAN_API_DATADOG
61+
from ddtrace.internal.constants import SamplingMechanism
5362
from ddtrace.internal.logger import get_logger
54-
from ddtrace.internal.sampling import SamplingMechanism
63+
from ddtrace.internal.sampling import SpanSamplingRule
5564
from ddtrace.internal.sampling import set_sampling_decision_maker
5665
from ddtrace.settings._config import config
5766

@@ -334,6 +343,55 @@ def _override_sampling_decision(self, decision: Optional[NumericType]):
334343
if key in self._local_root._metrics:
335344
del self._local_root._metrics[key]
336345

346+
def tags_match(self, rule: SamplingRule) -> bool:
347+
tag_match = True
348+
if rule._tag_value_matchers:
349+
tag_match = rule.check_tags(self.get_tags(), self.get_metrics())
350+
return tag_match
351+
352+
def matches(self, rule: SamplingRule) -> bool:
353+
"""
354+
Return if this span matches this rule
355+
356+
:param span: The span to match against
357+
:type span: :class:`ddtrace._trace.span.Span`
358+
:returns: Whether this span matches or not
359+
:rtype: :obj:`bool`
360+
"""
361+
tags_match = self.tags_match(rule)
362+
return tags_match and rule._matches((self.service, self.name, self.resource))
363+
364+
def apply_span_sampling_tags(self, rule: SpanSamplingRule) -> None:
365+
self.set_metric(_SINGLE_SPAN_SAMPLING_MECHANISM, SamplingMechanism.SPAN_SAMPLING_RULE)
366+
self.set_metric(_SINGLE_SPAN_SAMPLING_RATE, rule._sample_rate)
367+
# Only set this tag if it's not the default -1
368+
if rule._max_per_second != _SINGLE_SPAN_SAMPLING_MAX_PER_SEC_NO_LIMIT:
369+
self.set_metric(_SINGLE_SPAN_SAMPLING_MAX_PER_SEC, rule._max_per_second)
370+
371+
def sample(self, rule: SpanSamplingRule) -> bool:
372+
if rule._sample(self.span_id):
373+
if rule._limiter.is_allowed():
374+
self.apply_span_sampling_tags(rule)
375+
return True
376+
return False
377+
378+
def _set_sampling_tags(self, sampled: bool, sample_rate: float, mechanism: int) -> None:
379+
# Set the sampling mechanism
380+
set_sampling_decision_maker(self.context, mechanism)
381+
# Set the sampling psr rate
382+
if mechanism in (
383+
SamplingMechanism.LOCAL_USER_TRACE_SAMPLING_RULE,
384+
SamplingMechanism.REMOTE_USER_TRACE_SAMPLING_RULE,
385+
SamplingMechanism.REMOTE_DYNAMIC_TRACE_SAMPLING_RULE,
386+
):
387+
self.set_metric(_SAMPLING_RULE_DECISION, sample_rate)
388+
elif mechanism == SamplingMechanism.AGENT_RATE_BY_SERVICE:
389+
self.set_metric(_SAMPLING_AGENT_DECISION, sample_rate)
390+
# Set the sampling priority
391+
priorities = SAMPLING_MECHANISM_TO_PRIORITIES[mechanism]
392+
priority_index = _KEEP_PRIORITY_INDEX if sampled else _REJECT_PRIORITY_INDEX
393+
self.context.sampling_priority = priorities[priority_index]
394+
337395
def set_tag(self, key: _TagNameType, value: Any = None) -> None:
338396
"""Set a tag key/value pair on the span.
339397

ddtrace/appsec/_trace_utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
def _asm_manual_keep(span: Span) -> None:
2727
from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY
28-
from ddtrace.internal.sampling import SamplingMechanism
28+
from ddtrace.internal.constants import SamplingMechanism
2929

3030
span.set_tag(constants.MANUAL_KEEP_KEY)
3131
# set decision maker to ASM = -5
@@ -295,7 +295,7 @@ def block_request() -> None:
295295
meaning that if you capture the exception the request blocking could not work.
296296
"""
297297
if not asm_config._asm_enabled:
298-
log.warning("block_request() is disabled. To use this feature please enable" "Application Security Monitoring")
298+
log.warning("block_request() is disabled. To use this feature please enableApplication Security Monitoring")
299299
return
300300

301301
_asm_request_context.block_request()

ddtrace/internal/sampling.py

+3-69
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,8 @@
1414
except ImportError:
1515
from typing_extensions import TypedDict
1616

17-
from ddtrace._trace.sampling_rule import SamplingRule # noqa:F401
18-
from ddtrace.constants import _SAMPLING_AGENT_DECISION
19-
from ddtrace.constants import _SAMPLING_RULE_DECISION
20-
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MAX_PER_SEC
2117
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MAX_PER_SEC_NO_LIMIT
22-
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM
23-
from ddtrace.constants import _SINGLE_SPAN_SAMPLING_RATE
24-
from ddtrace.internal.constants import _KEEP_PRIORITY_INDEX
25-
from ddtrace.internal.constants import _REJECT_PRIORITY_INDEX
2618
from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY
27-
from ddtrace.internal.constants import SAMPLING_MECHANISM_TO_PRIORITIES
28-
from ddtrace.internal.constants import SamplingMechanism
2919
from ddtrace.internal.glob_matching import GlobMatcher
3020
from ddtrace.internal.logger import get_logger
3121
from ddtrace.settings._config import config
@@ -43,7 +33,6 @@
4333

4434
if TYPE_CHECKING: # pragma: no cover
4535
from ddtrace._trace.context import Context # noqa:F401
46-
from ddtrace._trace.span import Span # noqa:F401
4736

4837
# Big prime number to make hashing better distributed
4938
KNUTH_FACTOR = 1111111111111111111
@@ -126,28 +115,16 @@ def __init__(
126115
self._service_matcher = GlobMatcher(service) if service is not None else None
127116
self._name_matcher = GlobMatcher(name) if name is not None else None
128117

129-
def sample(self, span):
130-
# type: (Span) -> bool
131-
if self._sample(span):
132-
if self._limiter.is_allowed():
133-
self.apply_span_sampling_tags(span)
134-
return True
135-
return False
136-
137-
def _sample(self, span):
138-
# type: (Span) -> bool
118+
def _sample(self, span_id: int) -> bool:
139119
if self._sample_rate == 1:
140120
return True
141121
elif self._sample_rate == 0:
142122
return False
143123

144-
return ((span.span_id * KNUTH_FACTOR) % MAX_SPAN_ID) <= self._sampling_id_threshold
124+
return ((span_id * KNUTH_FACTOR) % MAX_SPAN_ID) <= self._sampling_id_threshold
145125

146-
def match(self, span):
147-
# type: (Span) -> bool
126+
def match(self, name: Optional[str], service: Optional[str]) -> bool:
148127
"""Determines if the span's service and name match the configured patterns"""
149-
name = span.name
150-
service = span.service
151128
# If a span lacks a name and service, we can't match on it
152129
if service is None and name is None:
153130
return False
@@ -169,14 +146,6 @@ def match(self, span):
169146
name_match = self._name_matcher.match(name)
170147
return service_match and name_match
171148

172-
def apply_span_sampling_tags(self, span):
173-
# type: (Span) -> None
174-
span.set_metric(_SINGLE_SPAN_SAMPLING_MECHANISM, SamplingMechanism.SPAN_SAMPLING_RULE)
175-
span.set_metric(_SINGLE_SPAN_SAMPLING_RATE, self._sample_rate)
176-
# Only set this tag if it's not the default -1
177-
if self._max_per_second != _SINGLE_SPAN_SAMPLING_MAX_PER_SEC_NO_LIMIT:
178-
span.set_metric(_SINGLE_SPAN_SAMPLING_MAX_PER_SEC, self._max_per_second)
179-
180149

181150
def get_span_sampling_rules() -> List[SpanSamplingRule]:
182151
json_rules = _get_span_sampling_json()
@@ -258,38 +227,3 @@ def _check_unsupported_pattern(string: str) -> None:
258227
for char in string:
259228
if char in unsupported_chars and config._raise:
260229
raise ValueError("Unsupported Glob pattern found, character:%r is not supported" % char)
261-
262-
263-
def is_single_span_sampled(span):
264-
# type: (Span) -> bool
265-
return span.get_metric(_SINGLE_SPAN_SAMPLING_MECHANISM) == SamplingMechanism.SPAN_SAMPLING_RULE
266-
267-
268-
def _set_sampling_tags(span, sampled, sample_rate, mechanism):
269-
# type: (Span, bool, float, int) -> None
270-
# Set the sampling mechanism
271-
set_sampling_decision_maker(span.context, mechanism)
272-
# Set the sampling psr rate
273-
if mechanism in (
274-
SamplingMechanism.LOCAL_USER_TRACE_SAMPLING_RULE,
275-
SamplingMechanism.REMOTE_USER_TRACE_SAMPLING_RULE,
276-
SamplingMechanism.REMOTE_DYNAMIC_TRACE_SAMPLING_RULE,
277-
):
278-
span.set_metric(_SAMPLING_RULE_DECISION, sample_rate)
279-
elif mechanism == SamplingMechanism.AGENT_RATE_BY_SERVICE:
280-
span.set_metric(_SAMPLING_AGENT_DECISION, sample_rate)
281-
# Set the sampling priority
282-
priorities = SAMPLING_MECHANISM_TO_PRIORITIES[mechanism]
283-
priority_index = _KEEP_PRIORITY_INDEX if sampled else _REJECT_PRIORITY_INDEX
284-
span.context.sampling_priority = priorities[priority_index]
285-
286-
287-
def _get_highest_precedence_rule_matching(span, rules):
288-
# type: (Span, List[SamplingRule]) -> Optional[SamplingRule]
289-
if not rules:
290-
return None
291-
292-
for rule in rules:
293-
if rule.matches(span):
294-
return rule
295-
return None

ddtrace/propagation/http.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ddtrace._trace.span import _get_64_lowest_order_bits_as_int
1818
from ddtrace._trace.span import _MetaDictType
1919
from ddtrace.appsec._constants import APPSEC
20+
from ddtrace.internal.constants import SamplingMechanism
2021
from ddtrace.internal.core import dispatch
2122
from ddtrace.settings._config import config
2223
from ddtrace.settings.asm import config as asm_config
@@ -48,7 +49,6 @@
4849
from ..internal.constants import W3C_TRACESTATE_KEY
4950
from ..internal.logger import get_logger
5051
from ..internal.sampling import SAMPLING_DECISION_TRACE_TAG_KEY
51-
from ..internal.sampling import SamplingMechanism
5252
from ..internal.sampling import validate_sampling_decision
5353
from ..internal.utils.http import w3c_tracestate_add_p
5454
from ._utils import get_wsgi_header

tests/integration/test_sampling.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def test_rate_limiter_on_spans(tracer):
233233
Ensure that the rate limiter is applied to spans
234234
"""
235235
from ddtrace._trace.sampler import DatadogSampler
236-
from ddtrace.internal.sampling import SamplingRule
236+
from ddtrace._trace.sampling_rule import SamplingRule
237237
from ddtrace.trace import tracer
238238

239239
# Rate limit is only applied if a sample rate or trace sample rule is set

tests/tracer/test_processors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
from ddtrace.constants import USER_REJECT
2222
from ddtrace.ext import SpanTypes
2323
from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS
24+
from ddtrace.internal.constants import SamplingMechanism
2425
from ddtrace.internal.processor.endpoint_call_counter import EndpointCallCounterProcessor
25-
from ddtrace.internal.sampling import SamplingMechanism
2626
from ddtrace.internal.sampling import SpanSamplingRule
2727
from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE
2828
from ddtrace.trace import Context

0 commit comments

Comments
 (0)