Skip to content

Commit 000bce9

Browse files
authored
Merge pull request #82 from lightstep/trace_context
Add support for Trace Context headers
2 parents b6bbc11 + a6cc5d8 commit 000bce9

File tree

6 files changed

+477
-2
lines changed

6 files changed

+477
-2
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ dist/*
1212
.idea
1313
.pytest_cache/
1414
.python-version
15+
tests/trace-context/trace-context

lightstep/trace_context.py

+311
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
from re import escape, compile as re_compile
2+
from random import getrandbits
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+
10+
from opentracing import SpanContextCorruptedException
11+
12+
13+
_LOG = getLogger(__name__)
14+
15+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#header-name
16+
_TRACEPARENT = "traceparent"
17+
_TRACESTATE = "tracestate"
18+
19+
# https://www.w3.org/TR/trace-context/#a-traceparent-is-received
20+
_HEXDIGITLOWER = r"[abcdef\d]"
21+
22+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version
23+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version-format
24+
_VERSION = re_compile(r"\s*(?P<version>{0}{{2}})-".format(_HEXDIGITLOWER))
25+
26+
# https://www.w3.org/TR/trace-context/#trace-id
27+
# https://www.w3.org/TR/trace-context/#parent-id
28+
# https://www.w3.org/TR/trace-context/#trace-flags
29+
_REMAINDER_MATCH_RE = (
30+
r"{0}{{2}}-"
31+
r"(?P<trace_id>{0}{{32}})-"
32+
r"(?P<parent_id>{0}{{16}})-"
33+
r"(?P<trace_flags>{0}{{2}})"
34+
).format(_HEXDIGITLOWER)
35+
36+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#versioning-of-traceparent
37+
_00_VERSION_REMAINDER = re_compile("".join([_REMAINDER_MATCH_RE, r"\s*$"]))
38+
_FUTURE_VERSION_REMAINDER = re_compile(
39+
"".join([_REMAINDER_MATCH_RE, r"(\s*$|-[^\s]+)"])
40+
)
41+
42+
_KEY_VALUE = re_compile(
43+
r"^\s*({0})=({1})\s*$".format(
44+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#tracestate-header
45+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#key
46+
(
47+
r"[{0}][-{0}\d_*/]{{0,255}}|"
48+
r"[{0}\d][-{0}\d_*/]{{0,240}}@[{0}][-{0}\d_*/]{{0,13}}"
49+
).format("a-z"),
50+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#value
51+
r"[ {0}]{{0,255}}[{0}]".format(
52+
escape(
53+
r"".join(
54+
[
55+
chr(character) for character in range(33, 127)
56+
if character not in [44, 61]
57+
]
58+
)
59+
)
60+
)
61+
)
62+
)
63+
64+
_BLANK = re_compile(r"^\s*$")
65+
66+
67+
class TraceContextPropagator(Propagator):
68+
"""
69+
Propagator for the W3C Trace Context format
70+
71+
The latest version of this Candidate Recommendation can be found here:
72+
https://www.w3.org/TR/trace-context/
73+
74+
This is an implementation for this specific version:
75+
https://www.w3.org/TR/2019/CR-trace-context-20190813/
76+
77+
The Candidate Recommendation will be referred to as "the document" in the
78+
comments of this implementation. This implementation adds comments which
79+
are URLs that point to the specific part of the document that explain the
80+
rationale of the code that follows the comment.
81+
82+
There is a set of test cases defined for testing any implementation of the
83+
document. This set of test cases can be found here:
84+
https://github.com/w3c/trace-context/tree/master/test
85+
86+
This set of test cases will be referred to as "the test suite" in the
87+
comments of this implementation.
88+
"""
89+
90+
def inject(self, span_context, carrier):
91+
92+
carrier[_TRACEPARENT] = "00-{}-{}-{}".format(
93+
format(span_context.trace_id, "032x"),
94+
format(span_context.span_id, "016x"),
95+
format(span_context.baggage.pop("trace-flags", 0), "02x")
96+
)
97+
98+
carrier.update(span_context.baggage)
99+
100+
def extract(self, carrier):
101+
traceparent = None
102+
tracestate = None
103+
104+
multiple_header_template = "Found more than one header value for {}"
105+
106+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#header-name
107+
108+
trace_context_free_carrier = {}
109+
110+
for key, value in carrier.items():
111+
112+
lower_key = key.lower()
113+
114+
# The document requires that the headers be accepted regardless of
115+
# the case of their characters. This means that the keys of carrier
116+
# may contain 2 or more strings that match traceparent or
117+
# tracestate when the case of the characters of such strings is
118+
# ignored. The document does not specify what is to be done in such
119+
# a situation, this implementation will raise an exception.
120+
if lower_key == _TRACEPARENT:
121+
122+
if traceparent is None:
123+
traceparent = value
124+
125+
else:
126+
raise SpanContextCorruptedException(
127+
multiple_header_template.format(_TRACEPARENT)
128+
)
129+
130+
elif lower_key == _TRACESTATE:
131+
132+
if tracestate is None:
133+
tracestate = value
134+
135+
else:
136+
raise SpanContextCorruptedException(
137+
multiple_header_template.format(_TRACEPARENT)
138+
)
139+
140+
else:
141+
trace_context_free_carrier[key] = value
142+
143+
if traceparent is None:
144+
# https://www.w3.org/TR/trace-context/#no-traceparent-received
145+
_LOG.warning("No traceparent was received")
146+
return SpanContext(
147+
trace_id=getrandbits(128), span_id=getrandbits(64)
148+
)
149+
150+
else:
151+
152+
version_match = _VERSION.match(traceparent)
153+
154+
if version_match is None:
155+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#versioning-of-traceparent
156+
_LOG.warning(
157+
"Unable to parse version from traceparent"
158+
)
159+
return SpanContext(
160+
trace_id=getrandbits(128), span_id=getrandbits(64)
161+
)
162+
163+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#version
164+
version = version_match.group("version")
165+
if version == "ff":
166+
_LOG.warning(
167+
"Forbidden value of 255 found in version"
168+
)
169+
return SpanContext(
170+
trace_id=getrandbits(128), span_id=getrandbits(64)
171+
)
172+
173+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#versioning-of-traceparent
174+
if int(version, 16) > 0:
175+
176+
if len(traceparent) < 55:
177+
_LOG.warning(
178+
"traceparent shorter than 55 characters found"
179+
)
180+
return SpanContext(
181+
trace_id=getrandbits(128), span_id=getrandbits(64)
182+
)
183+
184+
remainder_match = _FUTURE_VERSION_REMAINDER.match(traceparent)
185+
186+
else:
187+
remainder_match = _00_VERSION_REMAINDER.match(traceparent)
188+
189+
if remainder_match is None:
190+
# This will happen if any of the trace-id, parent-id or
191+
# trace-flags fields contains non-allowed characters.
192+
# The document specifies that traceparent must be ignored if
193+
# these kind of characters appear in trace-id and parent-id,
194+
# but it does not specify what to do if they appear in
195+
# trace-flags.
196+
# Here it is assumed that traceparent is to be ignored also if
197+
# non-allowed characters are present in trace-flags too.
198+
_LOG.warning(
199+
"Received an invalid traceparent: {}".format(traceparent)
200+
)
201+
return SpanContext(
202+
trace_id=getrandbits(128), span_id=getrandbits(64)
203+
)
204+
205+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#trace-id
206+
trace_id = remainder_match.group("trace_id")
207+
if trace_id == 32 * "0":
208+
_LOG.warning(
209+
"Forbidden value of {} found in trace-id".format(trace_id)
210+
)
211+
return SpanContext(
212+
trace_id=getrandbits(128), span_id=getrandbits(64)
213+
)
214+
215+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#parent-id
216+
parent_id = remainder_match.group("parent_id")
217+
if parent_id == 16 * "0":
218+
_LOG.warning(
219+
"Forbidden value of {}"
220+
" found in parent-id".format(parent_id)
221+
)
222+
return SpanContext(
223+
trace_id=getrandbits(128), span_id=getrandbits(64)
224+
)
225+
226+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#trace-flags
227+
raw_trace_flags = remainder_match.group("trace_flags")
228+
229+
trace_flags = []
230+
231+
for index, bit_flag in enumerate(
232+
zip(
233+
bin(int(raw_trace_flags, 16))[2:].zfill(8),
234+
# https://www.w3.org/TR/2019/CR-trace-context-20190813/#other-flags
235+
# Flags can be added in the next list as the document
236+
# progresses and they get defined. This list represents the
237+
# 8 bits that are available in trace-flags and their
238+
# respective meaning.
239+
[None, None, None, None, None, None, None, "sampled"]
240+
)
241+
):
242+
bit, flag = bit_flag
243+
244+
if int(bit):
245+
if flag is None:
246+
warn("Invalid flag set at bit {}".format(index))
247+
trace_flags.append("0")
248+
249+
else:
250+
trace_flags.append("1")
251+
252+
else:
253+
trace_flags.append("0")
254+
255+
trace_context_free_carrier["trace-flags"] = int(
256+
"".join(trace_flags), 2
257+
)
258+
259+
if tracestate is not None:
260+
261+
tracestate_dictionary = OrderedDict()
262+
263+
for counter, list_member in enumerate(tracestate.split(",")):
264+
# https://www.w3.org/TR/trace-context/#tracestate-header-field-values
265+
if counter > 31:
266+
_LOG.warning(
267+
"More than 32 list-members "
268+
"found in tracestate {}".format(tracestate)
269+
)
270+
break
271+
272+
# https://www.w3.org/TR/trace-context/#tracestate-header-field-values
273+
if _BLANK.match(list_member):
274+
_LOG.debug(
275+
"Ignoring empty tracestate list-member"
276+
)
277+
continue
278+
279+
key_value = _KEY_VALUE.match(list_member)
280+
281+
if key_value is None:
282+
_LOG.warning(
283+
"Invalid key/value pair found: {}".format(
284+
key_value
285+
)
286+
)
287+
break
288+
289+
key, value = key_value.groups()
290+
291+
if key in tracestate_dictionary.keys():
292+
_LOG.warning(
293+
"Duplicate tracestate key found: {}".format(key)
294+
)
295+
break
296+
297+
tracestate_dictionary[key] = value
298+
299+
else:
300+
trace_context_free_carrier[_TRACESTATE] = ",".join(
301+
[
302+
"{0}={1}".format(key, value)
303+
for key, value in tracestate_dictionary.items()
304+
]
305+
)
306+
307+
return SpanContext(
308+
trace_id=int(trace_id, 16),
309+
span_id=int(parent_id, 16),
310+
baggage=trace_context_free_carrier
311+
)

