Skip to content

Commit a7810cc

Browse files
feat(tracer): add remote config support of tags (#7515)
Following #7489, add remote configuration support for tags. This feature will enable users to set custom tags like `team` for their services. ![image](https://github.com/DataDog/dd-trace-py/assets/6321485/fd8c9a60-433b-42bb-9c3b-5985736d14ba) Since `tracer.set_tags()` exists, there is the risk of some confusion for customers for where a tag might be coming from when observing a span in the UI. I am treating `tracer.set_tags()` as tags local to the tracer, independent of the global config setting `config.tags` / `DD_TAGS`. This means that tags set via remote configuration will not be able to override those set by `tracer.set_tags()` which may not be desirable. In those cases I think we can recommend that users do not use the `tracer.set_tags()` method. Co-authored-by: Munir Abdinur <[email protected]>
1 parent 536752a commit a7810cc

File tree

9 files changed

+101
-8
lines changed

9 files changed

+101
-8
lines changed

ddtrace/internal/remoteconfig/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class RemoteConfigClientConfig(En):
5757

5858
class Capabilities(enum.IntFlag):
5959
APM_TRACING_SAMPLE_RATE = 1 << 12
60+
APM_TRACING_CUSTOM_TAGS = 1 << 15
6061

6162

6263
class RemoteConfigError(Exception):
@@ -357,7 +358,9 @@ def _parse_target(target, metadata):
357358
def _build_payload(self, state):
358359
# type: (Mapping[str, Any]) -> Mapping[str, Any]
359360
self._client_tracer["extra_services"] = list(ddtrace.config._get_extra_services())
360-
capabilities = appsec_rc_capabilities() | Capabilities.APM_TRACING_SAMPLE_RATE
361+
capabilities = (
362+
appsec_rc_capabilities() | Capabilities.APM_TRACING_SAMPLE_RATE | Capabilities.APM_TRACING_CUSTOM_TAGS
363+
)
361364
return dict(
362365
client=dict(
363366
id=self.id,

ddtrace/internal/telemetry/writer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ def _telemetry_entry(self, cfg_name: str) -> Tuple[str, str, _ConfigSource]:
344344
elif cfg_name == "trace_http_header_tags":
345345
name = "trace_header_tags"
346346
value = ",".join(":".join(x) for x in item.value().items())
347+
elif cfg_name == "tags":
348+
name = "trace_tags"
349+
value = ",".join(":".join(x) for x in item.value().items())
347350
else:
348351
raise ValueError("Unknown configuration item: %s" % cfg_name)
349352
return name, value, item.source()
@@ -365,6 +368,7 @@ def _app_started_event(self, register_app_shutdown=True):
365368
self._telemetry_entry("_trace_sample_rate"),
366369
self._telemetry_entry("logs_injection"),
367370
self._telemetry_entry("trace_http_header_tags"),
371+
self._telemetry_entry("tags"),
368372
(TELEMETRY_TRACING_ENABLED, config._tracing_enabled, "unknown"),
369373
(TELEMETRY_STARTUP_LOGS_ENABLED, config._startup_logs_enabled, "unknown"),
370374
(TELEMETRY_DSM_ENABLED, config._data_streams_enabled, "unknown"),

ddtrace/settings/config.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,15 @@ class _ConfigItem:
201201
"""Configuration item that tracks the value of a setting, and where it came from."""
202202

203203
def __init__(self, name, default, envs):
204-
# type: (str, _JSONType, List[Tuple[str, Callable[[str], Any]]]) -> None
204+
# type: (str, Union[_JSONType, Callable[[], _JSONType]], List[Tuple[str, Callable[[str], Any]]]) -> None
205205
self._name = name
206-
self._default_value = default
207206
self._env_value: _JSONType = None
208207
self._code_value: _JSONType = None
209208
self._rc_value: _JSONType = None
209+
if callable(default):
210+
self._default_value = default()
211+
else:
212+
self._default_value = default
210213
self._envs = envs
211214
for env_var, parser in envs:
212215
if env_var in os.environ:
@@ -261,6 +264,11 @@ def __repr__(self):
261264
)
262265

263266

267+
def _parse_global_tags(s):
268+
# cleanup DD_TAGS, because values will be inserted back in the optimal way (via _dd.git.* tags)
269+
return gitmetadata.clean_tags(parse_tags_str(s))
270+
271+
264272
def _default_config():
265273
# type: () -> Dict[str, _ConfigItem]
266274
return {
@@ -276,9 +284,14 @@ def _default_config():
276284
),
277285
"trace_http_header_tags": _ConfigItem(
278286
name="trace_http_header_tags",
279-
default={},
287+
default=lambda: {},
280288
envs=[("DD_TRACE_HEADER_TAGS", parse_tags_str)],
281289
),
290+
"tags": _ConfigItem(
291+
name="tags",
292+
default=lambda: {},
293+
envs=[("DD_TAGS", _parse_global_tags)],
294+
),
282295
}
283296

284297

@@ -393,9 +406,6 @@ def __init__(self):
393406
self.client_ip_header = os.getenv("DD_TRACE_CLIENT_IP_HEADER")
394407
self.retrieve_client_ip = asbool(os.getenv("DD_TRACE_CLIENT_IP_ENABLED", default=False))
395408

396-
# cleanup DD_TAGS, because values will be inserted back in the optimal way (via _dd.git.* tags)
397-
self.tags = gitmetadata.clean_tags(parse_tags_str(os.getenv("DD_TAGS") or ""))
398-
399409
self.propagation_http_baggage_enabled = asbool(
400410
os.getenv("DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED", default=False)
401411
)
@@ -714,6 +724,12 @@ def _handle_remoteconfig(self, data, test_tracer=None):
714724
if "tracing_sampling_rate" in lib_config:
715725
updated_items.append(("_trace_sample_rate", lib_config["tracing_sampling_rate"]))
716726

727+
if "tracing_tags" in lib_config:
728+
tags = lib_config["tracing_tags"]
729+
if tags:
730+
tags = {k: v for k, v in [t.split(":") for t in lib_config["tracing_tags"]]}
731+
updated_items.append(("tags", tags))
732+
717733
self._set_config_items([(k, v, "remote_config") for k, v in updated_items])
718734

719735
def enable_remote_configuration(self):

ddtrace/tracer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def __init__(
280280

281281
self._new_process = False
282282
config._subscribe(["_trace_sample_rate"], self._on_global_config_update)
283+
config._subscribe(["tags"], self._on_global_config_update)
283284

284285
def _atexit(self) -> None:
285286
key = "ctrl-break" if os.name == "nt" else "ctrl-c"
@@ -1068,3 +1069,5 @@ def _on_global_config_update(self, cfg, items):
10681069
sample_rate = None
10691070
sampler = DatadogSampler(default_sample_rate=sample_rate)
10701071
self._sampler = sampler
1072+
elif "tags" in items:
1073+
self._tags = cfg.tags.copy()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
tracer: Add support for remotely configuring trace tags.

tests/integration/test_settings.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def test_setting_origin_environment(test_agent_session, run_python_code_in_subpr
2121
"DD_TRACE_SAMPLE_RATE": "0.1",
2222
"DD_LOGS_INJECTION": "true",
2323
"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2",
24+
"DD_TAGS": "team:apm,component:web",
2425
}
2526
)
2627
out, err, status, _ = run_python_code_in_subprocess(
@@ -49,6 +50,11 @@ def test_setting_origin_environment(test_agent_session, run_python_code_in_subpr
4950
"value": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2",
5051
"origin": "env_var",
5152
}
53+
assert _get_latest_telemetry_config_item(events, "trace_tags") == {
54+
"name": "trace_tags",
55+
"value": "team:apm,component:web",
56+
"origin": "env_var",
57+
}
5258

5359

5460
@pytest.mark.skipif(AGENT_VERSION != "testagent", reason="Tests only compatible with a testagent")
@@ -59,6 +65,7 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess):
5965
"DD_TRACE_SAMPLE_RATE": "0.1",
6066
"DD_LOGS_INJECTION": "true",
6167
"DD_TRACE_HEADER_TAGS": "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2",
68+
"DD_TAGS": "team:apm,component:web",
6269
}
6370
)
6471
out, err, status, _ = run_python_code_in_subprocess(
@@ -67,6 +74,7 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess):
6774
config._trace_sample_rate = 0.2
6875
config.logs_injection = False
6976
config.trace_http_header_tags = {"header": "value"}
77+
config.tags = {"header": "value"}
7078
with tracer.trace("test") as span:
7179
pass
7280
""",
@@ -90,6 +98,11 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess):
9098
"value": "header:value",
9199
"origin": "code",
92100
}
101+
assert _get_latest_telemetry_config_item(events, "trace_tags") == {
102+
"name": "trace_tags",
103+
"value": "header:value",
104+
"origin": "code",
105+
}
93106

94107

95108
@pytest.mark.skipif(AGENT_VERSION != "testagent", reason="Tests only compatible with a testagent")

tests/internal/remoteconfig/test_remoteconfig_client_e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
def _expected_payload(
2020
rc_client,
21-
capabilities="EAA=",
21+
capabilities="kAA=",
2222
has_errors=False,
2323
targets_version=0,
2424
backend_client_state=None,

tests/internal/test_settings.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def _base_rc_config(cfg):
1616
"tracing_sampling_rate": None,
1717
"log_injection_enabled": None,
1818
"tracing_header_tags": None,
19+
"tracing_tags": None,
1920
"data_streams_enabled": None,
2021
}
2122
lib_config.update(cfg)
@@ -96,6 +97,24 @@ def _deleted_rc_config():
9697
"expected": {"trace_http_header_tags": {"header": "value"}},
9798
"expected_source": {"trace_http_header_tags": "code"},
9899
},
100+
{
101+
"env": {"DD_TAGS": "key:value,key2:value2"},
102+
"expected": {"tags": {"key": "value", "key2": "value2"}},
103+
"expected_source": {"tags": "env_var"},
104+
},
105+
{
106+
"env": {"DD_TAGS": "key:value,key2:value2"},
107+
"code": {"tags": {"k": "v", "k2": "v2"}},
108+
"expected": {"tags": {"k": "v", "k2": "v2"}},
109+
"expected_source": {"tags": "code"},
110+
},
111+
{
112+
"env": {"DD_TAGS": "key:value,key2:value2"},
113+
"code": {"tags": {"k": "v", "k2": "v2"}},
114+
"rc": {"tracing_tags": ["key1:val2", "key2:val3"]},
115+
"expected": {"tags": {"key1": "val2", "key2": "val3"}},
116+
"expected_source": {"tags": "remote_config"},
117+
},
99118
],
100119
)
101120
def test_settings(testcase, config, monkeypatch):
@@ -177,3 +196,31 @@ def test_remoteconfig_sampling_rate_user(run_python_code_in_subprocess):
177196
env=env,
178197
)
179198
assert status == 0, err.decode("utf-8")
199+
200+
201+
def test_remoteconfig_custom_tags(run_python_code_in_subprocess):
202+
env = os.environ.copy()
203+
env.update({"DD_TAGS": "team:apm"})
204+
out, err, status, _ = run_python_code_in_subprocess(
205+
"""
206+
from ddtrace import config, tracer
207+
from tests.internal.test_settings import _base_rc_config
208+
209+
with tracer.trace("test") as span:
210+
pass
211+
assert span.get_tag("team") == "apm"
212+
213+
config._handle_remoteconfig(_base_rc_config({"tracing_tags": ["team:onboarding"]}))
214+
215+
with tracer.trace("test") as span:
216+
pass
217+
assert span.get_tag("team") == "onboarding", span._meta
218+
219+
config._handle_remoteconfig(_base_rc_config({"tracing_tags": None}))
220+
with tracer.trace("test") as span:
221+
pass
222+
assert span.get_tag("team") == "apm"
223+
""",
224+
env=env,
225+
)
226+
assert status == 0, f"err={err.decode('utf-8')} out={out.decode('utf-8')}"

tests/telemetry/test_writer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time):
134134
{"name": "trace_sample_rate", "origin": "default", "value": "1.0"},
135135
{"name": "trace_header_tags", "origin": "default", "value": ""},
136136
{"name": "logs_injection_enabled", "origin": "default", "value": "false"},
137+
{"name": "trace_tags", "origin": "default", "value": ""},
137138
],
138139
key=lambda x: x["name"],
139140
),
@@ -193,6 +194,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
193194
env["DD_TRACE_WRITER_MAX_PAYLOAD_SIZE_BYTES"] = "9999"
194195
env["DD_TRACE_WRITER_INTERVAL_SECONDS"] = "30"
195196
env["DD_TRACE_WRITER_REUSE_CONNECTIONS"] = "True"
197+
env["DD_TAGS"] = "team:apm,component:web"
196198

197199
file = tmpdir.join("moon_ears.json")
198200
file.write('[{"service":"xy?","name":"a*c"}]')
@@ -266,6 +268,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
266268
{"name": "trace_sample_rate", "origin": "env_var", "value": "0.5"},
267269
{"name": "logs_injection_enabled", "origin": "env_var", "value": "true"},
268270
{"name": "trace_header_tags", "origin": "default", "value": ""},
271+
{"name": "trace_tags", "origin": "env_var", "value": "team:apm,component:web"},
269272
],
270273
key=lambda x: x["name"],
271274
)

0 commit comments

Comments
 (0)