From 3c9c61948699e585ed9e6b0728d814819e7d6c99 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Mon, 1 Dec 2025 11:23:47 +0100 Subject: [PATCH 1/2] chore(runtime_metrics): add process_tags --- ddtrace/internal/runtime/runtime_metrics.py | 15 +++++++-- ddtrace/internal/runtime/tag_collectors.py | 19 +++++++++++ tests/tracer/runtime/test_tag_collectors.py | 36 +++++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/ddtrace/internal/runtime/runtime_metrics.py b/ddtrace/internal/runtime/runtime_metrics.py index 1059937363f..282133802d3 100644 --- a/ddtrace/internal/runtime/runtime_metrics.py +++ b/ddtrace/internal/runtime/runtime_metrics.py @@ -18,6 +18,7 @@ from .metric_collectors import PSUtilRuntimeMetricCollector from .tag_collectors import PlatformTagCollector from .tag_collectors import PlatformTagCollectorV2 +from .tag_collectors import ProcessTagCollector from .tag_collectors import TracerTagCollector @@ -59,6 +60,12 @@ class TracerTags(RuntimeCollectorsIterable): COLLECTORS = [TracerTagCollector] +class ProcessTags(RuntimeCollectorsIterable): + # DEV: `None` means to allow all tags generated by ProcessTagsCollector + ENABLED = None + COLLECTORS = [ProcessTagCollector] + + class RuntimeMetrics(RuntimeCollectorsIterable): ENABLED = DEFAULT_RUNTIME_METRICS COLLECTORS = [ @@ -94,6 +101,8 @@ def __init__(self, interval=DEFAULT_RUNTIME_METRICS_INTERVAL, tracer=None, dogst else: self._platform_tags = self._format_tags(PlatformTags()) + self._process_tags = self._format_tags(ProcessTags()) + @classmethod def disable(cls) -> None: with cls._lock: @@ -140,9 +149,9 @@ def enable( def flush(self) -> None: # Ensure runtime metrics have up-to-date tags (ex: service, env, version) - rumtime_tags = self._format_tags(TracerTags()) + self._platform_tags - log.debug("Sending runtime metrics with the following tags: %s", rumtime_tags) - self._dogstatsd_client.constant_tags = rumtime_tags + runtime_tags = self._format_tags(TracerTags()) + self._platform_tags + self._process_tags + log.debug("Sending runtime metrics with the following tags: %s", runtime_tags) + self._dogstatsd_client.constant_tags = runtime_tags with self._dogstatsd_client: for key, value in self._runtime_metrics: diff --git a/ddtrace/internal/runtime/tag_collectors.py b/ddtrace/internal/runtime/tag_collectors.py index bf0943e3ea0..a6fb3891493 100644 --- a/ddtrace/internal/runtime/tag_collectors.py +++ b/ddtrace/internal/runtime/tag_collectors.py @@ -5,6 +5,7 @@ from ...constants import ENV_KEY from ...constants import VERSION_KEY +from .. import process_tags from ..constants import DEFAULT_SERVICE_NAME from .collector import ValueCollector from .constants import LANG @@ -97,3 +98,21 @@ def collect_fn(self, keys): tags = super(PlatformTagCollectorV2, self).collect_fn(keys) tags.append(("runtime-id", get_runtime_id())) return tags + + +class ProcessTagCollector(RuntimeTagCollector): + """Tag collector for process tags.""" + + def collect_fn(self, keys): + # DEV: we do not access direct process_tags_list so we can + # reload it in the tests + process_tags_list = process_tags.process_tags_list + if process_tags_list is None: + return [] + + tags = [] + for tag_string in process_tags_list: + key, value = tag_string.split(":") + tags.append((key, value)) + + return tags diff --git a/tests/tracer/runtime/test_tag_collectors.py b/tests/tracer/runtime/test_tag_collectors.py index b536d41e99f..5429e4fc23a 100644 --- a/tests/tracer/runtime/test_tag_collectors.py +++ b/tests/tracer/runtime/test_tag_collectors.py @@ -104,3 +104,39 @@ def process_trace(self, _): assert values == [ ("service", "my-service"), ], values + + +def test_process_tags_disabled_by_default(): + ptc = tag_collectors.ProcessTagCollector() + tags = list(ptc.collect()) + assert len(tags) == 0, "Process tags should be empty when not enabled" + + +@pytest.mark.subprocess(env={"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": "true"}) +def test_process_tags_enabled(): + 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_TAG + from ddtrace.internal.process_tags import ENTRYPOINT_WORKDIR_TAG + from ddtrace.internal.runtime import tag_collectors + 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() + + ptc = tag_collectors.ProcessTagCollector() + tags = list(ptc.collect()) + assert len(tags) == 4, f"Expected 4 process tags, got {len(tags)}: {tags}" + + tags_dict = dict(tags) + assert ENTRYPOINT_NAME_TAG in tags_dict + assert ENTRYPOINT_WORKDIR_TAG in tags_dict + assert ENTRYPOINT_BASEDIR_TAG in tags_dict + assert ENTRYPOINT_TYPE_TAG in tags_dict + + assert tags_dict[ENTRYPOINT_NAME_TAG] == "test_script" + assert tags_dict[ENTRYPOINT_WORKDIR_TAG] == "workdir" + assert tags_dict[ENTRYPOINT_BASEDIR_TAG] == "to" + assert tags_dict[ENTRYPOINT_TYPE_TAG] == "script" From 360c705f6032b1d3f1945ecbe6d8e026e2c02f97 Mon Sep 17 00:00:00 2001 From: Louis Tricot Date: Fri, 5 Dec 2025 11:09:38 +0100 Subject: [PATCH 2/2] review --- ddtrace/internal/runtime/runtime_metrics.py | 2 +- ddtrace/internal/runtime/tag_collectors.py | 10 +--------- tests/tracer/runtime/test_tag_collectors.py | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/ddtrace/internal/runtime/runtime_metrics.py b/ddtrace/internal/runtime/runtime_metrics.py index 282133802d3..e3dd306fa25 100644 --- a/ddtrace/internal/runtime/runtime_metrics.py +++ b/ddtrace/internal/runtime/runtime_metrics.py @@ -101,7 +101,7 @@ def __init__(self, interval=DEFAULT_RUNTIME_METRICS_INTERVAL, tracer=None, dogst else: self._platform_tags = self._format_tags(PlatformTags()) - self._process_tags = self._format_tags(ProcessTags()) + self._process_tags: List[str] = ProcessTags() # type: ignore[assignment] @classmethod def disable(cls) -> None: diff --git a/ddtrace/internal/runtime/tag_collectors.py b/ddtrace/internal/runtime/tag_collectors.py index a6fb3891493..40a650dc946 100644 --- a/ddtrace/internal/runtime/tag_collectors.py +++ b/ddtrace/internal/runtime/tag_collectors.py @@ -107,12 +107,4 @@ def collect_fn(self, keys): # DEV: we do not access direct process_tags_list so we can # reload it in the tests process_tags_list = process_tags.process_tags_list - if process_tags_list is None: - return [] - - tags = [] - for tag_string in process_tags_list: - key, value = tag_string.split(":") - tags.append((key, value)) - - return tags + return process_tags_list or [] diff --git a/tests/tracer/runtime/test_tag_collectors.py b/tests/tracer/runtime/test_tag_collectors.py index 5429e4fc23a..d9c84d2a1f7 100644 --- a/tests/tracer/runtime/test_tag_collectors.py +++ b/tests/tracer/runtime/test_tag_collectors.py @@ -127,10 +127,10 @@ def test_process_tags_enabled(): process_tag_reload() ptc = tag_collectors.ProcessTagCollector() - tags = list(ptc.collect()) + tags: list[str] = ptc.collect() assert len(tags) == 4, f"Expected 4 process tags, got {len(tags)}: {tags}" - tags_dict = dict(tags) + tags_dict = {k: v for k, v in (s.split(":") for s in tags)} assert ENTRYPOINT_NAME_TAG in tags_dict assert ENTRYPOINT_WORKDIR_TAG in tags_dict assert ENTRYPOINT_BASEDIR_TAG in tags_dict