Skip to content

Commit 0f8eb6c

Browse files
authored
Use a delegate sampler for each possible case in ParentBased Sampler (#1440)
1 parent 268cfbf commit 0f8eb6c

3 files changed

Lines changed: 176 additions & 29 deletions

File tree

opentelemetry-sdk/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Add meter reference to observers
66
([#1425](https://github.com/open-telemetry/opentelemetry-python/pull/1425))
7+
- Add local/remote samplers to parent based sampler
8+
([#1440](https://github.com/open-telemetry/opentelemetry-python/pull/1440))
79
- Add `fields` to propagators
810
([#1374](https://github.com/open-telemetry/opentelemetry-python/pull/1374))
911

opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
2828
A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given.
2929
30-
If the span being sampled has a parent, `ParentBased` will respect the parent span's sampling result. Otherwise, it returns the sampling result from the given delegate sampler.
30+
If the span being sampled has a parent, `ParentBased` will respect the parent delegate sampler. Otherwise, it returns the sampling result from the given root sampler.
3131
3232
Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 <https://github.com/open-telemetry/oteps/pull/115>`_).
3333
@@ -160,6 +160,13 @@ def get_description(self) -> str:
160160
return "AlwaysOnSampler"
161161

162162

163+
ALWAYS_OFF = StaticSampler(Decision.DROP)
164+
"""Sampler that never samples spans, regardless of the parent span's sampling decision."""
165+
166+
ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE)
167+
"""Sampler that always samples spans, regardless of the parent span's sampling decision."""
168+
169+
163170
class TraceIdRatioBased(Sampler):
164171
"""
165172
Sampler that makes sampling decisions probabalistically based on `rate`,
@@ -218,16 +225,33 @@ def get_description(self) -> str:
218225

219226
class ParentBased(Sampler):
220227
"""
221-
If a parent is set, follows the same sampling decision as the parent.
222-
Otherwise, uses the delegate provided at initialization to make a
228+
If a parent is set, applies the respective delegate sampler.
229+
Otherwise, uses the root provided at initialization to make a
223230
decision.
224231
225232
Args:
226-
delegate: The delegate sampler to use if parent is not set.
233+
root: Sampler called for spans with no parent (root spans).
234+
remote_parent_sampled: Sampler called for a remote sampled parent.
235+
remote_parent_not_sampled: Sampler called for a remote parent that is
236+
not sampled.
237+
local_parent_sampled: Sampler called for a local sampled parent.
238+
local_parent_not_sampled: Sampler called for a local parent that is
239+
not sampled.
227240
"""
228241

229-
def __init__(self, delegate: Sampler):
230-
self._delegate = delegate
242+
def __init__(
243+
self,
244+
root: Sampler,
245+
remote_parent_sampled: Sampler = ALWAYS_ON,
246+
remote_parent_not_sampled: Sampler = ALWAYS_OFF,
247+
local_parent_sampled: Sampler = ALWAYS_ON,
248+
local_parent_not_sampled: Sampler = ALWAYS_OFF,
249+
):
250+
self._root = root
251+
self._remote_parent_sampled = remote_parent_sampled
252+
self._remote_parent_not_sampled = remote_parent_not_sampled
253+
self._local_parent_sampled = local_parent_sampled
254+
self._local_parent_not_sampled = local_parent_not_sampled
231255

232256
def should_sample(
233257
self,
@@ -241,15 +265,22 @@ def should_sample(
241265
parent_span_context = get_current_span(
242266
parent_context
243267
).get_span_context()
244-
# respect the sampling flag of the parent if present
268+
# default to the root sampler
269+
sampler = self._root
270+
# respect the sampling and remote flag of the parent if present
245271
if parent_span_context is not None and parent_span_context.is_valid:
246-
decision = Decision.RECORD_AND_SAMPLE
247-
if not parent_span_context.trace_flags.sampled:
248-
decision = Decision.DROP
249-
attributes = None
250-
return SamplingResult(decision, attributes, trace_state)
251-
252-
return self._delegate.should_sample(
272+
if parent_span_context.is_remote:
273+
if parent_span_context.trace_flags.sampled:
274+
sampler = self._remote_parent_sampled
275+
else:
276+
sampler = self._remote_parent_not_sampled
277+
else:
278+
if parent_span_context.trace_flags.sampled:
279+
sampler = self._local_parent_sampled
280+
else:
281+
sampler = self._local_parent_not_sampled
282+
283+
return sampler.should_sample(
253284
parent_context=parent_context,
254285
trace_id=trace_id,
255286
name=name,
@@ -259,14 +290,17 @@ def should_sample(
259290
)
260291

261292
def get_description(self):
262-
return "ParentBased{{{}}}".format(self._delegate.get_description())
263-
264-
265-
ALWAYS_OFF = StaticSampler(Decision.DROP)
266-
"""Sampler that never samples spans, regardless of the parent span's sampling decision."""
293+
return (
294+
"ParentBased{{root:{},remoteParentSampled:{},remoteParentNotSampled:{},"
295+
"localParentSampled:{},localParentNotSampled:{}}}".format(
296+
self._root.get_description(),
297+
self._remote_parent_sampled.get_description(),
298+
self._remote_parent_not_sampled.get_description(),
299+
self._local_parent_sampled.get_description(),
300+
self._local_parent_not_sampled.get_description(),
301+
)
302+
)
267303

268-
ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE)
269-
"""Sampler that always samples spans, regardless of the parent span's sampling decision."""
270304

271305
DEFAULT_OFF = ParentBased(ALWAYS_OFF)
272306
"""Sampler that respects its parent span's sampling decision, but otherwise never samples."""

opentelemetry-sdk/tests/trace/test_sampling.py

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,15 @@ def test_probability_sampler_limits(self):
311311
almost_almost_always_on.bound, 0xFFFFFFFFFFFFFFFF,
312312
)
313313

314+
# pylint:disable=too-many-statements
314315
def exec_parent_based(self, parent_sampling_context):
315316
trace_state = trace.TraceState({"key": "value"})
316317
sampler = sampling.ParentBased(sampling.ALWAYS_ON)
318+
# Check that the sampling decision matches the parent context if given
317319
with parent_sampling_context(
318320
self._create_parent_span(trace_flags=TO_DEFAULT)
319321
) as context:
320-
# Check that the sampling decision matches the parent context if given
322+
# local, not sampled
321323
not_sampled_result = sampler.should_sample(
322324
context,
323325
0x7FFFFFFFFFFFFFFF,
@@ -329,11 +331,101 @@ def exec_parent_based(self, parent_sampling_context):
329331
self.assertEqual(not_sampled_result.attributes, {})
330332
self.assertEqual(not_sampled_result.trace_state, trace_state)
331333

334+
with parent_sampling_context(
335+
self._create_parent_span(trace_flags=TO_DEFAULT)
336+
) as context:
337+
sampler = sampling.ParentBased(
338+
root=sampling.ALWAYS_OFF,
339+
local_parent_not_sampled=sampling.ALWAYS_ON,
340+
)
341+
# local, not sampled -> opposite sampler
342+
sampled_result = sampler.should_sample(
343+
context,
344+
0x7FFFFFFFFFFFFFFF,
345+
"unsampled parent, sampling on",
346+
attributes={"sampled": "false"},
347+
trace_state=trace_state,
348+
)
349+
self.assertTrue(sampled_result.decision.is_sampled())
350+
self.assertEqual(sampled_result.attributes, {"sampled": "false"})
351+
self.assertEqual(sampled_result.trace_state, trace_state)
352+
353+
with parent_sampling_context(
354+
self._create_parent_span(trace_flags=TO_SAMPLED)
355+
) as context:
356+
sampler = sampling.ParentBased(sampling.ALWAYS_OFF)
357+
# local, sampled
358+
sampled_result = sampler.should_sample(
359+
context,
360+
0x8000000000000000,
361+
"sampled parent, sampling off",
362+
attributes={"sampled": "true"},
363+
trace_state=trace_state,
364+
)
365+
self.assertTrue(sampled_result.decision.is_sampled())
366+
self.assertEqual(sampled_result.attributes, {"sampled": "true"})
367+
self.assertEqual(sampled_result.trace_state, trace_state)
368+
332369
with parent_sampling_context(
333370
self._create_parent_span(trace_flags=TO_SAMPLED)
334371
) as context:
335-
sampler2 = sampling.ParentBased(sampling.ALWAYS_OFF)
336-
sampled_result = sampler2.should_sample(
372+
sampler = sampling.ParentBased(
373+
root=sampling.ALWAYS_ON,
374+
local_parent_sampled=sampling.ALWAYS_OFF,
375+
)
376+
# local, sampled -> opposite sampler
377+
not_sampled_result = sampler.should_sample(
378+
context,
379+
0x7FFFFFFFFFFFFFFF,
380+
"unsampled parent, sampling on",
381+
attributes={"sampled": "false"},
382+
trace_state=trace_state,
383+
)
384+
self.assertFalse(not_sampled_result.decision.is_sampled())
385+
self.assertEqual(not_sampled_result.attributes, {})
386+
self.assertEqual(not_sampled_result.trace_state, trace_state)
387+
388+
with parent_sampling_context(
389+
self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True)
390+
) as context:
391+
sampler = sampling.ParentBased(sampling.ALWAYS_ON)
392+
# remote, not sampled
393+
not_sampled_result = sampler.should_sample(
394+
context,
395+
0x7FFFFFFFFFFFFFFF,
396+
"unsampled parent, sampling on",
397+
attributes={"sampled": "false"},
398+
trace_state=trace_state,
399+
)
400+
self.assertFalse(not_sampled_result.decision.is_sampled())
401+
self.assertEqual(not_sampled_result.attributes, {})
402+
self.assertEqual(not_sampled_result.trace_state, trace_state)
403+
404+
with parent_sampling_context(
405+
self._create_parent_span(trace_flags=TO_DEFAULT, is_remote=True)
406+
) as context:
407+
sampler = sampling.ParentBased(
408+
root=sampling.ALWAYS_OFF,
409+
remote_parent_not_sampled=sampling.ALWAYS_ON,
410+
)
411+
# remote, not sampled -> opposite sampler
412+
sampled_result = sampler.should_sample(
413+
context,
414+
0x7FFFFFFFFFFFFFFF,
415+
"unsampled parent, sampling on",
416+
attributes={"sampled": "false"},
417+
trace_state=trace_state,
418+
)
419+
self.assertTrue(sampled_result.decision.is_sampled())
420+
self.assertEqual(sampled_result.attributes, {"sampled": "false"})
421+
self.assertEqual(sampled_result.trace_state, trace_state)
422+
423+
with parent_sampling_context(
424+
self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True)
425+
) as context:
426+
sampler = sampling.ParentBased(sampling.ALWAYS_OFF)
427+
# remote, sampled
428+
sampled_result = sampler.should_sample(
337429
context,
338430
0x8000000000000000,
339431
"sampled parent, sampling off",
@@ -344,10 +436,29 @@ def exec_parent_based(self, parent_sampling_context):
344436
self.assertEqual(sampled_result.attributes, {"sampled": "true"})
345437
self.assertEqual(sampled_result.trace_state, trace_state)
346438

347-
# for root span follow decision of delegate sampler
439+
with parent_sampling_context(
440+
self._create_parent_span(trace_flags=TO_SAMPLED, is_remote=True)
441+
) as context:
442+
sampler = sampling.ParentBased(
443+
root=sampling.ALWAYS_ON,
444+
remote_parent_sampled=sampling.ALWAYS_OFF,
445+
)
446+
# remote, sampled -> opposite sampler
447+
not_sampled_result = sampler.should_sample(
448+
context,
449+
0x7FFFFFFFFFFFFFFF,
450+
"unsampled parent, sampling on",
451+
attributes={"sampled": "false"},
452+
trace_state=trace_state,
453+
)
454+
self.assertFalse(not_sampled_result.decision.is_sampled())
455+
self.assertEqual(not_sampled_result.attributes, {})
456+
self.assertEqual(not_sampled_result.trace_state, trace_state)
457+
458+
# for root span follow decision of root sampler
348459
with parent_sampling_context(trace.INVALID_SPAN) as context:
349-
sampler3 = sampling.ParentBased(sampling.ALWAYS_OFF)
350-
not_sampled_result = sampler3.should_sample(
460+
sampler = sampling.ParentBased(sampling.ALWAYS_OFF)
461+
not_sampled_result = sampler.should_sample(
351462
context,
352463
0x8000000000000000,
353464
"parent, sampling off",
@@ -359,8 +470,8 @@ def exec_parent_based(self, parent_sampling_context):
359470
self.assertEqual(not_sampled_result.trace_state, trace_state)
360471

361472
with parent_sampling_context(trace.INVALID_SPAN) as context:
362-
sampler4 = sampling.ParentBased(sampling.ALWAYS_ON)
363-
sampled_result = sampler4.should_sample(
473+
sampler = sampling.ParentBased(sampling.ALWAYS_ON)
474+
sampled_result = sampler.should_sample(
364475
context,
365476
0x8000000000000000,
366477
"no parent, sampling on",

0 commit comments

Comments
 (0)