Skip to content

Commit 2ea1f8f

Browse files
authored
fix: split DD_TAGS either by space or comma (#105)
1 parent 36b651c commit 2ea1f8f

File tree

7 files changed

+213
-28
lines changed

7 files changed

+213
-28
lines changed

src/datadog/curl.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ class CurlImpl {
225225
void *user_data);
226226
static bool is_non_whitespace(unsigned char);
227227
static char to_lower(unsigned char);
228-
static StringView trim(StringView);
229228

230229
public:
231230
explicit CurlImpl(const std::shared_ptr<Logger> &, const Clock &,

src/datadog/parse_util.cpp

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <string>
1010

1111
#include "error.h"
12+
#include "string_util.h"
1213

1314
namespace datadog {
1415
namespace tracing {
@@ -154,28 +155,82 @@ Expected<std::unordered_map<std::string, std::string>> parse_tags(
154155
std::vector<StringView> list) {
155156
std::unordered_map<std::string, std::string> tags;
156157

158+
std::string key;
159+
std::string value;
160+
157161
for (const StringView &token : list) {
158162
const auto separator = std::find(token.begin(), token.end(), ':');
163+
159164
if (separator == token.end()) {
160-
std::string message;
161-
message += "Unable to parse a key/value from the tag text \"";
162-
append(message, token);
163-
// message +=
164-
// "\" because it does not contain the separator character \":\". "
165-
// "Error occurred in list of tags \"";
166-
// append(message, input);
167-
message += ".";
168-
return Error{Error::TAG_MISSING_SEPARATOR, std::move(message)};
165+
key = std::string{trim(token)};
166+
} else {
167+
key = std::string{trim(range(token.begin(), separator))};
168+
if (key.empty()) {
169+
continue;
170+
}
171+
value = std::string{trim(range(separator + 1, token.end()))};
169172
}
170173

171-
std::string key{token.begin(), separator};
172-
std::string value{separator + 1, token.end()};
173174
// If there are duplicate values, then the last one wins.
174175
tags.insert_or_assign(std::move(key), std::move(value));
175176
}
176177

177178
return tags;
178179
}
179180

181+
// This function scans the input string to identify a separator (',' or ' ').
182+
// Then, split tags using the identified separator and call `parse_tags` with
183+
// the resulting unordered_map.
184+
//
185+
// Why?
186+
// RFC: DD_TAGS - support space separation
187+
// The trace agent parses DD_TAGS as a space separated list of tags. The
188+
// tracers parse this as a comma-separated list. We need to have the tracers
189+
// parse DD_TAGS as a space-separated list when possible so that the agent and
190+
// tracers can use the same DD_TAGS strings while maintaining backwards
191+
// compatibility with comma-separated lists.
192+
Expected<std::unordered_map<std::string, std::string>> parse_tags(
193+
StringView input) {
194+
std::vector<StringView> tags;
195+
196+
size_t beg = 0;
197+
const size_t end = input.size();
198+
199+
char separator = 0;
200+
size_t i = beg;
201+
202+
for (; i < end; ++i) {
203+
if (input[i] == ',') {
204+
separator = ',';
205+
break;
206+
} else if (input[i] == ' ') {
207+
separator = ' ';
208+
break;
209+
}
210+
}
211+
212+
if (separator == 0) {
213+
goto capture_all;
214+
}
215+
216+
for (; i < end; ++i) {
217+
if (input[i] == separator) {
218+
auto tag = input.substr(beg, i - beg);
219+
if (tag.size() > 0) {
220+
tags.emplace_back(tag);
221+
}
222+
beg = i + 1;
223+
}
224+
}
225+
226+
capture_all:
227+
auto tag = input.substr(beg, i - beg);
228+
if (tag.size() > 0) {
229+
tags.emplace_back(tag);
230+
}
231+
232+
return parse_tags(tags);
233+
}
234+
180235
} // namespace tracing
181236
} // namespace datadog

src/datadog/parse_util.h

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,8 @@ std::vector<StringView> parse_list(StringView input);
4949
Expected<std::unordered_map<std::string, std::string>> parse_tags(
5050
std::vector<StringView> list);
5151

52-
inline Expected<std::unordered_map<std::string, std::string>> parse_tags(
53-
StringView input) {
54-
// Within a tag, the key and value are separated by a colon (":").
55-
return parse_tags(parse_list(input));
56-
}
52+
Expected<std::unordered_map<std::string, std::string>> parse_tags(
53+
StringView input);
5754

5855
} // namespace tracing
5956
} // namespace datadog

