Skip to content

Commit a2faf10

Browse files
committed
Addressing issue #4793 opentelemetry-propagator-b3 doesn't allow the receiver to make sampling decision when X-B3-Sampled header is absent. No unit test update yet.
1 parent 75c8d67 commit a2faf10

File tree

1 file changed

+93
-6
lines changed
  • propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3

1 file changed

+93
-6
lines changed

propagator/opentelemetry-propagator-b3/src/opentelemetry/propagators/b3/__init__.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import abc
1516
import typing
1617
from re import compile as re_compile
1718

@@ -30,6 +31,62 @@
3031
from opentelemetry.trace import format_span_id, format_trace_id
3132

3233

34+
class PropagatorSamplingArbiterInterface(abc.ABC):
35+
"""Interface for classes that can arbitrate the default sampling decision whenever
36+
explicit sampling information is absent from the carrier.
37+
38+
NOTE: This is consistent with B3 spec, which calls to "defer the decision
39+
to the receiver" whenever explicit sampling is absent from the carrier
40+
(https://github.com/openzipkin/b3-propagation)
41+
42+
NOTE: This is distinct from the OpenTelemetry `Sampler` interface, which
43+
makes sampling decisions when starting new spans. This interface is
44+
specifically for making sampling decisions when `B3MultiFormat` determines
45+
that sampled state is missing from carrier. And this interface accepts a
46+
different set of arguments than the `Sampler` interface.
47+
"""
48+
49+
@abc.abstractmethod
50+
def should_sample(
51+
self,
52+
carrier: CarrierT,
53+
context: typing.Optional[Context],
54+
getter: Getter,
55+
) -> bool:
56+
"""Determine whether the incoming trace should be sampled. This is invoked only when the
57+
carrier does not contain explicit sampling information.
58+
59+
Same parameters as were passed to `B3MultiFormat.extract`
60+
`
61+
:return: The sampling decision: True to sample, False to not sample.
62+
"""
63+
pass
64+
65+
66+
class StaticPropagatorSamplingArbiter(PropagatorSamplingArbiterInterface):
67+
"""A simple stateless PropagatorSamplingArbiterInterface implementation that always returns
68+
the same static sampling decision.
69+
"""
70+
71+
def __init__(self, should_sample: bool):
72+
self._should_sample = should_sample
73+
74+
def should_sample(
75+
self,
76+
carrier: CarrierT,
77+
context: typing.Optional[Context],
78+
getter: Getter,
79+
) -> bool:
80+
return self._should_sample
81+
82+
83+
# A stateless arbiter that always defaults missing sample state to sampling Deny
84+
default_to_deny_arbiter = StaticPropagatorSamplingArbiter(False)
85+
86+
# A stateless arbiter that always defaults missing sample state to sampling Accept
87+
default_to_accept_arbiter = StaticPropagatorSamplingArbiter(True)
88+
89+
3390
class B3MultiFormat(TextMapPropagator):
3491
"""Propagator for the B3 HTTP multi-header format.
3592
@@ -46,27 +103,50 @@ class B3MultiFormat(TextMapPropagator):
46103
_trace_id_regex = re_compile(r"[\da-fA-F]{16}|[\da-fA-F]{32}")
47104
_span_id_regex = re_compile(r"[\da-fA-F]{16}")
48105

106+
def __init__(
107+
self, *,
108+
sampling_arbiter: typing.Optional[PropagatorSamplingArbiterInterface] = None):
109+
"""
110+
NOTE: The B3 spec calls for "defer the decision to the receiver" whenever
111+
explicit sampling information is absent from the carrier; in this scenario
112+
we will invoke the given Propagator Sampling Arbiter to make the sampling
113+
decision. (https://github.com/openzipkin/b3-propagation)
114+
115+
:param sampling_arbiter: A user-supplied arbiter instance to decide the sampled state
116+
for our `extract()` method whenever sampling information is absent from carrier.
117+
If None, falls back to the legacy behavior for backward compatibility: defaults
118+
decision to Accept for single-header and to Deny for multi-header (in the
119+
event explicit sampling information is absent from the carrier).
120+
"""
121+
super().__init__()
122+
if sampling_arbiter is not None:
123+
self._single_sampling_arbiter = self._multi_sampling_arbiter = sampling_arbiter
124+
else:
125+
# This reflects the original hard-coded defaults for backward compatibility
126+
self._single_sampling_arbiter = default_to_accept_arbiter
127+
self._multi_sampling_arbiter = default_to_deny_arbiter
128+
49129
def extract(
50130
self,
51131
carrier: CarrierT,
52132
context: typing.Optional[Context] = None,
53133
getter: Getter = default_getter,
54134
) -> Context:
135+
""" Extracts SpanContext from the carrier."""
136+
original_context = context
55137
if context is None:
56138
context = Context()
57139
trace_id = trace.INVALID_TRACE_ID
58140
span_id = trace.INVALID_SPAN_ID
59-
sampled = "0"
141+
sampled = None
60142
flags = None
61143

62144
single_header = _extract_first_element(
63145
getter.get(carrier, self.SINGLE_HEADER_KEY)
64146
)
65147
if single_header:
66-
# The b3 spec calls for the sampling state to be
67-
# "deferred", which is unspecified. This concept does not
68-
# translate to SpanContext, so we set it as recorded.
69-
sampled = "1"
148+
sampling_arbiter = self._single_sampling_arbiter
149+
70150
fields = single_header.split("-", 4)
71151

72152
if len(fields) == 1:
@@ -78,6 +158,8 @@ def extract(
78158
elif len(fields) == 4:
79159
trace_id, span_id, sampled, _ = fields
80160
else:
161+
sampling_arbiter = self._multi_sampling_arbiter
162+
81163
trace_id = (
82164
_extract_first_element(getter.get(carrier, self.TRACE_ID_KEY))
83165
or trace_id
@@ -112,11 +194,16 @@ def extract(
112194
# header is set to allow.
113195
if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1":
114196
options |= trace.TraceFlags.SAMPLED
197+
elif sampled is None and sampling_arbiter.should_sample(
198+
carrier=carrier,
199+
context=original_context,
200+
getter=getter):
201+
options |= trace.TraceFlags.SAMPLED
115202

116203
return trace.set_span_in_context(
117204
trace.NonRecordingSpan(
118205
trace.SpanContext(
119-
# trace an span ids are encoded in hex, so must be converted
206+
# trace and span ids are encoded in hex, so must be converted
120207
trace_id=trace_id,
121208
span_id=span_id,
122209
is_remote=True,

0 commit comments

Comments
 (0)