Skip to content

Commit 26a6d09

Browse files
feat(baggage): auto-adding baggage to span tags (#13200)
Automatically adding baggage key-value pairs to span tags with the 'baggage.' prefix. Introducing DD_TRACE_BAGGAGE_TAG_KEYS config. [RFC](https://docs.google.com/document/d/1ZsgAcQHKjaBxDmhHup0zqFz07WYiOrbBksHPoXJe6u4/edit?tab=t.0#heading=h.q69qzmye8dfb) ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 02da078 commit 26a6d09

File tree

8 files changed

+159
-0
lines changed

8 files changed

+159
-0
lines changed

ddtrace/internal/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
# baggage
8686
DD_TRACE_BAGGAGE_MAX_ITEMS = 64
8787
DD_TRACE_BAGGAGE_MAX_BYTES = 8192
88+
BAGGAGE_TAG_PREFIX = "baggage."
8889

8990

9091
class SamplingMechanism(object):

ddtrace/propagation/http.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from ..internal.constants import _PROPAGATION_BEHAVIOR_RESTART
3636
from ..internal.constants import _PROPAGATION_STYLE_BAGGAGE
3737
from ..internal.constants import _PROPAGATION_STYLE_W3C_TRACECONTEXT
38+
from ..internal.constants import BAGGAGE_TAG_PREFIX
3839
from ..internal.constants import DD_TRACE_BAGGAGE_MAX_BYTES
3940
from ..internal.constants import DD_TRACE_BAGGAGE_MAX_ITEMS
4041
from ..internal.constants import HIGHER_ORDER_TRACE_ID_BITS as _HIGHER_ORDER_TRACE_ID_BITS
@@ -1153,6 +1154,21 @@ def my_controller(url, headers):
11531154
context._baggage = baggage_context.get_all_baggage_items()
11541155
else:
11551156
context = baggage_context
1157+
1158+
if config._baggage_tag_keys:
1159+
raw_keys = [k.strip() for k in config._baggage_tag_keys if k.strip()]
1160+
# wildcard: tag all baggage keys
1161+
if "*" in raw_keys:
1162+
tag_keys = baggage_context.get_all_baggage_items().keys()
1163+
else:
1164+
tag_keys = raw_keys
1165+
1166+
for stripped_key in tag_keys:
1167+
if (value := baggage_context.get_baggage_item(stripped_key)) is not None:
1168+
prefixed_key = BAGGAGE_TAG_PREFIX + stripped_key
1169+
if prefixed_key not in context._meta:
1170+
context._meta[prefixed_key] = value
1171+
11561172
if config._propagation_behavior_extract == _PROPAGATION_BEHAVIOR_RESTART:
11571173
link = HTTPPropagator._context_to_span_link(context, style, "propagation_behavior_extract")
11581174
context = Context(baggage=context.get_all_baggage_items(), span_links=[link] if link else [])

ddtrace/settings/_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,9 @@ def __init__(self):
579579
)
580580

581581
self._propagation_extract_first = _get_config("DD_TRACE_PROPAGATION_EXTRACT_FIRST", False, asbool)
582+
self._baggage_tag_keys = _get_config(
583+
"DD_TRACE_BAGGAGE_TAG_KEYS", ["user.id", "account.id", "session.id"], lambda x: x.strip().split(",")
584+
)
582585

583586
# Datadog tracer tags propagation
584587
x_datadog_tags_max_length = _get_config("DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH", 512, int)

docs/configuration.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,20 @@ Other
914914
version_added:
915915
v1.15.0:
916916

917+
DD_TRACE_BAGGAGE_TAG_KEYS:
918+
type: String
919+
default: "user.id,account.id,session.id"
920+
921+
description: |
922+
A comma-separated list of baggage keys to be automatically attached as tags on spans.
923+
For each key specified, if a corresponding baggage key is present and has a non-empty value,
924+
the key-value pair will be added to the span's metadata using the key name formatted as ``baggage.<key>``.
925+
If you want to turn off this feature, set the configuration value to an empty string.
926+
When set to `*`, **all** baggage keys will be converted into span tags. Use with caution: this may unintentionally expose sensitive or internal data if not used intentionally.
927+
928+
version_added:
929+
v3.6.0:
930+
917931
.. _Unified Service Tagging: https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging/
918932

919933
.. _Configure the Datadog Tracing Library: https://docs.datadoghq.com/tracing/trace_collection/library_config/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
baggage: Adds support for automatically adding baggage key-value pairs to span tags. Baggage items from incoming HTTP headers are attached to spans as tags with a `baggage.` prefix
5+
The `DD_TRACE_BAGGAGE_TAG_KEYS` configuration allows users to specify a comma-separated list of baggage keys for span tagging, by default the value is set to `user.id,account.id,session.id`.
6+
When set to `*`, all baggage keys will be converted into span tags. Setting it to an empty value disables baggage tagging.

tests/contrib/fastapi/test_fastapi.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,57 @@ def test_inferred_spans_api_gateway(client, tracer, test_spans, test, inferred_p
736736

737737
if test_headers["type"] == "distributed":
738738
assert web_span.trace_id == 1
739+
740+
741+
def test_baggage_span_tagging_default(client, tracer, test_spans):
742+
response = client.get("/", headers={"baggage": "user.id=123,account.id=456,region=us-west"})
743+
744+
assert response.status_code == 200
745+
746+
spans = test_spans.pop_traces()
747+
# Assume the request span is the first span in the first trace.
748+
request_span = spans[0][0]
749+
750+
assert request_span.get_tag("baggage.user.id") == "123"
751+
assert request_span.get_tag("baggage.account.id") == "456"
752+
# Since "region" is not in the default list, its baggage tag should not be present.
753+
assert request_span.get_tag("baggage.region") is None
754+
755+
756+
def test_baggage_span_tagging_no_headers(client, tracer, test_spans):
757+
response = client.get("/", headers={})
758+
assert response.status_code == 200
759+
760+
spans = test_spans.pop_traces()
761+
request_span = spans[0][0]
762+
763+
# None of the baggage tags should be present.
764+
assert request_span.get_tag("baggage.user.id") is None
765+
assert request_span.get_tag("baggage.account.id") is None
766+
assert request_span.get_tag("baggage.session.id") is None
767+
768+
769+
def test_baggage_span_tagging_empty_baggage(client, tracer, test_spans):
770+
response = client.get("/", headers={"baggage": ""})
771+
assert response.status_code == 200
772+
773+
spans = test_spans.pop_traces()
774+
request_span = spans[0][0]
775+
776+
# None of the baggage tags should be present.
777+
assert request_span.get_tag("baggage.user.id") is None
778+
assert request_span.get_tag("baggage.account.id") is None
779+
assert request_span.get_tag("baggage.session.id") is None
780+
781+
782+
def test_baggage_span_tagging_baggage_api(client, tracer, test_spans):
783+
response = client.get("/", headers={"baggage": ""})
784+
assert response.status_code == 200
785+
786+
spans = test_spans.pop_traces()
787+
request_span = spans[0][0]
788+
request_span.context.set_baggage_item("user.id", "123")
789+
# None of the baggage tags should be present since we only tag baggage during extraction from headers
790+
assert request_span.get_tag("baggage.account.id") is None
791+
assert request_span.get_tag("baggage.user.id") is None
792+
assert request_span.get_tag("baggage.session.id") is None

tests/telemetry/test_writer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
466466
{"name": "DD_TRACE_AGENT_PORT", "origin": "default", "value": None},
467467
{"name": "DD_TRACE_AGENT_TIMEOUT_SECONDS", "origin": "default", "value": 2.0},
468468
{"name": "DD_TRACE_API_VERSION", "origin": "env_var", "value": "v0.5"},
469+
{"name": "DD_TRACE_BAGGAGE_TAG_KEYS", "origin": "default", "value": "user.id,account.id,session.id"},
469470
{"name": "DD_TRACE_CLIENT_IP_ENABLED", "origin": "env_var", "value": True},
470471
{"name": "DD_TRACE_CLIENT_IP_HEADER", "origin": "default", "value": None},
471472
{"name": "DD_TRACE_COMPUTE_STATS", "origin": "env_var", "value": True},

tests/tracer/test_propagation.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3538,3 +3538,67 @@ def test_opentracer_propagator_baggage_extract():
35383538
}
35393539
context = HTTPPropagator.extract(headers)
35403540
assert context._baggage == {"key1": "value1"}
3541+
3542+
3543+
def test_baggage_span_tags_default():
3544+
headers = {"baggage": "user.id=123,correlation_id=abc,region=us-east"}
3545+
context = HTTPPropagator.extract(headers)
3546+
# Only "user.id" is in allowed_keys; expect its value to be tagged under the prefixed key.
3547+
assert context._meta.get("baggage.user.id") == "123"
3548+
# Other keys are not tagged.
3549+
assert "baggage.correlation_id" not in context._meta
3550+
assert "baggage.region" not in context._meta
3551+
3552+
3553+
@pytest.mark.subprocess(
3554+
env=dict(DD_TRACE_BAGGAGE_TAG_KEYS=""),
3555+
)
3556+
def test_baggage_span_tags_empty():
3557+
from ddtrace.propagation.http import HTTPPropagator
3558+
3559+
headers = {"baggage": "user.id=123,correlation_id=abc,region=us-east"}
3560+
context = HTTPPropagator.extract(headers)
3561+
assert "baggage.user.id" not in context._meta
3562+
assert "baggage.correlation_id" not in context._meta
3563+
assert "baggage.region" not in context._meta
3564+
3565+
3566+
@pytest.mark.subprocess(
3567+
env=dict(DD_TRACE_BAGGAGE_TAG_KEYS="user.id"),
3568+
)
3569+
def test_baggage_span_tags_specific_keys():
3570+
from ddtrace.propagation.http import HTTPPropagator
3571+
3572+
headers = {"baggage": "user.id=123,correlation_id=abc,region=us-east"}
3573+
context = HTTPPropagator.extract(headers)
3574+
assert context._meta.get("baggage.user.id") == "123"
3575+
assert "baggage.account.id" not in context._meta
3576+
assert "baggage.session.id" not in context._meta
3577+
3578+
3579+
@pytest.mark.subprocess(
3580+
env=dict(DD_TRACE_BAGGAGE_TAG_KEYS="user.id,ACCOUNT.ID"),
3581+
)
3582+
def test_baggage_span_tags_case_sensitive():
3583+
from ddtrace.propagation.http import HTTPPropagator
3584+
3585+
headers = {"baggage": "user.id=123,correlation_id=abc,region=us-east"}
3586+
context = HTTPPropagator.extract(headers)
3587+
assert context._meta.get("baggage.user.id") == "123"
3588+
assert context._meta.get("baggage.ACCOUNT.ID") is None
3589+
assert "baggage.account.id" not in context._meta
3590+
3591+
3592+
@pytest.mark.subprocess(
3593+
env=dict(DD_TRACE_BAGGAGE_TAG_KEYS="*"),
3594+
)
3595+
def test_baggage_span_tags_wildcard():
3596+
from ddtrace.propagation.http import HTTPPropagator
3597+
3598+
headers = {"baggage": "user.id=foo,correlation_id=car,color=blue,serverNode=DF 28"}
3599+
context = HTTPPropagator.extract(headers)
3600+
assert context._meta.get("baggage.user.id") == "foo"
3601+
assert context._meta.get("baggage.correlation_id") == "car"
3602+
assert context._meta.get("baggage.color") == "blue"
3603+
assert context._meta.get("baggage.serverNode") == "DF 28"
3604+
assert "baggage.session.id" not in context._meta

0 commit comments

Comments
 (0)