From d76a11d8f0555e80bfb8b5eaa1096081d48252b4 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Tue, 4 Nov 2025 15:20:38 +0100 Subject: [PATCH 01/25] chore(internal): add process_tags to first span of each payload --- ddtrace/_trace/processor/__init__.py | 4 ++ ddtrace/internal/constants.py | 1 + ddtrace/internal/process_tags/__init__.py | 76 ++++++++++++++++++++++ ddtrace/internal/process_tags/constants.py | 7 ++ ddtrace/settings/_config.py | 3 + 5 files changed, 91 insertions(+) create mode 100644 ddtrace/internal/process_tags/__init__.py create mode 100644 ddtrace/internal/process_tags/constants.py diff --git a/ddtrace/_trace/processor/__init__.py b/ddtrace/_trace/processor/__init__.py index 7b1fcec816e..43630c6329f 100644 --- a/ddtrace/_trace/processor/__init__.py +++ b/ddtrace/_trace/processor/__init__.py @@ -13,8 +13,10 @@ from ddtrace._trace.span import _get_64_highest_order_bits_as_hex from ddtrace.constants import _APM_ENABLED_METRIC_KEY as MK_APM_ENABLED from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM +from ddtrace.internal.constants import PROCESS_TAGS from ddtrace.internal import gitmetadata from ddtrace.internal import telemetry +from ddtrace.internal.process_tags import process_tags from ddtrace.internal.constants import COMPONENT from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS from ddtrace.internal.constants import LAST_DD_PARENT_ID_KEY @@ -250,6 +252,8 @@ def process_trace(self, trace: List[Span]) -> Optional[List[Span]]: span._update_tags_from_context() self._set_git_metadata(span) span._set_tag_str("language", "python") + if serialized_process_tags := process_tags.get_serialized_process_tags(): + span._set_tag_str(PROCESS_TAGS, serialized_process_tags) # for 128 bit trace ids if span.trace_id > MAX_UINT_64BITS: trace_id_hob = _get_64_highest_order_bits_as_hex(span.trace_id) diff --git a/ddtrace/internal/constants.py b/ddtrace/internal/constants.py index fc572d0ffaa..059439aa024 100644 --- a/ddtrace/internal/constants.py +++ b/ddtrace/internal/constants.py @@ -70,6 +70,7 @@ HTTP_REQUEST_PATH_PARAMETER = "http.request.path.parameter" REQUEST_PATH_PARAMS = "http.request.path_params" STATUS_403_TYPE_AUTO = {"status_code": 403, "type": "auto"} +PROCESS_TAGS = "_dd.tags.process" CONTAINER_ID_HEADER_NAME = "Datadog-Container-Id" diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py new file mode 100644 index 00000000000..ea62414952f --- /dev/null +++ b/ddtrace/internal/process_tags/__init__.py @@ -0,0 +1,76 @@ +import os +from pathlib import Path +import re +import sys +from typing import Callable +from typing import Dict +from typing import Optional + +from ddtrace.internal.forksafe import Lock +from ddtrace.internal.logger import get_logger +from ddtrace.settings._config import config + +from .constants import ENTRYPOINT_BASEDIR_TAG +from .constants import ENTRYPOINT_NAME_TAG +from .constants import ENTRYPOINT_TYPE_SCRIPT +from .constants import ENTRYPOINT_TYPE_TAG +from .constants import ENTRYPOINT_WORKDIR_TAG + + +log = get_logger(__name__) + +# outside of ProcessTags class for test purpose +def normalize_tag(value: str) -> str: + return re.sub(r"[^a-z0-9/._-]", "_", value.lower()) +class ProcessTags: + process_tags: Dict[str, str] = {} + + def __init__(self) -> None: + self._lock = Lock() + self._serialized: Optional[str] = None + self._enabled = config._process_tags_enabled + self.reload() + + def _serialize_process_tags(self) -> Optional[str]: + if self.process_tags and not self._serialized: + serialized_tags = ",".join(f"{key}:{value}" for key, value in self.process_tags.items()) + return serialized_tags + return None + + def get_serialized_process_tags(self) -> Optional[str]: + if not self._enabled: + return None + + with self._lock: + if not self._serialized: + self._serialized = self._serialize_process_tags() + return self._serialized + + def add_process_tag(self, key: str, value: Optional[str] = None, compute: Optional[Callable[[], str]] = None): + if not self._enabled: + return + + if compute: + try: + value = compute() + except Exception as e: + log.debug("failed to set %s process_tag: %s", key, e) + + if value: + with self._lock: + self.process_tags[key] = normalize_tag(value) + self._serialized = None + + def reload(self): + if not self._enabled: + return + + with self._lock: + self.process_tags = {} + + self.add_process_tag(ENTRYPOINT_WORKDIR_TAG, compute=lambda: os.path.basename(os.getcwd())) + self.add_process_tag(ENTRYPOINT_BASEDIR_TAG, compute=lambda: Path(sys.argv[0]).resolve().parent.name) + self.add_process_tag(ENTRYPOINT_NAME_TAG, compute=lambda: os.path.splitext(os.path.basename(sys.argv[0]))[0]) + self.add_process_tag(ENTRYPOINT_TYPE_TAG, value=ENTRYPOINT_TYPE_SCRIPT) + +process_tags = ProcessTags() \ No newline at end of file diff --git a/ddtrace/internal/process_tags/constants.py b/ddtrace/internal/process_tags/constants.py new file mode 100644 index 00000000000..a863549cfb5 --- /dev/null +++ b/ddtrace/internal/process_tags/constants.py @@ -0,0 +1,7 @@ +ENTRYPOINT_NAME_TAG = "entrypoint.name" +ENTRYPOINT_WORKDIR_TAG = "entrypoint.workdir" + +ENTRYPOINT_TYPE_TAG = "entrypoint.type" +ENTRYPOINT_TYPE_SCRIPT = "script" + +ENTRYPOINT_BASEDIR_TAG = "entrypoint.basedir" diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py index 78a33720203..6878d64f463 100644 --- a/ddtrace/settings/_config.py +++ b/ddtrace/settings/_config.py @@ -669,6 +669,9 @@ def __init__(self): self._trace_resource_renaming_always_simplified_endpoint = _get_config( "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT", default=False, modifier=asbool ) + self._process_tags_enabled = _get_config( + "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", default=False, modifier=asbool + ) # Long-running span interval configurations # Only supported for Ray spans for now From a3643d8a65b3380408b1dd49546d65e4dcde2971 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 5 Nov 2025 15:34:53 +0100 Subject: [PATCH 02/25] tests(process_tags): add tests --- .riot/requirements/1645326.txt | 19 ++++ riotfile.py | 5 + tests/process_tags/__init__.py | 0 tests/process_tags/test_process_tags.py | 124 ++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 .riot/requirements/1645326.txt create mode 100644 tests/process_tags/__init__.py create mode 100644 tests/process_tags/test_process_tags.py diff --git a/.riot/requirements/1645326.txt b/.riot/requirements/1645326.txt new file mode 100644 index 00000000000..9201e2642b1 --- /dev/null +++ b/.riot/requirements/1645326.txt @@ -0,0 +1,19 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/1645326.in +# +attrs==25.4.0 +coverage[toml]==7.11.0 +hypothesis==6.45.0 +iniconfig==2.3.0 +mock==5.2.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +pygments==2.19.2 +pytest==8.4.2 +pytest-cov==7.0.0 +pytest-mock==3.15.1 +sortedcontainers==2.4.0 diff --git a/riotfile.py b/riotfile.py index a30d01e1af6..10b3deab757 100644 --- a/riotfile.py +++ b/riotfile.py @@ -498,6 +498,11 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT command="pytest --no-cov {cmdargs} tests/coverage -s", pys=select_pys(max_version="3.12"), ), + Venv( + name="process_tags", + command="pytest -v {cmdargs} tests/process_tags/", + pys=select_pys(min_version="3.9") + ), Venv( name="internal", env={ diff --git a/tests/process_tags/__init__.py b/tests/process_tags/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/process_tags/test_process_tags.py b/tests/process_tags/test_process_tags.py new file mode 100644 index 00000000000..5096329c2da --- /dev/null +++ b/tests/process_tags/test_process_tags.py @@ -0,0 +1,124 @@ +from ddtrace.internal.process_tags import normalize_tag +from ddtrace.internal.process_tags import process_tags +from ddtrace.internal.constants import PROCESS_TAGS +from ddtrace.internal.process_tags.constants import ENTRYPOINT_BASEDIR_TAG, ENTRYPOINT_NAME_TAG, ENTRYPOINT_TYPE_SCRIPT, ENTRYPOINT_TYPE_TAG, ENTRYPOINT_WORKDIR_TAG +from tests.utils import TracerTestCase +from tests.utils import override_env +import sys +import os +from pathlib import Path + +def test_normalize_tag(): + assert normalize_tag("HelloWorld") == "helloworld" + assert normalize_tag("Hello@World!") == "hello_world_" + assert normalize_tag("HeLLo123") == "hello123" + assert normalize_tag("hello world") == "hello_world" + assert normalize_tag("a/b.c_d-e") == "a/b.c_d-e" + assert normalize_tag("héllø") == "h_ll_" + assert normalize_tag("") == "" + assert normalize_tag("💡⚡️") == "___" + assert normalize_tag("!foo@") == "_foo_" + assert normalize_tag("123_abc.DEF-ghi/jkl") == "123_abc.def-ghi/jkl" + assert normalize_tag("Env:Prod-Server#1") == "env_prod-server_1" + +class TestProcessTags(TracerTestCase): + + def test_process_tags_deactivated(self): + with self.tracer.trace("test"): + pass + + span = self.get_spans()[0] + assert span is not None + assert PROCESS_TAGS not in span._meta + + def test_process_tags_activated_with_override_env(self): + """Test process tags using override_env instead of run_in_subprocess""" + with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): + process_tags._enabled = True + process_tags.reload() + + with self.tracer.trace("test"): + pass + + process_tags._enabled = False + + span = self.get_spans()[0] + assert span is not None + assert PROCESS_TAGS in span._meta + + expected_name = "pytest" + expected_type = ENTRYPOINT_TYPE_SCRIPT + expected_basedir = Path(sys.argv[0]).resolve().parent.name + expected_workdir = os.path.basename(os.getcwd()) + + serialized_tags = span._meta[PROCESS_TAGS] + expected_raw = ( + f"{ENTRYPOINT_WORKDIR_TAG}:{expected_workdir}," + f"{ENTRYPOINT_BASEDIR_TAG}:{expected_basedir}," + f"{ENTRYPOINT_NAME_TAG}:{expected_name}," + f"{ENTRYPOINT_TYPE_TAG}:{expected_type}" + ) + assert serialized_tags == expected_raw + + tags_dict = dict(tag.split(":", 1) for tag in serialized_tags.split(",")) + assert tags_dict[ENTRYPOINT_NAME_TAG] == expected_name + assert tags_dict[ENTRYPOINT_TYPE_TAG] == expected_type + assert tags_dict[ENTRYPOINT_BASEDIR_TAG] == expected_basedir + assert tags_dict[ENTRYPOINT_WORKDIR_TAG] == expected_workdir + + def test_process_tags_only_on_local_root_span(self): + """Test that only local root spans get process tags, not children""" + with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): + process_tags._enabled = True + process_tags.reload() + + with self.tracer.trace("parent"): + with self.tracer.trace("child"): + pass + + process_tags._enabled = False + + spans = self.get_spans() + assert len(spans) == 2 + + parent = [s for s in spans if s.name == "parent"][0] + assert PROCESS_TAGS in parent._meta + + child = [s for s in spans if s.name == "child"][0] + assert PROCESS_TAGS not in child._meta + + def test_add_process_tag_compute_exception(self): + """Test error handling when compute raises exception""" + with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): + process_tags._enabled = True + process_tags.reload() + + def failing_compute(): + raise ValueError("Test exception") + + process_tags.add_process_tag("test.tag", compute=failing_compute) + + assert "test.tag" not in process_tags.process_tags + + process_tags.add_process_tag("test.working", value="value") + assert "test.working" in process_tags.process_tags + assert process_tags.process_tags["test.working"] == "value" + + process_tags._enabled = False + + def test_serialization_caching(self): + """Test that serialization is cached and invalidated properly""" + with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): + process_tags._enabled = True + process_tags.reload() + + result1 = process_tags.get_serialized_process_tags() + assert process_tags._serialized is not None + + process_tags.add_process_tag("custom.tag", value="test") + assert process_tags._serialized is None + + result3 = process_tags.get_serialized_process_tags() + assert "custom.tag:test" in result3 + + process_tags._enabled = False \ No newline at end of file From a52e8cc512e6ff4216b3443ff14ec4db4d989395 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 5 Nov 2025 15:43:28 +0100 Subject: [PATCH 03/25] lint --- ddtrace/_trace/processor/__init__.py | 4 ++-- ddtrace/internal/process_tags/__init__.py | 17 ++++++++------ riotfile.py | 6 +---- tests/process_tags/test_process_tags.py | 28 ++++++++++++++--------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/ddtrace/_trace/processor/__init__.py b/ddtrace/_trace/processor/__init__.py index 43630c6329f..fa2a2a74198 100644 --- a/ddtrace/_trace/processor/__init__.py +++ b/ddtrace/_trace/processor/__init__.py @@ -13,17 +13,17 @@ from ddtrace._trace.span import _get_64_highest_order_bits_as_hex from ddtrace.constants import _APM_ENABLED_METRIC_KEY as MK_APM_ENABLED from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM -from ddtrace.internal.constants import PROCESS_TAGS from ddtrace.internal import gitmetadata from ddtrace.internal import telemetry -from ddtrace.internal.process_tags import process_tags from ddtrace.internal.constants import COMPONENT from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS from ddtrace.internal.constants import LAST_DD_PARENT_ID_KEY from ddtrace.internal.constants import MAX_UINT_64BITS +from ddtrace.internal.constants import PROCESS_TAGS from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY from ddtrace.internal.constants import SamplingMechanism from ddtrace.internal.logger import get_logger +from ddtrace.internal.process_tags import process_tags from ddtrace.internal.rate_limiter import RateLimiter from ddtrace.internal.sampling import SpanSamplingRule from ddtrace.internal.sampling import get_span_sampling_rules diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index ea62414952f..0234ee2c861 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -19,21 +19,23 @@ log = get_logger(__name__) + # outside of ProcessTags class for test purpose def normalize_tag(value: str) -> str: return re.sub(r"[^a-z0-9/._-]", "_", value.lower()) -class ProcessTags: - process_tags: Dict[str, str] = {} + +class ProcessTags: def __init__(self) -> None: self._lock = Lock() self._serialized: Optional[str] = None self._enabled = config._process_tags_enabled + self._process_tags: Dict[str, str] = {} self.reload() def _serialize_process_tags(self) -> Optional[str]: - if self.process_tags and not self._serialized: - serialized_tags = ",".join(f"{key}:{value}" for key, value in self.process_tags.items()) + if self._process_tags and not self._serialized: + serialized_tags = ",".join(f"{key}:{value}" for key, value in self._process_tags.items()) return serialized_tags return None @@ -58,7 +60,7 @@ def add_process_tag(self, key: str, value: Optional[str] = None, compute: Option if value: with self._lock: - self.process_tags[key] = normalize_tag(value) + self._process_tags[key] = normalize_tag(value) self._serialized = None def reload(self): @@ -66,11 +68,12 @@ def reload(self): return with self._lock: - self.process_tags = {} + self._process_tags = {} self.add_process_tag(ENTRYPOINT_WORKDIR_TAG, compute=lambda: os.path.basename(os.getcwd())) self.add_process_tag(ENTRYPOINT_BASEDIR_TAG, compute=lambda: Path(sys.argv[0]).resolve().parent.name) self.add_process_tag(ENTRYPOINT_NAME_TAG, compute=lambda: os.path.splitext(os.path.basename(sys.argv[0]))[0]) self.add_process_tag(ENTRYPOINT_TYPE_TAG, value=ENTRYPOINT_TYPE_SCRIPT) -process_tags = ProcessTags() \ No newline at end of file + +process_tags = ProcessTags() diff --git a/riotfile.py b/riotfile.py index 10b3deab757..cb9a6a7e851 100644 --- a/riotfile.py +++ b/riotfile.py @@ -498,11 +498,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT command="pytest --no-cov {cmdargs} tests/coverage -s", pys=select_pys(max_version="3.12"), ), - Venv( - name="process_tags", - command="pytest -v {cmdargs} tests/process_tags/", - pys=select_pys(min_version="3.9") - ), + Venv(name="process_tags", command="pytest -v {cmdargs} tests/process_tags/", pys=select_pys(min_version="3.9")), Venv( name="internal", env={ diff --git a/tests/process_tags/test_process_tags.py b/tests/process_tags/test_process_tags.py index 5096329c2da..f4fc399ea9c 100644 --- a/tests/process_tags/test_process_tags.py +++ b/tests/process_tags/test_process_tags.py @@ -1,12 +1,18 @@ +import os +from pathlib import Path +import sys + +from ddtrace.internal.constants import PROCESS_TAGS from ddtrace.internal.process_tags import normalize_tag from ddtrace.internal.process_tags import process_tags -from ddtrace.internal.constants import PROCESS_TAGS -from ddtrace.internal.process_tags.constants import ENTRYPOINT_BASEDIR_TAG, ENTRYPOINT_NAME_TAG, ENTRYPOINT_TYPE_SCRIPT, ENTRYPOINT_TYPE_TAG, ENTRYPOINT_WORKDIR_TAG +from ddtrace.internal.process_tags.constants import ENTRYPOINT_BASEDIR_TAG +from ddtrace.internal.process_tags.constants import ENTRYPOINT_NAME_TAG +from ddtrace.internal.process_tags.constants import ENTRYPOINT_TYPE_SCRIPT +from ddtrace.internal.process_tags.constants import ENTRYPOINT_TYPE_TAG +from ddtrace.internal.process_tags.constants import ENTRYPOINT_WORKDIR_TAG from tests.utils import TracerTestCase from tests.utils import override_env -import sys -import os -from pathlib import Path + def test_normalize_tag(): assert normalize_tag("HelloWorld") == "helloworld" @@ -21,8 +27,8 @@ def test_normalize_tag(): assert normalize_tag("123_abc.DEF-ghi/jkl") == "123_abc.def-ghi/jkl" assert normalize_tag("Env:Prod-Server#1") == "env_prod-server_1" -class TestProcessTags(TracerTestCase): +class TestProcessTags(TracerTestCase): def test_process_tags_deactivated(self): with self.tracer.trace("test"): pass @@ -98,11 +104,11 @@ def failing_compute(): process_tags.add_process_tag("test.tag", compute=failing_compute) - assert "test.tag" not in process_tags.process_tags + assert "test.tag" not in process_tags._process_tags process_tags.add_process_tag("test.working", value="value") - assert "test.working" in process_tags.process_tags - assert process_tags.process_tags["test.working"] == "value" + assert "test.working" in process_tags._process_tags + assert process_tags._process_tags["test.working"] == "value" process_tags._enabled = False @@ -112,7 +118,7 @@ def test_serialization_caching(self): process_tags._enabled = True process_tags.reload() - result1 = process_tags.get_serialized_process_tags() + process_tags.get_serialized_process_tags() assert process_tags._serialized is not None process_tags.add_process_tag("custom.tag", value="test") @@ -121,4 +127,4 @@ def test_serialization_caching(self): result3 = process_tags.get_serialized_process_tags() assert "custom.tag:test" in result3 - process_tags._enabled = False \ No newline at end of file + process_tags._enabled = False From f943f2a8e60c174397afaa21c75b6081906912a2 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 5 Nov 2025 15:55:03 +0100 Subject: [PATCH 04/25] fix: suitespec --- tests/suitespec.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/suitespec.yml b/tests/suitespec.yml index fe0ece52ca8..9f77a21711c 100644 --- a/tests/suitespec.yml +++ b/tests/suitespec.yml @@ -95,6 +95,9 @@ components: - ddtrace/internal/opentelemetry/* opentracer: - ddtrace/opentracer/* + process_tags: + - ddtrace/internal/process_tags/* + - tests/process_tags/* profiling: - ddtrace/profiling/* - ddtrace/internal/datadog/profiling/* @@ -242,6 +245,13 @@ suites: - '@core' runner: riot pattern: ^openfeature$ + process_tags: + parallelism: 1 + paths: + - '@process_tags' + - '@core' + - '@tracing' + runner: riot telemetry: parallelism: 1 paths: From 660bd640218ad93d8006fa2f51134e75802dd818 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 5 Nov 2025 16:24:42 +0100 Subject: [PATCH 05/25] fix: telemetry test --- tests/telemetry/test_writer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py index a02848dbac1..5b9cebf2107 100644 --- a/tests/telemetry/test_writer.py +++ b/tests/telemetry/test_writer.py @@ -395,6 +395,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_USER_MODEL_LOGIN_FIELD", "origin": "default", "value": ""}, {"name": "DD_USER_MODEL_NAME_FIELD", "origin": "default", "value": ""}, {"name": "DD_VERSION", "origin": "default", "value": None}, + {"name": "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "origin": "default", "value": False}, { "name": "OTEL_EXPORTER_OTLP_ENDPOINT", "origin": "env_var", From 78dd521fde47cd042a6481cbe976da279892d4b8 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 5 Nov 2025 16:49:09 +0100 Subject: [PATCH 06/25] fix telemetry 2 --- tests/telemetry/test_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/telemetry/test_writer.py b/tests/telemetry/test_writer.py index 1347c66cbdd..362015f9e19 100644 --- a/tests/telemetry/test_writer.py +++ b/tests/telemetry/test_writer.py @@ -244,6 +244,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_ERROR_TRACKING_HANDLED_ERRORS_INCLUDE", "origin": "default", "value": ""}, {"name": "DD_EXCEPTION_REPLAY_CAPTURE_MAX_FRAMES", "origin": "default", "value": 8}, {"name": "DD_EXCEPTION_REPLAY_ENABLED", "origin": "env_var", "value": True}, + {"name": "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "origin": "default", "value": False}, {"name": "DD_FASTAPI_ASYNC_BODY_TIMEOUT_SECONDS", "origin": "default", "value": 0.1}, {"name": "DD_IAST_DEDUPLICATION_ENABLED", "origin": "default", "value": True}, {"name": "DD_IAST_ENABLED", "origin": "default", "value": False}, @@ -396,7 +397,6 @@ def test_app_started_event_configuration_override(test_agent_session, run_python {"name": "DD_USER_MODEL_LOGIN_FIELD", "origin": "default", "value": ""}, {"name": "DD_USER_MODEL_NAME_FIELD", "origin": "default", "value": ""}, {"name": "DD_VERSION", "origin": "default", "value": None}, - {"name": "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "origin": "default", "value": False}, { "name": "OTEL_EXPORTER_OTLP_ENDPOINT", "origin": "env_var", From dd584901b5ca43f568cd079052a90e35011b579c Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Thu, 6 Nov 2025 11:28:30 +0100 Subject: [PATCH 07/25] simplify process_tags (brett review) --- ddtrace/_trace/processor/__init__.py | 4 +- ddtrace/internal/process_tags/__init__.py | 75 ++++----- tests/process_tags/test_process_tags.py | 144 +++++------------- ...ocessTags.test_process_tags_activated.json | 26 ++++ ..._process_tags_only_on_local_root_span.json | 38 +++++ 5 files changed, 129 insertions(+), 158 deletions(-) create mode 100644 tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json create mode 100644 tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json diff --git a/ddtrace/_trace/processor/__init__.py b/ddtrace/_trace/processor/__init__.py index fa2a2a74198..09614618227 100644 --- a/ddtrace/_trace/processor/__init__.py +++ b/ddtrace/_trace/processor/__init__.py @@ -252,8 +252,8 @@ def process_trace(self, trace: List[Span]) -> Optional[List[Span]]: span._update_tags_from_context() self._set_git_metadata(span) span._set_tag_str("language", "python") - if serialized_process_tags := process_tags.get_serialized_process_tags(): - span._set_tag_str(PROCESS_TAGS, serialized_process_tags) + if process_tags: + span._set_tag_str(PROCESS_TAGS, process_tags) # for 128 bit trace ids if span.trace_id > MAX_UINT_64BITS: trace_id_hob = _get_64_highest_order_bits_as_hex(span.trace_id) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index 0234ee2c861..90b67fb4b18 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -2,11 +2,8 @@ from pathlib import Path import re import sys -from typing import Callable -from typing import Dict from typing import Optional -from ddtrace.internal.forksafe import Lock from ddtrace.internal.logger import get_logger from ddtrace.settings._config import config @@ -20,60 +17,42 @@ log = get_logger(__name__) -# outside of ProcessTags class for test purpose def normalize_tag(value: str) -> str: return re.sub(r"[^a-z0-9/._-]", "_", value.lower()) -class ProcessTags: - def __init__(self) -> None: - self._lock = Lock() - self._serialized: Optional[str] = None - self._enabled = config._process_tags_enabled - self._process_tags: Dict[str, str] = {} - self.reload() - - def _serialize_process_tags(self) -> Optional[str]: - if self._process_tags and not self._serialized: - serialized_tags = ",".join(f"{key}:{value}" for key, value in self._process_tags.items()) - return serialized_tags +def generate_process_tags() -> Optional[str]: + if not config._process_tags_enabled: return None - def get_serialized_process_tags(self) -> Optional[str]: - if not self._enabled: - return None - - with self._lock: - if not self._serialized: - self._serialized = self._serialize_process_tags() - return self._serialized - - def add_process_tag(self, key: str, value: Optional[str] = None, compute: Optional[Callable[[], str]] = None): - if not self._enabled: - return - - if compute: - try: - value = compute() - except Exception as e: - log.debug("failed to set %s process_tag: %s", key, e) + try: + return ",".join( + f"{key}:{normalize_tag(value)}" + for key, value in sorted( + [ + (ENTRYPOINT_WORKDIR_TAG, os.path.basename(os.getcwd())), + (ENTRYPOINT_BASEDIR_TAG, Path(sys.argv[0]).resolve().parent.name), + (ENTRYPOINT_NAME_TAG, os.path.splitext(os.path.basename(sys.argv[0]))[0]), + (ENTRYPOINT_TYPE_TAG, ENTRYPOINT_TYPE_SCRIPT), + ] + ) + ) + except Exception as e: + log.debug("failed to get process_tags: %s", e) + return None - if value: - with self._lock: - self._process_tags[key] = normalize_tag(value) - self._serialized = None - def reload(self): - if not self._enabled: - return +# For test purpose +def _process_tag_reload(): + global process_tags + process_tags = generate_process_tags() - with self._lock: - self._process_tags = {} + # Force update in the processor module for testing + import sys - self.add_process_tag(ENTRYPOINT_WORKDIR_TAG, compute=lambda: os.path.basename(os.getcwd())) - self.add_process_tag(ENTRYPOINT_BASEDIR_TAG, compute=lambda: Path(sys.argv[0]).resolve().parent.name) - self.add_process_tag(ENTRYPOINT_NAME_TAG, compute=lambda: os.path.splitext(os.path.basename(sys.argv[0]))[0]) - self.add_process_tag(ENTRYPOINT_TYPE_TAG, value=ENTRYPOINT_TYPE_SCRIPT) + if "ddtrace._trace.processor" in sys.modules: + processor_module = sys.modules["ddtrace._trace.processor"] + processor_module.process_tags = process_tags -process_tags = ProcessTags() +process_tags = generate_process_tags() diff --git a/tests/process_tags/test_process_tags.py b/tests/process_tags/test_process_tags.py index f4fc399ea9c..92d8beebfeb 100644 --- a/tests/process_tags/test_process_tags.py +++ b/tests/process_tags/test_process_tags.py @@ -1,35 +1,37 @@ -import os -from pathlib import Path -import sys +import pytest from ddtrace.internal.constants import PROCESS_TAGS +from ddtrace.internal.process_tags import _process_tag_reload from ddtrace.internal.process_tags import normalize_tag -from ddtrace.internal.process_tags import process_tags -from ddtrace.internal.process_tags.constants import ENTRYPOINT_BASEDIR_TAG -from ddtrace.internal.process_tags.constants import ENTRYPOINT_NAME_TAG -from ddtrace.internal.process_tags.constants import ENTRYPOINT_TYPE_SCRIPT -from ddtrace.internal.process_tags.constants import ENTRYPOINT_TYPE_TAG -from ddtrace.internal.process_tags.constants import ENTRYPOINT_WORKDIR_TAG +from ddtrace.settings._config import config from tests.utils import TracerTestCase -from tests.utils import override_env -def test_normalize_tag(): - assert normalize_tag("HelloWorld") == "helloworld" - assert normalize_tag("Hello@World!") == "hello_world_" - assert normalize_tag("HeLLo123") == "hello123" - assert normalize_tag("hello world") == "hello_world" - assert normalize_tag("a/b.c_d-e") == "a/b.c_d-e" - assert normalize_tag("héllø") == "h_ll_" - assert normalize_tag("") == "" - assert normalize_tag("💡⚡️") == "___" - assert normalize_tag("!foo@") == "_foo_" - assert normalize_tag("123_abc.DEF-ghi/jkl") == "123_abc.def-ghi/jkl" - assert normalize_tag("Env:Prod-Server#1") == "env_prod-server_1" +@pytest.mark.parametrize( + "input,expected", + [ + ("HelloWorld", "helloworld"), + ("Hello@World!", "hello_world_"), + ("HeLLo123", "hello123"), + ("hello world", "hello_world"), + ("a/b.c_d-e", "a/b.c_d-e"), + ("héllø", "h_ll_"), + ("", ""), + ("💡⚡️", "___"), + ("!foo@", "_foo_"), + ("123_abc.DEF-ghi/jkl", "123_abc.def-ghi/jkl"), + ("Env:Prod-Server#1", "env_prod-server_1"), + ], +) +def test_normalize_tag(input_tag, expected): + assert normalize_tag(input_tag) == expected class TestProcessTags(TracerTestCase): def test_process_tags_deactivated(self): + config._process_tags_enabled = False + _process_tag_reload() + with self.tracer.trace("test"): pass @@ -37,94 +39,20 @@ def test_process_tags_deactivated(self): assert span is not None assert PROCESS_TAGS not in span._meta - def test_process_tags_activated_with_override_env(self): + @pytest.mark.snapshot + def test_process_tags_activated(self): """Test process tags using override_env instead of run_in_subprocess""" - with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): - process_tags._enabled = True - process_tags.reload() - - with self.tracer.trace("test"): - pass - - process_tags._enabled = False - - span = self.get_spans()[0] - assert span is not None - assert PROCESS_TAGS in span._meta - - expected_name = "pytest" - expected_type = ENTRYPOINT_TYPE_SCRIPT - expected_basedir = Path(sys.argv[0]).resolve().parent.name - expected_workdir = os.path.basename(os.getcwd()) - - serialized_tags = span._meta[PROCESS_TAGS] - expected_raw = ( - f"{ENTRYPOINT_WORKDIR_TAG}:{expected_workdir}," - f"{ENTRYPOINT_BASEDIR_TAG}:{expected_basedir}," - f"{ENTRYPOINT_NAME_TAG}:{expected_name}," - f"{ENTRYPOINT_TYPE_TAG}:{expected_type}" - ) - assert serialized_tags == expected_raw + config._process_tags_enabled = True + _process_tag_reload() - tags_dict = dict(tag.split(":", 1) for tag in serialized_tags.split(",")) - assert tags_dict[ENTRYPOINT_NAME_TAG] == expected_name - assert tags_dict[ENTRYPOINT_TYPE_TAG] == expected_type - assert tags_dict[ENTRYPOINT_BASEDIR_TAG] == expected_basedir - assert tags_dict[ENTRYPOINT_WORKDIR_TAG] == expected_workdir + with self.tracer.trace("test"): + pass + @pytest.mark.snapshot def test_process_tags_only_on_local_root_span(self): """Test that only local root spans get process tags, not children""" - with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): - process_tags._enabled = True - process_tags.reload() - - with self.tracer.trace("parent"): - with self.tracer.trace("child"): - pass - - process_tags._enabled = False - - spans = self.get_spans() - assert len(spans) == 2 - - parent = [s for s in spans if s.name == "parent"][0] - assert PROCESS_TAGS in parent._meta - - child = [s for s in spans if s.name == "child"][0] - assert PROCESS_TAGS not in child._meta - - def test_add_process_tag_compute_exception(self): - """Test error handling when compute raises exception""" - with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): - process_tags._enabled = True - process_tags.reload() - - def failing_compute(): - raise ValueError("Test exception") - - process_tags.add_process_tag("test.tag", compute=failing_compute) - - assert "test.tag" not in process_tags._process_tags - - process_tags.add_process_tag("test.working", value="value") - assert "test.working" in process_tags._process_tags - assert process_tags._process_tags["test.working"] == "value" - - process_tags._enabled = False - - def test_serialization_caching(self): - """Test that serialization is cached and invalidated properly""" - with override_env(dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")): - process_tags._enabled = True - process_tags.reload() - - process_tags.get_serialized_process_tags() - assert process_tags._serialized is not None - - process_tags.add_process_tag("custom.tag", value="test") - assert process_tags._serialized is None - - result3 = process_tags.get_serialized_process_tags() - assert "custom.tag:test" in result3 - - process_tags._enabled = False + config._process_tags_enabled = True + _process_tag_reload() + with self.tracer.trace("parent"): + with self.tracer.trace("child"): + pass diff --git a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json b/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json new file mode 100644 index 00000000000..f68a188592a --- /dev/null +++ b/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json @@ -0,0 +1,26 @@ +[[ + { + "name": "test", + "service": "tests.process_tags", + "resource": "test", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "690c776100000000", + "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", + "language": "python", + "runtime-id": "cb0907fe4d334d8f952c546ae1bfe631" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 122042, + "start": 1762424673740002008 + }]] diff --git a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json b/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json new file mode 100644 index 00000000000..b38289fc80f --- /dev/null +++ b/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json @@ -0,0 +1,38 @@ +[[ + { + "name": "parent", + "service": "tests.process_tags", + "resource": "parent", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "690c77c500000000", + "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", + "language": "python", + "runtime-id": "24077c4537e349bca8226bf23bbfac75" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 125583, + "start": 1762424773059544096 + }, + { + "name": "child", + "service": "tests.process_tags", + "resource": "child", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "", + "error": 0, + "duration": 73458, + "start": 1762424773059582388 + }]] From 184ef53e54ec831bacbc7ab34b22d525075d5c0f Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Thu, 6 Nov 2025 11:30:18 +0100 Subject: [PATCH 08/25] update python version --- riotfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riotfile.py b/riotfile.py index 729c1f61994..68208829248 100644 --- a/riotfile.py +++ b/riotfile.py @@ -521,7 +521,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT command="pytest --no-cov {cmdargs} tests/coverage -s", pys=select_pys(max_version="3.12"), ), - Venv(name="process_tags", command="pytest -v {cmdargs} tests/process_tags/", pys=select_pys(min_version="3.9")), + Venv(name="process_tags", command="pytest -v {cmdargs} tests/process_tags/", pys=select_pys()), Venv( name="internal", env={ From be2973ed279808067193e722f5a25ef2c5dd5a26 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Thu, 6 Nov 2025 16:31:15 +0100 Subject: [PATCH 09/25] put tests within internal suite --- riotfile.py | 1 - .../test_process_tags.py | 2 +- tests/process_tags/__init__.py | 0 ...ProcessTags.test_process_tags_activated.json} | 10 +++++----- ...st_process_tags_only_on_local_root_span.json} | 16 ++++++++-------- tests/suitespec.yml | 11 +---------- 6 files changed, 15 insertions(+), 25 deletions(-) rename tests/{process_tags => internal}/test_process_tags.py (98%) delete mode 100644 tests/process_tags/__init__.py rename tests/snapshots/{tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json => tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json} (71%) rename tests/snapshots/{tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json => tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json} (69%) diff --git a/riotfile.py b/riotfile.py index 68208829248..d38850c428f 100644 --- a/riotfile.py +++ b/riotfile.py @@ -521,7 +521,6 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT command="pytest --no-cov {cmdargs} tests/coverage -s", pys=select_pys(max_version="3.12"), ), - Venv(name="process_tags", command="pytest -v {cmdargs} tests/process_tags/", pys=select_pys()), Venv( name="internal", env={ diff --git a/tests/process_tags/test_process_tags.py b/tests/internal/test_process_tags.py similarity index 98% rename from tests/process_tags/test_process_tags.py rename to tests/internal/test_process_tags.py index 92d8beebfeb..bff40de30a5 100644 --- a/tests/process_tags/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -8,7 +8,7 @@ @pytest.mark.parametrize( - "input,expected", + "input_tag,expected", [ ("HelloWorld", "helloworld"), ("Hello@World!", "hello_world_"), diff --git a/tests/process_tags/__init__.py b/tests/process_tags/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json similarity index 71% rename from tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json rename to tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json index f68a188592a..35cab17da03 100644 --- a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_activated.json +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json @@ -1,7 +1,7 @@ [[ { "name": "test", - "service": "tests.process_tags", + "service": "tests.internal", "resource": "test", "trace_id": 0, "span_id": 1, @@ -10,10 +10,10 @@ "error": 0, "meta": { "_dd.p.dm": "-0", - "_dd.p.tid": "690c776100000000", + "_dd.p.tid": "690cbe5800000000", "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", "language": "python", - "runtime-id": "cb0907fe4d334d8f952c546ae1bfe631" + "runtime-id": "dd59166f5fa34c50a0815ba3e3c2da93" }, "metrics": { "_dd.top_level": 1, @@ -21,6 +21,6 @@ "_sampling_priority_v1": 1, "process_id": 605 }, - "duration": 122042, - "start": 1762424673740002008 + "duration": 139041, + "start": 1762442840216120585 }]] diff --git a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json similarity index 69% rename from tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json rename to tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json index b38289fc80f..285e496b994 100644 --- a/tests/snapshots/tests.process_tags.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json @@ -1,7 +1,7 @@ [[ { "name": "parent", - "service": "tests.process_tags", + "service": "tests.internal", "resource": "parent", "trace_id": 0, "span_id": 1, @@ -10,10 +10,10 @@ "error": 0, "meta": { "_dd.p.dm": "-0", - "_dd.p.tid": "690c77c500000000", + "_dd.p.tid": "690cbe5800000000", "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", "language": "python", - "runtime-id": "24077c4537e349bca8226bf23bbfac75" + "runtime-id": "dd59166f5fa34c50a0815ba3e3c2da93" }, "metrics": { "_dd.top_level": 1, @@ -21,18 +21,18 @@ "_sampling_priority_v1": 1, "process_id": 605 }, - "duration": 125583, - "start": 1762424773059544096 + "duration": 97791, + "start": 1762442840225996585 }, { "name": "child", - "service": "tests.process_tags", + "service": "tests.internal", "resource": "child", "trace_id": 0, "span_id": 2, "parent_id": 1, "type": "", "error": 0, - "duration": 73458, - "start": 1762424773059582388 + "duration": 51542, + "start": 1762442840226028043 }]] diff --git a/tests/suitespec.yml b/tests/suitespec.yml index cf45ec08778..f10de96a450 100644 --- a/tests/suitespec.yml +++ b/tests/suitespec.yml @@ -95,9 +95,6 @@ components: - ddtrace/internal/opentelemetry/* opentracer: - ddtrace/opentracer/* - process_tags: - - ddtrace/internal/process_tags/* - - tests/process_tags/* profiling: - ddtrace/profiling/* - ddtrace/internal/datadog/profiling/* @@ -211,6 +208,7 @@ suites: - tests/internal/* - tests/submod/* - tests/cache/* + - tests/snapshots/tests.internal.* runner: riot snapshot: true lib_injection: @@ -245,13 +243,6 @@ suites: - '@core' runner: riot pattern: ^openfeature$ - process_tags: - parallelism: 1 - paths: - - '@process_tags' - - '@core' - - '@tracing' - runner: riot telemetry: parallelism: 1 paths: From c6b4d7f037f9189c0341b86069e483626c862cbb Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Thu, 6 Nov 2025 17:23:39 +0100 Subject: [PATCH 10/25] remove sys hack --- ddtrace/_trace/processor/__init__.py | 6 +++--- ddtrace/internal/process_tags/__init__.py | 8 -------- tests/internal/test_process_tags.py | 2 -- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/ddtrace/_trace/processor/__init__.py b/ddtrace/_trace/processor/__init__.py index 09614618227..e651c3f39a8 100644 --- a/ddtrace/_trace/processor/__init__.py +++ b/ddtrace/_trace/processor/__init__.py @@ -23,7 +23,7 @@ from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY from ddtrace.internal.constants import SamplingMechanism from ddtrace.internal.logger import get_logger -from ddtrace.internal.process_tags import process_tags +from ddtrace.internal import process_tags from ddtrace.internal.rate_limiter import RateLimiter from ddtrace.internal.sampling import SpanSamplingRule from ddtrace.internal.sampling import get_span_sampling_rules @@ -252,8 +252,8 @@ def process_trace(self, trace: List[Span]) -> Optional[List[Span]]: span._update_tags_from_context() self._set_git_metadata(span) span._set_tag_str("language", "python") - if process_tags: - span._set_tag_str(PROCESS_TAGS, process_tags) + if p_tags := process_tags.process_tags: + span._set_tag_str(PROCESS_TAGS, p_tags) # for 128 bit trace ids if span.trace_id > MAX_UINT_64BITS: trace_id_hob = _get_64_highest_order_bits_as_hex(span.trace_id) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index 90b67fb4b18..ccd64ad4b66 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -47,12 +47,4 @@ def _process_tag_reload(): global process_tags process_tags = generate_process_tags() - # Force update in the processor module for testing - import sys - - if "ddtrace._trace.processor" in sys.modules: - processor_module = sys.modules["ddtrace._trace.processor"] - processor_module.process_tags = process_tags - - process_tags = generate_process_tags() diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index bff40de30a5..c2a068be0a7 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -41,7 +41,6 @@ def test_process_tags_deactivated(self): @pytest.mark.snapshot def test_process_tags_activated(self): - """Test process tags using override_env instead of run_in_subprocess""" config._process_tags_enabled = True _process_tag_reload() @@ -50,7 +49,6 @@ def test_process_tags_activated(self): @pytest.mark.snapshot def test_process_tags_only_on_local_root_span(self): - """Test that only local root spans get process tags, not children""" config._process_tags_enabled = True _process_tag_reload() with self.tracer.trace("parent"): From c6cb1be00d8ff0a1232c8cf78d3598f4402612bd Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 7 Nov 2025 11:21:59 +0100 Subject: [PATCH 11/25] make tests compatible with CI --- tests/internal/test_process_tags.py | 21 +++++++++++-------- ...ocessTags.test_process_tags_activated.json | 10 ++++----- ..._process_tags_only_on_local_root_span.json | 14 ++++++------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index c2a068be0a7..1d9a9026055 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -5,6 +5,7 @@ from ddtrace.internal.process_tags import normalize_tag from ddtrace.settings._config import config from tests.utils import TracerTestCase +from unittest.mock import patch @pytest.mark.parametrize( @@ -41,16 +42,18 @@ def test_process_tags_deactivated(self): @pytest.mark.snapshot def test_process_tags_activated(self): - config._process_tags_enabled = True - _process_tag_reload() + with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + config._process_tags_enabled = True + _process_tag_reload() - with self.tracer.trace("test"): - pass + with self.tracer.trace("test"): + pass @pytest.mark.snapshot def test_process_tags_only_on_local_root_span(self): - config._process_tags_enabled = True - _process_tag_reload() - with self.tracer.trace("parent"): - with self.tracer.trace("child"): - pass + with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + config._process_tags_enabled = True + _process_tag_reload() + with self.tracer.trace("parent"): + with self.tracer.trace("child"): + pass diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json index 35cab17da03..32c551dba37 100644 --- a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json @@ -10,10 +10,10 @@ "error": 0, "meta": { "_dd.p.dm": "-0", - "_dd.p.tid": "690cbe5800000000", - "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", + "_dd.p.tid": "690dc7bc00000000", + "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", "language": "python", - "runtime-id": "dd59166f5fa34c50a0815ba3e3c2da93" + "runtime-id": "97a0041576ed4e53a93eff9971a69fca" }, "metrics": { "_dd.top_level": 1, @@ -21,6 +21,6 @@ "_sampling_priority_v1": 1, "process_id": 605 }, - "duration": 139041, - "start": 1762442840216120585 + "duration": 94000, + "start": 1762510780675377553 }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json index 285e496b994..bc1e4fa64f3 100644 --- a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json @@ -10,10 +10,10 @@ "error": 0, "meta": { "_dd.p.dm": "-0", - "_dd.p.tid": "690cbe5800000000", - "_dd.tags.process": "entrypoint.basedir:bin,entrypoint.name:pytest,entrypoint.type:script,entrypoint.workdir:project", + "_dd.p.tid": "690dc7bc00000000", + "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", "language": "python", - "runtime-id": "dd59166f5fa34c50a0815ba3e3c2da93" + "runtime-id": "97a0041576ed4e53a93eff9971a69fca" }, "metrics": { "_dd.top_level": 1, @@ -21,8 +21,8 @@ "_sampling_priority_v1": 1, "process_id": 605 }, - "duration": 97791, - "start": 1762442840225996585 + "duration": 107667, + "start": 1762510780688981511 }, { "name": "child", @@ -33,6 +33,6 @@ "parent_id": 1, "type": "", "error": 0, - "duration": 51542, - "start": 1762442840226028043 + "duration": 64625, + "start": 1762510780689007511 }]] From 74164666c5973ba40a0becef32b480e9a237977a Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 7 Nov 2025 11:37:41 +0100 Subject: [PATCH 12/25] lint --- ddtrace/_trace/processor/__init__.py | 2 +- ddtrace/internal/process_tags/__init__.py | 1 + tests/internal/test_process_tags.py | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ddtrace/_trace/processor/__init__.py b/ddtrace/_trace/processor/__init__.py index e651c3f39a8..acc38b0b74d 100644 --- a/ddtrace/_trace/processor/__init__.py +++ b/ddtrace/_trace/processor/__init__.py @@ -14,6 +14,7 @@ from ddtrace.constants import _APM_ENABLED_METRIC_KEY as MK_APM_ENABLED from ddtrace.constants import _SINGLE_SPAN_SAMPLING_MECHANISM from ddtrace.internal import gitmetadata +from ddtrace.internal import process_tags from ddtrace.internal import telemetry from ddtrace.internal.constants import COMPONENT from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS @@ -23,7 +24,6 @@ from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY from ddtrace.internal.constants import SamplingMechanism from ddtrace.internal.logger import get_logger -from ddtrace.internal import process_tags from ddtrace.internal.rate_limiter import RateLimiter from ddtrace.internal.sampling import SpanSamplingRule from ddtrace.internal.sampling import get_span_sampling_rules diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index ccd64ad4b66..30b6d6296df 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -47,4 +47,5 @@ def _process_tag_reload(): global process_tags process_tags = generate_process_tags() + process_tags = generate_process_tags() diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index 1d9a9026055..901bfb0fe47 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from ddtrace.internal.constants import PROCESS_TAGS @@ -5,7 +7,6 @@ from ddtrace.internal.process_tags import normalize_tag from ddtrace.settings._config import config from tests.utils import TracerTestCase -from unittest.mock import patch @pytest.mark.parametrize( From 0428dcd8778e677f5118a6b7d11806a05a8af4fa Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 10 Nov 2025 13:56:27 +0100 Subject: [PATCH 13/25] brett review --- .riot/requirements/1645326.txt | 19 ------ tests/internal/test_process_tags.py | 60 +++++++++++++++--- ...s_tags.TestProcessTags.test_edge_case.json | 26 ++++++++ ...ocessTags.test_process_tags_activated.json | 26 +++++--- ...essTags.test_process_tags_deactivated.json | 25 ++++++++ ...ocessTags.test_process_tags_edge_case.json | 26 ++++++++ ...stProcessTags.test_process_tags_error.json | 25 ++++++++ ..._process_tags_only_on_local_root_span.json | 38 ------------ ...sTags.test_process_tags_partial_flush.json | 61 +++++++++++++++++++ 9 files changed, 232 insertions(+), 74 deletions(-) delete mode 100644 .riot/requirements/1645326.txt create mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_edge_case.json create mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_deactivated.json create mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_edge_case.json create mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_error.json delete mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json create mode 100644 tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_partial_flush.json diff --git a/.riot/requirements/1645326.txt b/.riot/requirements/1645326.txt deleted file mode 100644 index 9201e2642b1..00000000000 --- a/.riot/requirements/1645326.txt +++ /dev/null @@ -1,19 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate .riot/requirements/1645326.in -# -attrs==25.4.0 -coverage[toml]==7.11.0 -hypothesis==6.45.0 -iniconfig==2.3.0 -mock==5.2.0 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -pygments==2.19.2 -pytest==8.4.2 -pytest-cov==7.0.0 -pytest-mock==3.15.1 -sortedcontainers==2.4.0 diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index 901bfb0fe47..558372b9c20 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -2,10 +2,11 @@ import pytest -from ddtrace.internal.constants import PROCESS_TAGS +from ddtrace.internal import process_tags from ddtrace.internal.process_tags import _process_tag_reload from ddtrace.internal.process_tags import normalize_tag from ddtrace.settings._config import config +from tests.subprocesstest import run_in_subprocess from tests.utils import TracerTestCase @@ -30,6 +31,17 @@ def test_normalize_tag(input_tag, expected): class TestProcessTags(TracerTestCase): + def setUp(self): + super(TestProcessTags, self).setUp() + self._original_process_tags_enabled = config._process_tags_enabled + self._original_process_tags = process_tags.process_tags + + def tearDown(self): + config._process_tags_enabled = self._original_process_tags_enabled + process_tags.process_tags = self._original_process_tags + super().tearDown() + + @pytest.mark.snapshot def test_process_tags_deactivated(self): config._process_tags_enabled = False _process_tag_reload() @@ -37,24 +49,52 @@ def test_process_tags_deactivated(self): with self.tracer.trace("test"): pass - span = self.get_spans()[0] - assert span is not None - assert PROCESS_TAGS not in span._meta - @pytest.mark.snapshot def test_process_tags_activated(self): with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): config._process_tags_enabled = True _process_tag_reload() - with self.tracer.trace("test"): + with self.tracer.trace("parent"): + with self.tracer.trace("child"): + pass + + @pytest.mark.snapshot + def test_process_tags_edge_case(self): + with patch("sys.argv", ["/test_script"]), patch("os.getcwd", return_value="/path/to/workdir"): + config._process_tags_enabled = True + _process_tag_reload() + + with self.tracer.trace("span"): pass @pytest.mark.snapshot - def test_process_tags_only_on_local_root_span(self): + def test_process_tags_error(self): + with patch("sys.argv", []), patch("os.getcwd", return_value="/path/to/workdir"): + config._process_tags_enabled = True + + with self.override_global_config(dict(_telemetry_enabled=False)): + with patch("ddtrace.internal.process_tags.log") as mock_log: + _process_tag_reload() + + with self.tracer.trace("span"): + pass + + # Check if debug log was called + mock_log.debug.assert_called_once() + call_args = mock_log.debug.call_args[0] + assert "failed to get process_tags" in call_args[0] + + @pytest.mark.snapshot + @run_in_subprocess(env_overrides=dict(DD_TRACE_PARTIAL_FLUSH_ENABLED="true", DD_TRACE_PARTIAL_FLUSH_MIN_SPANS="2")) + def test_process_tags_partial_flush(self): with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): config._process_tags_enabled = True _process_tag_reload() - with self.tracer.trace("parent"): - with self.tracer.trace("child"): - pass + + with self.override_global_config(dict(_partial_flush_enabled=True, _partial_flush_min_spans=2)): + with self.tracer.trace("parent"): + with self.tracer.trace("child1"): + pass + with self.tracer.trace("child2"): + pass diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_edge_case.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_edge_case.json new file mode 100644 index 00000000000..634d8e00dbd --- /dev/null +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_edge_case.json @@ -0,0 +1,26 @@ +[[ + { + "name": "span", + "service": "tests.internal", + "resource": "span", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911da3a00000000", + "_dd.tags.process": "entrypoint.basedir:,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", + "language": "python", + "runtime-id": "c9342b8003de45feb0bf56d32ece46a1" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 105292, + "start": 1762777658431833668 + }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json index 32c551dba37..8a3c1c18440 100644 --- a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_activated.json @@ -1,8 +1,8 @@ [[ { - "name": "test", + "name": "parent", "service": "tests.internal", - "resource": "test", + "resource": "parent", "trace_id": 0, "span_id": 1, "parent_id": 0, @@ -10,10 +10,10 @@ "error": 0, "meta": { "_dd.p.dm": "-0", - "_dd.p.tid": "690dc7bc00000000", + "_dd.p.tid": "6911dc5a00000000", "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", "language": "python", - "runtime-id": "97a0041576ed4e53a93eff9971a69fca" + "runtime-id": "2d5de91f8dd9442cad7faca5554a09f1" }, "metrics": { "_dd.top_level": 1, @@ -21,6 +21,18 @@ "_sampling_priority_v1": 1, "process_id": 605 }, - "duration": 94000, - "start": 1762510780675377553 - }]] + "duration": 231542, + "start": 1762778202287875128 + }, + { + "name": "child", + "service": "tests.internal", + "resource": "child", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "", + "error": 0, + "duration": 55500, + "start": 1762778202287999128 + }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_deactivated.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_deactivated.json new file mode 100644 index 00000000000..5de189e565f --- /dev/null +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_deactivated.json @@ -0,0 +1,25 @@ +[[ + { + "name": "test", + "service": "tests.internal", + "resource": "test", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911dc5a00000000", + "language": "python", + "runtime-id": "2d5de91f8dd9442cad7faca5554a09f1" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 22292, + "start": 1762778202327669586 + }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_edge_case.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_edge_case.json new file mode 100644 index 00000000000..eb8e7ddaa8b --- /dev/null +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_edge_case.json @@ -0,0 +1,26 @@ +[[ + { + "name": "span", + "service": "tests.internal", + "resource": "span", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911dc5a00000000", + "_dd.tags.process": "entrypoint.basedir:,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", + "language": "python", + "runtime-id": "2d5de91f8dd9442cad7faca5554a09f1" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 35458, + "start": 1762778202321224878 + }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_error.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_error.json new file mode 100644 index 00000000000..4af59e2e651 --- /dev/null +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_error.json @@ -0,0 +1,25 @@ +[[ + { + "name": "span", + "service": "tests.internal", + "resource": "span", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911db6e00000000", + "language": "python", + "runtime-id": "c59cb90aad3246579bc4421d1cca07c8" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 605 + }, + "duration": 102833, + "start": 1762777966446950922 + }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json deleted file mode 100644 index bc1e4fa64f3..00000000000 --- a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_only_on_local_root_span.json +++ /dev/null @@ -1,38 +0,0 @@ -[[ - { - "name": "parent", - "service": "tests.internal", - "resource": "parent", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "", - "error": 0, - "meta": { - "_dd.p.dm": "-0", - "_dd.p.tid": "690dc7bc00000000", - "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", - "language": "python", - "runtime-id": "97a0041576ed4e53a93eff9971a69fca" - }, - "metrics": { - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "process_id": 605 - }, - "duration": 107667, - "start": 1762510780688981511 - }, - { - "name": "child", - "service": "tests.internal", - "resource": "child", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "", - "error": 0, - "duration": 64625, - "start": 1762510780689007511 - }]] diff --git a/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_partial_flush.json b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_partial_flush.json new file mode 100644 index 00000000000..ef399836ce6 --- /dev/null +++ b/tests/snapshots/tests.internal.test_process_tags.TestProcessTags.test_process_tags_partial_flush.json @@ -0,0 +1,61 @@ +[[ + { + "name": "parent", + "service": "", + "resource": "parent", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911e0a800000000", + "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", + "language": "python", + "runtime-id": "9e17705896a74bc5ba68a054ff29e42e" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 629 + }, + "duration": 308417, + "start": 1762779304752393180 + }, + { + "name": "child1", + "service": "", + "resource": "child1", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "", + "error": 0, + "meta": { + "_dd.p.dm": "-0", + "_dd.p.tid": "6911e0a800000000", + "_dd.tags.process": "entrypoint.basedir:to,entrypoint.name:test_script,entrypoint.type:script,entrypoint.workdir:workdir", + "language": "python" + }, + "metrics": { + "_dd.py.partial_flush": 2, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1 + }, + "duration": 11500, + "start": 1762779304752417264 + }, + { + "name": "child2", + "service": "", + "resource": "child2", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "", + "error": 0, + "duration": 8167, + "start": 1762779304752455930 + }]] From 32ddf3530a777d4d52e036d2fd5c0a72ac0925fa Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 14 Nov 2025 13:44:21 +0100 Subject: [PATCH 14/25] improve tag normalization --- ddtrace/internal/process_tags/__init__.py | 7 ++++++- tests/internal/test_process_tags.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index 30b6d6296df..56e7aa6e092 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -16,9 +16,14 @@ log = get_logger(__name__) +_INVALID_CHARS_PATTERN = re.compile(r"[^a-z0-9/._-]") +_CONSECUTIVE_UNDERSCORES_PATTERN = re.compile(r"_{2,}") + def normalize_tag(value: str) -> str: - return re.sub(r"[^a-z0-9/._-]", "_", value.lower()) + normalized = _INVALID_CHARS_PATTERN.sub("_", value.lower()) + normalized = _CONSECUTIVE_UNDERSCORES_PATTERN.sub("_", normalized) + return normalized.strip("_") def generate_process_tags() -> Optional[str]: diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index 558372b9c20..d670a4b7c94 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -14,16 +14,22 @@ "input_tag,expected", [ ("HelloWorld", "helloworld"), - ("Hello@World!", "hello_world_"), + ("Hello@World!", "hello_world"), ("HeLLo123", "hello123"), ("hello world", "hello_world"), ("a/b.c_d-e", "a/b.c_d-e"), - ("héllø", "h_ll_"), + ("héllø", "h_ll"), ("", ""), - ("💡⚡️", "___"), - ("!foo@", "_foo_"), + ("💡⚡️", ""), + ("!foo@", "foo"), ("123_abc.DEF-ghi/jkl", "123_abc.def-ghi/jkl"), ("Env:Prod-Server#1", "env_prod-server_1"), + ("__hello__world__", "hello_world"), + ("___test___", "test"), + ("_leading", "leading"), + ("trailing_", "trailing"), + ("double__underscore", "double_underscore"), + ("test\x99\x8faaa", "test_aaa"), ], ) def test_normalize_tag(input_tag, expected): From 7cf4143772a198fb3d0ac2f409191fdb98e2069a Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 17 Nov 2025 11:33:31 +0100 Subject: [PATCH 15/25] gab review --- ddtrace/internal/process_tags/__init__.py | 20 +++++++------------- ddtrace/internal/process_tags/constants.py | 7 ------- tests/internal/test_process_tags.py | 14 +++++++------- tests/utils.py | 5 +++++ 4 files changed, 19 insertions(+), 27 deletions(-) delete mode 100644 ddtrace/internal/process_tags/constants.py diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index 56e7aa6e092..bcb2e976ea5 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -5,17 +5,17 @@ from typing import Optional from ddtrace.internal.logger import get_logger -from ddtrace.settings._config import config - -from .constants import ENTRYPOINT_BASEDIR_TAG -from .constants import ENTRYPOINT_NAME_TAG -from .constants import ENTRYPOINT_TYPE_SCRIPT -from .constants import ENTRYPOINT_TYPE_TAG -from .constants import ENTRYPOINT_WORKDIR_TAG +from ddtrace.internal.settings._config import config log = get_logger(__name__) +ENTRYPOINT_NAME_TAG = "entrypoint.name" +ENTRYPOINT_WORKDIR_TAG = "entrypoint.workdir" +ENTRYPOINT_TYPE_TAG = "entrypoint.type" +ENTRYPOINT_TYPE_SCRIPT = "script" +ENTRYPOINT_BASEDIR_TAG = "entrypoint.basedir" + _INVALID_CHARS_PATTERN = re.compile(r"[^a-z0-9/._-]") _CONSECUTIVE_UNDERSCORES_PATTERN = re.compile(r"_{2,}") @@ -47,10 +47,4 @@ def generate_process_tags() -> Optional[str]: return None -# For test purpose -def _process_tag_reload(): - global process_tags - process_tags = generate_process_tags() - - process_tags = generate_process_tags() diff --git a/ddtrace/internal/process_tags/constants.py b/ddtrace/internal/process_tags/constants.py deleted file mode 100644 index a863549cfb5..00000000000 --- a/ddtrace/internal/process_tags/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -ENTRYPOINT_NAME_TAG = "entrypoint.name" -ENTRYPOINT_WORKDIR_TAG = "entrypoint.workdir" - -ENTRYPOINT_TYPE_TAG = "entrypoint.type" -ENTRYPOINT_TYPE_SCRIPT = "script" - -ENTRYPOINT_BASEDIR_TAG = "entrypoint.basedir" diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index d670a4b7c94..624e7707589 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -3,11 +3,11 @@ import pytest from ddtrace.internal import process_tags -from ddtrace.internal.process_tags import _process_tag_reload from ddtrace.internal.process_tags import normalize_tag -from ddtrace.settings._config import config +from ddtrace.internal.settings._config import config from tests.subprocesstest import run_in_subprocess from tests.utils import TracerTestCase +from tests.utils import process_tag_reload @pytest.mark.parametrize( @@ -50,7 +50,7 @@ def tearDown(self): @pytest.mark.snapshot def test_process_tags_deactivated(self): config._process_tags_enabled = False - _process_tag_reload() + process_tag_reload() with self.tracer.trace("test"): pass @@ -59,7 +59,7 @@ def test_process_tags_deactivated(self): def test_process_tags_activated(self): with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): config._process_tags_enabled = True - _process_tag_reload() + process_tag_reload() with self.tracer.trace("parent"): with self.tracer.trace("child"): @@ -69,7 +69,7 @@ def test_process_tags_activated(self): def test_process_tags_edge_case(self): with patch("sys.argv", ["/test_script"]), patch("os.getcwd", return_value="/path/to/workdir"): config._process_tags_enabled = True - _process_tag_reload() + process_tag_reload() with self.tracer.trace("span"): pass @@ -81,7 +81,7 @@ def test_process_tags_error(self): with self.override_global_config(dict(_telemetry_enabled=False)): with patch("ddtrace.internal.process_tags.log") as mock_log: - _process_tag_reload() + process_tag_reload() with self.tracer.trace("span"): pass @@ -96,7 +96,7 @@ def test_process_tags_error(self): def test_process_tags_partial_flush(self): with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): config._process_tags_enabled = True - _process_tag_reload() + process_tag_reload() with self.override_global_config(dict(_partial_flush_enabled=True, _partial_flush_min_spans=2)): with self.tracer.trace("parent"): diff --git a/tests/utils.py b/tests/utils.py index 70193d61293..cfb235bb79f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -29,6 +29,7 @@ from ddtrace.constants import _SPAN_MEASURED_KEY from ddtrace.ext import http from ddtrace.internal import core +from ddtrace.internal import process_tags from ddtrace.internal.ci_visibility.writer import CIVisibilityWriter from ddtrace.internal.constants import HIGHER_ORDER_TRACE_ID_BITS from ddtrace.internal.encoding import JSONEncoder @@ -1615,3 +1616,7 @@ def override_third_party_packages(packages: List[str]): filename_to_package.cache_clear() is_third_party.cache_clear() + + +def process_tag_reload(): + process_tags.process_tags = process_tags.generate_process_tags() From ae202075f7192c5d5af84f571effc46782775b92 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Tue, 18 Nov 2025 14:14:02 +0100 Subject: [PATCH 16/25] improving normalization --- ddtrace/internal/process_tags/__init__.py | 31 +++++++-- tests/internal/test_process_tags.py | 82 +++++++++++++++++------ 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index bcb2e976ea5..a3876dae2c6 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -16,23 +16,42 @@ ENTRYPOINT_TYPE_SCRIPT = "script" ENTRYPOINT_BASEDIR_TAG = "entrypoint.basedir" -_INVALID_CHARS_PATTERN = re.compile(r"[^a-z0-9/._-]") _CONSECUTIVE_UNDERSCORES_PATTERN = re.compile(r"_{2,}") +_ALLOWED_CHARS = _ALLOWED_CHARS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789/:._-") +MAX_LENGTH = 200 -def normalize_tag(value: str) -> str: - normalized = _INVALID_CHARS_PATTERN.sub("_", value.lower()) - normalized = _CONSECUTIVE_UNDERSCORES_PATTERN.sub("_", normalized) - return normalized.strip("_") +def normalize_tag_value(value: str) -> str: + # we copy the behavior of the agent which + # checks the size on the original value and not on + # an intermediary normalized step + if len(value) > MAX_LENGTH: + value = value[:MAX_LENGTH] + + result = value.lower() + + def is_allowed_char(char: str) -> str: + # ASCII alphanumeric and special chars: / : . _ - + if char in _ALLOWED_CHARS: + return char + # Unicode letters and digits + if char.isalpha() or char.isdigit(): + return char + return "_" + + result = "".join(is_allowed_char(char) for char in result) + result = _CONSECUTIVE_UNDERSCORES_PATTERN.sub("_", result) + return result.strip("_") def generate_process_tags() -> Optional[str]: + print(config._process_tags_enabled) if not config._process_tags_enabled: return None try: return ",".join( - f"{key}:{normalize_tag(value)}" + f"{key}:{normalize_tag_value(value)}" for key, value in sorted( [ (ENTRYPOINT_WORKDIR_TAG, os.path.basename(os.getcwd())), diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index 624e7707589..ac99788a793 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -3,37 +3,73 @@ import pytest from ddtrace.internal import process_tags -from ddtrace.internal.process_tags import normalize_tag +from ddtrace.internal.process_tags import normalize_tag_value from ddtrace.internal.settings._config import config from tests.subprocesstest import run_in_subprocess from tests.utils import TracerTestCase from tests.utils import process_tag_reload +TEST_SCRIPT_PATH = "/path/to/test_script.py" +TEST_WORKDIR_PATH = "/path/to/workdir" + + @pytest.mark.parametrize( "input_tag,expected", [ - ("HelloWorld", "helloworld"), - ("Hello@World!", "hello_world"), - ("HeLLo123", "hello123"), - ("hello world", "hello_world"), - ("a/b.c_d-e", "a/b.c_d-e"), - ("héllø", "h_ll"), + # # Additional test cases from Go implementation + ("#test_starting_hash", "test_starting_hash"), + ("TestCAPSandSuch", "testcapsandsuch"), + ("Test Conversion Of Weird !@#$%^&**() Characters", "test_conversion_of_weird_characters"), + ("$#weird_starting", "weird_starting"), + ("allowed:c0l0ns", "allowed:c0l0ns"), + ("1love", "1love"), + ("/love2", "/love2"), + ("ünicöde", "ünicöde"), + ("ünicöde:metäl", "ünicöde:metäl"), + ("Data🐨dog🐶 繋がっ⛰てて", "data_dog_繋がっ_てて"), + (" spaces ", "spaces"), + (" #hashtag!@#spaces #__<># ", "hashtag_spaces"), + (":testing", ":testing"), + ("_foo", "foo"), + (":::test", ":::test"), + ("contiguous_____underscores", "contiguous_underscores"), + ("foo_", "foo"), + ("\u017Fodd_\u017Fcase\u017F", "\u017Fodd_\u017Fcase\u017F"), ("", ""), - ("💡⚡️", ""), - ("!foo@", "foo"), - ("123_abc.DEF-ghi/jkl", "123_abc.def-ghi/jkl"), - ("Env:Prod-Server#1", "env_prod-server_1"), - ("__hello__world__", "hello_world"), - ("___test___", "test"), - ("_leading", "leading"), - ("trailing_", "trailing"), - ("double__underscore", "double_underscore"), + (" ", ""), + ("ok", "ok"), + ("™Ö™Ö™™Ö™", "ö_ö_ö"), + ("AlsO:ök", "also:ök"), + (":still_ok", ":still_ok"), + ("___trim", "trim"), + ("12.:trim@", "12.:trim"), + ("12.:trim@@", "12.:trim"), + ("fun:ky__tag/1", "fun:ky_tag/1"), + ("fun:ky@tag/2", "fun:ky_tag/2"), + ("fun:ky@@@tag/3", "fun:ky_tag/3"), + ("tag:1/2.3", "tag:1/2.3"), + ("---fun:k####y_ta@#g/1_@@#", "---fun:k_y_ta_g/1"), + ("AlsO:œ#@ö))œk", "also:œ_ö_œk"), ("test\x99\x8faaa", "test_aaa"), + ("test\x99\x8f", "test"), + ("a" * 888, "a" * 200), + ("a" + "🐶" * 799 + "b", "a"), + ("a" + "\ufffd", "a"), + ("a" + "\ufffd" + "\ufffd", "a"), + ("a" + "\ufffd" + "\ufffd" + "b", "a_b"), + ( + "A00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + " 000000000000", + "a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "_0", + ), ], ) def test_normalize_tag(input_tag, expected): - assert normalize_tag(input_tag) == expected + assert normalize_tag_value(input_tag) == expected class TestProcessTags(TracerTestCase): @@ -57,7 +93,7 @@ def test_process_tags_deactivated(self): @pytest.mark.snapshot def test_process_tags_activated(self): - with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + with patch("sys.argv", [TEST_SCRIPT_PATH]), patch("os.getcwd", return_value=TEST_WORKDIR_PATH): config._process_tags_enabled = True process_tag_reload() @@ -67,7 +103,7 @@ def test_process_tags_activated(self): @pytest.mark.snapshot def test_process_tags_edge_case(self): - with patch("sys.argv", ["/test_script"]), patch("os.getcwd", return_value="/path/to/workdir"): + with patch("sys.argv", ["/test_script"]), patch("os.getcwd", return_value=TEST_WORKDIR_PATH): config._process_tags_enabled = True process_tag_reload() @@ -76,7 +112,7 @@ def test_process_tags_edge_case(self): @pytest.mark.snapshot def test_process_tags_error(self): - with patch("sys.argv", []), patch("os.getcwd", return_value="/path/to/workdir"): + with patch("sys.argv", []), patch("os.getcwd", return_value=TEST_WORKDIR_PATH): config._process_tags_enabled = True with self.override_global_config(dict(_telemetry_enabled=False)): @@ -89,12 +125,14 @@ def test_process_tags_error(self): # Check if debug log was called mock_log.debug.assert_called_once() call_args = mock_log.debug.call_args[0] - assert "failed to get process_tags" in call_args[0] + assert ( + "failed to get process_tags" in call_args[0] + ), f"Expected error message not found. Got: {call_args[0]}" @pytest.mark.snapshot @run_in_subprocess(env_overrides=dict(DD_TRACE_PARTIAL_FLUSH_ENABLED="true", DD_TRACE_PARTIAL_FLUSH_MIN_SPANS="2")) def test_process_tags_partial_flush(self): - with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + with patch("sys.argv", [TEST_SCRIPT_PATH]), patch("os.getcwd", return_value=TEST_WORKDIR_PATH): config._process_tags_enabled = True process_tag_reload() From efe28faf7708600c26e759206ad1d5c76ae559ce Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Tue, 18 Nov 2025 16:07:37 +0100 Subject: [PATCH 17/25] remove print --- ddtrace/internal/process_tags/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index a3876dae2c6..a544374c466 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -45,7 +45,6 @@ def is_allowed_char(char: str) -> str: def generate_process_tags() -> Optional[str]: - print(config._process_tags_enabled) if not config._process_tags_enabled: return None From e33db20d458a5dea74ae688c1ff0ba815bffb238 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 7 Nov 2025 14:04:40 +0100 Subject: [PATCH 18/25] chore(rc): add process tags --- ddtrace/internal/process_tags/__init__.py | 16 ++++++++++------ ddtrace/internal/remoteconfig/client.py | 5 +++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index a544374c466..b8e1a3a01bb 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -2,7 +2,9 @@ from pathlib import Path import re import sys +from typing import List from typing import Optional +from typing import Tuple from ddtrace.internal.logger import get_logger from ddtrace.internal.settings._config import config @@ -44,12 +46,12 @@ def is_allowed_char(char: str) -> str: return result.strip("_") -def generate_process_tags() -> Optional[str]: +def generate_process_tags() -> Tuple[Optional[str], Optional[List[str]]]: if not config._process_tags_enabled: - return None + return None, None try: - return ",".join( + process_tags_list = [ f"{key}:{normalize_tag_value(value)}" for key, value in sorted( [ @@ -59,10 +61,12 @@ def generate_process_tags() -> Optional[str]: (ENTRYPOINT_TYPE_TAG, ENTRYPOINT_TYPE_SCRIPT), ] ) - ) + ] + process_tags = ",".join(process_tags_list) + return process_tags, process_tags_list except Exception as e: log.debug("failed to get process_tags: %s", e) - return None + return None, None -process_tags = generate_process_tags() +process_tags, process_tags_list = generate_process_tags() diff --git a/ddtrace/internal/remoteconfig/client.py b/ddtrace/internal/remoteconfig/client.py index efa73f65d9f..b49e407f020 100644 --- a/ddtrace/internal/remoteconfig/client.py +++ b/ddtrace/internal/remoteconfig/client.py @@ -21,6 +21,7 @@ import ddtrace from ddtrace.internal import agent from ddtrace.internal import gitmetadata +from ddtrace.internal import process_tags from ddtrace.internal import runtime from ddtrace.internal.hostname import get_hostname from ddtrace.internal.logger import get_logger @@ -233,6 +234,10 @@ def __init__(self) -> None: app_version=ddtrace.config.version, tags=[":".join(_) for _ in tags.items()], ) + + if p_tags_list := process_tags.process_tags_list: + self._client_tracer["process_tags"] = p_tags_list + self.cached_target_files: List[AppliedConfigType] = [] self._products: MutableMapping[str, PubSub] = dict() From 8c6a18aa5e388d4e78d7f7bc8cb341ecb3f40663 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 17 Nov 2025 16:10:06 +0100 Subject: [PATCH 19/25] add tests --- .../remoteconfig/test_remoteconfig.py | 36 +++++++++++++++++++ tests/utils.py | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/internal/remoteconfig/test_remoteconfig.py b/tests/internal/remoteconfig/test_remoteconfig.py index 08e655e9ce0..5de99185253 100644 --- a/tests/internal/remoteconfig/test_remoteconfig.py +++ b/tests/internal/remoteconfig/test_remoteconfig.py @@ -802,3 +802,39 @@ def test_apm_tracing_sampling_rules_none_override(remote_config_worker): # Restore original config config.service = original_service config.env = original_env + + +def test_remote_config_payload_not_includes_process_tags(): + client = RemoteConfigClient() + payload = client._build_payload({}) + + assert "process_tags" not in payload["client"]["client_tracer"] + + +@pytest.mark.subprocess(env={"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": "True"}) +def test_remote_config_payload_includes_process_tags(): + from unittest.mock import patch + + from ddtrace.internal.process_tags import ENTRYPOINT_BASEDIR_TAG + from ddtrace.internal.process_tags import ENTRYPOINT_NAME_TAG + from ddtrace.internal.process_tags import ENTRYPOINT_TYPE_SCRIPT + from ddtrace.internal.process_tags import ENTRYPOINT_TYPE_TAG + from ddtrace.internal.process_tags import ENTRYPOINT_WORKDIR_TAG + from ddtrace.internal.remoteconfig.client import RemoteConfigClient + from tests.utils import process_tag_reload + + with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + process_tag_reload() + + client = RemoteConfigClient() + payload = client._build_payload({}) + + assert "process_tags" in payload["client"]["client_tracer"] + + process_tags = payload["client"]["client_tracer"]["process_tags"] + + assert isinstance(process_tags, list) + assert f"{ENTRYPOINT_BASEDIR_TAG}:to" in process_tags + assert f"{ENTRYPOINT_NAME_TAG}:test_script" in process_tags + assert f"{ENTRYPOINT_TYPE_TAG}:{ENTRYPOINT_TYPE_SCRIPT}" in process_tags + assert f"{ENTRYPOINT_WORKDIR_TAG}:workdir" in process_tags diff --git a/tests/utils.py b/tests/utils.py index cfb235bb79f..c88d26f5ab4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1619,4 +1619,4 @@ def override_third_party_packages(packages: List[str]): def process_tag_reload(): - process_tags.process_tags = process_tags.generate_process_tags() + process_tags.process_tags, process_tags.process_tags_list = process_tags.generate_process_tags() From f957552544a61e18d12d73674bd882c28e29d787 Mon Sep 17 00:00:00 2001 From: Louis Tricot <75956635+dubloom@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:28:33 +0100 Subject: [PATCH 20/25] Update tests/internal/test_process_tags.py --- tests/internal/test_process_tags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index ac99788a793..8c50bcfaaf7 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -17,7 +17,6 @@ @pytest.mark.parametrize( "input_tag,expected", [ - # # Additional test cases from Go implementation ("#test_starting_hash", "test_starting_hash"), ("TestCAPSandSuch", "testcapsandsuch"), ("Test Conversion Of Weird !@#$%^&**() Characters", "test_conversion_of_weird_characters"), From 914d52a3df6807b2a16a85dcae4e02329274386d Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 19 Nov 2025 10:37:42 +0100 Subject: [PATCH 21/25] add a test that activates the feature with env variable --- tests/internal/test_process_tags.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index 8c50bcfaaf7..de29fe14d89 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -3,6 +3,11 @@ import pytest from ddtrace.internal import process_tags +from ddtrace.internal.constants import PROCESS_TAGS +from ddtrace.internal.process_tags import ENTRYPOINT_BASEDIR_TAG +from ddtrace.internal.process_tags import ENTRYPOINT_NAME_TAG +from ddtrace.internal.process_tags import ENTRYPOINT_TYPE_TAG +from ddtrace.internal.process_tags import ENTRYPOINT_WORKDIR_TAG from ddtrace.internal.process_tags import normalize_tag_value from ddtrace.internal.settings._config import config from tests.subprocesstest import run_in_subprocess @@ -141,3 +146,19 @@ def test_process_tags_partial_flush(self): pass with self.tracer.trace("child2"): pass + + @run_in_subprocess(env_overrides=dict(DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="True")) + def test_process_tags_activated_with_env(self): + with self.tracer.trace("test"): + pass + + span = self.get_spans()[0] + + assert span is not None + assert PROCESS_TAGS in span._meta + + process_tags = span._meta[PROCESS_TAGS] + assert ENTRYPOINT_BASEDIR_TAG in process_tags + assert ENTRYPOINT_NAME_TAG in process_tags + assert ENTRYPOINT_TYPE_TAG in process_tags + assert ENTRYPOINT_WORKDIR_TAG in process_tags From d1f3d33579308857f4cd0f6e3306821c43247117 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 19 Nov 2025 11:56:22 +0100 Subject: [PATCH 22/25] tests fix attempt --- tests/internal/test_process_tags.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/internal/test_process_tags.py b/tests/internal/test_process_tags.py index de29fe14d89..783cad7b63d 100644 --- a/tests/internal/test_process_tags.py +++ b/tests/internal/test_process_tags.py @@ -81,10 +81,12 @@ def setUp(self): super(TestProcessTags, self).setUp() self._original_process_tags_enabled = config._process_tags_enabled self._original_process_tags = process_tags.process_tags + self._original_process_tags_list = process_tags.process_tags_list def tearDown(self): config._process_tags_enabled = self._original_process_tags_enabled process_tags.process_tags = self._original_process_tags + process_tags.process_tags_list = self._original_process_tags_list super().tearDown() @pytest.mark.snapshot From 34d86f5407b71a9747f6cfbd4c27b2ee1497e22a Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 19 Nov 2025 15:04:45 +0100 Subject: [PATCH 23/25] change patch to patch.object --- tests/internal/remoteconfig/test_remoteconfig.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/internal/remoteconfig/test_remoteconfig.py b/tests/internal/remoteconfig/test_remoteconfig.py index 5de99185253..f113889c30e 100644 --- a/tests/internal/remoteconfig/test_remoteconfig.py +++ b/tests/internal/remoteconfig/test_remoteconfig.py @@ -813,6 +813,8 @@ def test_remote_config_payload_not_includes_process_tags(): @pytest.mark.subprocess(env={"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": "True"}) def test_remote_config_payload_includes_process_tags(): + import os + import sys from unittest.mock import patch from ddtrace.internal.process_tags import ENTRYPOINT_BASEDIR_TAG @@ -823,7 +825,9 @@ def test_remote_config_payload_includes_process_tags(): from ddtrace.internal.remoteconfig.client import RemoteConfigClient from tests.utils import process_tag_reload - with patch("sys.argv", ["/path/to/test_script.py"]), patch("os.getcwd", return_value="/path/to/workdir"): + with patch.object(sys, "argv", ["/path/to/test_script.py"]), patch.object( + os, "getcwd", return_value="/path/to/workdir" + ): process_tag_reload() client = RemoteConfigClient() From 43d869719087806bae6d12e2fa006eddc3047c92 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Thu, 20 Nov 2025 14:13:09 +0100 Subject: [PATCH 24/25] lint --- ddtrace/internal/process_tags/__init__.py | 2 +- tests/internal/remoteconfig/test_remoteconfig.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ddtrace/internal/process_tags/__init__.py b/ddtrace/internal/process_tags/__init__.py index 9eb79bf1a81..a27e20b1969 100644 --- a/ddtrace/internal/process_tags/__init__.py +++ b/ddtrace/internal/process_tags/__init__.py @@ -2,9 +2,9 @@ from pathlib import Path import re import sys +from typing import List from typing import Optional from typing import Tuple -from typing import List from ddtrace.internal.logger import get_logger from ddtrace.internal.settings.process_tags import process_tags_config as config diff --git a/tests/internal/remoteconfig/test_remoteconfig.py b/tests/internal/remoteconfig/test_remoteconfig.py index f113889c30e..b5ffb3d2260 100644 --- a/tests/internal/remoteconfig/test_remoteconfig.py +++ b/tests/internal/remoteconfig/test_remoteconfig.py @@ -825,8 +825,9 @@ def test_remote_config_payload_includes_process_tags(): from ddtrace.internal.remoteconfig.client import RemoteConfigClient from tests.utils import process_tag_reload - with patch.object(sys, "argv", ["/path/to/test_script.py"]), patch.object( - os, "getcwd", return_value="/path/to/workdir" + with ( + patch.object(sys, "argv", ["/path/to/test_script.py"]), + patch.object(os, "getcwd", return_value="/path/to/workdir"), ): process_tag_reload() From 6bad34f68173145c75751bb8e02355d1e3db900d Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Wed, 19 Nov 2025 11:34:18 +0100 Subject: [PATCH 25/25] chore(dsm): add process tags to dsm --- ddtrace/internal/datastreams/processor.py | 3 ++ tests/datastreams/test_processor.py | 60 +++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/ddtrace/internal/datastreams/processor.py b/ddtrace/internal/datastreams/processor.py index a229f48387b..83db2876f44 100644 --- a/ddtrace/internal/datastreams/processor.py +++ b/ddtrace/internal/datastreams/processor.py @@ -16,6 +16,7 @@ from typing import Union # noqa:F401 from ddtrace.internal import compat +from ddtrace.internal import process_tags from ddtrace.internal.atexit import register_on_exit_signal from ddtrace.internal.constants import DEFAULT_SERVICE_NAME from ddtrace.internal.native import DDSketch @@ -288,6 +289,8 @@ def periodic(self): raw_payload["Env"] = compat.ensure_text(config.env) if config.version: raw_payload["Version"] = compat.ensure_text(config.version) + if p_tags := process_tags.process_tags: + raw_payload["ProcessTags"] = compat.ensure_text(p_tags) payload = packb(raw_payload) compressed = gzip_compress(payload) diff --git a/tests/datastreams/test_processor.py b/tests/datastreams/test_processor.py index 03153c4506b..7ffab58f869 100644 --- a/tests/datastreams/test_processor.py +++ b/tests/datastreams/test_processor.py @@ -1,7 +1,10 @@ +import gzip import os import time import mock +import msgpack +import pytest from ddtrace.internal.datastreams.processor import PROPAGATION_KEY from ddtrace.internal.datastreams.processor import PROPAGATION_KEY_BASE_64 @@ -15,6 +18,63 @@ mocked_time = 1642544540 +def _decode_datastreams_payload(payload): + decompressed = gzip.decompress(payload) + decoded = msgpack.unpackb(decompressed, raw=False, strict_map_key=False) + + return decoded + + +def test_periodic_payload_tags(): + processor = DataStreamsProcessor("http://localhost:8126") + try: + captured_payloads = [] + with mock.patch.object(processor, "_flush_stats_with_backoff", side_effect=captured_payloads.append): + processor.on_checkpoint_creation(1, 2, ["direction:out", "topic:topicA", "type:kafka"], mocked_time, 1, 1) + processor.periodic() + + assert captured_payloads, "expected periodic to send a payload" + decoded = _decode_datastreams_payload(captured_payloads[0]) + assert decoded["Service"] == processor._service + assert decoded["TracerVersion"] == processor._version + assert decoded["Lang"] == "python" + assert decoded["Hostname"] == processor._hostname + assert "ProcessTags" not in decoded + finally: + processor.stop() + processor.join() + + +@pytest.mark.subprocess( + env=dict( + DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED="true", + ) +) +def test_periodic_payload_process_tags(): + import mock + + from ddtrace.internal.datastreams.processor import DataStreamsProcessor + from tests.datastreams.test_processor import _decode_datastreams_payload + + processor = DataStreamsProcessor("http://localhost:8126") + try: + captured_payloads = [] + with mock.patch.object(processor, "_flush_stats_with_backoff", side_effect=captured_payloads.append): + processor.on_checkpoint_creation(1, 2, ["direction:out", "topic:topicA", "type:kafka"], 1642544540, 1, 1) + processor.periodic() + + assert captured_payloads, "expected periodic to send a payload" + decoded = _decode_datastreams_payload(captured_payloads[0]) + assert decoded["Service"] == processor._service + assert decoded["TracerVersion"] == processor._version + assert decoded["Lang"] == "python" + assert decoded["Hostname"] == processor._hostname + assert "ProcessTags" in decoded + finally: + processor.stop() + processor.join() + + def test_data_streams_processor(): now = time.time() processor.on_checkpoint_creation(1, 2, ["direction:out", "topic:topicA", "type:kafka"], now, 1, 1)