lightstep/tracer.py

+41
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ def __init__(self, enable_binary_format, recorder, scope_manager):
8282
self.register_propagator(Format.BINARY, BinaryPropagator())
8383
self.register_propagator(LightStepFormat.LIGHTSTEP_BINARY, LightStepBinaryPropagator())
8484

85+
def start_active_span(
86+
self,
87+
operation_name,
88+
child_of=None,
89+
references=None,
90+
tags=None,
91+
start_time=None,
92+
ignore_active_span=False,
93+
finish_on_close=True
94+
):
95+
96+
scope = super(_LightstepTracer, self).start_active_span(
97+
operation_name,
98+
child_of=child_of,
99+
references=references,
100+
tags=tags,
101+
start_time=start_time,
102+
ignore_active_span=ignore_active_span,
103+
finish_on_close=finish_on_close
104+
)
105+
106+
class ScopePatch(scope.__class__):
107+
108+
def __exit__(self, exc_type, exc_val, exc_tb):
109+
110+
self.span.context.trace_id = int(
111+
format(self.span.context.trace_id, "032x")[:16], 16
112+
)
113+
114+
result = super(self.__class__, self).__exit__(
115+
exc_type, exc_val, exc_tb
116+
)
117+
118+
return result
119+
120+
# This monkey patching is done because LightStep requires for the
121+
# trace_id to be 64b long.
122+
scope.__class__ = ScopePatch
123+
124+
return scope
125+
85126
def flush(self):
86127
"""Force a flush of buffered Span data to the LightStep collector."""
87128
self.recorder.flush()

0 commit comments

Comments
 (0)