Skip to content

Commit 719e21a

Browse files
author
alrex
authored
Merge pull request #80 from lightstep/b3_format
Add support for B3 headers
2 parents b5a6930 + e113707 commit 719e21a

File tree

3 files changed

+290
-0
lines changed

3 files changed

+290
-0
lines changed

examples/http/context_in_headers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import opentracing
3030
import opentracing.ext.tags
3131
import lightstep
32+
from opentracing import Format
33+
from lightstep.b3_propagator import B3Propagator
3234

3335

3436
class RemoteHandler(BaseHTTPRequestHandler):
@@ -145,6 +147,12 @@ def lightstep_tracer_from_args():
145147
if __name__ == '__main__':
146148
with lightstep_tracer_from_args() as tracer:
147149
opentracing.tracer = tracer
150+
151+
opentracing.tracer.register_propagator(Format.TEXT_MAP, B3Propagator())
152+
opentracing.tracer.register_propagator(
153+
Format.HTTP_HEADERS, B3Propagator()
154+
)
155+
148156
global _exit_code
149157
_exit_code = 0
150158

lightstep/b3_propagator.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from warnings import warn
2+
from logging import getLogger
3+
4+
from basictracer.propagator import Propagator
5+
from basictracer.context import SpanContext
6+
from opentracing import SpanContext as OTSpanContext
7+
from opentracing import SpanContextCorruptedException
8+
9+
_LOG = getLogger(__name__)
10+
_SINGLE_HEADER = "b3"
11+
# Lower case is used here as the B3 specification recommends
12+
_TRACEID = "x-b3-traceid"
13+
_SPANID = "x-b3-spanid"
14+
_PARENTSPANID = "x-b3-parentspanid"
15+
_SAMPLED = "x-b3-sampled"
16+
_FLAGS = "x-b3-flags"
17+
18+
19+
class B3Propagator(Propagator):
20+
"""
21+
Propagator for the B3 HTTP header format.
22+
23+
See: https://github.com/openzipkin/b3-propagation
24+
"""
25+
26+
def inject(self, span_context, carrier):
27+
28+
traceid = span_context.trace_id
29+
spanid = span_context.span_id
30+
31+
baggage = span_context.baggage
32+
33+
parentspanid = baggage.pop(_PARENTSPANID, None)
34+
if parentspanid is not None:
35+
carrier[_PARENTSPANID] = parentspanid
36+
37+
flags = baggage.pop(_FLAGS, None)
38+
if flags is not None:
39+
carrier[_FLAGS] = flags
40+
41+
sampled = baggage.pop(_SAMPLED, None)
42+
if sampled is not None:
43+
if flags == 1:
44+
_LOG.warning(
45+
"x-b3-flags: 1 implies x-b3-sampled: 1, not sending "
46+
"the value of x-b3-sampled"
47+
)
48+
else:
49+
if sampled in [True, False]:
50+
warn(
51+
"The value of x-b3-sampled should "
52+
"be {} instead of {}".format(
53+
int(sampled), sampled
54+
)
55+
)
56+
carrier[_SAMPLED] = int(sampled)
57+
58+
if sampled is flags is (traceid and spanid) is None:
59+
warn(
60+
"If not propagating only the sampling state, traceid and "
61+
"spanid must be defined"
62+
)
63+
64+
carrier.update(baggage)
65+
66+
if traceid is not None:
67+
carrier[_TRACEID] = format(traceid, "x").ljust(32, "0")
68+
if spanid is not None:
69+
carrier[_SPANID] = format(spanid, "016x")
70+
71+
def extract(self, carrier):
72+
73+
case_insensitive_carrier = {}
74+
for key, value in carrier.items():
75+
for b3_key in [
76+
_SINGLE_HEADER,
77+
_TRACEID,
78+
_SPANID,
79+
_PARENTSPANID,
80+
_SAMPLED,
81+
_FLAGS,
82+
]:
83+
if key.lower() == b3_key:
84+
case_insensitive_carrier[b3_key] = value
85+
else:
86+
case_insensitive_carrier[key] = value
87+
88+
carrier = case_insensitive_carrier
89+
baggage = {}
90+
91+
if _SINGLE_HEADER in carrier.keys():
92+
fields = carrier.pop(_SINGLE_HEADER).split("-", 4)
93+
baggage.update(carrier)
94+
len_fields = len(fields)
95+
if len_fields == 1:
96+
sampled = fields[0]
97+
elif len_fields == 2:
98+
traceid, spanid = fields
99+
elif len_fields == 3:
100+
traceid, spanid, sampled = fields
101+
else:
102+
traceid, spanid, sampled, parent_spanid = fields
103+
baggage[_PARENTSPANID] = int(parent_spanid, 16)
104+
if sampled == "d":
105+
baggage[_FLAGS] = 1
106+
else:
107+
baggage[_SAMPLED] = int(sampled, 16)
108+
else:
109+
traceid = carrier.pop(_TRACEID, None)
110+
spanid = carrier.pop(_SPANID, None)
111+
parentspanid = carrier.pop(_PARENTSPANID, None)
112+
sampled = carrier.pop(_SAMPLED, None)
113+
flags = carrier.pop(_FLAGS, None)
114+
115+
if sampled is flags is (traceid and spanid) is None:
116+
117+
raise SpanContextCorruptedException()
118+
119+
if parentspanid is not None:
120+
baggage[_PARENTSPANID] = int(parentspanid, 16)
121+
122+
if flags == 1:
123+
baggage[_FLAGS] = flags
124+
if sampled is not None:
125+
warn(
126+
"x-b3-flags: 1 implies x-b3-sampled: 1, ignoring "
127+
"the received value of x-b3-sampled"
128+
)
129+
elif sampled is not None:
130+
baggage[_SAMPLED] = int(sampled, 16)
131+
132+
baggage.update(carrier)
133+
134+
if baggage == OTSpanContext.EMPTY_BAGGAGE:
135+
baggage = None
136+
137+
return SpanContext(
138+
trace_id=int(traceid, 16),
139+
span_id=int(spanid, 16),
140+
baggage=baggage
141+
)

