Skip to content

Commit 7db021d

Browse files
authored
fix(internal): support URLs in DD_TAGS variable (backport #4183) (#4226)
This is an automatic backport of pull request #4183 done by [Mergify](https://mergify.com). --- <details> <summary>Mergify commands and options</summary> <br /> More conditions and actions can be found in the [documentation](https://docs.mergify.com/). You can also trigger Mergify actions by commenting on this pull request: - `@Mergifyio refresh` will re-evaluate the rules - `@Mergifyio rebase` will rebase this PR on its base branch - `@Mergifyio update` will merge the base branch into this PR - `@Mergifyio backport <destination>` will backport this PR on `<destination>` branch Additionally, on Mergify [dashboard](https://dashboard.mergify.com/) you can: - look at your merge queues - generate the Mergify configuration with the config editor. Finally, you can contact us on https://mergify.com </details>
1 parent 2e3165c commit 7db021d

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
@@ -53,89 +53,44 @@ def test_asbool(self):
5353
("key: val", dict(key=" val"), None),
5454
("key key: val", {"key key": " val"}, None),
5555
("key: val,key2:val2", dict(key=" val", key2="val2"), None),
56-
(" key: val,key2:val2", {" key": " val", "key2": "val2"}, None),
56+
(" key: val,key2:val2", {"key": " val", "key2": "val2"}, None),
5757
("key key2:val1", {"key key2": "val1"}, None),
58-
(
59-
"key:val key2:val:2",
60-
dict(),
61-
[mock.call(_LOG_ERROR_MALFORMED_TAG_STRING, "key:val key2:val:2")],
62-
),
58+
("key:val key2:val:2", {"key": "val", "key2": "val:2"}, None),
6359
(
6460
"key:val,key2:val2 key3:1234.23",
6561
dict(),
6662
[mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key:val,key2:val2 key3:1234.23")],
6763
),
68-
(
69-
"key:val key2:val2 key3: ",
70-
dict(key="val", key2="val2"),
71-
[
72-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key3:", "key:val key2:val2 key3: "),
73-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", "key:val key2:val2 key3: "),
74-
],
75-
),
64+
("key:val key2:val2 key3: ", dict(key="val", key2="val2", key3=""), None),
7665
(
7766
"key:val key2:val 2",
7867
dict(key="val", key2="val"),
7968
[mock.call(_LOG_ERROR_MALFORMED_TAG, "2", "key:val key2:val 2")],
8069
),
8170
(
8271
"key: val key2:val2 key3:val3",
83-
{"key2": "val2", "key3": "val3"},
84-
[
85-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key: val key2:val2 key3:val3"),
86-
mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3"),
87-
],
88-
),
89-
(
90-
"key:,key3:val1,",
91-
dict(key3="val1"),
92-
[
93-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key:", "key:,key3:val1,"),
94-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", "key:,key3:val1,"),
95-
],
96-
),
97-
(
98-
",",
99-
dict(),
100-
[
101-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", ","),
102-
mock.call(_LOG_ERROR_MALFORMED_TAG, "", ","),
103-
],
104-
),
105-
(
106-
":,:",
107-
dict(),
108-
[
109-
mock.call(_LOG_ERROR_MALFORMED_TAG, ":", ":,:"),
110-
mock.call(_LOG_ERROR_MALFORMED_TAG, ":", ":,:"),
111-
],
112-
),
113-
(
114-
"key,key2:val1",
115-
dict(key2="val1"),
116-
[mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")],
117-
),
118-
("key2:val1:", dict(), [mock.call(_LOG_ERROR_MALFORMED_TAG_STRING, "key2:val1:")]),
119-
(
120-
"key,key2,key3",
121-
dict(),
122-
[
123-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2,key3"),
124-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key2", "key,key2,key3"),
125-
mock.call(_LOG_ERROR_MALFORMED_TAG, "key3", "key,key2,key3"),
126-
],
72+
{"key": "", "key2": "val2", "key3": "val3"},
73+
[mock.call(_LOG_ERROR_MALFORMED_TAG, "val", "key: val key2:val2 key3:val3")],
12774
),
75+
("key:,key3:val1,", dict(key3="val1", key=""), None),
76+
(",", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "")]),
77+
(":,:", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, ":,:")]),
78+
("key,key2:val1", {"key2": "val1"}, [mock.call(_LOG_ERROR_MALFORMED_TAG, "key", "key,key2:val1")]),
79+
("key2:val1:", {"key2": "val1:"}, None),
80+
("key,key2,key3", dict(), [mock.call(_LOG_ERROR_FAIL_SEPARATOR, "key,key2,key3")]),
81+
("foo:bar,foo:baz", dict(foo="baz"), None),
82+
("hash:asd url:https://github.com/foo/bar", dict(hash="asd", url="https://github.com/foo/bar"), None),
12883
],
12984
)
13085
def test_parse_env_tags(tag_str, expected_tags, error_calls):
13186
with mock.patch("ddtrace.internal.utils.formats.log") as log:
13287
tags = parse_tags_str(tag_str)
13388
assert tags == expected_tags
13489
if error_calls:
135-
assert log.error.call_count == len(error_calls)
90+
assert log.error.call_count == len(error_calls), log.error.call_args_list
13691
log.error.assert_has_calls(error_calls)
13792
else:
138-
assert log.error.call_count == 0
93+
assert log.error.call_count == 0, log.error.call_args_list
13994

14095

14196
def test_no_states():

0 commit comments

Comments
 (0)