Skip to content

Commit a62eeb0

Browse files
committed
Add support for Trace Context headers
Closes #79
1 parent b5a6930 commit a62eeb0

File tree

3 files changed

+296
-0
lines changed

3 files changed

+296
-0
lines changed

lightstep/propagation.py

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from __future__ import absolute_import
66

77

8+
# The TRACE_CONTEXT format represents SpanContexts in Trace Context format.
9+
# https://www.w3.org/TR/trace-context/
10+
TRACE_CONTEXT = "trace_context"
11+
12+
813
class LightStepFormat(object):
914
"""A namespace for lightstep supported carrier formats.
1015

lightstep/trace_context.py

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
from re import match, findall
2+
from random import choice
3+
from warnings import warn
4+
from logging import getLogger
5+
from collections import OrderedDict
6+
7+
from basictracer.propagator import Propagator
8+
from basictracer.context import SpanContext
9+
from opentracing import SpanContextCorruptedException
10+
11+
_LOG = getLogger(__name__)
12+
13+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#header-name
14+
_TRACEPARENT = "traceparent"
15+
_TRACESTATE = "tracestate"
16+
17+
18+
class TraceContextPropagator(Propagator):
19+
"""
20+
Propagator for the W3C Trace Context format
21+
22+
The latest version of this Candidate Recommendation can be found here:
23+
https://www.w3.org/TR/trace-context/
24+
25+
This is an implementation for this specific version:
26+
https://www.w3.org/TR/2019/CR-trace-context-20190813/
27+
28+
The Candidate Recommendation will be referred to as "the document" in the
29+
comments of this implementation. This implementation adds comments which
30+
are URLs that point to the specific part of the document that explain the
31+
rationale of the code that follows the comment.
32+
"""
33+
34+
def inject(self, span_context, carrier):
35+
36+
pass
37+
38+
def extract(self, carrier):
39+
40+
traceparent = None
41+
tracestate = None
42+
43+
multiple_header_template = "Found more than one header value for {}"
44+
45+
# FIXME Define more specific exceptions and use them instead of the
46+
# generic ones that are being raised below
47+
48+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#header-name
49+
for key, value in carrier.items():
50+
51+
lower_key = key.lower()
52+
53+
# The document requires that the headers be accepted regardless of
54+
# the case of their characters. This means that the keys of carrier
55+
# may contain 2 or more strings that match traceparent or
56+
# tracestate when the case of the characters of such strings is
57+
# ignored. The document does not specify what is to be done in such
58+
# a situation, this implementation will raise an exception.
59+
# FIXME should this be reported to the W3C Trace Context team?
60+
if lower_key == _TRACEPARENT:
61+
62+
if traceparent is None:
63+
traceparent = value
64+
65+
else:
66+
raise Exception(
67+
multiple_header_template.format(_TRACEPARENT)
68+
)
69+
70+
if lower_key == _TRACESTATE:
71+
72+
if tracestate is None:
73+
tracestate = value
74+
75+
else:
76+
raise Exception(
77+
multiple_header_template.format(_TRACEPARENT)
78+
)
79+
80+
hexdigitlower = r"[abcdef\d]"
81+
82+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version
83+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version-format
84+
version_remainder_match = match(
85+
r"(?P<version>{}{{2}})-"
86+
r"(?P<remainder>.*)"
87+
.format(hexdigitlower),
88+
traceparent
89+
)
90+
91+
if version_remainder_match is None:
92+
_LOG.warning(
93+
"Unable to parse version from traceparent, restarting trace"
94+
)
95+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#versioning-of-traceparent
96+
return SpanContext(
97+
trace_id=int(
98+
"".join(
99+
[choice("abcdef0123456789") for character in range(16)]
100+
),
101+
16
102+
),
103+
# FIXME Add span_id here too
104+
)
105+
106+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version
107+
version = version_remainder_match.group("version")
108+
if version == "ff":
109+
raise SpanContextCorruptedException(
110+
"Forbidden value of 255 found in version"
111+
)
112+
113+
try:
114+
remainder_match = match(
115+
(
116+
r"(?P<trace-id>{0}{{1,32}})-"
117+
r"(?P<parent-id>{0}{{16}})-"
118+
r"(?P<trace-flags>{0}{{2}})"
119+
).format(hexdigitlower),
120+
version_remainder_match.group("remainder")
121+
)
122+
except Exception as error:
123+
124+
error
125+
126+
from ipdb import set_trace
127+
set_trace()
128+
129+
True
130+
131+
if remainder_match is None:
132+
# This will happen if any of the trace-id, parent-id or trace-flags
133+
# fields contains non-allowed characters.
134+
# The document specifies that traceparent must be ignored if
135+
# these kind of characters appear in trace-id and parent-id,
136+
# but it does not specify what to do if they appear in trace-flags.
137+
# Here it is assumed that traceparent is to be ignored also if
138+
# non-allowed characters are present in trace-flags too.
139+
# FIXME confirm this assumption
140+
raise SpanContextCorruptedException(
141+
"Received an invalid traceparent: {}".format(traceparent)
142+
)
143+
144+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#trace-id
145+
trace_id = remainder_match.group("trace-id")
146+
if trace_id == 32 * "0":
147+
raise SpanContextCorruptedException(
148+
"Forbidden value of {} found in trace-id".format(trace_id)
149+
)
150+
151+
trace_id_extra_characters = len(trace_id) - 16
152+
153+
# FIXME The code inside this if and its corresponding elif needs to
154+
# be confirmed. There is still discussion regarding how trace-id is
155+
# to be handled in the document.
156+
if trace_id_extra_characters > 0:
157+
_LOG.debug(
158+
"Truncating {} extra characters from trace-id"
159+
.format(trace_id_extra_characters)
160+
)
161+
162+
trace_id = trace_id[trace_id_extra_characters:]
163+
164+
elif trace_id_extra_characters < 0:
165+
_LOG.debug(
166+
"Padding {} extra left zeroes in trace-id"
167+
.format(trace_id_extra_characters)
168+
)
169+
170+
trace_id = "".join(
171+
["0" * abs(trace_id_extra_characters), trace_id]
172+
)
173+
174+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#parent-id
175+
parent_id = remainder_match.group("parent-id")
176+
if parent_id == 16 * "0":
177+
raise SpanContextCorruptedException(
178+
"Forbidden value of {}"
179+
" found in parent-id".format(parent_id)
180+
)
181+
182+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#trace-flags
183+
raw_trace_flags = traceparent.group("trace-flags")
184+
# FIXME implement handling of trace-flags
185+
186+
trace_flags = []
187+
for index, bit, flag in enumerate(
188+
zip(
189+
bin(int(raw_trace_flags, 16))[2:].zfill(8),
190+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#other-flags
191+
# Flags can be added in the next list as the document
192+
# progresses and they get defined.
193+
[None, None, None, None, None, None, None, "sampled"]
194+
)
195+
):
196+
197+
if int(bit):
198+
if flag is None:
199+
warn("Invalid flag set at bit {}".format(index))
200+
201+
else:
202+
trace_flags.append(flag)
203+
204+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#tracestate-header
205+
# As the document indicates in the URL before, failure to parse
206+
# traceparent must stop the parsing of tracestate. This stoppage is
207+
# achieved here by raising SpanContextCorruptedException when one of
208+
# these parsing failures is found.
209+
210+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#key
211+
key_re = (
212+
r"[{0}]([{0}/d_-*/]){{0,255}}|"
213+
r"[{0}/d]([{0}/d_-*/]){{0,240}}@{0}([{0}/d_-*/]){{0,13}}"
214+
).format(r"".join([chr(character) for character in range(97, 123)]))
215+
216+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#value
217+
value_re = r"( |[{0}]){{0,255}}[{0}]".format(
218+
r"".join(
219+
[
220+
chr(character) for character in range(33, 127)
221+
if character not in [44, 61]
222+
]
223+
)
224+
)
225+
226+
tracestate_match = match(
227+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#list
228+
r"{0}={1}| *( *, *({0}={1}| *)){{0,31}}".format(key_re, value_re),
229+
tracestate
230+
)
231+
232+
# FIXME Add span_id when initializing SpanContext too.
233+
span_context = SpanContext(
234+
trace_id=int(trace_id, 16),
235+
baggage={}
236+
)
237+
238+
if tracestate_match is None:
239+
warn("Invalid tracestate found: {}".format(tracestate))
240+
241+
else:
242+
tracestate = OrderedDict()
243+
244+
for key, value in findall(
245+
r"({0})=({1}))".format(key_re, value_re), tracestate
246+
):
247+
248+
tracestate[key] = value
249+
250+
span_context._tracestate = tracestate
251+
252+
return span_context
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from unittest import TestCase
2+
3+
from lightstep import Tracer
4+
from lightstep.propagation import TRACE_CONTEXT
5+
from lightstep.trace_context import TraceContextPropagator
6+
7+
8+
class TraceContextPropagatorTest(TestCase):
9+
def setUp(self):
10+
self._tracer = Tracer(
11+
periodic_flush_seconds=0,
12+
collector_host='localhost'
13+
)
14+
(
15+
self._tracer.
16+
register_propagator(TRACE_CONTEXT, TraceContextPropagator())
17+
)
18+
19+
def tracer(self):
20+
return self._tracer
21+
22+
def tearDown(self):
23+
self._tracer.flush()
24+
25+
def test_extract(self):
26+
# FIXME Properly test that a randomly-initialized SpanContext is
27+
# returned here. Consider using a fixed random seed to make the tests
28+
# deterministic.
29+
self.tracer().extract(
30+
TRACE_CONTEXT, {
31+
"traceparent": "asdfas"
32+
}
33+
)
34+
35+
self.tracer().extract(
36+
TRACE_CONTEXT, {
37+
"traceparent": "00-23-123-00"
38+
}
39+
)

0 commit comments

Comments
 (0)