Skip to content

Commit

Permalink
Implementing TraceContext (fixes open-telemetry#116)
Browse files Browse the repository at this point in the history
This introduces a w3c TraceContext propagator, primarily inspired by opencensus.
  • Loading branch information
toumorokoshi committed Sep 27, 2019
1 parent ca10173 commit b01a50c
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#

import re
import typing

import opentelemetry.trace as trace
Expand All @@ -22,18 +22,128 @@


class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat):
"""TODO: extracts and injects using w3c TraceContext's headers.
"""Extracts and injects using w3c TraceContext's headers.
"""

_TRACEPARENT_HEADER_NAME = "traceparent"
_TRACESTATE_HEADER_NAME = "tracestate"
_TRACEPARENT_HEADER_FORMAT = (
"^[ \t]*([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})"
+ "(-.*)?[ \t]*$"
)
_TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT)

@classmethod
def extract(
self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T
cls, get_from_carrier: httptextformat.Getter[_T], carrier: _T
) -> trace.SpanContext:
return trace.INVALID_SPAN_CONTEXT
"""Extracts a valid SpanContext from the carrier.
If a header
"""
header = get_from_carrier(carrier, cls._TRACEPARENT_HEADER_NAME)

if not header:
return trace.INVALID_SPAN_CONTEXT

match = re.search(cls._TRACEPARENT_HEADER_FORMAT_RE, header[0])
if not match:
return trace.INVALID_SPAN_CONTEXT

version = match.group(1)
trace_id = match.group(2)
span_id = match.group(3)
trace_options = match.group(4)

if trace_id == "0" * 32 or span_id == "0" * 16:
return trace.INVALID_SPAN_CONTEXT

if version == "00":
if match.group(5):
return trace.INVALID_SPAN_CONTEXT
if version == "ff":
return trace.INVALID_SPAN_CONTEXT

tracestate = trace.TraceState()
for tracestate_header in get_from_carrier(
carrier, cls._TRACESTATE_HEADER_NAME
):
tracestate.update(_parse_tracestate(tracestate_header))

span_context = trace.SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
trace_options=trace.TraceOptions(trace_options),
trace_state=tracestate,
)

return span_context

@classmethod
def inject(
self,
cls,
context: trace.SpanContext,
set_in_carrier: httptextformat.Setter[_T],
carrier: _T,
) -> None:
pass
if context == trace.INVALID_SPAN_CONTEXT:
return
traceparent_string = "-".join(
[
"00",
format(context.trace_id, "032x"),
format(context.span_id, "016x"),
format(context.trace_options, "02x"),
]
)
set_in_carrier(
carrier, cls._TRACEPARENT_HEADER_NAME, traceparent_string
)
if context.trace_state:
tracestate_string = _format_tracestate(context.trace_state)
set_in_carrier(
carrier, cls._TRACESTATE_HEADER_NAME, tracestate_string
)


_DELIMITER_FORMAT = "[ \t]*,[ \t]*"
_MEMBER_FORMAT = "(%s)(=)(%s)" % (
trace.TraceState.KEY_FORMAT,
trace.TraceState.VALUE_FORMAT,
)

_DELIMITER_FORMAT_RE = re.compile(_DELIMITER_FORMAT)
_MEMBER_FORMAT_RE = re.compile(_MEMBER_FORMAT)


def _parse_tracestate(string: str) -> trace.TraceState:
"""Parse a w3c tracestate header into a TraceState.
Args:
string: the value of the tracestate header.
Returns:
A valid TraceState that contains values extracted from
the tracestate header.
"""
tracestate = trace.TraceState()
for member in re.split(_DELIMITER_FORMAT_RE, string):
match = _MEMBER_FORMAT_RE.match(member)
if not match:
raise ValueError("illegal key-value format %r" % (member))
key, _eq, value = match.groups()
tracestate[key] = value
return tracestate


def _format_tracestate(tracestate: trace.TraceState) -> str:
"""Parse a w3c tracestate header into a TraceState.
Args:
tracestate: the tracestate header to write
Returns:
A string that adheres to the w3c tracestate
header format.
"""
return ",".join(map(lambda key: key + "=" + tracestate[key], tracestate))
33 changes: 32 additions & 1 deletion opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@
"""

import enum
import re
import typing
from collections import OrderedDict
from contextlib import contextmanager

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


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

MAX_TRACESTATE_VALUES = 32
KEY_WITHOUT_VENDOR_FORMAT = r"[a-z][_0-9a-z\-\*\/]{0,255}"
KEY_WITH_VENDOR_FORMAT = (
r"[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"
)
KEY_FORMAT = KEY_WITHOUT_VENDOR_FORMAT + "|" + KEY_WITH_VENDOR_FORMAT
VALUE_FORMAT = (
r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"
)

KEY_VALIDATION_RE = re.compile("^" + KEY_FORMAT + "$")
VALUE_VALIDATION_RE = re.compile("^" + VALUE_FORMAT + "$")

@classmethod
def get_default(cls) -> "TraceState":
return cls()

def __setitem__(self, key: str, value: str) -> None:
# According to the w3c spec, we can only store 32 values
if len(self) >= self.MAX_TRACESTATE_VALUES:
return
# TODO: I believe the otel spec calls for no exceptions
# that interfere with execution in the API.
# if not isinstance(key, str):
# raise ValueError("key must be an instance of str")
# if not re.match(self._KEY_VALIDATION_RE, key):
# raise ValueError("illegal key provided")
# if not isinstance(value, str):
# raise ValueError("value must be an instance of str")
# if not re.match(self._VALUE_VALIDATION_RE, value):
# raise ValueError("illegal value provided")
super().__setitem__(key, value)


DEFAULT_TRACE_STATE = TraceState.get_default()

Expand Down
Empty file.
Empty file.
Loading

0 comments on commit b01a50c

Please sign in to comment.