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(propagation): add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT to handle x-org propagation #11631

Merged
merged 38 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
75e962b
chore: use guess-next-dev instead of release-branch-semver [2.18] (#1…
erikayasuda Dec 16, 2024
6bb2036
fix(iast): check context is enable in request and builtins patched fu…
github-actions[bot] Dec 18, 2024
9924f37
chore(ci): upgrade python for build action [backport 2.18] (#11782)
github-actions[bot] Dec 18, 2024
e1b10df
add extract behavior feat
ZStriker19 Dec 19, 2024
6811970
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Dec 20, 2024
8ce6f80
allow override of config
ZStriker19 Dec 20, 2024
ffd8fe2
add ref for system-tests
ZStriker19 Dec 20, 2024
34190b1
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Dec 20, 2024
290d9e6
add tracecontext headers to default case
ZStriker19 Dec 20, 2024
87532da
don't drop extract contexts lacking trace_id
ZStriker19 Jan 7, 2025
b32a467
fix extracting baggage and update so that baggage only context is used
ZStriker19 Jan 8, 2025
837e213
fix no span on execution context error
ZStriker19 Jan 9, 2025
97554cd
merge main
ZStriker19 Jan 9, 2025
4436896
cover case of only span_link
ZStriker19 Jan 9, 2025
4dee1de
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 9, 2025
57b3239
make sure span_link always added to root_span
ZStriker19 Jan 9, 2025
a0b2c34
Update ddtrace/_trace/tracer.py
ZStriker19 Jan 10, 2025
cf57ae8
Update ddtrace/contrib/trace_utils.py
ZStriker19 Jan 10, 2025
83498fa
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 10, 2025
d5293e0
use public api to access baggage
ZStriker19 Jan 10, 2025
af2c94f
fix merge conflict
ZStriker19 Jan 10, 2025
37117d2
docs and update rn
ZStriker19 Jan 10, 2025
5a40b91
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 10, 2025
721752d
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 10, 2025
7f64859
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 13, 2025
df77575
Update ddtrace/_trace/tracer.py
ZStriker19 Jan 13, 2025
69f5c6e
Update releasenotes/notes/propagation_behavior_extract-3d16765cfd0748…
ZStriker19 Jan 13, 2025
77aeffd
update logic and nits from comments
ZStriker19 Jan 13, 2025
6d1b8f4
fix extract setting when behavior is ignore
ZStriker19 Jan 13, 2025
5d5edff
use constant
ZStriker19 Jan 13, 2025
f68abcd
reset system-tests run to target main
ZStriker19 Jan 13, 2025
60b5e69
fix test after config changes
ZStriker19 Jan 14, 2025
f463abc
Merge branch 'main' into zachg/handle_cross_org_propagation
ZStriker19 Jan 14, 2025
23a8bc7
breakingbut passing system-tests maybe
ZStriker19 Jan 14, 2025
02c5649
Revert "reset system-tests run to target main"
ZStriker19 Jan 14, 2025
9e4c8e5
Revert "breakingbut passing system-tests maybe"
ZStriker19 Jan 14, 2025
3b7e390
Reapply "reset system-tests run to target main"
ZStriker19 Jan 14, 2025
a9de823
add baggage fix rn
ZStriker19 Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
with:
persist-credentials: false
repository: 'DataDog/system-tests'
ref: 'zach.montoya/poc/propagation-behavior'
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

- name: Build agent
run: ./build.sh -i agent
Expand Down Expand Up @@ -65,6 +66,7 @@ jobs:
with:
persist-credentials: false
repository: 'DataDog/system-tests'
ref: 'zach.montoya/poc/propagation-behavior'

- name: Checkout dd-trace-py
uses: actions/checkout@v4
Expand Down Expand Up @@ -117,6 +119,8 @@ jobs:
with:
persist-credentials: false
repository: 'DataDog/system-tests'
ref: 'zach.montoya/poc/propagation-behavior'


- name: Build runner
uses: ./.github/actions/install_runner
Expand Down Expand Up @@ -290,6 +294,7 @@ jobs:
with:
persist-credentials: false
repository: 'DataDog/system-tests'
ref: 'zach.montoya/poc/propagation-behavior'
- name: Checkout dd-trace-py
uses: actions/checkout@v4
with:
Expand Down
3 changes: 1 addition & 2 deletions ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,7 @@ def _start_span(
service = config.service_mapping.get(service, service)
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

links = context._span_links if not parent else []

if trace_id:
if trace_id or links:
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
# child_of a non-empty context, so either a local child span or from a remote context
span = Span(
name=name,
Expand Down
9 changes: 6 additions & 3 deletions ddtrace/contrib/trace_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -577,7 +577,10 @@ 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 a baggage only or span_link only context will be tossed out
if current_context and (not context.trace_id or current_context.trace_id == context.trace_id):
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down
4 changes: 4 additions & 0 deletions ddtrace/internal/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
63 changes: 41 additions & 22 deletions ddtrace/propagation/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
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_IGNORE
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
Expand Down Expand Up @@ -974,12 +976,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
Expand All @@ -988,6 +990,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:
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
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]
Expand All @@ -996,23 +1016,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:
Expand Down Expand Up @@ -1130,24 +1141,29 @@ def my_controller(url, headers):
:param dict headers: HTTP headers to extract tracing attributes.
:return: New `Context` with propagated attributes.
"""
if not headers:
return Context()
context = Context()
if not headers or config._propagation_behavior_extract == _PROPAGATION_BEHAVIOR_IGNORE:
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
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

# 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)
Expand All @@ -1159,9 +1175,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

Expand Down
8 changes: 8 additions & 0 deletions ddtrace/settings/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ddtrace.vendor.debtcollector import deprecate

from ..internal import gitmetadata
from ..internal.constants import _PROPAGATION_BEHAVIOR_DEFAULT
from ..internal.constants import _PROPAGATION_STYLE_DEFAULT
from ..internal.constants import DEFAULT_BUFFER_SIZE
from ..internal.constants import DEFAULT_MAX_PAYLOAD_SIZE
Expand Down Expand Up @@ -540,6 +541,10 @@ def __init__(self):

self._propagation_extract_first = _get_config("DD_TRACE_PROPAGATION_EXTRACT_FIRST", False, asbool)

self._propagation_behavior_extract = _get_config(
["DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT"], _PROPAGATION_BEHAVIOR_DEFAULT, self._lower
)

# Datadog tracer tags propagation
x_datadog_tags_max_length = _get_config("DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH", 512, int)
if x_datadog_tags_max_length < 0:
Expand Down Expand Up @@ -978,3 +983,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()
20 changes: 20 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

DD_TRACE_PROPAGATION_STYLE_INJECT:
default: |
``tracecontext,datadog``
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
propagation: This introduces the environment variable ``DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT``
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
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.
4 changes: 3 additions & 1 deletion tests/telemetry/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ 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_SITE"] = "datadoghq.com"
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved
env["DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT"] = "restart"

# 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
Expand Down Expand Up @@ -446,6 +447,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"},
Expand Down
Loading
Loading