tests/b3_propagator_test.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from unittest import TestCase
2+
3+
from pytest import raises
4+
from opentracing import SpanContextCorruptedException
5+
6+
from opentracing import Format
7+
from lightstep import Tracer
8+
from lightstep.b3_propagator import B3Propagator
9+
10+
11+
class B3PropagatorTest(TestCase):
12+
def setUp(self):
13+
self._tracer = Tracer(
14+
periodic_flush_seconds=0,
15+
collector_host="localhost"
16+
)
17+
self._tracer.register_propagator(Format.HTTP_HEADERS, B3Propagator())
18+
19+
def tracer(self):
20+
return self._tracer
21+
22+
def tearDown(self):
23+
self._tracer.flush()
24+
25+
def test_inject(self):
26+
carrier = {}
27+
span = self.tracer().start_span("test_inject")
28+
span.set_baggage_item("checked", "baggage")
29+
self.tracer().inject(span.context, Format.HTTP_HEADERS, carrier)
30+
self.assertEqual(
31+
carrier,
32+
{
33+
"x-b3-traceid": (
34+
format(span.context.trace_id, "x").ljust(32, "0")
35+
),
36+
"x-b3-spanid": format(span.context.span_id, "016x"),
37+
"checked": "baggage"
38+
}
39+
)
40+
41+
carrier = {}
42+
span = self.tracer().start_span("test_inject")
43+
span.set_baggage_item("x-b3-flags", 1)
44+
span.set_baggage_item("x-b3-sampled", 0)
45+
self.tracer().inject(span.context, Format.HTTP_HEADERS, carrier)
46+
self.assertEqual(
47+
carrier,
48+
{
49+
"x-b3-traceid": (
50+
format(span.context.trace_id, "x").ljust(32, "0")
51+
),
52+
"x-b3-spanid": format(span.context.span_id, "016x"),
53+
"x-b3-flags": 1,
54+
}
55+
)
56+
57+
def test_extract_multiple_headers(self):
58+
59+
result = self.tracer().extract(
60+
Format.HTTP_HEADERS,
61+
{
62+
"x-b3-traceid": format(12, "032x"),
63+
"x-b3-spanid": format(345, "016x"),
64+
"checked": "baggage"
65+
}
66+
)
67+
68+
self.assertEqual(12, result.trace_id)
69+
self.assertEqual(345, result.span_id)
70+
self.assertEqual({"checked": "baggage"}, result.baggage)
71+
72+
result = self.tracer().extract(
73+
Format.HTTP_HEADERS,
74+
{
75+
"x-b3-traceid": format(12, "032x"),
76+
"x-b3-spanid": format(345, "016x"),
77+
"x-b3-flags": 1,
78+
"x-b3-sampled": 0
79+
}
80+
)
81+
82+
self.assertEqual(12, result.trace_id)
83+
self.assertEqual(345, result.span_id)
84+
self.assertEqual({"x-b3-flags": 1}, result.baggage)
85+
86+
def test_extract_single_header(self):
87+
result = self.tracer().extract(
88+
Format.HTTP_HEADERS,
89+
{
90+
"b3": "a12-b34-1-c56",
91+
"checked": "baggage"
92+
}
93+
)
94+
self.assertEqual(2578, result.trace_id)
95+
self.assertEqual(2868, result.span_id)
96+
self.assertDictEqual(
97+
{
98+
"x-b3-sampled": 1,
99+
"x-b3-parentspanid": 3158,
100+
"checked": "baggage"
101+
},
102+
result.baggage
103+
)
104+
105+
result = self.tracer().extract(
106+
Format.HTTP_HEADERS,
107+
{
108+
"b3": "a12-b34-d-c56",
109+
"checked": "baggage"
110+
}
111+
)
112+
self.assertEqual(2578, result.trace_id)
113+
self.assertEqual(2868, result.span_id)
114+
self.assertDictEqual(
115+
{
116+
"x-b3-flags": 1,
117+
"x-b3-parentspanid": 3158,
118+
"checked": "baggage"
119+
},
120+
result.baggage
121+
)
122+
123+
def test_invalid_traceid_spanid(self):
124+
125+
with raises(SpanContextCorruptedException):
126+
self.tracer().extract(
127+
Format.HTTP_HEADERS,
128+
{
129+
"x-b3-spanid": format(345, "016x"),
130+
"checked": "baggage"
131+
}
132+
)
133+
134+
with raises(SpanContextCorruptedException):
135+
self.tracer().extract(
136+
Format.HTTP_HEADERS,
137+
{
138+
"x-b3-traceid": format(345, "032x"),
139+
"checked": "baggage"
140+
}
141+
)

0 commit comments

Comments
 (0)