Skip to content

Commit f029bcd

Browse files
kornholireyang
authored andcommitted
Implement B3 propagation (census-instrumentation#265)
* Implement B3 propagation * Update copyright year * Clarify comments a little bit * Fix to_headers * Implement deserialization for single-header b3 format * Adjust how TraceOptions is constructed * Formatting
1 parent cc41b35 commit f029bcd

3 files changed

Lines changed: 296 additions & 2 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
16+
from opencensus.trace.span_context import SpanContext, INVALID_SPAN_ID
17+
from 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+
25+
class 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+
if len(sampled) != 1:
66+
return SpanContext(from_header=False)
67+
68+
sampled = sampled in ('1', 'd')
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+
}

opencensus/trace/span_context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import logging
1818
import re
19+
import six
1920
import uuid
2021

2122
from opencensus.trace import trace_options
@@ -100,7 +101,7 @@ def _check_span_id(self, span_id):
100101
"""
101102
if span_id is None:
102103
return None
103-
assert isinstance(span_id, str)
104+
assert isinstance(span_id, six.string_types)
104105

105106
if span_id is INVALID_SPAN_ID:
106107
logging.warning(
@@ -130,7 +131,7 @@ def _check_trace_id(self, trace_id):
130131
:rtype: str
131132
:returns: Trace_id for the current context.
132133
"""
133-
assert isinstance(trace_id, str)
134+
assert isinstance(trace_id, six.string_types)
134135

