1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import abc
1516import typing
1617from re import compile as re_compile
1718
3031from 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+
3390class 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