Skip to content

Commit

Permalink
feat: support w3c trace context (#4170)
Browse files Browse the repository at this point in the history
We want to support W3C propagation headers which is what Otel uses. w3c standard.
Currently we're only adding support for traceparent to propagate the trace context. We are not yet adding support for tracestate but are planning to in future work.

Co-authored-by: Tahir H. Butt <tahir.butt@datadoghq.com>
Co-authored-by: Kyle Verhoog <kyle@verhoog.ca>
Co-authored-by: Gabriele N. Tornetta <P403n1x87@users.noreply.github.com>
  • Loading branch information
4 people committed Sep 20, 2022
1 parent e315198 commit e652b99
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 13 deletions.
3 changes: 2 additions & 1 deletion ddtrace/internal/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
PROPAGATION_STYLE_DATADOG = "datadog"
PROPAGATION_STYLE_B3 = "b3"
PROPAGATION_STYLE_B3_SINGLE_HEADER = "b3 single header"
PROPAGATION_STYLE_W3C = "w3c"
PROPAGATION_STYLE_ALL = frozenset(
[PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3, PROPAGATION_STYLE_B3_SINGLE_HEADER]
[PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3, PROPAGATION_STYLE_B3_SINGLE_HEADER, PROPAGATION_STYLE_W3C]
) # type: FrozenSet[str]


Expand Down
132 changes: 120 additions & 12 deletions ddtrace/propagation/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ..internal.constants import PROPAGATION_STYLE_B3
from ..internal.constants import PROPAGATION_STYLE_B3_SINGLE_HEADER
from ..internal.constants import PROPAGATION_STYLE_DATADOG
from ..internal.constants import PROPAGATION_STYLE_W3C
from ..internal.logger import get_logger
from ..internal.sampling import validate_sampling_decision
from ..span import _MetaDictType
Expand All @@ -41,6 +42,7 @@
_HTTP_HEADER_B3_SAMPLED = "x-b3-sampled"
_HTTP_HEADER_B3_FLAGS = "x-b3-flags"
_HTTP_HEADER_TAGS = "x-datadog-tags"
_HTTP_HEADER_W3C_TRACEPARENT = "traceparent"


def _possible_header(header):
Expand All @@ -60,6 +62,7 @@ def _possible_header(header):
_POSSIBLE_HTTP_HEADER_B3_SPAN_IDS = _possible_header(_HTTP_HEADER_B3_SPAN_ID)
_POSSIBLE_HTTP_HEADER_B3_SAMPLEDS = _possible_header(_HTTP_HEADER_B3_SAMPLED)
_POSSIBLE_HTTP_HEADER_B3_FLAGS = _possible_header(_HTTP_HEADER_B3_FLAGS)
_POSSIBLE_HTTP_HEADER_W3C_TRACEPARENT = _possible_header(_HTTP_HEADER_W3C_TRACEPARENT)


def _extract_header_value(possible_header_names, headers, default=None):
Expand All @@ -71,20 +74,20 @@ def _extract_header_value(possible_header_names, headers, default=None):
return default


def _b3_id_to_dd_id(b3_id):
def _hex_id_to_dd_id(hex_id):
# type: (str) -> int
"""Helper to convert B3 trace/span hex ids into Datadog compatible ints
"""Helper to convert hexadecimal trace/span hex ids into Datadog compatible ints
If the id is > 64 bit then truncate the trailing 64 bit.
"463ac35c9f6413ad48485a3953bb6124" -> "48485a3953bb6124" -> 5208512171318403364
"""
return int(b3_id[-16:], 16)
return int(hex_id[-16:], 16)


def _dd_id_to_b3_id(dd_id):
def _dd_id_to_hex_id(dd_id):
# type: (int) -> str
"""Helper to convert Datadog trace/span int ids into B3 compatible hex ids"""
"""Helper to convert Datadog trace/span int ids into hexadecimal compatible ids"""
# DEV: `hex(dd_id)` will give us `0xDEADBEEF`
# DEV: this gives us lowercase hex, which is what we want
return "{:x}".format(dd_id)
Expand Down Expand Up @@ -297,8 +300,8 @@ def _inject(span_context, headers):
log.debug("tried to inject invalid context %r", span_context)
return

headers[_HTTP_HEADER_B3_TRACE_ID] = _dd_id_to_b3_id(span_context.trace_id)
headers[_HTTP_HEADER_B3_SPAN_ID] = _dd_id_to_b3_id(span_context.span_id)
headers[_HTTP_HEADER_B3_TRACE_ID] = _dd_id_to_hex_id(span_context.trace_id)
headers[_HTTP_HEADER_B3_SPAN_ID] = _dd_id_to_hex_id(span_context.span_id)
sampling_priority = span_context.sampling_priority
# Propagate priority only if defined
if sampling_priority is not None:
Expand Down Expand Up @@ -339,9 +342,9 @@ def _extract(headers):
trace_id = None
span_id = None
if trace_id_val is not None:
trace_id = _b3_id_to_dd_id(trace_id_val) or None
trace_id = _hex_id_to_dd_id(trace_id_val) or None
if span_id_val is not None:
span_id = _b3_id_to_dd_id(span_id_val) or None
span_id = _hex_id_to_dd_id(span_id_val) or None

sampling_priority = None
if sampled is not None:
Expand Down Expand Up @@ -414,7 +417,7 @@ def _inject(span_context, headers):
log.debug("tried to inject invalid context %r", span_context)
return

single_header = "{}-{}".format(_dd_id_to_b3_id(span_context.trace_id), _dd_id_to_b3_id(span_context.span_id))
single_header = "-".join((_dd_id_to_hex_id(span_context.trace_id), _dd_id_to_hex_id(span_context.span_id)))
sampling_priority = span_context.sampling_priority
if sampling_priority is not None:
if sampling_priority <= 0:
Expand Down Expand Up @@ -457,9 +460,9 @@ def _extract(headers):
# DEV: We are allowed to have only x-b3-sampled/flags
# DEV: Do not allow `0` for trace id or span id, use None instead
if trace_id_val is not None:
trace_id = _b3_id_to_dd_id(trace_id_val) or None
trace_id = _hex_id_to_dd_id(trace_id_val) or None
if span_id_val is not None:
span_id = _b3_id_to_dd_id(span_id_val) or None
span_id = _hex_id_to_dd_id(span_id_val) or None

sampling_priority = None
if sampled is not None:
Expand All @@ -483,6 +486,105 @@ def _extract(headers):
return None


class _W3CTraceContext:
"""Helper class to inject/extract W3C Trace Context
https://www.w3.org/TR/trace-context/
Overview:
- ``traceparent`` describes the position of the incoming request in its
trace graph in a portable, fixed-length format. Its design focuses on
fast parsing. Every tracing tool MUST properly set traceparent even when
it only relies on vendor-specific information in tracestate
- ``tracestate`` extends traceparent with vendor-specific data represented
by a set of name/value pairs. Storing information in tracestate is
optional. [Unsupported]
The format for ``traceparent`` is::
HEXDIGLC = DIGIT / "a" / "b" / "c" / "d" / "e" / "f"
value = version "-" version-format
version = 2HEXDIGLC
version-format = trace-id "-" parent-id "-" trace-flags
trace-id = 32HEXDIGLC
parent-id = 16HEXDIGLC
trace-flags = 2HEXDIGLC
Example value of HTTP ``traceparent`` header::
value = 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
base16(version) = 00
base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736
base16(parent-id) = 00f067aa0ba902b7
base16(trace-flags) = 01 // sampled
Implementation details:
- Datadog Trace and Span IDs are 64-bit unsigned integers.
- The W3C Trace Context Trace ID is a 16-byte hexadecimal string. This is
transformed for propagation between Datadog by taking the lower
8-bytes.
- The tracestate header will be supported in a future release.
"""

@staticmethod
def _inject(span_context, headers):
# type: (Context, Dict[str, str]) -> None
# use hex to convert back, the trace id will be pre-pended with a bunch of 0s to fill in for previous values
trace_id = span_context.trace_id
span_id = span_context.span_id
if trace_id is None or span_id is None:
log.debug("tried to inject invalid context %r", span_context)
return

sampling_priority = span_context.sampling_priority
trace_flags = 1 if sampling_priority and sampling_priority >= AUTO_KEEP else 0

# There is currently only a single version so we always start with 00
traceparent = "00-{:032x}-{:016x}-{:02x}".format(trace_id, span_id, trace_flags)
headers[_HTTP_HEADER_W3C_TRACEPARENT] = traceparent

@staticmethod
def _extract(headers):
# type: (Dict[str, str]) -> Optional[Context]
tp = _extract_header_value(_POSSIBLE_HTTP_HEADER_W3C_TRACEPARENT, headers)
if tp is None:
return None

version, trace_id_hex, span_id_hex, trace_flags_hex = tp.split("-")

try:
# currently 00 is the only version format, but if future versions come up we may need to add changes
if version != "00":
log.warning("unsupported traceparent version:%s . Will still attempt to parse.", (version))

if len(trace_id_hex) == 32 and len(span_id_hex) == 16 and len(trace_flags_hex) >= 2:
trace_id = _hex_id_to_dd_id(trace_id_hex)
span_id = _hex_id_to_dd_id(span_id_hex)

# All 0s are invalid values
assert trace_id != 0
assert span_id != 0

trace_flags = _hex_id_to_dd_id(trace_flags_hex)
# there's currently only one trace flag, which denotes sampling priority was set to keep
sampling_priority = float(trace_flags & 0x1)

else:
log.debug("W3C traceparent hex length incorrect: %s", tp)

return Context(
trace_id=trace_id,
span_id=span_id,
sampling_priority=sampling_priority,
)
except (ValueError, AssertionError):
log.exception("received invalid w3c traceparent: %s.", tp)
return None


class HTTPPropagator(object):
"""A HTTP Propagator using HTTP headers as carrier."""

Expand Down Expand Up @@ -517,6 +619,8 @@ def parent_call():
_B3MultiHeader._inject(span_context, headers)
if PROPAGATION_STYLE_B3_SINGLE_HEADER in config._propagation_style_inject:
_B3SingleHeader._inject(span_context, headers)
if PROPAGATION_STYLE_W3C in config._propagation_style_inject:
_W3CTraceContext._inject(span_context, headers)

@staticmethod
def extract(headers):
Expand Down Expand Up @@ -557,6 +661,10 @@ def my_controller(url, headers):
context = _B3SingleHeader._extract(normalized_headers)
if context is not None:
return context
if PROPAGATION_STYLE_W3C in config._propagation_style_extract:
context = _W3CTraceContext._extract(normalized_headers)
if context is not None:
return context
except Exception:
log.debug("error while extracting context propagation headers", exc_info=True)
return Context()
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ subdomains
submodule
submodules
timestamp
traceparent
tweens
uWSGI
unbuffered
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
features:
- |
Add support trace propagation using W3C Trace Context with HTTP header ``traceparent``.
Note: ``tracestate`` HTTP header is not yet supported.
See :ref:`DD_TRACE_PROPAGATION_STYLE_EXTRACT <dd-trace-propagation-style-extract>` and
:ref:`DD_TRACE_PROPAGATION_STYLE_INJECT <dd-trace-propagation-style-inject>`
configuration documentation to enable.
45 changes: 45 additions & 0 deletions tests/tracer/test_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from ddtrace.internal.constants import PROPAGATION_STYLE_B3
from ddtrace.internal.constants import PROPAGATION_STYLE_B3_SINGLE_HEADER
from ddtrace.internal.constants import PROPAGATION_STYLE_DATADOG
from ddtrace.internal.constants import PROPAGATION_STYLE_W3C
from ddtrace.propagation._utils import get_wsgi_header
from ddtrace.propagation.http import HTTPPropagator
from ddtrace.propagation.http import HTTP_HEADER_ORIGIN
Expand All @@ -21,6 +22,7 @@
from ddtrace.propagation.http import _HTTP_HEADER_B3_SPAN_ID
from ddtrace.propagation.http import _HTTP_HEADER_B3_TRACE_ID
from ddtrace.propagation.http import _HTTP_HEADER_TAGS
from ddtrace.propagation.http import _HTTP_HEADER_W3C_TRACEPARENT

from ..utils import override_global_config

Expand Down Expand Up @@ -402,12 +404,19 @@ def test_get_wsgi_header(tracer):
B3_SINGLE_HEADERS_INVALID = {
_HTTP_HEADER_B3_SINGLE: "NON_HEX_VALUE-e457b5a2e4d86bd1-1",
}
W3C_HEADERS_VALID = {
_HTTP_HEADER_W3C_TRACEPARENT: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
}
W3C_HEADERS_INVALID = {
_HTTP_HEADER_W3C_TRACEPARENT: "00-a3ce929d0e0e4736-00f067aa0ba902b7-01",
}


ALL_HEADERS = {}
ALL_HEADERS.update(DATADOG_HEADERS_VALID)
ALL_HEADERS.update(B3_HEADERS_VALID)
ALL_HEADERS.update(B3_SINGLE_HEADERS_VALID)
ALL_HEADERS.update(W3C_HEADERS_VALID)

EXTRACT_FIXTURES = [
# Datadog headers
Expand Down Expand Up @@ -692,6 +701,24 @@ def test_get_wsgi_header(tracer):
B3_SINGLE_HEADERS_VALID,
CONTEXT_EMPTY,
),
# w3c headers
(
"valid_w3c_simple",
[PROPAGATION_STYLE_W3C],
W3C_HEADERS_VALID,
{
"trace_id": 11803532876627986230,
"span_id": 67667974448284343,
"sampling_priority": 1,
"dd_origin": None,
},
),
(
"invalid_w3c",
[PROPAGATION_STYLE_W3C],
W3C_HEADERS_INVALID,
CONTEXT_EMPTY,
),
# All valid headers
(
"valid_all_headers_default_style",
Expand Down Expand Up @@ -849,6 +876,7 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
env["DD_TRACE_PROPAGATION_STYLE_EXTRACT"] = ",".join(styles)
stdout, stderr, status, _ = run_python_code_in_subprocess(code=code, env=env)
assert status == 0, (stdout, stderr)
print(stderr)
assert stderr == b"", (stdout, stderr)

result = json.loads(stdout.decode())
Expand Down Expand Up @@ -1065,6 +1093,19 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
},
{_HTTP_HEADER_B3_SINGLE: "b5a2814f70060771-7197677932a62370"},
),
# w3c only
(
"valid_w3c_simple",
[PROPAGATION_STYLE_W3C],
VALID_DATADOG_CONTEXT,
{_HTTP_HEADER_W3C_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-01"},
),
(
"invalid_w3c_style",
[PROPAGATION_STYLE_W3C],
{},
{},
),
# All styles
(
"valid_all_styles",
Expand All @@ -1079,6 +1120,7 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
_HTTP_HEADER_B3_SPAN_ID: "7197677932a62370",
_HTTP_HEADER_B3_SAMPLED: "1",
_HTTP_HEADER_B3_SINGLE: "b5a2814f70060771-7197677932a62370-1",
_HTTP_HEADER_W3C_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-01",
},
),
(
Expand All @@ -1093,6 +1135,7 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
_HTTP_HEADER_B3_SPAN_ID: "7197677932a62370",
_HTTP_HEADER_B3_FLAGS: "1",
_HTTP_HEADER_B3_SINGLE: "b5a2814f70060771-7197677932a62370-d",
_HTTP_HEADER_W3C_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-01",
},
),
(
Expand All @@ -1107,6 +1150,7 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
_HTTP_HEADER_B3_SPAN_ID: "7197677932a62370",
_HTTP_HEADER_B3_SAMPLED: "0",
_HTTP_HEADER_B3_SINGLE: "b5a2814f70060771-7197677932a62370-0",
_HTTP_HEADER_W3C_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-00",
},
),
(
Expand All @@ -1122,6 +1166,7 @@ def test_propagation_extract(name, styles, headers, expected_context, run_python
_HTTP_HEADER_B3_TRACE_ID: "b5a2814f70060771",
_HTTP_HEADER_B3_SPAN_ID: "7197677932a62370",
_HTTP_HEADER_B3_SINGLE: "b5a2814f70060771-7197677932a62370",
_HTTP_HEADER_W3C_TRACEPARENT: "00-0000000000000000b5a2814f70060771-7197677932a62370-00",
},
),
]
Expand Down

0 comments on commit e652b99

Please sign in to comment.