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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 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
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'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going to remove these, just doing because struggling to build tracer locally


- 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
9 changes: 5 additions & 4 deletions ddtrace/contrib/trace_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,14 +560,12 @@ def activate_distributed_headers(tracer, int_config=None, request_headers=None,
"""
if override is False:
return None
ZStriker19 marked this conversation as resolved.
Show resolved Hide resolved

if override or (int_config and distributed_tracing_enabled(int_config)):
context = HTTPPropagator.extract(request_headers)

# Only need to activate the new context if something was propagated
if not context.trace_id:
if not context.trace_id and not context._baggage:
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 +575,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, however if we
Copy link
Contributor Author

@ZStriker19 ZStriker19 Jan 9, 2025

Choose a reason for hiding this comment

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

We accept contexts lacking trace_id to cover the following cases:

  1. Baggage only extraction
  2. DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart which can result in span_link only extraction:

Unfortunately we can't compare baggages to tell if they're the same as the original headers, since the original incoming baggage could've been modified before we try to activate new headers in a nested framework. However, an alternative to tossing out an incoming baggage after we have an active context would be to merge them in some way, however I think that this is worse since it's possible customers may want to delete items from baggage.

I think the same applies to how span links could be modified as well.

# already have a current_context then a baggage only context will be tossed out
if current_context and (not context.trace_id or current_context.trace_id == 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,
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
61 changes: 40 additions & 21 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:
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:
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 @@ -1162,6 +1178,9 @@ def my_controller(url, headers):
context._baggage = baggage_context._baggage
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()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Homepage = "https://github.com/DataDog/dd-trace-py"
"Source Code" = "https://github.com/DataDog/dd-trace-py/"

[tool.setuptools_scm]
version_scheme = "release-branch-semver" # Must be "release-branch-semver" for now in main, see https://github.com/DataDog/dd-trace-py/issues/8801
version_scheme = "guess-next-dev" # Must be "release-branch-semver" for now in main, see https://github.com/DataDog/dd-trace-py/issues/8801
write_to = "ddtrace/_version.py"

[tool.cython-lint]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features:
- |
propagation: This 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 should have no change from the current behavior of always propagating valid headers.
With ``ignore`` ignoring all incoming headers and with ``restart`` turning incoming headers into a span links and propagating baggage items.
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"
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