Skip to content

Commit b01a50c

Browse files
committed
Implementing TraceContext (fixes open-telemetry#116)
This introduces a w3c TraceContext propagator, primarily inspired by opencensus.
1 parent ca10173 commit b01a50c

File tree

5 files changed

+356
-7
lines changed

5 files changed

+356
-7
lines changed

opentelemetry-api/src/opentelemetry/context/propagation/tracecontexthttptextformat.py

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
#
15-
15+
import re
1616
import typing
1717

1818
import opentelemetry.trace as trace
@@ -22,18 +22,128 @@
2222

2323

2424
class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat):
25-
"""TODO: extracts and injects using w3c TraceContext's headers.
25+
"""Extracts and injects using w3c TraceContext's headers.
2626
"""
2727

28+
_TRACEPARENT_HEADER_NAME = "traceparent"
29+
_TRACESTATE_HEADER_NAME = "tracestate"
30+
_TRACEPARENT_HEADER_FORMAT = (
31+
"^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})"
32+
+ "(-.*)?[ \t]*$"
33+
)
34+
_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT)
35+
36+
@classmethod
2837
def extract(
29-
self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T
38+
cls, get_from_carrier: httptextformat.Getter[_T], carrier: _T
3039
) -> trace.SpanContext:
31-
return trace.INVALID_SPAN_CONTEXT
40+
"""Extracts a valid SpanContext from the carrier.
41+
42+
If a header
43+
"""
44+
header = get_from_carrier(carrier, cls._TRACEPARENT_HEADER_NAME)
45+
46+
if not header:
47+
return trace.INVALID_SPAN_CONTEXT
48+
49+
match = re.search(cls._TRACEPARENT_HEADER_FORMAT_RE, header[0])
50+
if not match:
51+
return trace.INVALID_SPAN_CONTEXT
52+
53+
version = match.group(1)
54+
trace_id = match.group(2)
55+
span_id = match.group(3)
56+
trace_options = match.group(4)
3257

58+
if trace_id == "0" * 32 or span_id == "0" * 16:
59+
return trace.INVALID_SPAN_CONTEXT
60+
61+
if version == "00":
62+
if match.group(5):
63+
return trace.INVALID_SPAN_CONTEXT
64+
if version == "ff":
65+
return trace.INVALID_SPAN_CONTEXT
66+
67+
tracestate = trace.TraceState()
68+
for tracestate_header in get_from_carrier(
69+
carrier, cls._TRACESTATE_HEADER_NAME
70+
):
71+
tracestate.update(_parse_tracestate(tracestate_header))
72+
73+
span_context = trace.SpanContext(
74+
trace_id=int(trace_id, 16),
75+
span_id=int(span_id, 16),
76+
trace_options=trace.TraceOptions(trace_options),
77+
trace_state=tracestate,
78+
)
79+
80+
return span_context
81+
82+
@classmethod
3383
def inject(
34-
self,
84+
cls,
3585
context: trace.SpanContext,
3686
set_in_carrier: httptextformat.Setter[_T],
3787
carrier: _T,
3888
) -> None:
39-
pass
89+
if context == trace.INVALID_SPAN_CONTEXT:
90+
return
91+
traceparent_string = "-".join(
92+
[
93+
"00",
94+
format(context.trace_id, "032x"),
95+
format(context.span_id, "016x"),
96+
format(context.trace_options, "02x"),
97+
]
98+
)
99+
set_in_carrier(
100+
carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string
101+
)
102+
if context.trace_state:
103+
tracestate_string = _format_tracestate(context.trace_state)
104+
set_in_carrier(
105+
carrier, cls._TRACESTATE_HEADER_NAME, tracestate_string
106+
)
107+
108+
109+
_DELIMITER_FORMAT = "[ \t]*,[ \t]*"
110+
_MEMBER_FORMAT = "(%s)(=)(%s)" % (
111+
trace.TraceState.KEY_FORMAT,
112+
trace.TraceState.VALUE_FORMAT,
113+
)
114+
115+
_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT)
116+
_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT)
117+
118+
119+
def _parse_tracestate(string: str) -> trace.TraceState:
120+
"""Parse a w3c tracestate header into a TraceState.
121+
122+
Args:
123+
string: the value of the tracestate header.
124+
125+
Returns:
126+
A valid TraceState that contains values extracted from
127+
the tracestate header.
128+
"""
129+
tracestate = trace.TraceState()
130+
for member in re.split(_DELIMITER_FORMAT_RE, string):
131+
match = _MEMBER_FORMAT_RE.match(member)
132+
if not match:
133+
raise ValueError("illegal key-value format %r" % (member))
134+
key, _eq, value = match.groups()
135+
tracestate[key] = value
136+
return tracestate
137+
138+
139+
def _format_tracestate(tracestate: trace.TraceState) -> str:
140+
"""Parse a w3c tracestate header into a TraceState.
141+
142+
Args:
143+
tracestate: the tracestate header to write
144+
145+
Returns:
146+
A string that adheres to the w3c tracestate
147+
header format.
148+
"""
149+
return ",".join(map(lambda key: key + "=" + tracestate[key], tracestate))

opentelemetry-api/src/opentelemetry/trace/__init__.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
"""
6363

