diff --git a/.circleci/config.yml b/.circleci/config.yml index 82dd2a21..b06f7953 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,7 @@ jobs: resource_class: small steps: - checkout - - run: find bin/ -executable -type f | xargs shellcheck + - run: find bin/ -executable -type f -print0 | xargs -0 shellcheck build-bazel: parameters: diff --git a/BUILD.bazel b/BUILD.bazel index d78fd98a..82f024cf 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -47,6 +47,7 @@ cc_library( "src/datadog/threaded_event_scheduler.cpp", "src/datadog/tracer_config.cpp", "src/datadog/tracer.cpp", + "src/datadog/trace_id.cpp", "src/datadog/trace_sampler_config.cpp", "src/datadog/trace_sampler.cpp", "src/datadog/trace_segment.cpp", @@ -101,6 +102,7 @@ cc_library( "src/datadog/threaded_event_scheduler.h", "src/datadog/tracer_config.h", "src/datadog/tracer.h", + "src/datadog/trace_id.h", "src/datadog/trace_sampler_config.h", "src/datadog/trace_sampler.h", "src/datadog/trace_segment.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 6656675f..19c37e22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,6 +116,7 @@ target_sources(dd_trace_cpp PRIVATE src/datadog/threaded_event_scheduler.cpp src/datadog/tracer_config.cpp src/datadog/tracer.cpp + src/datadog/trace_id.cpp src/datadog/trace_sampler_config.cpp src/datadog/trace_sampler.cpp src/datadog/trace_segment.cpp @@ -176,6 +177,7 @@ target_sources(dd_trace_cpp PUBLIC src/datadog/threaded_event_scheduler.h src/datadog/tracer_config.h src/datadog/tracer.h + src/datadog/trace_id.h src/datadog/trace_sampler_config.h src/datadog/trace_sampler.h src/datadog/trace_segment.h diff --git a/bin/check b/bin/check index 2d5b4fde..79c4f5b2 100755 --- a/bin/check +++ b/bin/check @@ -11,3 +11,4 @@ cd "$(dirname "$0")"/.. bin/format --dry-run -Werror bin/test bin/bazel-build +find bin/ -executable -type f -print0 | xargs -0 shellcheck diff --git a/src/datadog/environment.h b/src/datadog/environment.h index fbcd78c3..472bada9 100644 --- a/src/datadog/environment.h +++ b/src/datadog/environment.h @@ -46,7 +46,8 @@ namespace environment { MACRO(DD_TRACE_SAMPLING_RULES) \ MACRO(DD_TRACE_STARTUP_LOGS) \ MACRO(DD_TRACE_TAGS_PROPAGATION_MAX_LENGTH) \ - MACRO(DD_VERSION) + MACRO(DD_VERSION) \ + MACRO(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED) #define WITH_COMMA(ARG) ARG, diff --git a/src/datadog/extracted_data.h b/src/datadog/extracted_data.h index 0f182d7f..33b72215 100644 --- a/src/datadog/extracted_data.h +++ b/src/datadog/extracted_data.h @@ -9,22 +9,17 @@ #include #include "optional.h" +#include "trace_id.h" namespace datadog { namespace tracing { struct ExtractedData { - Optional trace_id; + Optional trace_id; Optional parent_id; Optional origin; std::vector> trace_tags; Optional sampling_priority; - // If this `ExtractedData` was created on account of `PropagationStyle::W3C` - // and trace context was successfully extracted, then `full_w3c_trace_id_hex` - // contains the hex-encoded 128-bit trace ID. `trace_id` will be the least - // significant 64 bits of the same value. `full_w3c_trace_id_hex` is used for - // the `W3C` injection style. - Optional full_w3c_trace_id_hex; // If this `ExtractedData` was created on account of `PropagationStyle::W3C`, // then `additional_w3c_tracestate` contains the parts of the "tracestate" // header that are not the "dd" (Datadog) entry. If there are no other parts, diff --git a/src/datadog/hex.h b/src/datadog/hex.h index dec38a2e..593ddbbc 100644 --- a/src/datadog/hex.h +++ b/src/datadog/hex.h @@ -1,6 +1,6 @@ #pragma once -// This component provides a function, `hex`, for formatting an integral value +// This component provides functions for formatting an unsigned integral value // in hexadecimal. #include @@ -12,12 +12,14 @@ namespace datadog { namespace tracing { -// Return the specified `value` formatted as a lower-case hexadecimal string -// without any leading zeroes. -template -std::string hex(Integer value) { - // 4 bits per hex digit char, and then +1 char for possible minus sign - char buffer[std::numeric_limits::digits / 4 + 1]; +// Return the specified unsigned `value` formatted as a lower-case hexadecimal +// string without any leading zeroes. +template +std::string hex(UnsignedInteger value) { + static_assert(!std::numeric_limits::is_signed); + + // 4 bits per hex digit char + char buffer[std::numeric_limits::digits / 4]; const int base = 16; auto result = @@ -27,5 +29,25 @@ std::string hex(Integer value) { return std::string{std::begin(buffer), result.ptr}; } +// Return the specified unsigned `value` formatted as a lower-case hexadecimal +// string with leading zeroes. +template +std::string hex_padded(UnsignedInteger value) { + static_assert(!std::numeric_limits::is_signed); + + // 4 bits per hex digit char. + char buffer[std::numeric_limits::digits / 4]; + + const int base = 16; + auto result = + std::to_chars(std::begin(buffer), std::end(buffer), value, base); + assert(result.ec == std::errc()); + + const auto num_zeroes = sizeof(buffer) - (result.ptr - std::begin(buffer)); + std::string padded(num_zeroes, '0'); + padded.append(std::begin(buffer), result.ptr); + return padded; +} + } // namespace tracing } // namespace datadog \ No newline at end of file diff --git a/src/datadog/id_generator.cpp b/src/datadog/id_generator.cpp index bccae8ca..5f186bef 100644 --- a/src/datadog/id_generator.cpp +++ b/src/datadog/id_generator.cpp @@ -16,7 +16,7 @@ namespace { extern "C" void on_fork(); #endif -class DefaultIDGenerator { +class Uint64Generator { std::mt19937_64 generator_; // The distribution used is for a _signed_ integer, and the default minimum // value is zero. @@ -26,7 +26,7 @@ class DefaultIDGenerator { std::uniform_int_distribution distribution_; public: - DefaultIDGenerator() { + Uint64Generator() { seed_with_random(); // If a process links to this library and then calls `fork`, the `generator_` in // the parent and child processes will produce the exact same sequence of @@ -48,17 +48,36 @@ class DefaultIDGenerator { void seed_with_random() { generator_.seed(std::random_device{}()); } }; -thread_local DefaultIDGenerator thread_local_generator; +thread_local Uint64Generator thread_local_generator; #ifndef _MSC_VER void on_fork() { thread_local_generator.seed_with_random(); } #endif -} // namespace +class DefaultIDGenerator : public IDGenerator { + const bool trace_id_128_bit_; + + public: + explicit DefaultIDGenerator(bool trace_id_128_bit) + : trace_id_128_bit_(trace_id_128_bit) {} + + TraceID trace_id() const override { + TraceID result; + result.low = thread_local_generator(); + if (trace_id_128_bit_) { + result.high = thread_local_generator(); + } + return result; + } -const IDGenerator default_id_generator = []() { - return thread_local_generator(); + std::uint64_t span_id() const override { return thread_local_generator(); } }; +} // namespace + +std::shared_ptr default_id_generator(bool trace_id_128_bit) { + return std::make_shared(trace_id_128_bit); +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/id_generator.h b/src/datadog/id_generator.h index 39363c30..32ffa2ec 100644 --- a/src/datadog/id_generator.h +++ b/src/datadog/id_generator.h @@ -3,23 +3,29 @@ // This component provides facilities for generating sequences of IDs used as // span IDs and trace IDs. // -// `IDGenerator` is an alias for `std::function`. +// `IDGenerator` is an interface for generating trace IDs and span IDs. // // `default_id_generator` is an `IDGenerator` that produces a thread-local // pseudo-random sequence of uniformly distributed 63-bit unsigned integers. The -// sequence is randomly seeded one per thread and anytime the process forks. -// The IDs are 63-bit (instead of 64-bit) to ease compatibility with peer -// runtimes that don't have a native 64-bit unsigned numeric type. +// sequence is randomly seeded once per thread and anytime the process forks. #include -#include +#include + +#include "trace_id.h" namespace datadog { namespace tracing { -using IDGenerator = std::function; +class IDGenerator { + public: + virtual ~IDGenerator() = default; + + virtual std::uint64_t span_id() const = 0; + virtual TraceID trace_id() const = 0; +}; -extern const IDGenerator default_id_generator; +std::shared_ptr default_id_generator(bool trace_id_128_bit); } // namespace tracing } // namespace datadog diff --git a/src/datadog/limiter.cpp b/src/datadog/limiter.cpp index 63f66b38..80c0f01f 100644 --- a/src/datadog/limiter.cpp +++ b/src/datadog/limiter.cpp @@ -2,7 +2,6 @@ #include #include -#include #include namespace datadog { diff --git a/src/datadog/sampling_util.h b/src/datadog/sampling_util.h index bf9c6921..825484ae 100644 --- a/src/datadog/sampling_util.h +++ b/src/datadog/sampling_util.h @@ -11,6 +11,16 @@ namespace datadog { namespace tracing { +// Return a hash value for the specified `value`. `value` is one of the +// following: +// +// - a 64-bit span ID +// - a 64-bit trace ID +// - the lower 64 bits of a 128-bit trace ID +// +// The resulting hash value is compared with an upper bound provided by +// `max_id_from_rate` (below) to determine whether the span/trace associated +// with `value` is eligible for keeping on statistical grounds. inline std::uint64_t knuth_hash(std::uint64_t value) { return value * UINT64_C(1111111111111111111); } diff --git a/src/datadog/span.cpp b/src/datadog/span.cpp index 0156b86c..fc5e7089 100644 --- a/src/datadog/span.cpp +++ b/src/datadog/span.cpp @@ -15,7 +15,8 @@ namespace datadog { namespace tracing { Span::Span(SpanData* data, const std::shared_ptr& trace_segment, - const IDGenerator& generate_span_id, const Clock& clock) + const std::function& generate_span_id, + const Clock& clock) : trace_segment_(trace_segment), data_(data), generate_span_id_(generate_span_id), @@ -62,7 +63,7 @@ void Span::inject(DictWriter& writer) const { std::uint64_t Span::id() const { return data_->span_id; } -std::uint64_t Span::trace_id() const { return data_->trace_id; } +TraceID Span::trace_id() const { return data_->trace_id; } Optional Span::parent_id() const { if (data_->parent_id == 0) { diff --git a/src/datadog/span.h b/src/datadog/span.h index 4d7512fe..534ed706 100644 --- a/src/datadog/span.h +++ b/src/datadog/span.h @@ -41,14 +41,15 @@ // via the `set_end_time` member function prior to the span's destruction. #include +#include #include #include #include "clock.h" #include "error.h" -#include "id_generator.h" #include "optional.h" #include "string_view.h" +#include "trace_id.h" namespace datadog { namespace tracing { @@ -61,19 +62,18 @@ class TraceSegment; class Span { std::shared_ptr trace_segment_; SpanData* data_; - IDGenerator generate_span_id_; + std::function generate_span_id_; Clock clock_; Optional end_time_; public: - // Create a span whose properties are stored in the specified `data` and that - // is associated with the specified `trace_segment`. Optionally specify - // `generate_span_id` to generate IDs of child spans, and a `clock` to - // determine start and end times. If `generate_span_id` and `clock` are not - // specified`, then `default_id_generator` and `default_clock` are used - // instead respectively. + // Create a span whose properties are stored in the specified `data`, that is + // associated with the specified `trace_segment`, that uses the specified + // `generate_span_id` to generate IDs of child spans, and that uses the + // specified `clock` to determine start and end times. Span(SpanData* data, const std::shared_ptr& trace_segment, - const IDGenerator& generate_span_id, const Clock& clock); + const std::function& generate_span_id, + const Clock& clock); Span(const Span&) = delete; Span(Span&&) = default; Span& operator=(Span&&) = default; @@ -95,7 +95,7 @@ class Span { // Return this span's ID (span ID). std::uint64_t id() const; // Return the ID of the trace of which this span is a part. - std::uint64_t trace_id() const; + TraceID trace_id() const; // Return the ID of this span's parent span, or return null if this span has // no parent. Optional parent_id() const; diff --git a/src/datadog/span_data.cpp b/src/datadog/span_data.cpp index fea93cad..b168baac 100644 --- a/src/datadog/span_data.cpp +++ b/src/datadog/span_data.cpp @@ -78,7 +78,7 @@ Expected msgpack_encode(std::string& destination, const SpanData& span) { return msgpack::pack_string(destination, span.resource); }, "trace_id", [&](auto& destination) { - msgpack::pack_integer(destination, span.trace_id); + msgpack::pack_integer(destination, span.trace_id.low); return Expected{}; }, "span_id", [&](auto& destination) { diff --git a/src/datadog/span_data.h b/src/datadog/span_data.h index 3e9b00fa..9c0ab0ee 100644 --- a/src/datadog/span_data.h +++ b/src/datadog/span_data.h @@ -12,6 +12,7 @@ #include "expected.h" #include "optional.h" #include "string_view.h" +#include "trace_id.h" namespace datadog { namespace tracing { @@ -24,7 +25,7 @@ struct SpanData { std::string service_type; std::string name; std::string resource; - std::uint64_t trace_id = 0; + TraceID trace_id; std::uint64_t span_id = 0; std::uint64_t parent_id = 0; TimePoint start; diff --git a/src/datadog/tags.cpp b/src/datadog/tags.cpp index db5828b2..513935ee 100644 --- a/src/datadog/tags.cpp +++ b/src/datadog/tags.cpp @@ -29,6 +29,7 @@ const std::string span_sampling_mechanism = "_dd.span_sampling.mechanism"; const std::string span_sampling_rule_rate = "_dd.span_sampling.rule_rate"; const std::string span_sampling_limit = "_dd.span_sampling.max_per_second"; const std::string w3c_extraction_error = "_dd.w3c_extraction_error"; +const std::string trace_id_high = "_dd.p.tid"; } // namespace internal diff --git a/src/datadog/tags.h b/src/datadog/tags.h index 996cc58b..030007b2 100644 --- a/src/datadog/tags.h +++ b/src/datadog/tags.h @@ -33,6 +33,7 @@ extern const std::string span_sampling_mechanism; extern const std::string span_sampling_rule_rate; extern const std::string span_sampling_limit; extern const std::string w3c_extraction_error; +extern const std::string trace_id_high; } // namespace internal // Return whether the specified `tag_name` is reserved for use internal to this diff --git a/src/datadog/trace_id.cpp b/src/datadog/trace_id.cpp new file mode 100644 index 00000000..7f0ead9d --- /dev/null +++ b/src/datadog/trace_id.cpp @@ -0,0 +1,88 @@ +#include "trace_id.h" + +#include "hex.h" +#include "parse_util.h" + +namespace datadog { +namespace tracing { + +TraceID::TraceID() : low(0), high(0) {} + +TraceID::TraceID(std::uint64_t low) : low(low), high(0) {} + +TraceID::TraceID(std::uint64_t low, std::uint64_t high) + : low(low), high(high) {} + +std::string TraceID::hex_padded() const { + std::string result; + if (high) { + result += ::datadog::tracing::hex_padded(high); + } else { + result.append(16, '0'); + } + result += ::datadog::tracing::hex_padded(low); + return result; +} + +Expected TraceID::parse_hex(StringView input) { + const auto parse_hex_piece = + [input](StringView piece) -> Expected { + auto result = parse_uint64(piece, 16); + if (auto *error = result.if_error()) { + std::string prefix = "Unable to parse trace ID from \""; + append(prefix, input); + prefix += "\": "; + return error->with_prefix(prefix); + } + return result; + }; + + // A 64-bit integer is at most 16 hex characters. If the input is no + // longer than that, then it will all fit in `TraceID::low`. + if (input.size() <= 16) { + auto result = parse_hex_piece(input); + if (auto *error = result.if_error()) { + return std::move(*error); + } + return TraceID(*result); + } + + // Parse the lower part and the higher part separately. + const auto divider = input.begin() + (input.size() - 16); + const auto high_hex = range(input.begin(), divider); + const auto low_hex = range(divider, input.end()); + TraceID trace_id; + + auto result = parse_hex_piece(low_hex); + if (auto *error = result.if_error()) { + return std::move(*error); + } + trace_id.low = *result; + + result = parse_hex_piece(high_hex); + if (auto *error = result.if_error()) { + return std::move(*error); + } + trace_id.high = *result; + + return trace_id; +} + +bool operator==(TraceID left, TraceID right) { + return left.low == right.low && left.high == right.high; +} + +bool operator!=(TraceID left, TraceID right) { + return left.low != right.low || left.high != right.high; +} + +bool operator==(TraceID left, std::uint64_t right) { + return left == TraceID{right}; +} + +bool operator!=(TraceID left, std::uint64_t right) { + return left != TraceID{right}; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/trace_id.h b/src/datadog/trace_id.h new file mode 100644 index 00000000..d0e39a5c --- /dev/null +++ b/src/datadog/trace_id.h @@ -0,0 +1,48 @@ +#pragma once + +// This component provides a `struct`, `TraceID`, that represents an opaque, +// unique identifier for a trace. +// `TraceID` is 128 bits wide, though in some contexts only the lower 64 bits +// are used. + +#include +#include + +#include "expected.h" +#include "string_view.h" + +namespace datadog { +namespace tracing { + +struct TraceID { + std::uint64_t low; + std::uint64_t high; + + // Create a zero trace ID. + TraceID(); + + // Create a trace ID whose lower 64 bits are the specified `low` and whose + // higher 64 bits are zero. + explicit TraceID(std::uint64_t low); + + // Create a trace ID whose lower 64 bits are the specified `low` and whose + // higher 64 bits are the specified `high`. + TraceID(std::uint64_t low, std::uint64_t high); + + // Return a 32 character lower-case hexadecimal representation of this trace + // ID, padded with zeroes on the left. + std::string hex_padded() const; + + // Return a `TraceID` parsed from the specified hexadecimal string, or return + // an `Error`. It is an error of the input contains any non-hexadecimal + // characters. + static Expected parse_hex(StringView); +}; + +bool operator==(TraceID, TraceID); +bool operator!=(TraceID, TraceID); +bool operator==(TraceID, std::uint64_t); +bool operator!=(TraceID, std::uint64_t); + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/trace_sampler.cpp b/src/datadog/trace_sampler.cpp index 1938b57a..ae74865b 100644 --- a/src/datadog/trace_sampler.cpp +++ b/src/datadog/trace_sampler.cpp @@ -40,7 +40,7 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { decision.limiter_max_per_second = limiter_max_per_second_; decision.configured_rate = rule.sample_rate; const std::uint64_t threshold = max_id_from_rate(rule.sample_rate); - if (knuth_hash(span.trace_id) < threshold) { + if (knuth_hash(span.trace_id.low) < threshold) { const auto result = limiter_.allow(); if (result.allowed) { decision.priority = int(SamplingPriority::USER_KEEP); @@ -75,7 +75,7 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { } const std::uint64_t threshold = max_id_from_rate(*decision.configured_rate); - if (knuth_hash(span.trace_id) < threshold) { + if (knuth_hash(span.trace_id.low) < threshold) { decision.priority = int(SamplingPriority::AUTO_KEEP); } else { decision.priority = int(SamplingPriority::AUTO_DROP); diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index dec56176..7ab1d464 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -66,7 +66,6 @@ TraceSegment::TraceSegment( std::size_t tags_header_max_size, std::vector> trace_tags, Optional sampling_decision, - Optional full_w3c_trace_id_hex, Optional additional_w3c_tracestate, Optional additional_datadog_w3c_tracestate, std::unique_ptr local_root) @@ -82,7 +81,6 @@ TraceSegment::TraceSegment( trace_tags_(std::move(trace_tags)), num_finished_spans_(0), sampling_decision_(std::move(sampling_decision)), - full_w3c_trace_id_hex_(std::move(full_w3c_trace_id_hex)), additional_w3c_tracestate_(std::move(additional_w3c_tracestate)), additional_datadog_w3c_tracestate_( std::move(additional_datadog_w3c_tracestate)) { @@ -271,7 +269,7 @@ void TraceSegment::inject(DictWriter& writer, const SpanData& span) { for (const auto style : injection_styles_) { switch (style) { case PropagationStyle::DATADOG: - writer.set("x-datadog-trace-id", std::to_string(span.trace_id)); + writer.set("x-datadog-trace-id", std::to_string(span.trace_id.low)); writer.set("x-datadog-parent-id", std::to_string(span.span_id)); writer.set("x-datadog-sampling-priority", std::to_string(sampling_priority)); @@ -282,8 +280,12 @@ void TraceSegment::inject(DictWriter& writer, const SpanData& span) { spans_.front()->tags, *logger_); break; case PropagationStyle::B3: - writer.set("x-b3-traceid", hex(span.trace_id)); - writer.set("x-b3-spanid", hex(span.span_id)); + if (span.trace_id.high) { + writer.set("x-b3-traceid", span.trace_id.hex_padded()); + } else { + writer.set("x-b3-traceid", hex_padded(span.trace_id.low)); + } + writer.set("x-b3-spanid", hex_padded(span.span_id)); writer.set("x-b3-sampled", std::to_string(int(sampling_priority > 0))); if (origin_) { writer.set("x-datadog-origin", *origin_); @@ -292,9 +294,9 @@ void TraceSegment::inject(DictWriter& writer, const SpanData& span) { spans_.front()->tags, *logger_); break; case PropagationStyle::W3C: - writer.set("traceparent", - encode_traceparent(span.trace_id, full_w3c_trace_id_hex_, - span.span_id, sampling_priority)); + writer.set( + "traceparent", + encode_traceparent(span.trace_id, span.span_id, sampling_priority)); writer.set("tracestate", encode_tracestate(sampling_priority, origin_, trace_tags, additional_datadog_w3c_tracestate_, diff --git a/src/datadog/trace_segment.h b/src/datadog/trace_segment.h index 0a8ccc42..d0d9004b 100644 --- a/src/datadog/trace_segment.h +++ b/src/datadog/trace_segment.h @@ -67,7 +67,6 @@ class TraceSegment { std::vector> spans_; std::size_t num_finished_spans_; Optional sampling_decision_; - Optional full_w3c_trace_id_hex_; Optional additional_w3c_tracestate_; Optional additional_datadog_w3c_tracestate_; bool awaiting_delegated_sampling_decision_ = false; @@ -83,7 +82,6 @@ class TraceSegment { Optional origin, std::size_t tags_header_max_size, std::vector> trace_tags, Optional sampling_decision, - Optional full_w3c_trace_id_hex, Optional additional_w3c_tracestate, Optional additional_datadog_w3c_tracestate, std::unique_ptr local_root); diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index dcc5c142..45cece9d 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -7,6 +7,7 @@ #include "dict_reader.h" #include "environment.h" #include "extracted_data.h" +#include "hex.h" #include "json.hpp" #include "logger.h" #include "net_util.h" @@ -41,9 +42,28 @@ void handle_trace_tags(StringView trace_tags, ExtractedData& result, } for (auto& [key, value] : *maybe_trace_tags) { - if (starts_with(key, "_dd.p.")) { - result.trace_tags.emplace_back(std::move(key), std::move(value)); + if (!starts_with(key, "_dd.p.")) { + continue; } + + 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: ")); + span_tags[tags::internal::propagation_error] = "decoding_error"; + } + // Note that this assumes the lower 64 bits of the trace ID have already + // been extracted (i.e. we look for X-Datadog-Trace-ID first). + if (result.trace_id) { + result.trace_id->high = *high; + } + } + + result.trace_tags.emplace_back(std::move(key), std::move(value)); } } @@ -83,7 +103,9 @@ Expected extract_datadog( if (auto* error = trace_id.if_error()) { return std::move(*error); } - result.trace_id = *trace_id; + if (*trace_id) { + result.trace_id = TraceID(**trace_id); + } auto parent_id = extract_id_header(headers, "x-datadog-parent-id", "parent span", "Datadog", 10); @@ -121,15 +143,20 @@ Expected extract_datadog( } Expected extract_b3( - const DictReader& headers, - std::unordered_map& span_tags, Logger& logger) { + const DictReader& headers, std::unordered_map&, + Logger&) { ExtractedData result; - auto trace_id = extract_id_header(headers, "x-b3-traceid", "trace", "B3", 16); - if (auto* error = trace_id.if_error()) { - return std::move(*error); + if (auto found = headers.lookup("x-b3-traceid")) { + auto parsed = TraceID::parse_hex(*found); + if (auto* error = parsed.if_error()) { + std::string prefix = "Could not extract B3-style trace ID from \""; + append(prefix, *found); + prefix += "\": "; + return error->with_prefix(prefix); + } + result.trace_id = *parsed; } - result.trace_id = *trace_id; auto parent_id = extract_id_header(headers, "x-b3-spanid", "parent span", "B3", 16); @@ -153,17 +180,6 @@ Expected extract_b3( result.sampling_priority = *sampling_priority; } - // Origin and trace tags are still extracted, but from the Datadog headers. - auto origin = headers.lookup("x-datadog-origin"); - if (origin) { - result.origin = std::string(*origin); - } - - auto trace_tags = headers.lookup("x-datadog-tags"); - if (trace_tags) { - handle_trace_tags(*trace_tags, result, span_tags, logger); - } - return result; } @@ -202,10 +218,19 @@ void log_startup_message(Logger& logger, StringView tracer_version_string, } // namespace Tracer::Tracer(const FinalizedTracerConfig& config) - : Tracer(config, default_id_generator, default_clock) {} + : Tracer(config, default_id_generator(config.trace_id_128_bit), + default_clock) {} + +Tracer::Tracer(const FinalizedTracerConfig& config, + const std::shared_ptr& generator) + : Tracer(config, generator, default_clock) {} + +Tracer::Tracer(const FinalizedTracerConfig& config, const Clock& clock) + : Tracer(config, default_id_generator(config.trace_id_128_bit), clock) {} Tracer::Tracer(const FinalizedTracerConfig& config, - const IDGenerator& generator, const Clock& clock) + const std::shared_ptr& generator, + const Clock& clock) : logger_(config.logger), collector_(/* see constructor body */), trace_sampler_( @@ -241,19 +266,25 @@ Span Tracer::create_span() { return create_span(SpanConfig{}); } Span Tracer::create_span(const SpanConfig& config) { auto span_data = std::make_unique(); span_data->apply_config(*defaults_, config, clock_); - span_data->span_id = generator_(); - span_data->trace_id = span_data->span_id; + std::vector> trace_tags; + span_data->trace_id = generator_->trace_id(); + if (span_data->trace_id.high) { + trace_tags.emplace_back(tags::internal::trace_id_high, + hex(span_data->trace_id.high)); + } + span_data->span_id = span_data->trace_id.low; span_data->parent_id = 0; const auto span_data_ptr = span_data.get(); const auto segment = std::make_shared( logger_, collector_, trace_sampler_, span_sampler_, defaults_, injection_styles_, hostname_, nullopt /* origin */, tags_header_max_size_, - std::vector>{} /* trace_tags */, - nullopt /* sampling_decision */, nullopt /* full_w3c_trace_id_hex */, + std::move(trace_tags), nullopt /* sampling_decision */, nullopt /* additional_w3c_tracestate */, nullopt /* additional_datadog_w3c_tracestate*/, std::move(span_data)); - Span span{span_data_ptr, segment, generator_, clock_}; + Span span{span_data_ptr, segment, + [generator = generator_]() { return generator->span_id(); }, + clock_}; return span; } @@ -300,8 +331,8 @@ Expected Tracer::extract_span(const DictReader& reader, } auto& [trace_id, parent_id, origin, trace_tags, sampling_priority, - full_w3c_trace_id_hex, additional_w3c_tracestate, - additional_datadog_w3c_tracestate] = extracted_data; + additional_w3c_tracestate, additional_datadog_w3c_tracestate] = + extracted_data; // Some information might be missing. // Here are the combinations considered: @@ -332,7 +363,13 @@ Expected Tracer::extract_span(const DictReader& reader, std::string message; message += "There's no parent span ID to extract, but there is a trace ID: "; - message += std::to_string(*trace_id); + message += "[hexadecimal = "; + message += trace_id->hex_padded(); + if (trace_id->high == 0) { + message += ", decimal = "; + message += std::to_string(trace_id->low); + } + message += ']'; return Error{Error::MISSING_PARENT_SPAN_ID, std::move(message)}; } @@ -349,7 +386,7 @@ Expected Tracer::extract_span(const DictReader& reader, assert(trace_id); span_data->apply_config(*defaults_, config, clock_); - span_data->span_id = generator_(); + span_data->span_id = generator_->span_id(); span_data->trace_id = *trace_id; span_data->parent_id = *parent_id; @@ -369,9 +406,11 @@ Expected Tracer::extract_span(const DictReader& reader, logger_, collector_, trace_sampler_, span_sampler_, defaults_, injection_styles_, hostname_, std::move(origin), tags_header_max_size_, std::move(trace_tags), std::move(sampling_decision), - std::move(full_w3c_trace_id_hex), std::move(additional_w3c_tracestate), + std::move(additional_w3c_tracestate), std::move(additional_datadog_w3c_tracestate), std::move(span_data)); - Span span{span_data_ptr, segment, generator_, clock_}; + Span span{span_data_ptr, segment, + [generator = generator_]() { return generator->span_id(); }, + clock_}; return span; } diff --git a/src/datadog/tracer.h b/src/datadog/tracer.h index b894c791..b7ec2fa4 100644 --- a/src/datadog/tracer.h +++ b/src/datadog/tracer.h @@ -31,7 +31,7 @@ class Tracer { std::shared_ptr collector_; std::shared_ptr trace_sampler_; std::shared_ptr span_sampler_; - IDGenerator generator_; + std::shared_ptr generator_; Clock clock_; std::shared_ptr defaults_; std::vector injection_styles_; @@ -44,7 +44,11 @@ class Tracer { // - using the specified `generator` to create trace IDs and span IDs // - using the specified `clock` to get the current time. explicit Tracer(const FinalizedTracerConfig& config); - Tracer(const FinalizedTracerConfig& config, const IDGenerator& generator, + Tracer(const FinalizedTracerConfig& config, + const std::shared_ptr& generator); + Tracer(const FinalizedTracerConfig& config, const Clock& clock); + Tracer(const FinalizedTracerConfig& config, + const std::shared_ptr& generator, const Clock& clock); // Create a new trace and return the root span of the trace. Optionally diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 6bf95b1a..48ee55cc 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -360,6 +360,13 @@ Expected finalize_config(const TracerConfig &config) { result.report_hostname = config.report_hostname; result.tags_header_size = config.tags_header_size; + if (auto enabled_env = + lookup(environment::DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED)) { + result.trace_id_128_bit = !falsy(*enabled_env); + } else { + result.trace_id_128_bit = config.trace_id_128_bit; + } + return result; } diff --git a/src/datadog/tracer_config.h b/src/datadog/tracer_config.h index cde67adf..b517723b 100644 --- a/src/datadog/tracer_config.h +++ b/src/datadog/tracer_config.h @@ -97,6 +97,12 @@ struct TracerConfig { // `log_on_startup` is overridden by the `DD_TRACE_STARTUP_LOGS` environment // variable. bool log_on_startup = true; + + // `trace_id_128_bit` indicates whether the tracer will generate 128-bit trace + // IDs. If true, the tracer will generate 128-bit trace IDs. If false, the + // tracer will generate 64-bit trace IDs. `trace_id_128_bit` is overridden by + // the `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` environment variable. + bool trace_id_128_bit = false; }; // `FinalizedTracerConfig` contains `Tracer` implementation details derived from @@ -124,6 +130,7 @@ class FinalizedTracerConfig { std::size_t tags_header_size; std::shared_ptr logger; bool log_on_startup; + bool trace_id_128_bit; }; // Return a `FinalizedTracerConfig` from the specified `config` and from any diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index a376dd26..afa265b5 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -48,11 +48,11 @@ Optional extract_traceparent(ExtractedData& result, static const auto& pattern = "([0-9a-f]{2})" // hex version number (match group 1) "-" - "([0-9a-f]{16}([0-9a-f]{16}))" // hex trace ID (match groups 2 and 3) + "([0-9a-f]{32})" // hex trace ID (match group 2) "-" - "([0-9a-f]{16})" // hex parent span ID (match group 4) + "([0-9a-f]{16})" // hex parent span ID (match group 3) "-" - "([0-9a-f]{2})" // hex "trace-flags" (match group 5) + "([0-9a-f]{2})" // hex "trace-flags" (match group 4) "(?:$|-.*)"; // either the end, or a hyphen preceding further fields static const std::regex regex{pattern}; @@ -62,7 +62,7 @@ Optional extract_traceparent(ExtractedData& result, } assert(match.ready()); - assert(match.size() == 5 + 1); + assert(match.size() == 4 + 1); const auto to_string_view = [](const auto& submatch) { assert(submatch.first <= submatch.second); @@ -74,20 +74,17 @@ Optional extract_traceparent(ExtractedData& result, return "invalid_version"; } - result.full_w3c_trace_id_hex = std::string{to_string_view(match[2])}; - if (result.full_w3c_trace_id_hex->find_first_not_of('0') == - std::string::npos) { + result.trace_id = *TraceID::parse_hex(to_string_view(match[2])); + if (result.trace_id == 0) { return "trace_id_zero"; } - result.trace_id = *parse_uint64(to_string_view(match[3]), 16); - - result.parent_id = *parse_uint64(to_string_view(match[4]), 16); + result.parent_id = *parse_uint64(to_string_view(match[3]), 16); if (*result.parent_id == 0) { return "parent_id_zero"; } - const auto flags = *parse_uint64(to_string_view(match[5]), 16); + const auto flags = *parse_uint64(to_string_view(match[4]), 16); result.sampling_priority = int(flags & 1); return nullopt; @@ -217,6 +214,12 @@ void parse_datadog_tracestate(ExtractedData& result, StringView datadog_value) { const auto tag_suffix = key.substr(2); std::string tag_name = "_dd.p."; append(tag_name, tag_suffix); + // The "_dd.p.tid" trace tag is ignored in this context, since its value + // is better inferred from the higher 64 bits of the trace ID. + if (tag_name == "_dd.p.tid") { + pair_begin = pair_end == end ? end : pair_end + 1; + continue; + } // The tag value was encoded with all '=' replaced by '~'. Undo that // transformation. std::string decoded_value{value}; @@ -294,27 +297,18 @@ Expected extract_w3c( return result; } -std::string encode_traceparent( - std::uint64_t trace_id, const Optional& full_w3c_trace_id_hex, - std::uint64_t span_id, int sampling_priority) { +std::string encode_traceparent(TraceID trace_id, std::uint64_t span_id, + int sampling_priority) { std::string result; // version result += "00-"; // trace ID - if (full_w3c_trace_id_hex) { - result += *full_w3c_trace_id_hex; - } else { - auto hexed = hex(trace_id); - result.append(16 + (16 - hexed.size()), '0'); // leading zeroes - result += hexed; - } + result += trace_id.hex_padded(); result += '-'; // span ID - auto hexed = hex(span_id); - result.append(16 - hexed.size(), '0'); // leading zeroes - result += hexed; + result += hex_padded(span_id); result += '-'; // flags @@ -339,7 +333,9 @@ std::string encode_datadog_tracestate( for (const auto& [key, value] : trace_tags) { const StringView prefix = "_dd.p."; - if (!starts_with(key, prefix)) { + if (!starts_with(key, prefix) || key == tags::internal::trace_id_high) { + // Either it's not a propagation tag, or it's one of the propagation tags + // that need not be included in tracestate. continue; } diff --git a/src/datadog/w3c_propagation.h b/src/datadog/w3c_propagation.h index 57946ca9..dd4f6f25 100644 --- a/src/datadog/w3c_propagation.h +++ b/src/datadog/w3c_propagation.h @@ -11,6 +11,7 @@ #include "expected.h" #include "extracted_data.h" #include "optional.h" +#include "trace_id.h" namespace datadog { namespace tracing { @@ -31,9 +32,8 @@ Expected extract_w3c( // `trace_id` or the optionally specified `full_w3c_trace_id_hex` as the trace // ID, the specified `span_id` as the parent ID, and trace flags deduced from // the specified `sampling_priority`. -std::string encode_traceparent( - std::uint64_t trace_id, const Optional& full_w3c_trace_id_hex, - std::uint64_t span_id, int sampling_priority); +std::string encode_traceparent(TraceID trace_id, std::uint64_t span_id, + int sampling_priority); // Return a value for the "tracestate" header containing the specified fields. std::string encode_tracestate( diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 928c152f..84b7dc9e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -26,6 +26,7 @@ add_executable(tests test_smoke.cpp test_span.cpp test_span_sampler.cpp + test_trace_id.cpp test_trace_segment.cpp test_tracer_config.cpp test_tracer.cpp diff --git a/test/test.cpp b/test/test.cpp index 801d68a7..6c0e2dd0 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -23,3 +23,13 @@ std::ostream& operator<<(std::ostream& stream, } } // namespace std + +namespace datadog { +namespace tracing { + +std::ostream& operator<<(std::ostream& stream, TraceID trace_id) { + return stream << "0x" << trace_id.hex_padded(); +} + +} // namespace tracing +} // namespace datadog diff --git a/test/test.h b/test/test.h index 0cf17b1b..bce5a3f6 100644 --- a/test/test.h +++ b/test/test.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -42,5 +43,7 @@ std::ostream& operator<<(std::ostream& stream, return stream << expected.error(); } +std::ostream& operator<<(std::ostream&, TraceID); + } // namespace tracing } // namespace datadog diff --git a/test/test_span.cpp b/test/test_span.cpp index ada7df6f..29c2f0c6 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -3,6 +3,7 @@ // for propagation. #include +#include #include #include #include @@ -11,11 +12,9 @@ #include #include -#include #include #include #include -#include #include #include "matchers.h" @@ -352,23 +351,6 @@ TEST_CASE("property setters") { } } -namespace { - -template -std::string hex(Integer value) { - // 4 bits per hex digit char, and then +1 char for possible minus sign - char buffer[std::numeric_limits::digits / 4 + 1]; - - const int base = 16; - auto result = - std::to_chars(std::begin(buffer), std::end(buffer), value, base); - assert(result.ec == std::errc()); - - return std::string{std::begin(buffer), result.ptr}; -} - -} // namespace - // Trace context injection is implemented in `TraceSegment`, but it's part of // the interface of `Span`, so the test is here. TEST_CASE("injection") { @@ -380,25 +362,32 @@ TEST_CASE("injection") { auto finalized_config = finalize_config(config); REQUIRE(finalized_config); - auto generator = []() { return 42; }; - Tracer tracer{*finalized_config, generator, default_clock}; + // Override the tracer's ID generator to always return a fixed value. + struct Generator : public IDGenerator { + const std::uint64_t id; + explicit Generator(std::uint64_t id) : id(id) {} + TraceID trace_id() const override { return TraceID(id); } + std::uint64_t span_id() const override { return id; } + }; + Tracer tracer{*finalized_config, std::make_shared(42)}; SECTION("trace ID, parent ID ,and sampling priority") { auto span = tracer.create_span(); + REQUIRE(span.trace_id() == 42); + REQUIRE(span.id() == 42); + const int priority = 3; // 😱 span.trace_segment().override_sampling_priority(priority); MockDictWriter writer; span.inject(writer); const auto& headers = writer.items; - REQUIRE(headers.at("x-datadog-trace-id") == - std::to_string(span.trace_id())); - REQUIRE(headers.at("x-datadog-parent-id") == std::to_string(span.id())); - REQUIRE(headers.at("x-datadog-sampling-priority") == - std::to_string(priority)); - REQUIRE(headers.at("x-b3-traceid") == hex(span.trace_id())); - REQUIRE(headers.at("x-b3-spanid") == hex(span.id())); - REQUIRE(headers.at("x-b3-sampled") == std::to_string(int(priority > 0))); + REQUIRE(headers.at("x-datadog-trace-id") == "42"); + REQUIRE(headers.at("x-datadog-parent-id") == "42"); + REQUIRE(headers.at("x-datadog-sampling-priority") == "3"); + REQUIRE(headers.at("x-b3-traceid") == "000000000000002a"); + REQUIRE(headers.at("x-b3-spanid") == "000000000000002a"); + REQUIRE(headers.at("x-b3-sampled") == "1"); } SECTION("origin and trace tags") { @@ -479,8 +468,14 @@ TEST_CASE("injecting W3C traceparent header") { // Override the tracer's ID generator to always return `expected_parent_id`. constexpr std::uint64_t expected_parent_id = 0xcafebabe; - Tracer tracer{*finalized_config, [=]() { return expected_parent_id; }, - default_clock}; + struct Generator : public IDGenerator { + const std::uint64_t id; + explicit Generator(std::uint64_t id) : id(id) {} + TraceID trace_id() const override { return TraceID(id); } + std::uint64_t span_id() const override { return id; } + }; + Tracer tracer{*finalized_config, + std::make_shared(expected_parent_id)}; const std::unordered_map input_headers{ // https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers @@ -509,10 +504,14 @@ TEST_CASE("injecting W3C traceparent header") { REQUIRE(finalized_config); // Override the tracer's ID generator to always return a fixed value. - // This will be both the trace ID and the parent (root) span ID. constexpr std::uint64_t expected_id = 0xcafebabe; - Tracer tracer{*finalized_config, [=]() { return expected_id; }, - default_clock}; + struct Generator : public IDGenerator { + const std::uint64_t id; + explicit Generator(std::uint64_t id) : id(id) {} + TraceID trace_id() const override { return TraceID(id); } + std::uint64_t span_id() const override { return id; } + }; + Tracer tracer{*finalized_config, std::make_shared(expected_id)}; auto span = tracer.create_span(); @@ -690,3 +689,56 @@ TEST_CASE("injecting W3C tracestate header") { REQUIRE(logger->error_count() == 0); } + +TEST_CASE("128-bit trace ID injection") { + TracerConfig config; + config.defaults.service = "testsvc"; + config.logger = std::make_shared(); + config.trace_id_128_bit = true; + config.injection_styles.clear(); + config.injection_styles.push_back(PropagationStyle::W3C); + config.injection_styles.push_back(PropagationStyle::DATADOG); + config.injection_styles.push_back(PropagationStyle::B3); + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + + class MockIDGenerator : public IDGenerator { + const TraceID trace_id_; + + public: + explicit MockIDGenerator(TraceID trace_id) : trace_id_(trace_id) {} + TraceID trace_id() const override { return trace_id_; } + // `span_id` won't be called, because root spans use the lower part of + // `trace_id` for the span ID. + std::uint64_t span_id() const override { return 42; } + }; + + const TraceID trace_id{0xcafebabecafebabeULL, 0xdeadbeefdeadbeefULL}; + Tracer tracer{*finalized, std::make_shared(trace_id)}; + + auto span = tracer.create_span(); + span.trace_segment().override_sampling_priority(2); + MockDictWriter writer; + span.inject(writer); + + // PropagationStyle::DATADOG + auto found = writer.items.find("x-datadog-trace-id"); + REQUIRE(found != writer.items.end()); + REQUIRE(found->second == std::to_string(trace_id.low)); + found = writer.items.find("x-datadog-tags"); + REQUIRE(found != writer.items.end()); + REQUIRE(found->second.find("_dd.p.tid=deadbeefdeadbeef") != + std::string::npos); + + // PropagationStyle::W3C + found = writer.items.find("traceparent"); + REQUIRE(found != writer.items.end()); + REQUIRE(found->second == + "00-deadbeefdeadbeefcafebabecafebabe-cafebabecafebabe-01"); + + // PropagationStyle::B3 + found = writer.items.find("x-b3-traceid"); + REQUIRE(found != writer.items.end()); + REQUIRE(found->second == "deadbeefdeadbeefcafebabecafebabe"); +} diff --git a/test/test_trace_id.cpp b/test/test_trace_id.cpp new file mode 100644 index 00000000..5c2e2bb6 --- /dev/null +++ b/test/test_trace_id.cpp @@ -0,0 +1,120 @@ +// This test covers operations defined for `class TraceID` in `trace_id.h`. + +#include +#include +#include + +#include "test.h" + +using namespace datadog::tracing; + +TEST_CASE("TraceID defaults to zero") { + TraceID id1; + REQUIRE(id1.low == 0); + REQUIRE(id1.high == 0); + + TraceID id2{0xdeadbeef}; + REQUIRE(id2.low == 0xdeadbeef); + REQUIRE(id2.high == 0); +} + +TEST_CASE("TraceID parsed from hexadecimal") { + struct TestCase { + int line; + std::string input; + Optional expected_id; + Optional expected_error = nullopt; + }; + + // clang-format off + const auto test_case = GENERATE(values({ + {__LINE__, "00001", TraceID(1)}, + {__LINE__, "0000000000000000000000000000000000000000000001", TraceID(1)}, + {__LINE__, "", nullopt, Error::INVALID_INTEGER}, + {__LINE__, "nonsense", nullopt, Error::INVALID_INTEGER}, + {__LINE__, "1000000000000000000000000000000000000000000000", nullopt, Error::OUT_OF_RANGE_INTEGER}, + {__LINE__, "deadbeefdeadbeef", TraceID{0xdeadbeefdeadbeefULL}}, + {__LINE__, "0xdeadbeefdeadbeef", nullopt, Error::INVALID_INTEGER}, + {__LINE__, "cafebabecafebabedeadbeefdeadbeef", TraceID{0xdeadbeefdeadbeefULL, 0xcafebabecafebabe}}, + {__LINE__, "caxxxxxxcafebabedeadbeefdeadbeef", nullopt, Error::INVALID_INTEGER}, + {__LINE__, "cafebabecafebabedeaxxxxxxxxdbeef", nullopt, Error::INVALID_INTEGER}, + })); + // clang-format on + + CAPTURE(test_case.line); + CAPTURE(test_case.input); + const auto result = TraceID::parse_hex(test_case.input); + if (test_case.expected_error) { + REQUIRE_FALSE(result); + REQUIRE(result.error().code == *test_case.expected_error); + } else { + REQUIRE(result); + REQUIRE(*result == *test_case.expected_id); + } +} + +TEST_CASE("TraceID comparisons") { + // First, comparing integers with the `TraceID.low`. + REQUIRE(TraceID{12345} == 12345); + REQUIRE_FALSE(TraceID{12345} != 12345); + REQUIRE(TraceID{12345} != 54321); + REQUIRE_FALSE(TraceID{12345} == 54321); + REQUIRE(TraceID{6789, 12345} != 12345); + REQUIRE_FALSE(TraceID{6789, 12345} == 12345); + + // Second, comparing trace IDs with other trace IDs. + struct TestCase { + int line; + std::string name; + TraceID left; + TraceID right; + bool equal; + }; + + // clang-format off + const auto test_case = GENERATE(values({ + {__LINE__, "defaults", TraceID{}, TraceID{}, true}, + {__LINE__, "lowers equal", TraceID{0xcafebabe}, TraceID{0xcafebabe}, true}, + {__LINE__, "lowers not equal", TraceID{0xcafebabe}, TraceID{0xdeadbeef}, false}, + {__LINE__, "highers zeroness agree", TraceID{0xcafebabe, 0xdeadbeef}, TraceID{0xcafebabe, 0xdeadbeef}, true}, + {__LINE__, "highers zeroness disagree", TraceID{0xdeadbeef}, TraceID{0xcafebabe, 0xdeadbeef}, false}, + {__LINE__, "highers disagree", TraceID{0xdeadbeef, 0xdeadbeef}, TraceID{0xcafebabe, 0xdeadbeef}, false}, + })); + // clang-format on + + CAPTURE(test_case.line); + CAPTURE(test_case.name); + if (test_case.equal) { + REQUIRE(test_case.left == test_case.right); + REQUIRE_FALSE(test_case.left != test_case.right); + } else { + REQUIRE_FALSE(test_case.left == test_case.right); + REQUIRE(test_case.left != test_case.right); + } +} + +TEST_CASE("TraceID serialization") { + struct TestCase { + int line; + std::string trace_id_source; + TraceID trace_id; + std::string expected_hex; + }; + +#define CASE(TRACE_ID, HEX) \ + { __LINE__, #TRACE_ID, TRACE_ID, HEX } + // clang-format off + const auto test_case = GENERATE(values({ + CASE(TraceID(), "00000000000000000000000000000000"), + CASE(TraceID(16), "00000000000000000000000000000010"), + CASE(TraceID(0xcafebabe), "000000000000000000000000cafebabe"), + CASE(TraceID(0, 1), "00000000000000010000000000000000"), + CASE(TraceID(15, 0xcafebabe), "00000000cafebabe000000000000000f"), + })); +// clang-format on +#undef CASE + + CAPTURE(test_case.line); + CAPTURE(test_case.trace_id_source); + REQUIRE(test_case.trace_id.hex_padded() == test_case.expected_hex); +} diff --git a/test/test_trace_sampler.cpp b/test/test_trace_sampler.cpp index f99dc749..76fb469a 100644 --- a/test/test_trace_sampler.cpp +++ b/test/test_trace_sampler.cpp @@ -133,7 +133,7 @@ TEST_CASE("trace sampling rate limiter") { TimePoint current_time = default_clock(); // Modify `current_time` to advance the clock. auto clock = [¤t_time]() { return current_time; }; - Tracer tracer{*finalized, default_id_generator, clock}; + Tracer tracer{*finalized, clock}; for (std::size_t i = 0; i < test_case.burst_size; ++i) { auto span = tracer.create_span(); diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 0ad69d04..3bf09902 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -2,15 +2,18 @@ // spans and for extracting spans from propagated trace context. #include +#include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -261,6 +264,7 @@ TEST_CASE("span extraction") { SECTION("extraction failures") { struct TestCase { + int line; std::string name; std::vector extraction_styles; std::unordered_map headers; @@ -269,74 +273,94 @@ TEST_CASE("span extraction") { }; auto test_case = GENERATE(values({ - {"no span", {PropagationStyle::DATADOG}, {}, Error::NO_SPAN_TO_EXTRACT}, - {"missing trace ID", + {__LINE__, + "no span", + {PropagationStyle::DATADOG}, + {}, + Error::NO_SPAN_TO_EXTRACT}, + {__LINE__, + "missing trace ID", {PropagationStyle::DATADOG}, {{"x-datadog-parent-id", "456"}}, Error::MISSING_TRACE_ID}, - {"missing parent span ID", + {__LINE__, + "missing parent span ID", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "123"}}, Error::MISSING_PARENT_SPAN_ID}, - {"missing parent span ID, but it's ok because origin", + {__LINE__, + "missing parent span ID, but it's ok because origin", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "123"}, {"x-datadog-origin", "anything"}}, nullopt}, - {"bad x-datadog-trace-id", + {__LINE__, + "bad x-datadog-trace-id", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "f"}, {"x-datadog-parent-id", "456"}}, Error::INVALID_INTEGER}, - {"bad x-datadog-trace-id (2)", + {__LINE__, + "bad x-datadog-trace-id (2)", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "99999999999999999999999999"}, {"x-datadog-parent-id", "456"}}, Error::OUT_OF_RANGE_INTEGER}, - {"bad x-datadog-parent-id", + {__LINE__, + "bad x-datadog-parent-id", {PropagationStyle::DATADOG}, {{"x-datadog-parent-id", "f"}, {"x-datadog-trace-id", "456"}}, Error::INVALID_INTEGER}, - {"bad x-datadog-parent-id (2)", + {__LINE__, + "bad x-datadog-parent-id (2)", {PropagationStyle::DATADOG}, {{"x-datadog-parent-id", "99999999999999999999999999"}, {"x-datadog-trace-id", "456"}}, Error::OUT_OF_RANGE_INTEGER}, - {"bad x-datadog-sampling-priority", + {__LINE__, + "bad x-datadog-sampling-priority", {PropagationStyle::DATADOG}, {{"x-datadog-parent-id", "123"}, {"x-datadog-trace-id", "456"}, {"x-datadog-sampling-priority", "keep"}}, Error::INVALID_INTEGER}, - {"bad x-datadog-sampling-priority (2)", + {__LINE__, + "bad x-datadog-sampling-priority (2)", {PropagationStyle::DATADOG}, {{"x-datadog-parent-id", "123"}, {"x-datadog-trace-id", "456"}, {"x-datadog-sampling-priority", "99999999999999999999999999"}}, Error::OUT_OF_RANGE_INTEGER}, - {"bad x-b3-traceid", + {__LINE__, + "bad x-b3-traceid", {PropagationStyle::B3}, {{"x-b3-traceid", "0xdeadbeef"}, {"x-b3-spanid", "def"}}, Error::INVALID_INTEGER}, - {"bad x-b3-traceid (2)", + {__LINE__, + "bad x-b3-traceid (2)", {PropagationStyle::B3}, - {{"x-b3-traceid", "ffffffffffffffffffffffffffffff"}, + {{"x-b3-traceid", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, {"x-b3-spanid", "def"}}, Error::OUT_OF_RANGE_INTEGER}, - {"bad x-b3-spanid", + {__LINE__, + "bad x-b3-spanid", {PropagationStyle::B3}, {{"x-b3-spanid", "0xdeadbeef"}, {"x-b3-traceid", "def"}}, Error::INVALID_INTEGER}, - {"bad x-b3-spanid (2)", + {__LINE__, + "bad x-b3-spanid (2)", {PropagationStyle::B3}, {{"x-b3-spanid", "ffffffffffffffffffffffffffffff"}, {"x-b3-traceid", "def"}}, Error::OUT_OF_RANGE_INTEGER}, - {"bad x-b3-sampled", + {__LINE__, + "bad x-b3-sampled", {PropagationStyle::B3}, {{"x-b3-traceid", "abc"}, {"x-b3-spanid", "def"}, {"x-b3-sampled", "true"}}, Error::INVALID_INTEGER}, - {"bad x-b3-sampled (2)", + {__LINE__, + "bad x-b3-sampled (2)", {PropagationStyle::B3}, {{"x-b3-traceid", "abc"}, {"x-b3-spanid", "def"}, @@ -344,6 +368,7 @@ TEST_CASE("span extraction") { Error::OUT_OF_RANGE_INTEGER}, })); + CAPTURE(test_case.line); CAPTURE(test_case.name); config.extraction_styles = test_case.extraction_styles; @@ -377,50 +402,57 @@ TEST_CASE("span extraction") { SECTION("extracted span has the expected properties") { struct TestCase { + int line; std::string name; std::vector extraction_styles; std::unordered_map headers; - std::uint64_t expected_trace_id; + TraceID expected_trace_id; Optional expected_parent_id; Optional expected_sampling_priority; }; auto test_case = GENERATE(values({ - {"datadog style", + {__LINE__, + "datadog style", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "123"}, {"x-datadog-parent-id", "456"}, {"x-datadog-sampling-priority", "2"}}, - 123, + TraceID(123), 456, 2}, - {"datadog style without sampling priority", + {__LINE__, + "datadog style without sampling priority", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "123"}, {"x-datadog-parent-id", "456"}}, - 123, + TraceID(123), 456, nullopt}, - {"datadog style without sampling priority and without parent ID", + {__LINE__, + "datadog style without sampling priority and without parent ID", {PropagationStyle::DATADOG}, {{"x-datadog-trace-id", "123"}, {"x-datadog-origin", "whatever"}}, - 123, + TraceID(123), nullopt, nullopt}, - {"B3 style", + {__LINE__, + "B3 style", {PropagationStyle::B3}, {{"x-b3-traceid", "abc"}, {"x-b3-spanid", "def"}, {"x-b3-sampled", "0"}}, - 0xabc, + TraceID(0xabc), 0xdef, 0}, - {"B3 style without sampling priority", + {__LINE__, + "B3 style without sampling priority", {PropagationStyle::B3}, {{"x-b3-traceid", "abc"}, {"x-b3-spanid", "def"}}, - 0xabc, + TraceID(0xabc), 0xdef, nullopt}, - {"Datadog overriding B3", + {__LINE__, + "Datadog overriding B3", {PropagationStyle::DATADOG, PropagationStyle::B3}, {{"x-datadog-trace-id", "255"}, {"x-datadog-parent-id", "14"}, @@ -428,32 +460,36 @@ TEST_CASE("span extraction") { {"x-b3-traceid", "fff"}, {"x-b3-spanid", "ef"}, {"x-b3-sampled", "0"}}, - 255, + TraceID(255), 14, 0}, - {"Datadog overriding B3, without sampling priority", + {__LINE__, + "Datadog overriding B3, without sampling priority", {PropagationStyle::DATADOG, PropagationStyle::B3}, {{"x-datadog-trace-id", "255"}, {"x-datadog-parent-id", "14"}, {"x-b3-traceid", "fff"}, {"x-b3-spanid", "ef"}}, - 255, + TraceID(255), 14, nullopt}, - {"B3 after Datadog found no context", + {__LINE__, + "B3 after Datadog found no context", {PropagationStyle::DATADOG, PropagationStyle::B3}, {{"x-b3-traceid", "ff"}, {"x-b3-spanid", "e"}}, - 0xff, + TraceID(0xff), 0xe, nullopt}, - {"Datadog after B3 found no context", + {__LINE__, + "Datadog after B3 found no context", {PropagationStyle::B3, PropagationStyle::DATADOG}, {{"x-b3-traceid", "fff"}, {"x-b3-spanid", "ef"}}, - 0xfff, + TraceID(0xfff), 0xef, nullopt}, })); + CAPTURE(test_case.line); CAPTURE(test_case.name); config.extraction_styles = test_case.extraction_styles; @@ -519,8 +555,7 @@ TEST_CASE("span extraction") { std::string name; Optional traceparent; Optional expected_error_tag_value = {}; - Optional expected_full_trace_id = {}; - Optional expected_trace_id = {}; + Optional expected_trace_id = {}; Optional expected_parent_id = {}; Optional expected_sampling_priority = {}; }; @@ -531,46 +566,42 @@ TEST_CASE("span extraction") { {__LINE__, "valid: w3.org example 1", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", // traceparent nullopt, - "4bf92f3577b34da6a3ce929d0e0e4736", // expected_full_trace_id - 11803532876627986230ULL, // expected_trace_id + *TraceID::parse_hex("4bf92f3577b34da6a3ce929d0e0e4736"), // expected_trace_id 67667974448284343ULL, // expected_parent_id 1}, // expected_sampling_priority {__LINE__, "valid: w3.org example 2", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", // traceparent nullopt, - "4bf92f3577b34da6a3ce929d0e0e4736", // expected_full_trace_id - 11803532876627986230ULL, // expected_trace_id + *TraceID::parse_hex("4bf92f3577b34da6a3ce929d0e0e4736"), // expected_trace_id 67667974448284343ULL, // expected_parent_id 0}, // expected_sampling_priority {__LINE__, "valid: future version", "06-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", // traceparent nullopt, - "4bf92f3577b34da6a3ce929d0e0e4736", // expected_full_trace_id - 11803532876627986230ULL, // expected_trace_id + *TraceID::parse_hex("4bf92f3577b34da6a3ce929d0e0e4736"), // expected_trace_id 67667974448284343ULL, // expected_parent_id 0}, // expected_sampling_priority {__LINE__, "valid: future version with extra fields", "06-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00-af-delta", // traceparent nullopt, - "4bf92f3577b34da6a3ce929d0e0e4736", // expected_full_trace_id - 11803532876627986230ULL, // expected_trace_id + *TraceID::parse_hex("4bf92f3577b34da6a3ce929d0e0e4736"), // expected_trace_id 67667974448284343ULL, // expected_parent_id 0}, // expected_sampling_priority - + {__LINE__, "no traceparent", nullopt}, // traceparent - + {__LINE__, "invalid: not enough fields", "06-4bf92f3577b34da6a3ce929d0e0e4736", // traceparent "malformed_traceparent"}, // expected_error_tag_value - + {__LINE__, "invalid: missing hyphen", "064bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", // traceparent "malformed_traceparent"}, // expected_error_tag_value - + {__LINE__, "invalid: extra data not preceded by hyphen", "06-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00af-delta", // traceparent "malformed_traceparent"}, // expected_error_tag_value @@ -687,7 +718,7 @@ TEST_CASE("span extraction") { traceparent_drop, // traceparent "", // tracestate 0}, // expected_sampling_priority - + {__LINE__, "no dd entry", traceparent_drop, // traceparent "foo=hello,@thingy/thing=wah;wah;wah", // tracestate @@ -696,7 +727,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "foo=hello,@thingy/thing=wah;wah;wah", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "empty entry", traceparent_drop, // traceparent "foo=hello,,bar=thing", // tracestate @@ -705,7 +736,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "foo=hello,,bar=thing", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "malformed entry", traceparent_drop, // traceparent "foo=hello,chicken,bar=thing", // tracestate @@ -714,7 +745,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "foo=hello,chicken,bar=thing", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "stuff before dd entry", traceparent_drop, // traceparent "foo=hello,bar=baz,dd=", // tracestate @@ -723,7 +754,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "foo=hello,bar=baz", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "stuff after dd entry", traceparent_drop, // traceparent "dd=,foo=hello,bar=baz", // tracestate @@ -732,7 +763,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "foo=hello,bar=baz", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "stuff before and after dd entry", traceparent_drop, // traceparent "chicken=yes,nuggets=yes,dd=,foo=hello,bar=baz", // tracestate @@ -741,7 +772,7 @@ TEST_CASE("span extraction") { {}, // expected_trace_tags "chicken=yes,nuggets=yes,foo=hello,bar=baz", // expected_additional_w3c_tracestate nullopt}, // expected_additional_datadog_w3c_tracestate - + {__LINE__, "dd entry with empty subentries", traceparent_drop, // traceparent "dd=foo:bar;;;;;baz:bam;;;", // tracestate @@ -769,6 +800,15 @@ TEST_CASE("span extraction") { nullopt, // expected_additional_w3c_tracestate "x:wow;y:wow"}, // expected_additional_datadog_w3c_tracestate + {__LINE__, "_dd.p.tid trace tag is ignored", + traceparent_drop, // traceparent + "dd=t.tid:deadbeef;t.foo:bar", // tracestate + 0, // expected_sampling_priority + nullopt, // expected_origin + {{"_dd.p.foo", "bar"}}, // expected_trace_tags + nullopt, // expected_additional_w3c_tracestate + nullopt}, // expected_additional_datadog_w3c_tracestate + {__LINE__, "traceparent and tracestate sampling agree (1/4)", traceparent_drop, // traceparent "dd=s:0", // tracestate @@ -808,12 +848,12 @@ TEST_CASE("span extraction") { traceparent_keep, // traceparent "dd=s:-1", // tracestate 1}, // expected_sampling_priority - + {__LINE__, "invalid sampling priority (1/2)", traceparent_drop, // traceparent "dd=s:oops", // tracestate 0}, // expected_sampling_priority - + {__LINE__, "invalid sampling priority (2/2)", traceparent_keep, // traceparent "dd=s:oops", // tracestate @@ -907,3 +947,81 @@ TEST_CASE("report hostname") { REQUIRE(tracer.create_span().trace_segment().hostname() == get_hostname()); } } + +TEST_CASE("create 128-bit trace IDs") { + TracerConfig config; + config.defaults.service = "testsvc"; + config.trace_id_128_bit = true; + const auto collector = std::make_shared(); + config.collector = collector; + const auto logger = std::make_shared(); + config.logger = logger; + config.extraction_styles.clear(); + config.extraction_styles.push_back(PropagationStyle::W3C); + config.extraction_styles.push_back(PropagationStyle::DATADOG); + config.extraction_styles.push_back(PropagationStyle::B3); + const auto finalized = finalize_config(config); + REQUIRE(finalized); + Tracer tracer{*finalized}; + + SECTION("are generated") { + const auto span = tracer.create_span(); + // The chance that it's zero is ~2**(-64), which I'm willing to neglect. + REQUIRE(span.trace_id().high != 0); + } + + SECTION("result in _dd.p.tid trace tag being sent to collector") { + TraceID generated_id; + { + const auto span = tracer.create_span(); + generated_id = span.trace_id(); + } + CAPTURE(logger->entries); + REQUIRE(logger->error_count() == 0); + REQUIRE(collector->span_count() == 1); + const auto& span = collector->first_span(); + const auto found = span.tags.find(tags::internal::trace_id_high); + REQUIRE(found != span.tags.end()); + const auto high = parse_uint64(found->second, 16); + REQUIRE(high); + REQUIRE(*high == generated_id.high); + } + + SECTION("extracted from W3C") { + std::unordered_map headers; + headers["traceparent"] = + "00-deadbeefdeadbeefcafebabecafebabe-0000000000000001-01"; + 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"); + } + + SECTION("extracted from Datadog (_dd.p.tid)") { + std::unordered_map headers; + headers["x-datadog-trace-id"] = "4"; + headers["x-datadog-parent-id"] = "42"; + headers["x-datadog-tags"] = "_dd.p.tid=beef"; + MockDictReader reader{headers}; + const auto span = tracer.extract_span(reader); + CAPTURE(logger->entries); + REQUIRE(logger->error_count() == 0); + REQUIRE(span); + REQUIRE(span->trace_id().hex_padded() == + "000000000000beef0000000000000004"); + } + + SECTION("extracted from B3") { + std::unordered_map headers; + headers["x-b3-traceid"] = "deadbeefdeadbeefcafebabecafebabe"; + headers["x-b3-spanid"] = "42"; + 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"); + } +} diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index ac3da999..e354cc4d 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -96,13 +96,16 @@ class SomewhatSecureTemporaryFile : public std::fstream { SomewhatSecureTemporaryFile() try { namespace fs = std::filesystem; + const auto generator = default_id_generator(false); + const auto random = [&]() { return generator->span_id(); }; + // The goal is to create a file whose name is like // "/tmp/342394898324/239489029034", where the directory under /tmp has // permissions such that only the current user can read/write/cd it. const auto tmp = fs::temp_directory_path(); const int max_attempts = 5; for (int i = 0; i < max_attempts; ++i) { - const auto dir = tmp / std::to_string(default_id_generator()); + const auto dir = tmp / std::to_string(random()); std::error_code err; if (!fs::create_directory(dir, err)) { continue; @@ -111,7 +114,7 @@ class SomewhatSecureTemporaryFile : public std::fstream { if (err) { continue; } - const auto file = dir / std::to_string(default_id_generator()); + const auto file = dir / std::to_string(random()); if (fs::exists(file, err) || err) { continue; } @@ -1192,3 +1195,55 @@ TEST_CASE("TracerConfig propagation styles") { } } } + +TEST_CASE("configure 128-bit trace IDs") { + TracerConfig config; + config.defaults.service = "testsvc"; + + SECTION("defaults to false") { REQUIRE(config.trace_id_128_bit == false); } + + SECTION("value honored in finalizer") { + const auto value = GENERATE(true, false); + config.trace_id_128_bit = value; + const auto finalized = finalize_config(config); + REQUIRE(finalized); + REQUIRE(finalized->trace_id_128_bit == value); + } + + SECTION("value overridden by DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED") { + struct TestCase { + int line; + std::string env_value; + bool expected_value; + }; + + // clang-format off + const auto test_case = GENERATE(values({ + {__LINE__, "true", true}, + {__LINE__, "false", false}, + {__LINE__, "no", false}, + {__LINE__, "nein", true}, + {__LINE__, "0", false}, + {__LINE__, "", true}, + })); + // clang-format on + + CAPTURE(test_case.line); + CAPTURE(test_case.env_value); + + EnvGuard guard{"DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED", + test_case.env_value}; + + config.trace_id_128_bit = true; + CAPTURE(config.trace_id_128_bit); + auto finalized = finalize_config(config); + REQUIRE(finalized); + REQUIRE(finalized->trace_id_128_bit == test_case.expected_value); + + config.trace_id_128_bit = false; + CAPTURE(config.trace_id_128_bit); + finalized = finalize_config(config); + REQUIRE(finalized); + REQUIRE(finalized->trace_id_128_bit == test_case.expected_value); + } +}