Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/datadog/parse_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace {
template <typename Integer>
Expected<Integer> parse_integer(StringView input, int base, StringView kind) {
Integer value;
input = strip(input);
const auto status = std::from_chars(input.begin(), input.end(), value, base);
if (status.ec == std::errc::invalid_argument) {
std::string message;
Expand Down
50 changes: 34 additions & 16 deletions src/datadog/tracer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ namespace datadog {
namespace tracing {
namespace {

// Parse the high 64 bits of a trace ID from the specified `value`. If `value`
// is correctly formatted, then return the resulting bits. If `value` is
// incorrectly formatted then return `nullopt`.
Optional<std::uint64_t> parse_trace_id_high(const std::string& value) {
if (value.size() != 16) {
return nullopt;
}

auto high = parse_uint64(value, 16);
if (high) {
return *high;
}

return nullopt;
}

// Decode the specified `trace_tags` and integrate them into the specified
// `result`. If an error occurs, add a `tags::internal::propagation_error` tag
// to the specified `span_tags` and log a diagnostic using the specified
Expand All @@ -48,12 +64,8 @@ void handle_trace_tags(StringView trace_tags, ExtractedData& result,

if (key == tags::internal::trace_id_high) {
// _dd.p.tid contains the high 64 bits of the trace ID.
auto high = parse_uint64(value, 16);
if (auto* error = high.if_error()) {
logger.log_error(
error->with_prefix("Unable to parse high bits of the trace ID in "
"Datadog style from the "
"\"_dd.p.tid\" trace tag: "));
const Optional<std::uint64_t> high = parse_trace_id_high(value);
if (!high) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider tagging things in this block instead. It's the point where the error is being handled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could remove the span_tags argument from parse_trace_id_high, and duplicate the setting of "malformed_tid " ... in the two places where it is used. Yeah, I like that better.

span_tags[tags::internal::propagation_error] = "malformed_tid " + value;
continue;
}
Expand Down Expand Up @@ -274,7 +286,7 @@ Span Tracer::create_span(const SpanConfig& config) {
span_data->trace_id = generator_->trace_id(span_data->start);
if (span_data->trace_id.high) {
trace_tags.emplace_back(tags::internal::trace_id_high,
hex(span_data->trace_id.high));
hex_padded(span_data->trace_id.high));
}
span_data->span_id = span_data->trace_id.low;
span_data->parent_id = 0;
Expand Down Expand Up @@ -401,21 +413,27 @@ Expected<Span> Tracer::extract_span(const DictReader& reader,
//
// First, though, if the `trace_id_high` tag is already set and has a bogus
// value or a value inconsistent with the trace ID, tag an error.
const std::string hex_high = hex(span_data->trace_id.high);
const auto hex_high = hex_padded(span_data->trace_id.high);
const auto extant = std::find_if(
trace_tags.begin(), trace_tags.end(), [&](const auto& pair) {
return pair.first == tags::internal::trace_id_high;
});
if (extant == trace_tags.end()) {
trace_tags.emplace_back(tags::internal::trace_id_high, hex_high);
} else if (!parse_uint64(extant->second, 16)) {
span_data->tags[tags::internal::propagation_error] =
"malformed_tid " + extant->second;
extant->second = hex_high;
} else if (extant->second != hex_high) {
span_data->tags[tags::internal::propagation_error] =
"inconsistent_tid " + extant->second;
extant->second = hex_high;
} else {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment here, supporting the comment above, describing what's going on. For example, the appearance of extant->second = hex_high; twice might be surprising.

// There is already a `trace_id_high` tag. `hex_high` is its proper value.
// Check if the extant value is malformed or different from `hex_high`. In
// either case, tag an error and overwrite the tag with `hex_high`.
const Optional<std::uint64_t> high = parse_trace_id_high(extant->second);
if (!high) {
span_data->tags[tags::internal::propagation_error] =
"malformed_tid " + extant->second;
extant->second = hex_high;
} else if (*high != span_data->trace_id.high) {
span_data->tags[tags::internal::propagation_error] =
"inconsistent_tid " + extant->second;
extant->second = hex_high;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ add_executable(tests
test_glob.cpp
test_limiter.cpp
test_msgpack.cpp
test_parse_util.cpp
test_smoke.cpp
test_span.cpp
test_span_sampler.cpp
Expand Down
135 changes: 135 additions & 0 deletions test/test_parse_util.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#include <datadog/error.h>
#include <datadog/parse_util.h>

#include <cassert>
#include <cstdint>
#include <limits>
#include <string>
#include <variant>

#include "test.h"

using namespace datadog::tracing;

TEST_CASE("parse_int") {
struct TestCase {
int line;
std::string name;
std::string argument;
int base;
std::variant<int, Error::Code> expected;
};

// clang-format off
auto test_case = GENERATE(values<TestCase>({
{__LINE__, "zero (dec)", "0", 10, 0},
{__LINE__, "zeros (dec)", "000", 10, 0},
{__LINE__, "zero (hex)", "0", 16, 0},
{__LINE__, "zeros (hex)", "000", 16, 0},
{__LINE__, "leading whitespace (dec 1)", " 42", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (dec 2)", "\t42", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (dec 3)", "\n42", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 1)", "42 ", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 2)", "42\t", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 3)", "42\n", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 1)", " 42", 16, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 2)", "\t42", 16, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 3)", "\n42", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 1)", "42 ", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 2)", "42\t", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 3)", "42\n", 16, Error::INVALID_INTEGER},
{__LINE__, "no hex prefix", "0xbeef", 16, Error::INVALID_INTEGER},
{__LINE__, "dec rejects hex", "42beef", 10, Error::INVALID_INTEGER},
{__LINE__, "hex accepts hex", "42beef", 16, 0x42beef},
{__LINE__, "no trailing nonsense (dec)", "42xyz", 10, Error::INVALID_INTEGER},
{__LINE__, "no trailing nonsense (hex)", "42xyz", 16, Error::INVALID_INTEGER},
{__LINE__, "no leading nonsense (dec)", "xyz42", 10, Error::INVALID_INTEGER},
{__LINE__, "no leading nonsense (hex)", "xyz42", 16, Error::INVALID_INTEGER},
{__LINE__, "overflow", std::to_string(std::numeric_limits<int>::max()) + "0", 10, Error::OUT_OF_RANGE_INTEGER},
{__LINE__, "negative (dec)", "-10", 10, -10},
{__LINE__, "negative (hex)", "-a", 16, -10},
{__LINE__, "lower case", "a", 16, 10},
{__LINE__, "upper case", "A", 16, 10},
{__LINE__, "underflow", std::to_string(std::numeric_limits<int>::min()) + "0", 10, Error::OUT_OF_RANGE_INTEGER},
}));
// clang-format on

CAPTURE(test_case.line);
CAPTURE(test_case.name);
CAPTURE(test_case.argument);
CAPTURE(test_case.base);

const auto result = parse_int(test_case.argument, test_case.base);
if (std::holds_alternative<int>(test_case.expected)) {
const int& expected = std::get<int>(test_case.expected);
REQUIRE(result);
REQUIRE(*result == expected);
} else {
assert(std::holds_alternative<Error::Code>(test_case.expected));
const Error::Code& expected = std::get<Error::Code>(test_case.expected);
REQUIRE(!result);
REQUIRE(result.error().code == expected);
}
}

// This test case is similar to the one above, except that negative numbers are
// not supported, and the underflow and overflow values are different.
TEST_CASE("parse_uint64") {
struct TestCase {
int line;
std::string name;
std::string argument;
int base;
std::variant<std::uint64_t, Error::Code> expected;
};

// clang-format off
auto test_case = GENERATE(values<TestCase>({
{__LINE__, "zero (dec)", "0", 10, UINT64_C(0)},
{__LINE__, "zeros (dec)", "000", 10, UINT64_C(0)},
{__LINE__, "zero (hex)", "0", 16, UINT64_C(0)},
{__LINE__, "zeros (hex)", "000", 16, UINT64_C(0)},
{__LINE__, "leading whitespace (dec 1)", " 42", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (dec 2)", "\t42", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (dec 3)", "\n42", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 1)", "42 ", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 2)", "42\t", 10, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (dec 3)", "42\n", 10, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 1)", " 42", 16, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 2)", "\t42", 16, Error::INVALID_INTEGER},
{__LINE__, "leading whitespace (hex 3)", "\n42", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 1)", "42 ", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 2)", "42\t", 16, Error::INVALID_INTEGER},
{__LINE__, "trailing whitespace (hex 3)", "42\n", 16, Error::INVALID_INTEGER},
{__LINE__, "no hex prefix", "0xbeef", 16, Error::INVALID_INTEGER},
{__LINE__, "dec rejects hex", "42beef", 10, Error::INVALID_INTEGER},
{__LINE__, "hex accepts hex", "42beef", 16, UINT64_C(0x42beef)},
{__LINE__, "no trailing nonsense (dec)", "42xyz", 10, Error::INVALID_INTEGER},
{__LINE__, "no trailing nonsense (hex)", "42xyz", 16, Error::INVALID_INTEGER},
{__LINE__, "no leading nonsense (dec)", "xyz42", 10, Error::INVALID_INTEGER},
{__LINE__, "no leading nonsense (hex)", "xyz42", 16, Error::INVALID_INTEGER},
{__LINE__, "overflow", std::to_string(std::numeric_limits<std::uint64_t>::max()) + "0", 10, Error::OUT_OF_RANGE_INTEGER},
{__LINE__, "negative (dec)", "-10", 10, Error::INVALID_INTEGER},
{__LINE__, "negative (hex)", "-a", 16, Error::INVALID_INTEGER},
{__LINE__, "lower case", "a", 16, UINT64_C(10)},
{__LINE__, "upper case", "A", 16, UINT64_C(10)},
}));
// clang-format on

CAPTURE(test_case.line);
CAPTURE(test_case.name);
CAPTURE(test_case.argument);
CAPTURE(test_case.base);

const auto result = parse_uint64(test_case.argument, test_case.base);
if (std::holds_alternative<std::uint64_t>(test_case.expected)) {
const std::uint64_t& expected = std::get<std::uint64_t>(test_case.expected);
REQUIRE(result);
REQUIRE(*result == expected);
} else {
assert(std::holds_alternative<Error::Code>(test_case.expected));
const Error::Code& expected = std::get<Error::Code>(test_case.expected);
REQUIRE(!result);
REQUIRE(result.error().code == expected);
}
}
25 changes: 23 additions & 2 deletions test/test_tracer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1042,11 +1042,30 @@ TEST_CASE("128-bit trace IDs") {
trace_id = span->trace_id();
}

SECTION("are, for W3C, extracted preferentially from traceparent") {
auto tid = GENERATE(values<std::string>({"decade", "deadbeefdeadbeed"}));
std::unordered_map<std::string, std::string> headers;
headers["traceparent"] =
"00-deadbeefdeadbeefcafebabecafebabe-0000000000000001-01";
// The _dd.p.tid value below is either malformed or inconsistent with the
// trace ID in the traceparent.
// It will be ignored, and the resulting _dd.p.tid value will be consistent
// with the higher part of the trace ID in traceparent: "deadbeefdeadbeef".
headers["tracestate"] = "dd=t.tid:" + tid;
MockDictReader reader{headers};
const auto span = tracer.extract_span(reader);
CAPTURE(logger->entries);
REQUIRE(logger->error_count() == 0);
REQUIRE(span);
REQUIRE(hex(span->trace_id().high) == "deadbeefdeadbeef");
trace_id = span->trace_id();
}

SECTION("are extracted from Datadog (_dd.p.tid)") {
std::unordered_map<std::string, std::string> headers;
headers["x-datadog-trace-id"] = "4";
headers["x-datadog-parent-id"] = "42";
headers["x-datadog-tags"] = "_dd.p.tid=beef";
headers["x-datadog-tags"] = "_dd.p.tid=000000000000beef";
MockDictReader reader{headers};
const auto span = tracer.extract_span(reader);
CAPTURE(logger->entries);
Expand Down Expand Up @@ -1094,7 +1113,9 @@ TEST_CASE(

auto test_case = GENERATE(values<TestCase>(
{{__LINE__, "invalid _dd.p.tid", "noodle", "malformed_tid "},
{__LINE__, "_dd.p.tid inconsistent with trace ID", "adfeed",
{__LINE__, "short _dd.p.tid", "beef", "malformed_tid "},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably test with values of length 0, 1, 15, and 17.
4 and 22 seems a bit arbitrary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is arbitrary. For the long tests I held down 0 until it looked longer than 16. For the short test I omitted any 0s.

1, 15, and 17, to be just below and above 16? Yeah, I see your point. I'll leave this, though.

{__LINE__, "long _dd.p.tid", "000000000000000000beef", "malformed_tid "},
{__LINE__, "_dd.p.tid inconsistent with trace ID", "0000000000adfeed",
"inconsistent_tid "}}));

CAPTURE(test_case.line);
Expand Down