6464
import enum
65+
import re
6566
import typing
67+
from collections import OrderedDict
6668
from contextlib import contextmanager
6769

6870
from opentelemetry.util import loader, types
@@ -250,7 +252,7 @@ def get_default(cls) -> "TraceOptions":
250252
DEFAULT_TRACE_OPTIONS = TraceOptions.get_default()
251253

252254

253-
class TraceState(typing.Dict[str, str]):
255+
class TraceState(OrderedDict):
254256
"""A list of key-value pairs representing vendor-specific trace info.
255257
256258
Keys and values are strings of up to 256 printable US-ASCII characters.
@@ -261,10 +263,39 @@ class TraceState(typing.Dict[str, str]):
261263
https://www.w3.org/TR/trace-context/#tracestate-field
262264
"""
263265

266+
MAX_TRACESTATE_VALUES = 32
267+
KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}"
268+
KEY_WITH_VENDOR_FORMAT = (
269+
r"[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"
270+
)
271+
KEY_FORMAT = KEY_WITHOUT_VENDOR_FORMAT + "|" + KEY_WITH_VENDOR_FORMAT
272+
VALUE_FORMAT = (
273+
r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"
274+
)
275+
276+
KEY_VALIDATION_RE = re.compile("^" + KEY_FORMAT + "$")
277+
VALUE_VALIDATION_RE = re.compile("^" + VALUE_FORMAT + "$")
278+
264279
@classmethod
265280
def get_default(cls) -> "TraceState":
266281
return cls()
267282

283+
def __setitem__(self, key: str, value: str) -> None:
284+
# According to the w3c spec, we can only store 32 values
285+
if len(self) >= self.MAX_TRACESTATE_VALUES:
286+
return
287+
# TODO: I believe the otel spec calls for no exceptions
288+
# that interfere with execution in the API.
289+
# if not isinstance(key, str):
290+
# raise ValueError("key must be an instance of str")
291+
# if not re.match(self._KEY_VALIDATION_RE, key):
292+
# raise ValueError("illegal key provided")
293+
# if not isinstance(value, str):
294+
# raise ValueError("value must be an instance of str")
295+
# if not re.match(self._VALUE_VALIDATION_RE, value):
296+
# raise ValueError("illegal value provided")
297+
super().__setitem__(key, value)
298+
268299

269300
DEFAULT_TRACE_STATE = TraceState.get_default()
270301

opentelemetry-api/tests/context/__init__.py

Whitespace-only changes.

opentelemetry-api/tests/context/propagation/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)