Skip to content

Commit 6ef35fb

Browse files
fix(internal): support URLs in DD_TAGS variable (#4183) (#4228)
This change allows for URLs to be passed as values to tag labels via the DD_TAGS environment variable. NOTE This might constitute a slight breaking change, as configuration that was valid before might no longer work with the new logic. (cherry picked from commit e315198) Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent 083962f commit 6ef35fb

File tree

3 files changed

+61
-94
lines changed

3 files changed

+61
-94
lines changed

ddtrace/internal/utils/formats.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import logging
2-
import re
32
from typing import Any
43
from typing import Dict
54
from typing import List
65
from typing import Optional
76
from typing import Text
7+
from typing import Tuple
88
from typing import TypeVar
99
from typing import Union
1010

@@ -22,9 +22,6 @@
2222

2323
T = TypeVar("T")
2424

25-
# Tags `key:value` must be separated by either comma or space
26-
_TAGS_NOT_SEPARATED = re.compile(r":[^,\s]+:")
27-
2825
log = logging.getLogger(__name__)
2926

3027

@@ -79,21 +76,43 @@ def parse_tags_str(tags_str):
7976
:param tags_str: A string of the above form to parse tags from.
8077
:return: A dict containing the tags that were parsed.
8178
"""
82-
parsed_tags = {} # type: Dict[str, str]
8379
if not tags_str:
84-
return parsed_tags
80+
return {}
8581

86-
if _TAGS_NOT_SEPARATED.search(tags_str):
87-
log.error("Malformed tag string with tags not separated by comma or space '%s'.", tags_str)
88-
return parsed_tags
82+
TAGSEP = ", "
8983

90-
# Identify separator based on which successfully identifies the correct
91-
# number of valid tags
92-
numtagseps = tags_str.count(":")
93-
for sep in [",", " "]:
94-
if sum(":" in _ for _ in tags_str.split(sep)) == numtagseps:
95-
break
96-
else:
84+
def parse_tags(tags):
85+
# type: (List[str]) -> Tuple[List[Tuple[str, str]], List[str]]
86+
parsed_tags = []
87+
invalids = []
88+
89+
for tag in tags:
90+
key, sep, value = tag.partition(":")
91+
if not sep or not key or "," in key:
92+
invalids.append(tag)
93+
else:
94+
parsed_tags.append((key, value))
95+
96+
return parsed_tags, invalids
97+
98+
tags_str = tags_str.strip(TAGSEP)
99+
100+
# Take the maximal set of tags that can be parsed correctly for a given separator
101+
tag_list = [] # type: List[Tuple[str, str]]
102+
invalids = []
103+
for sep in TAGSEP:
104+
ts = tags_str.split(sep)
105+
tags, invs = parse_tags(ts)
106+
if len(tags) > len(tag_list):
107+
tag_list = tags
108+
invalids = invs
109+
elif len(tags) == len(tag_list) > 1:
110+
# Both separators produce the same number of tags.
111+
# DEV: This only works when checking between two separators.
112+
tag_list[:] = []
113+
invalids[:] = []
114+
115+
if not tag_list:
97116
log.error(
98117
(
99118
"Failed to find separator for tag string: '%s'.\n"
@@ -103,25 +122,11 @@ def parse_tags_str(tags_str):
103122
),
104123
tags_str,
105124
)
106-
return parsed_tags
107125

108-
for tag in tags_str.split(sep):
109-
try:
110-
key, value = tag.split(":", 1)
111-
112-
# Validate the tag
113-
if key == "" or value == "" or value.endswith(":"):
114-
raise ValueError
115-
except ValueError:
116-
log.error(
117-
"Malformed tag in tag pair '%s' from tag string '%s'.",
118-
tag,
119-
tags_str,
120-
)
121-
else:
122-
parsed_tags[key] = value
123-
124-
return parsed_tags
126+
for tag in invalids:
127+
log.error("Malformed tag in tag pair '%s' from tag string '%s'.", tag, tags_str)
128+
129+
return dict(tag_list)
125130

126131

127132
def stringify_cache_args(args):
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
fixes:
3+
- |
4+
Fix parsing of the ``DD_TAGS`` environment variable value to include
5+
support for values with colons (e.g. URLs). Also fixed the parsing of
6+
invalid tags that begin with a space (e.g. ``DD_TAGS=" key:val"`` will now
7+
produce a tag with label ``key``, instead of `` key``, and value ``val``).

tests/tracer/test_utils.py

Lines changed: 15 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -52,89 +52,44 @@ def test_asbool(self):
5252
("key: val", dict(key=" val"), None),
5353
("key key: val", {"key key": " val"}, None),
5454
("key: val,key2:val2", dict(key=" val", key2="val2"), None),
55-
(" key: val,key2:val2", {" key": " val", "key2": "val2"}, None),
55+
(" key: val,key2:val2", {"key": " val", "key2": "val2"}, None),
5656
("key key2:val1", {"key key2": "val1"}, None),
57-
(
58-
"key:val key2:val:2",
59-
dict(),
60-
[mock.call(_LOG_ERROR_MALFORMED_TAG_STRING, "key:val key2:val:2")],
61-
),
57+
("key:val key2:val:2", {"key": "val", "key2": "val:2"}, None),
6258
(
6359
"key:val,key2:val2 key3:1234.23",
6460
dict(),
6561
[mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key:val,key2:val2 key3:1234.23")],
6662
),
67-
(
68-
"key:val key2:val2 key3: ",
69-
dict(key="val", key2="val2"),
70-
[
71-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key3:", "key:val key2:val2 key3: "),
72-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", "key:val key2:val2 key3: "),
73-
],
74-
),
63+
("key:val key2:val2 key3: ", dict(key="val", key2="val2", key3=""), None),
7564
(
7665
"key:val key2:val 2",
7766
dict(key="val", key2="val"),
7867
[mock.call(_LOG_ERROR_MALFORMED_TAG, "2", "key:val key2:val 2")],
7968
),
8069
(
8170
"key: val key2:val2 key3:val3",
82-
{"key2": "val2", "key3": "val3"},
83-
[
84-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key: val key2:val2 key3:val3"),
85-
mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3"),
86-
],
87-
),
88-
(
89-
"key:,key3:val1,",
90-
dict(key3="val1"),
91-
[
92-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key:,key3:val1,"),
93-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", "key:,key3:val1,"),
94-
],
95-
),
96-
(
97-
",",
98-
dict(),
99-
[
100-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", ","),
101-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", ","),
102-
],
103-
),
104-
(
105-
":,:",
106-
dict(),
107-
[
108-
mock.call(_LOG_ERROR_MALFORMED_TAG, ":", ":,:"),
109-
mock.call(_LOG_ERROR_MALFORMED_TAG, ":", ":,:"),
110-
],
111-
),
112-
(
113-
"key,key2:val1",
114-
dict(key2="val1"),
115-
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")],
116-
),
117-
("key2:val1:", dict(), [mock.call(_LOG_ERROR_MALFORMED_TAG_STRING, "key2:val1:")]),
118-
(
119-
"key,key2,key3",
120-
dict(),
121-
[
122-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2,key3"),
123-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key2", "key,key2,key3"),
124-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key3", "key,key2,key3"),
125-
],
71+
{"key": "", "key2": "val2", "key3": "val3"},
72+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3")],
12673
),
74+
("key:,key3:val1,", dict(key3="val1", key=""), None),
75+
(",", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "")]),
76+
(":,:", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, ":,:")]),
77+
("key,key2:val1", {"key2": "val1"}, [mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")]),
78+
("key2:val1:", {"key2": "val1:"}, None),
79+
("key,key2,key3", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key,key2,key3")]),
80+
("foo:bar,foo:baz", dict(foo="baz"), None),
81+
("hash:asd url:https://github.com/foo/bar", dict(hash="asd", url="https://github.com/foo/bar"), None),
12782
],
12883
)
12984
def test_parse_env_tags(tag_str, expected_tags, error_calls):
13085
with mock.patch("ddtrace.internal.utils.formats.log") as log:
13186
tags = parse_tags_str(tag_str)
13287
assert tags == expected_tags
13388
if error_calls:
134-
assert log.error.call_count == len(error_calls)
89+
assert log.error.call_count == len(error_calls), log.error.call_args_list
13590
log.error.assert_has_calls(error_calls)
13691
else:
137-
assert log.error.call_count == 0
92+
assert log.error.call_count == 0, log.error.call_args_list
13893

13994

14095
def test_no_states():

0 commit comments

Comments
 (0)