Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support w3c trace context #4170

Merged
merged 35 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0039a16
feat: add w3c trace context http header propagation
majorgreys Sep 2, 2022
7f7d6d8
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 6, 2022
78758e3
initial _extract method
ZStriker19 Sep 6, 2022
c7591f3
initial _inject
ZStriker19 Sep 7, 2022
ff2d76e
release note
ZStriker19 Sep 7, 2022
12ac12e
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 8, 2022
5e71810
basic inject and extract testing
ZStriker19 Sep 8, 2022
84993c9
add traceparent to ignored spelling list
ZStriker19 Sep 8, 2022
28b09fb
Revert "add traceparent to ignored spelling list"
ZStriker19 Sep 8, 2022
dbfedda
add traceparent to wordlist and fix hex change for 2.7
ZStriker19 Sep 8, 2022
17af475
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 8, 2022
48bd4ac
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 9, 2022
de95ad6
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 12, 2022
672bc5b
Update ddtrace/propagation/http.py
ZStriker19 Sep 12, 2022
2f87b20
Update releasenotes/notes/add-w3c-traceparent-propagation-789324b2e13…
ZStriker19 Sep 12, 2022
c145cbc
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 13, 2022
c411c8e
Update releasenotes/notes/add-w3c-traceparent-propagation-789324b2e13…
ZStriker19 Sep 15, 2022
6286166
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 15, 2022
fbf6139
nits and refactoring
ZStriker19 Sep 15, 2022
bc081b0
Update ddtrace/propagation/http.py
ZStriker19 Sep 16, 2022
ba1f99a
Update ddtrace/propagation/http.py
ZStriker19 Sep 16, 2022
4a17016
Update ddtrace/propagation/http.py
ZStriker19 Sep 16, 2022
9d9bb87
nits
ZStriker19 Sep 16, 2022
a6d40ef
nits
ZStriker19 Sep 16, 2022
1f1b14f
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 16, 2022
7deeaac
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 16, 2022
7ef5da2
Update ddtrace/propagation/http.py
ZStriker19 Sep 19, 2022
4795a97
Update releasenotes/notes/add-w3c-traceparent-propagation-789324b2e13…
ZStriker19 Sep 19, 2022
e7e9ac0
Update ddtrace/propagation/http.py
ZStriker19 Sep 19, 2022
26048fa
formatting
ZStriker19 Sep 19, 2022
c190efd
Update releasenotes/notes/add-w3c-traceparent-propagation-789324b2e13…
ZStriker19 Sep 19, 2022
1d602aa
Merge branch '1.x' into majorgreys/AIT-3988
majorgreys Sep 20, 2022
370a4f2
Merge branch '1.x' into majorgreys/AIT-3988
ZStriker19 Sep 20, 2022
777b580
Merge branch '1.x' into majorgreys/AIT-3988
mergify[bot] Sep 20, 2022
11dddbd
Merge branch '1.x' into majorgreys/AIT-3988
mergify[bot] Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
130 changes: 118 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,103 @@ 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.
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

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.
"""
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

@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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

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 +617,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 +659,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 @@ -167,6 +167,7 @@ subdomains
submodule
submodules
timestamp
traceparent
tweens
uWSGI
unbuffered
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
Add support for W3C traceparent propagation.
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

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