diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 502e12ece07..9752d82ff83 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -775,8 +775,7 @@ def _start_span( service = config.service_mapping.get(service, service) links = context._span_links if not parent else [] - - if trace_id: + if trace_id or links or context._baggage: # child_of a non-empty context, so either a local child span or from a remote context span = Span( name=name, diff --git a/ddtrace/contrib/trace_utils.py b/ddtrace/contrib/trace_utils.py index db8509d8c35..644475b02ab 100644 --- a/ddtrace/contrib/trace_utils.py +++ b/ddtrace/contrib/trace_utils.py @@ -565,9 +565,9 @@ def activate_distributed_headers(tracer, int_config=None, request_headers=None, context = HTTPPropagator.extract(request_headers) # Only need to activate the new context if something was propagated - if not context.trace_id: + # The new context must have one of these values in order for it to be activated + if not context.trace_id and not context._baggage and not context._span_links: return None - # Do not reactivate a context with the same trace id # DEV: An example could be nested web frameworks, when one layer already # parsed request headers and activated them. @@ -577,7 +577,14 @@ def activate_distributed_headers(tracer, int_config=None, request_headers=None, # app = Flask(__name__) # Traced via Flask instrumentation # app = DDWSGIMiddleware(app) # Extra layer on top for WSGI current_context = tracer.current_trace_context() - if current_context and current_context.trace_id == context.trace_id: + + # We accept incoming contexts with only baggage or only span_links, however if we + # already have a current_context then an incoming context not + # containing a trace_id or containing the same trace_id + # should not be activated. + if current_context and ( + not context.trace_id or (context.trace_id and context.trace_id == current_context.trace_id) + ): log.debug( "will not activate extracted Context(trace_id=%r, span_id=%r), a context with that trace id is already active", # noqa: E501 context.trace_id, diff --git a/ddtrace/internal/constants.py b/ddtrace/internal/constants.py index 4efdc754ef3..c4255035c41 100644 --- a/ddtrace/internal/constants.py +++ b/ddtrace/internal/constants.py @@ -19,6 +19,10 @@ _PROPAGATION_STYLE_NONE, _PROPAGATION_STYLE_BAGGAGE, ) +_PROPAGATION_BEHAVIOR_CONTINUE = "continue" +_PROPAGATION_BEHAVIOR_IGNORE = "ignore" +_PROPAGATION_BEHAVIOR_RESTART = "restart" +_PROPAGATION_BEHAVIOR_DEFAULT = _PROPAGATION_BEHAVIOR_CONTINUE W3C_TRACESTATE_KEY = "tracestate" W3C_TRACEPARENT_KEY = "traceparent" W3C_TRACESTATE_PARENT_ID_KEY = "p" diff --git a/ddtrace/propagation/http.py b/ddtrace/propagation/http.py index 563ee838d84..0cd5b69db46 100644 --- a/ddtrace/propagation/http.py +++ b/ddtrace/propagation/http.py @@ -40,6 +40,7 @@ from ..internal._tagset import decode_tagset_string from ..internal._tagset import encode_tagset_values from ..internal.compat import ensure_text +from ..internal.constants import _PROPAGATION_BEHAVIOR_RESTART from ..internal.constants import _PROPAGATION_STYLE_BAGGAGE from ..internal.constants import _PROPAGATION_STYLE_NONE from ..internal.constants import _PROPAGATION_STYLE_W3C_TRACECONTEXT @@ -974,12 +975,12 @@ class HTTPPropagator(object): """ @staticmethod - def _extract_configured_contexts_avail(normalized_headers): + def _extract_configured_contexts_avail(normalized_headers: Dict[str, str]) -> Tuple[List[Context], List[str]]: contexts = [] styles_w_ctx = [] for prop_style in config._propagation_style_extract: propagator = _PROP_STYLES[prop_style] - context = propagator._extract(normalized_headers) + context = propagator._extract(normalized_headers) # type: ignore # baggage is handled separately if prop_style == _PROPAGATION_STYLE_BAGGAGE: continue @@ -988,6 +989,24 @@ def _extract_configured_contexts_avail(normalized_headers): styles_w_ctx.append(prop_style) return contexts, styles_w_ctx + @staticmethod + def _context_to_span_link(context: Context, style: str, reason: str) -> Optional[SpanLink]: + # encoding expects at least trace_id and span_id + if context.span_id and context.trace_id: + return SpanLink( + context.trace_id, + context.span_id, + flags=1 if context.sampling_priority and context.sampling_priority > 0 else 0, + tracestate=( + context._meta.get(W3C_TRACESTATE_KEY, "") if style == _PROPAGATION_STYLE_W3C_TRACECONTEXT else None + ), + attributes={ + "reason": reason, + "context_headers": style, + }, + ) + return None + @staticmethod def _resolve_contexts(contexts, styles_w_ctx, normalized_headers): primary_context = contexts[0] @@ -996,23 +1015,14 @@ def _resolve_contexts(contexts, styles_w_ctx, normalized_headers): for context in contexts[1:]: style_w_ctx = styles_w_ctx[contexts.index(context)] # encoding expects at least trace_id and span_id - if context.span_id and context.trace_id and context.trace_id != primary_context.trace_id: - links.append( - SpanLink( - context.trace_id, - context.span_id, - flags=1 if context.sampling_priority and context.sampling_priority > 0 else 0, - tracestate=( - context._meta.get(W3C_TRACESTATE_KEY, "") - if style_w_ctx == _PROPAGATION_STYLE_W3C_TRACECONTEXT - else None - ), - attributes={ - "reason": "terminated_context", - "context_headers": style_w_ctx, - }, - ) + if context.trace_id and context.trace_id != primary_context.trace_id: + link = HTTPPropagator._context_to_span_link( + context, + style_w_ctx, + "terminated_context", ) + if link: + links.append(link) # if trace_id matches and the propagation style is tracecontext # add the tracestate to the primary context elif style_w_ctx == _PROPAGATION_STYLE_W3C_TRACECONTEXT: @@ -1130,17 +1140,19 @@ def my_controller(url, headers): :param dict headers: HTTP headers to extract tracing attributes. :return: New `Context` with propagated attributes. """ + context = Context() if not headers: - return Context() + return context try: + style = "" normalized_headers = {name.lower(): v for name, v in headers.items()} - context = Context() # tracer configured to extract first only if config._propagation_extract_first: # loop through the extract propagation styles specified in order, return whatever context we get first for prop_style in config._propagation_style_extract: propagator = _PROP_STYLES[prop_style] context = propagator._extract(normalized_headers) + style = prop_style if config.propagation_http_baggage_enabled is True: _attach_baggage_to_context(normalized_headers, context) break @@ -1148,6 +1160,9 @@ def my_controller(url, headers): # loop through all extract propagation styles else: contexts, styles_w_ctx = HTTPPropagator._extract_configured_contexts_avail(normalized_headers) + # check that styles_w_ctx is not empty + if styles_w_ctx: + style = styles_w_ctx[0] if contexts: context = HTTPPropagator._resolve_contexts(contexts, styles_w_ctx, normalized_headers) @@ -1159,9 +1174,12 @@ def my_controller(url, headers): baggage_context = _BaggageHeader._extract(normalized_headers) if baggage_context._baggage != {}: if context: - context._baggage = baggage_context._baggage + context._baggage = baggage_context.get_all_baggage_items() else: context = baggage_context + if config._propagation_behavior_extract == _PROPAGATION_BEHAVIOR_RESTART: + link = HTTPPropagator._context_to_span_link(context, style, "propagation_behavior_extract") + context = Context(baggage=context.get_all_baggage_items(), span_links=[link] if link else []) return context diff --git a/ddtrace/settings/config.py b/ddtrace/settings/config.py index 65baf99ccb3..6ee75fbe6d8 100644 --- a/ddtrace/settings/config.py +++ b/ddtrace/settings/config.py @@ -20,7 +20,10 @@ from ddtrace.vendor.debtcollector import deprecate from ..internal import gitmetadata +from ..internal.constants import _PROPAGATION_BEHAVIOR_DEFAULT +from ..internal.constants import _PROPAGATION_BEHAVIOR_IGNORE from ..internal.constants import _PROPAGATION_STYLE_DEFAULT +from ..internal.constants import _PROPAGATION_STYLE_NONE from ..internal.constants import DEFAULT_BUFFER_SIZE from ..internal.constants import DEFAULT_MAX_PAYLOAD_SIZE from ..internal.constants import DEFAULT_PROCESSING_INTERVAL @@ -529,11 +532,23 @@ def __init__(self): # Propagation styles # DD_TRACE_PROPAGATION_STYLE_EXTRACT and DD_TRACE_PROPAGATION_STYLE_INJECT # take precedence over DD_TRACE_PROPAGATION_STYLE - self._propagation_style_extract = _parse_propagation_styles( - _get_config( - ["DD_TRACE_PROPAGATION_STYLE_EXTRACT", "DD_TRACE_PROPAGATION_STYLE"], _PROPAGATION_STYLE_DEFAULT - ) + # if DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT is set to ignore + # we set DD_TRACE_PROPAGATION_STYLE_EXTRACT to [_PROPAGATION_STYLE_NONE] since no extraction will heeded + self._propagation_behavior_extract = _get_config( + ["DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT"], _PROPAGATION_BEHAVIOR_DEFAULT, self._lower ) + if self._propagation_behavior_extract != _PROPAGATION_BEHAVIOR_IGNORE: + self._propagation_style_extract = _parse_propagation_styles( + _get_config( + ["DD_TRACE_PROPAGATION_STYLE_EXTRACT", "DD_TRACE_PROPAGATION_STYLE"], _PROPAGATION_STYLE_DEFAULT + ) + ) + else: + log.debug( + """DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT is set to ignore, + setting DD_TRACE_PROPAGATION_STYLE_EXTRACT to empty list""" + ) + self._propagation_style_extract = [_PROPAGATION_STYLE_NONE] self._propagation_style_inject = _parse_propagation_styles( _get_config(["DD_TRACE_PROPAGATION_STYLE_INJECT", "DD_TRACE_PROPAGATION_STYLE"], _PROPAGATION_STYLE_DEFAULT) ) @@ -978,3 +993,6 @@ def convert_rc_trace_sampling_rules(self, rc_rules: List[Dict[str, Any]]) -> Opt return json.dumps(rc_rules) else: return None + + def _lower(self, value): + return value.lower() diff --git a/docs/configuration.rst b/docs/configuration.rst index 35bb63fac20..455272f318d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -368,6 +368,26 @@ Trace Context propagation version_added: v1.7.0: The ``b3multi`` propagation style was added and ``b3`` was deprecated in favor it. + DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: + default: | + ``continue`` + + description: | + String for how to handle incoming request headers that are extracted for propagation of trace info. + + The supported values are ``continue``, ``restart``, and ``ignore``. + + After extracting the headers for propagation, this configuration determines what is done with them. + + The default value is ``continue`` which always propagates valid headers. + ``ignore`` ignores all incoming headers and ``restart`` turns the first extracted valid propagation header + into a span link and propagates baggage if present. + + Example: ``DD_TRACE_PROPAGATION_STYLE_EXTRACT="ignore"`` to ignore all incoming headers and to start a root span without a parent. + + version_added: + v2.20.0: + DD_TRACE_PROPAGATION_STYLE_INJECT: default: | ``tracecontext,datadog`` diff --git a/releasenotes/notes/propagation_behavior_extract-3d16765cfd07485b.yaml b/releasenotes/notes/propagation_behavior_extract-3d16765cfd07485b.yaml new file mode 100644 index 00000000000..6e1def89993 --- /dev/null +++ b/releasenotes/notes/propagation_behavior_extract-3d16765cfd07485b.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + propagation: Introduces the environment variable ``DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT`` + to control the behavior of the extraction of distributed tracing headers. The values, ``continue`` (default), + ``ignore``, and ``restart``, are supported. The default value is ``continue`` which has no change from the current behavior of always propagating valid headers. + ``ignore`` ignores all incoming headers, never propagating the incoming trace information + and ``restart`` turns the first extracted propagation style into a span link and propagates baggage if extracted. + +fixes: + - | + propagation: Fixes an issue where the baggage header was not being propagated when the baggage header was the only header extracted. + With this fix, the baggage header is now propagated when it is the only header extracted. diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py index 69fe7969fc7..8d4030c84a9 100644 --- a/tests/telemetry/test_writer.py +++ b/tests/telemetry/test_writer.py @@ -268,7 +268,9 @@ def test_app_started_event_configuration_override(test_agent_session, run_python env["DD_SPAN_SAMPLING_RULES_FILE"] = str(file) env["DD_TRACE_PARTIAL_FLUSH_ENABLED"] = "false" env["DD_TRACE_PARTIAL_FLUSH_MIN_SPANS"] = "3" + env["DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT"] = "restart" env["DD_SITE"] = "datadoghq.com" + # By default telemetry collection is enabled after 10 seconds, so we either need to # to sleep for 10 seconds or manually call _app_started() to generate the app started event. # This delay allows us to collect start up errors and dynamic configurations @@ -446,6 +448,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_TRACE_OTEL_ENABLED", "origin": "env_var", "value": True}, {"name": "DD_TRACE_PARTIAL_FLUSH_ENABLED", "origin": "env_var", "value": False}, {"name": "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", "origin": "env_var", "value": 3}, + {"name": "DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT", "origin": "env_var", "value": "restart"}, {"name": "DD_TRACE_PROPAGATION_EXTRACT_FIRST", "origin": "default", "value": False}, {"name": "DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED", "origin": "default", "value": False}, {"name": "DD_TRACE_PROPAGATION_STYLE_EXTRACT", "origin": "env_var", "value": "tracecontext"}, diff --git a/tests/tracer/test_propagation.py b/tests/tracer/test_propagation.py index 0d4c5d7c01d..c15439ae825 100644 --- a/tests/tracer/test_propagation.py +++ b/tests/tracer/test_propagation.py @@ -16,6 +16,8 @@ from ddtrace.constants import AUTO_REJECT from ddtrace.constants import USER_KEEP from ddtrace.constants import USER_REJECT +from ddtrace.internal.constants import _PROPAGATION_BEHAVIOR_IGNORE +from ddtrace.internal.constants import _PROPAGATION_BEHAVIOR_RESTART from ddtrace.internal.constants import _PROPAGATION_STYLE_BAGGAGE from ddtrace.internal.constants import _PROPAGATION_STYLE_NONE from ddtrace.internal.constants import _PROPAGATION_STYLE_W3C_TRACECONTEXT @@ -1529,6 +1531,9 @@ def test_extract_tracecontext(headers, expected_context): HTTP_HEADER_PARENT_ID: "parent_id", HTTP_HEADER_SAMPLING_PRIORITY: "sample", } + +DATADOG_BAGGAGE_HEADERS_VALID = {**DATADOG_HEADERS_VALID, "baggage": "key1=val1,key2=val2"} + B3_HEADERS_VALID = { _HTTP_HEADER_B3_TRACE_ID: "80f198ee56343ba864fe8b2a57d3eff7", _HTTP_HEADER_B3_SPAN_ID: "a2fb4a1d1a96d312", @@ -1582,6 +1587,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_default", None, + None, DATADOG_HEADERS_VALID, { "trace_id": 13088165645273925489, @@ -1594,6 +1600,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_default_wsgi", None, + None, {get_wsgi_header(name): value for name, value in DATADOG_HEADERS_VALID.items()}, { "trace_id": 13088165645273925489, @@ -1606,6 +1613,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_no_priority", None, + None, DATADOG_HEADERS_VALID_NO_PRIORITY, { "trace_id": 13088165645273925489, @@ -1618,12 +1626,14 @@ def test_extract_tracecontext(headers, expected_context): ( "invalid_datadog", [PROPAGATION_STYLE_DATADOG], + None, DATADOG_HEADERS_INVALID, CONTEXT_EMPTY, ), ( "valid_datadog_explicit_style", [PROPAGATION_STYLE_DATADOG], + None, DATADOG_HEADERS_VALID, { "trace_id": 13088165645273925489, @@ -1636,6 +1646,7 @@ def test_extract_tracecontext(headers, expected_context): ( "invalid_datadog_negative_trace_id", [PROPAGATION_STYLE_DATADOG], + None, { HTTP_HEADER_TRACE_ID: "-1", HTTP_HEADER_PARENT_ID: "5678", @@ -1647,6 +1658,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_explicit_style_wsgi", [PROPAGATION_STYLE_DATADOG], + None, {get_wsgi_header(name): value for name, value in DATADOG_HEADERS_VALID.items()}, { "trace_id": 13088165645273925489, @@ -1659,6 +1671,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_all_styles", [PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_B3_SINGLE], + None, DATADOG_HEADERS_VALID, { "trace_id": 13088165645273925489, @@ -1671,13 +1684,29 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_datadog_no_datadog_style", [PROPAGATION_STYLE_B3_MULTI], + None, DATADOG_HEADERS_VALID, CONTEXT_EMPTY, ), + ( + "valid_datadog_and_baggage_default", + None, + None, + DATADOG_BAGGAGE_HEADERS_VALID, + { + "trace_id": 13088165645273925489, + "span_id": 5678, + "sampling_priority": 1, + "dd_origin": "synthetics", + "meta": {"_dd.p.dm": "-3"}, + "baggage": {"key1": "val1", "key2": "val2"}, + }, + ), # B3 headers ( "valid_b3_simple", [PROPAGATION_STYLE_B3_MULTI], + None, B3_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -1689,6 +1718,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_wsgi", [PROPAGATION_STYLE_B3_MULTI], + None, {get_wsgi_header(name): value for name, value in B3_HEADERS_VALID.items()}, { "trace_id": TRACE_ID, @@ -1700,6 +1730,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_flags", [PROPAGATION_STYLE_B3_MULTI], + None, { _HTTP_HEADER_B3_TRACE_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_TRACE_ID], _HTTP_HEADER_B3_SPAN_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_SPAN_ID], @@ -1715,6 +1746,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_with_parent_id", [PROPAGATION_STYLE_B3_MULTI], + None, { _HTTP_HEADER_B3_TRACE_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_TRACE_ID], _HTTP_HEADER_B3_SPAN_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_SPAN_ID], @@ -1731,6 +1763,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_only_trace_and_span_id", [PROPAGATION_STYLE_B3_MULTI], + None, { _HTTP_HEADER_B3_TRACE_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_TRACE_ID], _HTTP_HEADER_B3_SPAN_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_SPAN_ID], @@ -1745,6 +1778,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_only_trace_id", [PROPAGATION_STYLE_B3_MULTI], + None, { _HTTP_HEADER_B3_TRACE_ID: B3_HEADERS_VALID[_HTTP_HEADER_B3_TRACE_ID], }, @@ -1758,24 +1792,28 @@ def test_extract_tracecontext(headers, expected_context): ( "invalid_b3", [PROPAGATION_STYLE_B3_MULTI], + None, B3_HEADERS_INVALID, CONTEXT_EMPTY, ), ( "valid_b3_default_style", None, + None, B3_HEADERS_VALID, CONTEXT_EMPTY, ), ( "valid_b3_no_b3_style", [PROPAGATION_STYLE_B3_SINGLE], + None, B3_HEADERS_VALID, CONTEXT_EMPTY, ), ( "valid_b3_all_styles", [PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_B3_SINGLE], + None, B3_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -1788,6 +1826,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_simple", [PROPAGATION_STYLE_B3_SINGLE], + None, B3_SINGLE_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -1799,6 +1838,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_simple", [PROPAGATION_STYLE_B3_SINGLE], + None, { get_wsgi_header(_HTTP_HEADER_B3_SINGLE): B3_SINGLE_HEADERS_VALID[_HTTP_HEADER_B3_SINGLE], }, @@ -1812,6 +1852,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_simple", [PROPAGATION_STYLE_B3_SINGLE], + None, { get_wsgi_header(_HTTP_HEADER_B3_SINGLE): B3_SINGLE_HEADERS_VALID[_HTTP_HEADER_B3_SINGLE], }, @@ -1825,6 +1866,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_only_sampled", [PROPAGATION_STYLE_B3_SINGLE], + None, { _HTTP_HEADER_B3_SINGLE: "1", }, @@ -1838,6 +1880,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_only_trace_and_span_id", [PROPAGATION_STYLE_B3_SINGLE], + None, { _HTTP_HEADER_B3_SINGLE: "80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1", }, @@ -1851,12 +1894,14 @@ def test_extract_tracecontext(headers, expected_context): ( "invalid_b3_single_header", [PROPAGATION_STYLE_B3_SINGLE], + None, B3_SINGLE_HEADERS_INVALID, CONTEXT_EMPTY, ), ( "valid_b3_single_header_all_styles", [PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_B3_SINGLE], + None, B3_SINGLE_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -1868,6 +1913,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_extra_data", [PROPAGATION_STYLE_B3_SINGLE], + None, {_HTTP_HEADER_B3_SINGLE: B3_SINGLE_HEADERS_VALID[_HTTP_HEADER_B3_SINGLE] + "-05e3ac9a4f6e3b90-extra-data-here"}, { "trace_id": TRACE_ID, @@ -1879,18 +1925,21 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_b3_single_header_default_style", None, + None, B3_SINGLE_HEADERS_VALID, CONTEXT_EMPTY, ), ( "valid_b3_single_header_no_b3_single_header_style", [PROPAGATION_STYLE_B3_MULTI], + None, B3_SINGLE_HEADERS_VALID, CONTEXT_EMPTY, ), ( "baggage_case_insensitive", None, + None, {"BAgGage": "key1=val1,key2=val2"}, { "baggage": {"key1": "val1", "key2": "val2"}, @@ -1900,6 +1949,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_default_style", None, + None, ALL_HEADERS, { "trace_id": 13088165645273925489, @@ -1928,6 +1978,7 @@ def test_extract_tracecontext(headers, expected_context): PROPAGATION_STYLE_B3_SINGLE, _PROPAGATION_STYLE_W3C_TRACECONTEXT, ], + None, ALL_HEADERS, { "trace_id": 13088165645273925489, @@ -1968,6 +2019,7 @@ def test_extract_tracecontext(headers, expected_context): PROPAGATION_STYLE_B3_SINGLE, _PROPAGATION_STYLE_W3C_TRACECONTEXT, ], + None, {get_wsgi_header(name): value for name, value in ALL_HEADERS.items()}, { "trace_id": 13088165645273925489, @@ -2003,6 +2055,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_datadog_style", [PROPAGATION_STYLE_DATADOG], + None, ALL_HEADERS, { "trace_id": 13088165645273925489, @@ -2015,6 +2068,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_datadog_style_wsgi", [PROPAGATION_STYLE_DATADOG], + None, {get_wsgi_header(name): value for name, value in ALL_HEADERS.items()}, { "trace_id": 13088165645273925489, @@ -2027,6 +2081,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_b3_style", [PROPAGATION_STYLE_B3_MULTI], + None, ALL_HEADERS, { "trace_id": TRACE_ID, @@ -2038,6 +2093,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_b3_style_wsgi", [PROPAGATION_STYLE_B3_MULTI], + None, {get_wsgi_header(name): value for name, value in ALL_HEADERS.items()}, { "trace_id": TRACE_ID, @@ -2049,6 +2105,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_both_b3_styles", [PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_B3_SINGLE], + None, ALL_HEADERS, { "trace_id": TRACE_ID, @@ -2060,6 +2117,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_b3_single_style", [PROPAGATION_STYLE_B3_SINGLE], + None, ALL_HEADERS, { "trace_id": TRACE_ID, @@ -2072,6 +2130,7 @@ def test_extract_tracecontext(headers, expected_context): # name, styles, headers, expected_context, "none_style", [_PROPAGATION_STYLE_NONE], + None, ALL_HEADERS, { "trace_id": None, @@ -2084,6 +2143,7 @@ def test_extract_tracecontext(headers, expected_context): # name, styles, headers, expected_context, "none_and_other_prop_style_still_extracts", [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_NONE], + None, ALL_HEADERS, { "trace_id": 13088165645273925489, @@ -2097,6 +2157,7 @@ def test_extract_tracecontext(headers, expected_context): ( "order_matters_B3_SINGLE_HEADER_first", [PROPAGATION_STYLE_B3_SINGLE, PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_DATADOG], + None, B3_SINGLE_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -2113,6 +2174,7 @@ def test_extract_tracecontext(headers, expected_context): PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_W3C_TRACECONTEXT, ], + None, B3_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -2124,6 +2186,7 @@ def test_extract_tracecontext(headers, expected_context): ( "order_matters_B3_second_no_Datadog_headers", [PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3_MULTI], + None, B3_HEADERS_VALID, { "trace_id": TRACE_ID, @@ -2135,6 +2198,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_b3_single_style_wsgi", [PROPAGATION_STYLE_B3_SINGLE], + None, {get_wsgi_header(name): value for name, value in ALL_HEADERS.items()}, { "trace_id": TRACE_ID, @@ -2153,6 +2217,7 @@ def test_extract_tracecontext(headers, expected_context): _PROPAGATION_STYLE_W3C_TRACECONTEXT, PROPAGATION_STYLE_B3_SINGLE, ], + None, DATADOG_TRACECONTEXT_MATCHING_TRACE_ID_HEADERS, { "trace_id": _get_64_lowest_order_bits_as_int(TRACE_ID), @@ -2170,6 +2235,7 @@ def test_extract_tracecontext(headers, expected_context): ( "no_additional_tracestate_support_when_present_but_trace_ids_do_not_match", [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_W3C_TRACECONTEXT], + None, {**DATADOG_HEADERS_VALID, **TRACECONTEXT_HEADERS_VALID_RUM_NO_SAMPLING_DECISION}, { "trace_id": 13088165645273925489, @@ -2191,18 +2257,21 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_all_headers_no_style", [], + None, ALL_HEADERS, CONTEXT_EMPTY, ), ( "valid_all_headers_no_style_wsgi", [], + None, {get_wsgi_header(name): value for name, value in ALL_HEADERS.items()}, CONTEXT_EMPTY, ), ( "datadog_tracecontext_conflicting_span_ids", [PROPAGATION_STYLE_DATADOG, _PROPAGATION_STYLE_W3C_TRACECONTEXT], + None, { HTTP_HEADER_TRACE_ID: "9291375655657946024", HTTP_HEADER_PARENT_ID: "15", @@ -2215,6 +2284,135 @@ def test_extract_tracecontext(headers, expected_context): "meta": {"_dd.p.dm": "-3", LAST_DD_PARENT_ID_KEY: "000000000000000f"}, }, ), + ( + "valid_datadog_default_w_restart_behavior", + None, + _PROPAGATION_BEHAVIOR_RESTART, + DATADOG_HEADERS_VALID, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "span_links": [ + SpanLink( + trace_id=13088165645273925489, + span_id=5678, + tracestate=None, + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "datadog"}, + ) + ], + }, + ), + ( + "valid_datadog_tracecontext_and_baggage_default_w_restart_behavior", + None, + _PROPAGATION_BEHAVIOR_RESTART, + {**DATADOG_BAGGAGE_HEADERS_VALID, **TRACECONTEXT_HEADERS_VALID}, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "baggage": {"key1": "val1", "key2": "val2"}, + "span_links": [ + SpanLink( + trace_id=13088165645273925489, + span_id=5678, + tracestate=None, + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "datadog"}, + ) + ], + }, + ), + # All valid headers + ( + "valid_all_headers_default_style_w_restart_behavior", + None, + _PROPAGATION_BEHAVIOR_RESTART, + ALL_HEADERS, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "span_links": [ + SpanLink( + trace_id=13088165645273925489, + span_id=5678, + tracestate=None, + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "datadog"}, + ) + ], + }, + ), + ( + "valid_all_headers_trace_context_datadog_style_w_restart_behavior", + [_PROPAGATION_STYLE_W3C_TRACECONTEXT, PROPAGATION_STYLE_DATADOG], + _PROPAGATION_BEHAVIOR_RESTART, + ALL_HEADERS, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "span_links": [ + SpanLink( + trace_id=171395628812617415352188477958425669623, + span_id=67667974448284343, + tracestate="dd=s:2;o:rum;t.dm:-4;t.usr.id:baz64,congo=t61rcWkgMzE", + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "tracecontext"}, + ) + ], + }, + ), + ( + "valid_all_headers_all_styles_w_restart_behavior", + [PROPAGATION_STYLE_B3_MULTI, PROPAGATION_STYLE_B3_SINGLE, _PROPAGATION_STYLE_W3C_TRACECONTEXT], + _PROPAGATION_BEHAVIOR_RESTART, + ALL_HEADERS, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "span_links": [ + SpanLink( + trace_id=171395628812617415352188477958425669623, + span_id=67667974448284343, + tracestate=None, + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "b3multi"}, + ) + ], + }, + ), + ( + "valid_all_headers_and_baggage_trace_context_datadog_style_w_restart_behavior", + None, + _PROPAGATION_BEHAVIOR_RESTART, + {**ALL_HEADERS, **DATADOG_BAGGAGE_HEADERS_VALID}, + { + "trace_id": None, + "span_id": None, + "sampling_priority": None, + "dd_origin": None, + "baggage": {"key1": "val1", "key2": "val2"}, + "span_links": [ + SpanLink( + trace_id=13088165645273925489, + span_id=5678, + tracestate=None, + flags=1, + attributes={"reason": "propagation_behavior_extract", "context_headers": "datadog"}, + ) + ], + }, + ), ] # Only add fixtures here if they can't pass both test_propagation_extract_env @@ -2225,6 +2423,7 @@ def test_extract_tracecontext(headers, expected_context): # can't be tested correctly via test_propagation_extract_w_config. It is tested separately "valid_tracecontext_simple", [_PROPAGATION_STYLE_W3C_TRACECONTEXT], + None, TRACECONTEXT_HEADERS_VALID_BASIC, { "trace_id": TRACE_ID, @@ -2241,6 +2440,7 @@ def test_extract_tracecontext(headers, expected_context): ( "valid_tracecontext_rum_no_sampling_decision", [_PROPAGATION_STYLE_W3C_TRACECONTEXT], + None, TRACECONTEXT_HEADERS_VALID_RUM_NO_SAMPLING_DECISION, { "trace_id": TRACE_ID, @@ -2252,11 +2452,24 @@ def test_extract_tracecontext(headers, expected_context): }, }, ), + # Only works for env since config is modified at startup to set + # propagation_style_extract to [None] if DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT is set to ignore + ( + "valid_datadog_default_w_ignore_behavior", + None, + _PROPAGATION_BEHAVIOR_IGNORE, + DATADOG_HEADERS_VALID, + CONTEXT_EMPTY, + ), ] -@pytest.mark.parametrize("name,styles,headers,expected_context", EXTRACT_FIXTURES + EXTRACT_FIXTURES_ENV_ONLY) -def test_propagation_extract_env(name, styles, headers, expected_context, run_python_code_in_subprocess): +@pytest.mark.parametrize( + "name,styles,extract_behavior,headers,expected_context", EXTRACT_FIXTURES + EXTRACT_FIXTURES_ENV_ONLY +) +def test_propagation_extract_env( + name, styles, extract_behavior, headers, expected_context, run_python_code_in_subprocess +): # Execute the test code in isolation to ensure env variables work as expected code = """ import json @@ -2274,18 +2487,24 @@ def test_propagation_extract_env(name, styles, headers, expected_context, run_py env = os.environ.copy() if styles is not None: env["DD_TRACE_PROPAGATION_STYLE"] = ",".join(styles) + if extract_behavior is not None: + env["DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT"] = extract_behavior stdout, stderr, status, _ = run_python_code_in_subprocess(code=code, env=env) print(stderr, stdout) assert status == 0, (stdout, stderr) -@pytest.mark.parametrize("name,styles,headers,expected_context", EXTRACT_FIXTURES) -def test_propagation_extract_w_config(name, styles, headers, expected_context, run_python_code_in_subprocess): +@pytest.mark.parametrize("name,styles,extract_behavior,headers,expected_context", EXTRACT_FIXTURES) +def test_propagation_extract_w_config( + name, styles, extract_behavior, headers, expected_context, run_python_code_in_subprocess +): # Setting via ddtrace.config works as expected too # DEV: This also helps us get code coverage reporting overrides = {} if styles is not None: overrides["_propagation_style_extract"] = styles + if extract_behavior is not None: + overrides["_propagation_behavior_extract"] = extract_behavior with override_global_config(overrides): context = HTTPPropagator.extract(headers) if not expected_context.get("tracestate"): diff --git a/tests/utils.py b/tests/utils.py index de0129f75a3..5b98fe42d1b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -123,6 +123,7 @@ def override_global_config(values): "_health_metrics_enabled", "_propagation_style_extract", "_propagation_style_inject", + "_propagation_behavior_extract", "_x_datadog_tags_max_length", "_128_bit_trace_id_enabled", "_x_datadog_tags_enabled",