1# Copyright 2018, OpenCensus Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16from opencensus.trace.span_context import INVALID_SPAN_ID, SpanContext
17from opencensus.trace.trace_options import TraceOptions
18
19_STATE_HEADER_KEY = 'b3'
20_TRACE_ID_KEY = 'x-b3-traceid'
21_SPAN_ID_KEY = 'x-b3-spanid'
22_SAMPLED_KEY = 'x-b3-sampled'
23
24
25class B3FormatPropagator(object):
26    """Propagator for the B3 HTTP header format.
27
28    See: https://github.com/openzipkin/b3-propagation
29    """
30
31    def from_headers(self, headers):
32        """Generate a SpanContext object from B3 propagation headers.
33
34        :type headers: dict
35        :param headers: HTTP request headers.
36
37        :rtype: :class:`~opencensus.trace.span_context.SpanContext`
38        :returns: SpanContext generated from B3 propagation headers.
39        """
40        if headers is None:
41            return SpanContext(from_header=False)
42
43        trace_id, span_id, sampled = None, None, None
44
45        state = headers.get(_STATE_HEADER_KEY)
46        if state:
47            fields = state.split('-', 4)
48
49            if len(fields) == 1:
50                sampled = fields[0]
51            elif len(fields) == 2:
52                trace_id, span_id = fields
53            elif len(fields) == 3:
54                trace_id, span_id, sampled = fields
55            elif len(fields) == 4:
56                trace_id, span_id, sampled, _parent_span_id = fields
57            else:
58                return SpanContext(from_header=False)
59        else:
60            trace_id = headers.get(_TRACE_ID_KEY)
61            span_id = headers.get(_SPAN_ID_KEY)
62            sampled = headers.get(_SAMPLED_KEY)
63
64        if sampled is not None:
65            # The specification encodes an enabled tracing decision as "1".
66            # In the wild pre-standard implementations might still send "true".
67            # "d" is set in the single header case when debugging is enabled.
68            sampled = sampled.lower() in ('1', 'd', 'true')
69        else:
70            # If there's no incoming sampling decision, it was deferred to us.
71            # Even though we set it to False here, we might still sample
72            # depending on the tracer configuration.
73            sampled = False
74
75        trace_options = TraceOptions()
76        trace_options.set_enabled(sampled)
77
78        # TraceId and SpanId headers both have to exist
79        if not trace_id or not span_id:
80            return SpanContext(trace_options=trace_options)
81
82        # Convert 64-bit trace ids to 128-bit
83        if len(trace_id) == 16:
84            trace_id = '0'*16 + trace_id
85
86        span_context = SpanContext(
87            trace_id=trace_id,
88            span_id=span_id,
89            trace_options=trace_options,
90            from_header=True
91        )
92
93        return span_context
94
95    def to_headers(self, span_context):
96        """Convert a SpanContext object to B3 propagation headers.
97
98        :type span_context:
99            :class:`~opencensus.trace.span_context.SpanContext`
100        :param span_context: SpanContext object.
101
102        :rtype: dict
103        :returns: B3 propagation headers.
104        """
105
106        if not span_context.span_id:
107            span_id = INVALID_SPAN_ID
108        else:
109            span_id = span_context.span_id
110
111        sampled = span_context.trace_options.enabled
112
113        return {
114            _TRACE_ID_KEY: span_context.trace_id,
115            _SPAN_ID_KEY: span_id,
116            _SAMPLED_KEY: '1' if sampled else '0'
117        }
118