src/datadog/string_util.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,12 @@ std::string join_tags(
7070
});
7171
}
7272

73+
StringView trim(StringView str) {
74+
str.remove_prefix(std::min(str.find_first_not_of(' '), str.size()));
75+
const auto pos = str.find_last_not_of(' ');
76+
if (pos != str.npos) str.remove_suffix(str.size() - pos - 1);
77+
return str;
78+
}
79+
7380
} // namespace tracing
7481
} // namespace datadog

src/datadog/string_util.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <unordered_map>
55

66
#include "propagation_style.h"
7+
#include "string_view.h"
78

89
namespace datadog {
910
namespace tracing {
@@ -25,5 +26,7 @@ std::string join_propagation_styles(const std::vector<PropagationStyle>&);
2526
std::string join_tags(
2627
const std::unordered_map<std::string, std::string>& values);
2728

29+
StringView trim(StringView);
30+
2831
} // namespace tracing
2932
} // namespace datadog

test/test_parse_util.cpp

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
using namespace datadog::tracing;
1313

14-
TEST_CASE("parse_int") {
14+
#define PARSE_UTIL_TEST(x) TEST_CASE(x, "[parse_util]")
15+
16+
PARSE_UTIL_TEST("parse_int") {
1517
struct TestCase {
1618
int line;
1719
std::string name;
@@ -74,7 +76,7 @@ TEST_CASE("parse_int") {
7476

7577
// This test case is similar to the one above, except that negative numbers are
7678
// not supported, and the underflow and overflow values are different.
77-
TEST_CASE("parse_uint64") {
79+
PARSE_UTIL_TEST("parse_uint64") {
7880
struct TestCase {
7981
int line;
8082
std::string name;
@@ -133,3 +135,101 @@ TEST_CASE("parse_uint64") {
133135
REQUIRE(result.error().code == expected);
134136
}
135137
}
138+
139+
PARSE_UTIL_TEST("parse_tags") {
140+
struct TestCase {
141+
int line;
142+
StringView name;
143+
StringView input;
144+
std::unordered_map<std::string, std::string> expected;
145+
};
146+
147+
auto test_case = GENERATE(values<TestCase>({
148+
{__LINE__,
149+
"space separated tags",
150+
"env:test aKey:aVal bKey:bVal cKey:",
151+
{
152+
{"env", "test"},
153+
{"aKey", "aVal"},
154+
{"bKey", "bVal"},
155+
{"cKey", ""},
156+
}},
157+
{__LINE__,
158+
"comma separated tags",
159+
"env:test aKey:aVal bKey:bVal cKey:",
160+
{
161+
{"env", "test"},
162+
{"aKey", "aVal"},
163+
{"bKey", "bVal"},
164+
{"cKey", ""},
165+
}},
166+
{__LINE__,
167+
"mixed separator but comma first",
168+
"env:test,aKey:aVal bKey:bVal cKey:",
169+
{
170+
{"env", "test"},
171+
{"aKey", "aVal bKey:bVal cKey:"},
172+
}},
173+
{__LINE__,
174+
"mixed separator but space first",
175+
"env:test bKey :bVal dKey: dVal cKey:",
176+
{
177+
{"env", "test"},
178+
{"bKey", ""},
179+
{"dKey", ""},
180+
{"dVal", ""},
181+
{"cKey", ""},
182+
}},
183+
{__LINE__,
184+
"mixed separator but space first",
185+
"env:keyWithA:Semicolon bKey:bVal cKey",
186+
{
187+
{"env", "keyWithA:Semicolon"},
188+
{"bKey", "bVal"},
189+
{"cKey", ""},
190+
}},
191+
// {__LINE__,
192+
// "mixed separator edge case",
193+
// "env:keyWith: , , Lots:Of:Semicolons ",
194+
// {
195+
// {"env", "keyWith:"},
196+
// {"Lots", "Of:Semicolons"},
197+
// }},
198+
{__LINE__,
199+
"comma separated but some tags without value",
200+
"a:b,c,d",
201+
{
202+
{"a", "b"},
203+
{"c", ""},
204+
{"d", ""},
205+
}},
206+
{__LINE__,
207+
"one separator without value",
208+
"a,1",
209+
{
210+
{"a", ""},
211+
{"1", ""},
212+
}},
213+
{__LINE__,
214+
"no separator",
215+
"a:b:c:d",
216+
{
217+
{"a", "b:c:d"},
218+
}},
219+
{__LINE__,
220+
"input is trimed",
221+
"key1:val1, key2 : val2 ",
222+
{
223+
{"key1", "val1"},
224+
{"key2", "val2"},
225+
}},
226+
}));
227+
228+
CAPTURE(test_case.line);
229+
CAPTURE(test_case.name);
230+
CAPTURE(test_case.input);
231+
232+
const auto tags = parse_tags(test_case.input);
233+
REQUIRE(tags);
234+
CHECK(*tags == test_case.expected);
235+
}

test/test_tracer_config.cpp

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,21 +213,45 @@ TEST_CASE("TracerConfig::defaults") {
213213

214214
auto test_case = GENERATE(values<TestCase>({
215215
{"empty", "", {}, nullopt},
216-
{"missing colon", "foo", {}, Error::TAG_MISSING_SEPARATOR},
216+
{"missing colon",
217+
"foo",
218+
{
219+
{"foo", ""},
220+
},
221+
nullopt},
217222
{"trailing comma",
218223
"foo:bar, baz:123,",
219-
{},
220-
Error::TAG_MISSING_SEPARATOR},
221-
{"overwrite value", "foo:baz", {{"foo", "baz"}}, nullopt},
224+
{
225+
{"foo", "bar"},
226+
{"baz", "123"},
227+
},
228+
nullopt},
229+
{"overwrite value",
230+
"foo:baz",
231+
{
232+
{"foo", "baz"},
233+
},
234+
nullopt},
222235
{"additional values",
223236
"baz:123, bam:three",
224-
{{"baz", "123"}, {"bam", "three"}},
237+
{
238+
{"baz", "123"},
239+
{"bam", "three"},
240+
},
225241
nullopt},
226242
{"commas optional",
227243
"baz:123 bam:three",
228-
{{"baz", "123"}, {"bam", "three"}},
244+
{
245+
{"baz", "123"},
246+
{"bam", "three"},
247+
},
248+
nullopt},
249+
{"last one wins",
250+
"baz:123 baz:three",
251+
{
252+
{"baz", "three"},
253+
},
229254
nullopt},
230-
{"last one wins", "baz:123 baz:three", {{"baz", "three"}}, nullopt},
231255
}));
232256

233257
// This will be overriden by the DD_TAGS environment variable.
@@ -1053,8 +1077,8 @@ TEST_CASE("TracerConfig::span_sampler") {
10531077
REQUIRE(file.is_open());
10541078
// We could do any of the failures tested in the "must be valid"
10551079
// section, since it's the same parser. Instead, just to cover the
1056-
// code path specific to DD_SPAN_SAMPLING_RULES_FILE, pick any error,
1057-
// e.g. invalid JSON.
1080+
// code path specific to DD_SPAN_SAMPLING_RULES_FILE, pick any
1081+
// error, e.g. invalid JSON.
10581082
file << "this is clearly not JSON";
10591083
file.close();
10601084
const EnvGuard guard{"DD_SPAN_SAMPLING_RULES_FILE",

0 commit comments

Comments
 (0)