135136
if trace_id is _INVALID_TRACE_ID:
136137
logging.warning(
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
import unittest
16+
import mock
17+
18+
from opencensus.trace.span_context import INVALID_SPAN_ID
19+
from opencensus.trace.propagation import b3_format
20+
21+
22+
class TestB3FormatPropagator(unittest.TestCase):
23+
24+
def test_from_headers_no_headers(self):
25+
propagator = b3_format.B3FormatPropagator()
26+
span_context = propagator.from_headers(None)
27+
28+
self.assertFalse(span_context.from_header)
29+
30+
def test_from_headers_keys_exist(self):
31+
test_trace_id = '6e0c63257de34c92bf9efcd03927272e'
32+
test_span_id = '00f067aa0ba902b7'
33+
test_sampled = '1'
34+
35+
headers = {
36+
b3_format._TRACE_ID_KEY: test_trace_id,
37+
b3_format._SPAN_ID_KEY: test_span_id,
38+
b3_format._SAMPLED_KEY: test_sampled,
39+
}
40+
41+
propagator = b3_format.B3FormatPropagator()
42+
span_context = propagator.from_headers(headers)
43+
44+
self.assertEqual(span_context.trace_id, test_trace_id)
45+
self.assertEqual(span_context.span_id, test_span_id)
46+
self.assertEqual(
47+
span_context.trace_options.enabled,
48+
bool(test_sampled)
49+
)
50+
51+
def test_from_headers_keys_not_exist(self):
52+
propagator = b3_format.B3FormatPropagator()
53+
span_context = propagator.from_headers({})
54+
55+
self.assertIsNotNone(span_context.trace_id)
56+
self.assertIsNone(span_context.span_id)
57+
self.assertFalse(span_context.trace_options.enabled)
58+
59+
def test_from_headers_64bit_traceid(self):
60+
test_trace_id = 'bf9efcd03927272e'
61+
test_span_id = '00f067aa0ba902b7'
62+
63+
headers = {
64+
b3_format._TRACE_ID_KEY: test_trace_id,
65+
b3_format._SPAN_ID_KEY: test_span_id,
66+
}
67+
68+
propagator = b3_format.B3FormatPropagator()
69+
span_context = propagator.from_headers(headers)
70+
71+
converted_trace_id = "0"*16 + test_trace_id
72+
73+
self.assertEqual(span_context.trace_id, converted_trace_id)
74+
self.assertEqual(span_context.span_id, test_span_id)
75+
76+
def test_to_headers_has_span_id(self):
77+
test_trace_id = '6e0c63257de34c92bf9efcd03927272e'
78+
test_span_id = '00f067aa0ba902b7'
79+
test_options = '1'
80+
81+
span_context = mock.Mock()
82+
span_context.trace_id = test_trace_id
83+
span_context.span_id = test_span_id
84+
span_context.trace_options.trace_options_byte = test_options
85+
86+
propagator = b3_format.B3FormatPropagator()
87+
headers = propagator.to_headers(span_context)
88+
89+
self.assertEqual(headers[b3_format._TRACE_ID_KEY], test_trace_id)
90+
self.assertEqual(headers[b3_format._SPAN_ID_KEY], test_span_id)
91+
self.assertEqual(headers[b3_format._SAMPLED_KEY], test_options)
92+
93+
def test_to_headers_no_span_id(self):
94+
test_trace_id = '6e0c63257de34c92bf9efcd03927272e'
95+
test_options = '1'
96+
97+
span_context = mock.Mock()
98+
span_context.trace_id = test_trace_id
99+
span_context.span_id = None
100+
span_context.trace_options.trace_options_byte = test_options
101+
102+
propagator = b3_format.B3FormatPropagator()
103+
headers = propagator.to_headers(span_context)
104+
105+
self.assertEqual(headers[b3_format._TRACE_ID_KEY], test_trace_id)
106+
self.assertEqual(headers.get(b3_format._SPAN_ID_KEY), INVALID_SPAN_ID)
107+
self.assertEqual(headers[b3_format._SAMPLED_KEY], test_options)
108+
109+
def test_from_single_header_keys_exist(self):
110+
trace_id = "80f198ee56343ba864fe8b2a57d3eff7"
111+
span_id = "e457b5a2e4d86bd1"
112+
113+
headers = {
114+
'b3': "{}-{}-d-05e3ac9a4f6e3b90".format(trace_id, span_id)
115+
}
116+
propagator = b3_format.B3FormatPropagator()
117+
span_context = propagator.from_headers(headers)
118+
119+
self.assertEqual(span_context.trace_id, trace_id)
120+
self.assertEqual(span_context.span_id, span_id)
121+
self.assertEqual(span_context.trace_options.enabled, True)
122+
123+
def test_from_headers_invalid_single_header(self):
124+
headers = {
125+
'b3': "01234567890123456789012345678901;o=1"
126+
}
127+
propagator = b3_format.B3FormatPropagator()
128+
span_context = propagator.from_headers(headers)
129+
130+
self.assertFalse(span_context.from_header)
131+
132+
def test_from_headers_invalid_single_header_fields(self):
133+
headers = {
134+
'b3': "a-b-c-d-e-f-g"
135+
}
136+
propagator = b3_format.B3FormatPropagator()
137+
span_context = propagator.from_headers(headers)
138+
139+
self.assertFalse(span_context.from_header)
140+
141+
def test_from_single_header_deny_sampling(self):
142+
headers = {
143+
'b3': "0"
144+
}
145+
propagator = b3_format.B3FormatPropagator()
146+
span_context = propagator.from_headers(headers)
147+
148+
self.assertEqual(span_context.trace_options.enabled, False)
149+
150+
def test_from_single_header_defer_sampling(self):
151+
trace_id = "80f198ee56343ba864fe8b2a57d3eff7"
152+
span_id = "e457b5a2e4d86bd1"
153+
headers = {
154+
'b3': "{}-{}".format(trace_id, span_id)
155+
}
156+
propagator = b3_format.B3FormatPropagator()
157+
span_context = propagator.from_headers(headers)
158+
159+
self.assertEqual(span_context.trace_id, trace_id)
160+
self.assertEqual(span_context.span_id, span_id)
161+
162+
def test_from_single_header_precedence(self):
163+
headers = {
164+
'b3': "80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1",
165+
'X-B3-TraceId': '6e0c63257de34c92bf9efcd03927272e',
166+
'X-B3-SpanId': '00f067aa0ba902b7'
167+
}
168+
propagator = b3_format.B3FormatPropagator()
169+
span_context = propagator.from_headers(headers)
170+
171+
self.assertEqual(
172+
span_context.trace_id,
173+
"80f198ee56343ba864fe8b2a57d3eff7"
174+
)
175+
self.assertEqual(span_context.span_id, "e457b5a2e4d86bd1")
176+
self.assertEqual(span_context.trace_options.enabled, True)

0 commit comments

Comments
 (0)