diff --git a/BUILD.bazel b/BUILD.bazel index e46dac60..9fecbd67 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -5,6 +5,7 @@ cc_library( "src/datadog/telemetry/metrics.cpp", "src/datadog/telemetry/log.h", "src/datadog/telemetry/telemetry.cpp", + "src/datadog/baggage.cpp", "src/datadog/base64.cpp", "src/datadog/cerr_logger.cpp", "src/datadog/clock.cpp", @@ -78,6 +79,7 @@ cc_library( "src/datadog/w3c_propagation.h", ], hdrs = [ + "include/datadog/baggage.h", "include/datadog/cerr_logger.h", "include/datadog/clock.h", "include/datadog/collector.h", diff --git a/CMakeLists.txt b/CMakeLists.txt index 419cb0af..92ef08c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ target_sources(dd_trace_cpp-objects src/datadog/telemetry/configuration.cpp src/datadog/telemetry/metrics.cpp src/datadog/telemetry/telemetry.cpp + src/datadog/baggage.cpp src/datadog/base64.cpp src/datadog/cerr_logger.cpp src/datadog/clock.cpp diff --git a/Dockerfile b/Dockerfile index c8fb423e..03cfb4aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,3 +36,5 @@ run chmod +x /tmp/install-cmake && /tmp/install-cmake && rm /tmp/install-cmake copy bin/install-lcov /tmp/install-lcov run chmod +x /tmp/install-lcov && /tmp/install-lcov && rm /tmp/install-lcov +run curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/main/install.sh | bash + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d96e803c..52983aca 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(baggage) add_subdirectory(hasher) add_subdirectory(http-server) diff --git a/examples/baggage/CMakeLists.txt b/examples/baggage/CMakeLists.txt new file mode 100644 index 00000000..5b36932c --- /dev/null +++ b/examples/baggage/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(baggage-example main.cpp) +target_link_libraries(baggage-example dd_trace_cpp-static) diff --git a/examples/baggage/main.cpp b/examples/baggage/main.cpp new file mode 100644 index 00000000..da981238 --- /dev/null +++ b/examples/baggage/main.cpp @@ -0,0 +1,95 @@ +#include +#include + +#include + +namespace dd = datadog::tracing; + +struct CinReader : public dd::DictReader { + std::string input; + + dd::Optional lookup(dd::StringView key) const override { + return input; + } + + void visit( + const std::function& + visitor) const override{}; +}; + +std::istream& operator>>(std::istream& is, CinReader& reader) { + is >> reader.input; + return is; +} + +std::ostream& operator<<(std::ostream& os, dd::Baggage::Error error) { + using dd::Baggage; + switch (error.code) { + case Baggage::Error::MISSING_HEADER: + os << "missing `baggage` header"; + break; + case Baggage::Error::MALFORMED_BAGGAGE_HEADER: { + os << "malformed `baggage` header"; + if (error.pos) { + os << " at position " << *error.pos; + } + } break; + case Baggage::Error::MAXIMUM_CAPACITY_REACHED: + os << "maximum number of bagge items reached"; + break; + case Baggage::Error::MAXIMUM_BYTES_REACHED: + os << "maximum amount of bytes written"; + break; + default: + os << "unknown error code"; + break; + } + return os; +} + +int main() { + dd::TracerConfig cfg; + cfg.log_on_startup = false; + cfg.telemetry.enabled = false; + cfg.agent.remote_configuration_enabled = false; + const auto finalized_cfg = datadog::tracing::finalize_config(cfg); + if (auto error = finalized_cfg.if_error()) { + std::cerr << "Failed to initialize the tracer: " << error->message + << std::endl; + return error->code; + } + + dd::Tracer tracer(*finalized_cfg); + + std::cout + << "This program demonstrates how to use baggage, a feature that allows " + "metadata (key-value pairs) to be attached to a request and " + "propagated across services.\n" + "Baggage can be useful for passing contextual information, such as " + "user IDs, session tokens, or request attributes, between different " + "components of a distributed system.\n\n" + "This example lets you input baggage values, validate them and " + "displays the baggage content parsed.\n" + "You can enter baggage manually or provide it through a file, try:\n" + "- k1=v1,k2=v2\n" + "- ,invalid=input\n" + "or ./baggage-example < list-of-baggages.txt\n\n"; + + CinReader reader; + std::cout << "Enter baggage (or 'CTRL+C' to quit): "; + while (std::getline(std::cin, reader.input)) { + auto baggage = tracer.extract_baggage(reader); + if (!baggage) { + std::cout << "Error parsing \"" << reader.input + << "\": " << baggage.error() << ".\n"; + } else { + std::cout << "Baggage key-value parsed: \n"; + baggage->visit([](dd::StringView key, dd::StringView value) { + std::cout << key << ": " << value << std::endl; + }); + } + + std::cout << "\nEnter baggage (or 'CTRL+C' to quit): "; + } + return 0; +} diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt index 72a3a267..23e618da 100644 --- a/fuzz/CMakeLists.txt +++ b/fuzz/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(base64) +add_subdirectory(tracing) add_subdirectory(w3c-propagation) diff --git a/fuzz/tracing/CMakeLists.txt b/fuzz/tracing/CMakeLists.txt new file mode 100644 index 00000000..9708969c --- /dev/null +++ b/fuzz/tracing/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(baggage-fuzz baggage.cpp) + +add_dependencies(baggage-fuzz dd_trace_cpp-static) + +target_include_directories(baggage-fuzz + PRIVATE + ${CMAKE_SOURCE_DIR}/src +) + +target_link_libraries(baggage-fuzz dd_trace_cpp-static) + +add_target_to_group(baggage-fuzz dd_trace_cpp-fuzzers) diff --git a/fuzz/tracing/baggage.cpp b/fuzz/tracing/baggage.cpp new file mode 100644 index 00000000..94856543 --- /dev/null +++ b/fuzz/tracing/baggage.cpp @@ -0,0 +1,33 @@ +#include +#include +#include + +#include + +namespace dd = datadog::tracing; + +class MapReader : public dd::DictReader { + std::unordered_map map_; + + public: + ~MapReader() override = default; + + MapReader(std::unordered_map map) + : map_(std::move(map)) {} + + dd::Optional lookup(dd::StringView key) const override { + auto it = map_.find(std::string(key)); + if (it == map_.cend()) return dd::nullopt; + + return it->second; + } + + void visit(const std::function&) const override{}; +}; + +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, size_t size) { + MapReader reader({{"baggage", std::string((const char*)data, size)}}); + dd::Baggage::extract(reader); + return 0; +} diff --git a/include/datadog/baggage.h b/include/datadog/baggage.h new file mode 100644 index 00000000..49df4b1d --- /dev/null +++ b/include/datadog/baggage.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace datadog { +namespace tracing { + +/// OpenTelemetry-like implementation of the Baggage concept. +/// Baggage is a key-value store meant to propagate data across services and +/// processes boundaries. +/// +/// Baggage are extracted from any tracing context implementing the `DictReader` +/// interface using `Baggage::extract`. +/// +/// Baggages are injected to any tracing context implementing the `DictWriter` +/// interface using the `inject` method. +class Baggage { + public: + struct Error final { + enum Code : char { + /// Baggage propagation is disabled. This may be due to one of the + /// following + /// reasons: + /// - `baggage` is not set as an extraction or injection propagation + /// style. + /// - The maximum number of items is less than 0. + /// - The number of bytes is less than 3. + DISABLED, + MISSING_HEADER, + MALFORMED_BAGGAGE_HEADER, + MAXIMUM_CAPACITY_REACHED, + MAXIMUM_BYTES_REACHED, + }; + Code code; + Optional pos; + + Error(Code in_code) : code(in_code), pos(nullopt) {} + Error(Code in_code, size_t position) : code(in_code), pos(position) {} + }; + + struct Options final { + size_t max_bytes; + size_t max_items; + }; + + static constexpr size_t default_max_capacity = 64; + static constexpr Options default_options{2048, default_max_capacity}; + + /// Extracts a Baggage instance from a `DictReader` and creates a Baggage + /// instance if no errors are encounters . + /// + /// @param `reader` The input `DictReader` from which to extract the data. + /// @return A `Baggage` instance or an `Error`. + static Expected extract(const DictReader& reader); + + /// Initializes an empty Baggage with the default maximum capacity. + Baggage() = default; + + /// Initializes an empty Baggage instance with the given maximum capacity. + /// + /// @param `max_capacity` The maximum capacity for this Baggage instance. + Baggage(size_t max_capacity); + + /// Initializes a Baggage instance using the provided unordered_map of + /// key-value pairs. The maximum capacity can also be specified. + /// + /// @param `baggage_map` The map containing key-value pairs to initialize the + /// Baggage. + /// @param `max_capacity` The maximum capacity for this Baggage instance. + Baggage(std::unordered_map, + size_t max_capacity = default_max_capacity); + + /// Checks if the Baggage contains a specified key. + /// + /// @param `key` The key to check. + /// @return `true` if the key exists in the Baggage; otherwise, `false`. + bool contains(StringView key) const; + + /// Retrieves the value associated with a specified key. + /// + /// @param `key` The key to retrieve the value for. + /// @return An `Optional` containing the value if the key exists, + /// or an empty Optional if the key is not found. + Optional get(StringView key) const; + + /// Adds a key-value pair to the Baggage. + /// + /// This function will attempt to add the given key-value pair to the Baggage. + /// If the maximum capacity has been reached, the insertion will fail. + /// If a `key` already exists, its value will be overwritten with `value`. + /// + /// @param `key` The key to insert. + /// @param `value` The value to associate with the key. + /// @return `true` if the key-value pair was successfully added; `false` if + /// the maximum capacity was reached. + bool set(std::string key, std::string value); + + /// Removes the key-value pair corresponding to the specified key. + /// + /// @param `key` The key to remove from the Baggage. + void remove(StringView key); + + /// Removes all key-value pair. + void clear(); + + /// Retrieves the number of items stored. + size_t size() const; + + /// Returns whether any items are stored. + bool empty() const; + + /// Visits each key-value pair in the Baggage and invoke the provided + /// visitor function for each key-value pair in the Baggage. + /// + /// @param `visitor` A function object that will be called for each + /// key-value pair. + void visit(std::function&& visitor); + + /// Injects the Baggage data into a `DictWriter` with the constraint that + /// the amount of bytes written does not exceed the specified maximum byte + /// limit. + /// + /// @param `writer` The DictWriter to inject the data into. + /// @param `opts` Injection options. + /// @return An `Expected`, which may either succeed or contain an + /// error. + Expected inject(DictWriter& writer, + const Options& opts = default_options) const; + + /// Equality operator for comparing two Baggage instances. + inline bool operator==(const Baggage& rhs) const { + return baggage_ == rhs.baggage_; + } + + private: + const size_t max_capacity_ = Baggage::default_max_capacity; + std::unordered_map baggage_; +}; + +} // namespace tracing +} // namespace datadog diff --git a/include/datadog/config.h b/include/datadog/config.h index 18b73031..df017e54 100644 --- a/include/datadog/config.h +++ b/include/datadog/config.h @@ -25,6 +25,8 @@ enum class ConfigName : char { TRACE_SAMPLING_LIMIT, TRACE_SAMPLING_RULES, SPAN_SAMPLING_RULES, + TRACE_BAGGAGE_MAX_BYTES, + TRACE_BAGGAGE_MAX_ITEMS, }; // Represents metadata for configuration parameters diff --git a/include/datadog/environment.h b/include/datadog/environment.h index 19d734ec..8fd9a32e 100644 --- a/include/datadog/environment.h +++ b/include/datadog/environment.h @@ -55,6 +55,8 @@ namespace environment { MACRO(DD_TELEMETRY_METRICS_ENABLED) \ MACRO(DD_TELEMETRY_METRICS_INTERVAL_SECONDS) \ MACRO(DD_TELEMETRY_DEBUG) \ + MACRO(DD_TRACE_BAGGAGE_MAX_ITEMS) \ + MACRO(DD_TRACE_BAGGAGE_MAX_BYTES) \ MACRO(DD_TELEMETRY_LOG_COLLECTION_ENABLED) #define WITH_COMMA(ARG) ARG, diff --git a/include/datadog/error.h b/include/datadog/error.h index bb7b88c6..a0c103a2 100644 --- a/include/datadog/error.h +++ b/include/datadog/error.h @@ -76,6 +76,8 @@ struct Error { DATADOG_AGENT_INVALID_REMOTE_CONFIG_POLL_INTERVAL = 51, SAMPLING_DELEGATION_RESPONSE_INVALID_JSON = 52, REMOTE_CONFIGURATION_INVALID_INPUT = 53, + BAGGAGE_MAXIMUM_BYTES_REACHED = 54, + BAGGAGE_MAXIMUM_ITEMS_REACHED = 55, }; Code code; diff --git a/include/datadog/expected.h b/include/datadog/expected.h index 3b7f3e6f..14cc5e0e 100644 --- a/include/datadog/expected.h +++ b/include/datadog/expected.h @@ -45,7 +45,7 @@ namespace datadog { namespace tracing { -template +template class Expected { std::variant data_; @@ -98,93 +98,93 @@ class Expected { const Error* if_error() const&& = delete; }; -template -Expected::Expected(const Value& value) : data_(value) {} +template +Expected::Expected(const Value& value) : data_(value) {} -template -Expected::Expected(Value&& value) : data_(std::move(value)) {} +template +Expected::Expected(Value&& value) : data_(std::move(value)) {} -template -Expected::Expected(const Error& error) : data_(error) {} +template +Expected::Expected(const Error& error) : data_(error) {} -template -Expected::Expected(Error&& error) : data_(std::move(error)) {} +template +Expected::Expected(Error&& error) : data_(std::move(error)) {} -template -bool Expected::has_value() const noexcept { +template +bool Expected::has_value() const noexcept { return std::holds_alternative(data_); } -template -Expected::operator bool() const noexcept { +template +Expected::operator bool() const noexcept { return has_value(); } -template -Value& Expected::value() & { +template +Value& Expected::value() & { return std::get<0>(data_); } -template -const Value& Expected::value() const& { +template +const Value& Expected::value() const& { return std::get<0>(data_); } -template -Value&& Expected::value() && { +template +Value&& Expected::value() && { return std::move(std::get<0>(data_)); } -template -const Value&& Expected::value() const&& { +template +const Value&& Expected::value() const&& { return std::move(std::get<0>(data_)); } -template -Value& Expected::operator*() & { +template +Value& Expected::operator*() & { return value(); } -template -const Value& Expected::operator*() const& { +template +const Value& Expected::operator*() const& { return value(); } -template -Value&& Expected::operator*() && { +template +Value&& Expected::operator*() && { return std::move(value()); } -template -const Value&& Expected::operator*() const&& { +template +const Value&& Expected::operator*() const&& { return std::move(value()); } -template -Value* Expected::operator->() { +template +Value* Expected::operator->() { return &value(); } -template -const Value* Expected::operator->() const { +template +const Value* Expected::operator->() const { return &value(); } -template -Error& Expected::error() & { +template +Error& Expected::error() & { return std::get<1>(data_); } -template -const Error& Expected::error() const& { +template +const Error& Expected::error() const& { return std::get<1>(data_); } -template -Error&& Expected::error() && { +template +Error&& Expected::error() && { return std::move(std::get<1>(data_)); } -template -const Error&& Expected::error() const&& { +template +const Error&& Expected::error() const&& { return std::move(std::get<1>(data_)); } -template -Error* Expected::if_error() & { +template +Error* Expected::if_error() & { return std::get_if<1>(&data_); } -template -const Error* Expected::if_error() const& { +template +const Error* Expected::if_error() const& { return std::get_if<1>(&data_); } diff --git a/include/datadog/propagation_style.h b/include/datadog/propagation_style.h index e16a994d..075be151 100644 --- a/include/datadog/propagation_style.h +++ b/include/datadog/propagation_style.h @@ -22,6 +22,8 @@ enum class PropagationStyle { // propagation is disabled in the relevant direction (extraction or // injection). NONE, + // Baggage header + BAGGAGE, }; StringView to_string_view(PropagationStyle style); diff --git a/include/datadog/tracer.h b/include/datadog/tracer.h index 949b19b4..a6e758df 100644 --- a/include/datadog/tracer.h +++ b/include/datadog/tracer.h @@ -13,11 +13,13 @@ #include #include +#include "baggage.h" #include "clock.h" #include "expected.h" #include "id_generator.h" #include "optional.h" #include "span.h" +#include "span_config.h" #include "tracer_config.h" #include "tracer_signature.h" @@ -52,6 +54,9 @@ class Tracer { // read to determine if the process is instrumented with a tracer and to // retrieve relevant tracing information. std::shared_ptr metadata_file_; + Baggage::Options baggage_opts_; + bool baggage_injection_enabled_; + bool baggage_extraction_enabled_; public: // Create a tracer configured using the specified `config`, and optionally: @@ -83,8 +88,23 @@ class Tracer { Span extract_or_create_span(const DictReader& reader, const SpanConfig& config); - // Return a JSON object describing this Tracer's configuration. It is the same - // JSON object that was logged when this Tracer was created. + // Create a baggage. + Baggage create_baggage(); + + // Return the extracted baggage from the specified `reader`. + // An error is returned if an error occurs during extraction. + Expected extract_baggage(const DictReader& reader); + + // Return the extracted baggage from the specified `reader`, or an empty + // baggage is there is no baggage to extract if an error occurs during + // extraction. + Baggage extract_or_create_baggage(const DictReader& reader); + + // Inject baggage into the specified `reader`. + Expected inject(const Baggage& baggage, DictWriter& writer); + + // Return a JSON object describing this Tracer's configuration. It is the + // same JSON object that was logged when this Tracer was created. std::string config() const; private: diff --git a/include/datadog/tracer_config.h b/include/datadog/tracer_config.h index 3752cb26..d32922a7 100644 --- a/include/datadog/tracer_config.h +++ b/include/datadog/tracer_config.h @@ -11,6 +11,7 @@ #include #include +#include "baggage.h" #include "clock.h" #include "datadog_agent_config.h" #include "expected.h" @@ -160,6 +161,11 @@ struct TracerConfig { // programmatic value in Datadog's Active Configuration, whereas it is // actually the default value for the integration. Optional report_service_as_default; + /// The maximum number of baggage items that can be stored or propagated. + Optional baggage_max_items; + /// The maximum amount of bytes allowed to be written during tracing context + /// injection. + Optional baggage_max_bytes; }; // `FinalizedTracerConfig` contains `Tracer` implementation details derived from @@ -196,6 +202,7 @@ class FinalizedTracerConfig final { bool delegate_trace_sampling; bool report_traces; std::unordered_map metadata; + Baggage::Options baggage_opts; }; // Return a `FinalizedTracerConfig` from the specified `config` and from any diff --git a/src/datadog/baggage.cpp b/src/datadog/baggage.cpp new file mode 100644 index 00000000..a0a8f6e7 --- /dev/null +++ b/src/datadog/baggage.cpp @@ -0,0 +1,284 @@ +#include + +namespace datadog { +namespace tracing { + +namespace { + +/// Whitespace in RFC 7230 section 3.2.3 definition +/// BDNF: +/// - OWS = *(SP / HTAB) +/// - SP = SPACE (0x20) +/// - HTAB = Horizontal tab (0x09) +constexpr bool is_whitespace(char c) { return c == 0x20 || c == 0x09; } + +constexpr bool is_allowed_key_char(char c) { + // clang-format off + return (c >= 0x30 && c <= 0x39) ///< [0-9] + || (c >= 0x41 && c <= 0x5A) ///< [a-z] + || (c >= 0x61 && c <= 0x7A) ///< [A-Z] + || (c == 0x21) ///< "!" + || (c >= 0x23 && c <= 0x27) ///< "#" / "$" / "%" / "&" / "'" + || (c == 0x2A) ///< "*" + || (c == 0x2B) ///< "+" + || (c == 0x2D) ///< "-" + || (c == 0x2E) ///< "." + || (c == 0x5E) ///< "^" + || (c == 0x5F) ///< "_" + || (c == 0x60) ///< "`" + || (c == 0x7C) ///< "|" + || (c == 0x7E); ///< "~" + // clang-format on +} + +constexpr bool is_allowed_value_char(char c) { + // clang-format off + return (c == 0x21) ///< "!" + || (c >= 0x23 && c <= 0x2B) ///< "#" / "$" / "%" / "&" / "'" / "(" / /< ")" / "*" / "+" / "," / "-" + || (c >= 0x2D && c <= 0x5B) ///< "-" / "." / "/" / [0-9] / ";' / "<" / "=" / ">" / "?" / "@" / [A-Z] + || (c >= 0x5D && c <= 0x7E); ///< "]" / "^" / "_" / "`" / [a-z] + // clang-format on +} + +Expected, Baggage::Error> +parse_baggage(StringView input) { + std::unordered_map result; + if (input.empty()) return result; + + enum class state : char { + leading_spaces_key, + key, + trailing_spaces_key, + leading_spaces_value, + value, + trailing_spaces_value, + properties, + } internal_state = state::leading_spaces_key; + + size_t beg = 0; + size_t tmp_end = 0; + + StringView key; + StringView value; + + const size_t end = input.size(); + + for (size_t i = 0; i < end; ++i) { + auto c = input[i]; + + switch (internal_state) { + case state::leading_spaces_key: { + if (!is_whitespace(c)) { + beg = i; + tmp_end = i; + internal_state = state::key; + goto key; + } + } break; + + case state::key: { + key: + if (c == '=') { + tmp_end = i; + goto consume_key; + } else if (is_whitespace(c)) { + tmp_end = i; + internal_state = state::trailing_spaces_key; + } else if (!is_allowed_key_char(c)) { + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, i}; + } + } break; + + case state::trailing_spaces_key: { + if (c == '=') { + consume_key: + size_t count = tmp_end - beg; + if (count < 1) + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, i}; + + key = StringView{input.data() + beg, count}; + internal_state = state::leading_spaces_value; + } else if (!is_whitespace(c)) { + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, i}; + } + } break; + + case state::leading_spaces_value: { + if (!is_whitespace(c)) { + beg = i; + tmp_end = i; + internal_state = state::value; + goto value; + } + } break; + + case state::value: { + value: + if (c == ',') { + tmp_end = i; + goto consume_value; + } else if (c == ';') { + tmp_end = i; + internal_state = state::properties; + } else if (is_whitespace(c)) { + tmp_end = i; + internal_state = state::trailing_spaces_value; + } else if (!is_allowed_value_char(c)) { + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, i}; + } + } break; + + case state::properties: { + if (c == ',') { + goto consume_value; + } + } break; + + case state::trailing_spaces_value: { + if (c == ',') { + consume_value: + size_t count = tmp_end - beg; + if (count < 1) + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, + tmp_end}; + + value = StringView{input.data() + beg, count}; + result.emplace(std::string(key), std::string(value)); + beg = i; + tmp_end = i; + internal_state = state::leading_spaces_key; + } else if (c == ';') { + internal_state = state::properties; + } else if (!is_whitespace(c)) { + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, i}; + } + } break; + } + } + + if (internal_state == state::value) { + value = StringView{input.data() + beg, end - beg}; + result.emplace(std::string(key), std::string(value)); + } else if (internal_state == state::trailing_spaces_value || + internal_state == state::properties) { + value = StringView{input.data() + beg, tmp_end - beg}; + result.emplace(std::string(key), std::string(value)); + } else { + return Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER, end}; + } + + return result; +} + +} // namespace + +Baggage::Baggage(size_t max_capacity) : max_capacity_(max_capacity) { + (void)max_capacity_; +} + +Baggage::Baggage(std::unordered_map baggage, + size_t max_capacity) + : max_capacity_(max_capacity), baggage_(std::move(baggage)) {} + +Optional Baggage::get(StringView key) const { + auto it = baggage_.find(std::string(key)); + if (it == baggage_.cend()) return nullopt; + + return it->second; +} + +bool Baggage::set(std::string key, std::string value) { + baggage_[key] = value; + return true; +} + +void Baggage::remove(StringView key) { baggage_.erase(std::string(key)); } + +void Baggage::clear() { baggage_.clear(); } + +size_t Baggage::size() const { return baggage_.size(); } + +bool Baggage::empty() const { return baggage_.empty(); } + +bool Baggage::contains(StringView key) const { + auto found = baggage_.find(std::string(key)); + return found != baggage_.cend(); +} + +void Baggage::visit(std::function&& visitor) { + for (const auto& [key, value] : baggage_) { + visitor(key, value); + } +} + +Expected Baggage::inject(DictWriter& writer, const Options& opts) const { + auto n = baggage_.size(); + if (n == 0) return {}; + + Expected res; + if (n > opts.max_items) { + std::string err_msg = "injected "; + err_msg += std::to_string(opts.max_items); + err_msg += " out of "; + err_msg += std::to_string(baggage_.size()); + err_msg += " baggage items"; + res = datadog::tracing::Error{ + datadog::tracing::Error::Code::BAGGAGE_MAXIMUM_ITEMS_REACHED, err_msg}; + } + + // TODO(@dmehala): Memory alloc optimization, (re)use fixed size buffer. + std::string seralized_baggage; + seralized_baggage.reserve(opts.max_bytes); + + auto it = baggage_.cbegin(); + seralized_baggage += it->first; + seralized_baggage += "="; + seralized_baggage += it->second; + if (seralized_baggage.size() > opts.max_bytes) { + return datadog::tracing::Error{ + datadog::tracing::Error::Code::BAGGAGE_MAXIMUM_BYTES_REACHED, + "reached maximum bytes size limit"}; + } + + size_t items = 1; + for (it++; it != baggage_.cend() && ++items < opts.max_items; ++it) { + std::string buffer; + buffer += ","; + buffer += it->first; + buffer += "="; + buffer += it->second; + + if (buffer.size() + seralized_baggage.size() > opts.max_bytes) { + res = datadog::tracing::Error{ + datadog::tracing::Error::Code::BAGGAGE_MAXIMUM_BYTES_REACHED, + "reached maximum bytes size limit"}; + break; + } + + seralized_baggage += buffer; + } + + /// NOTE(@dmehala): It is the writer's responsibility to write the header, + /// including percent-encoding. + writer.set("baggage", seralized_baggage); + return res; +} + +Expected Baggage::extract(const DictReader& headers) { + auto found = headers.lookup("baggage"); + if (!found) { + return Baggage::Error{Error::MISSING_HEADER}; + } + + // TODO(@dmehala): Avoid allocation + auto bv = parse_baggage(*found); + if (auto error = bv.if_error()) { + return *error; + } + + Baggage result(*bv); + return result; +} + +} // namespace tracing +} // namespace datadog diff --git a/src/datadog/propagation_style.cpp b/src/datadog/propagation_style.cpp index 1b944a19..3d93c60a 100644 --- a/src/datadog/propagation_style.cpp +++ b/src/datadog/propagation_style.cpp @@ -18,6 +18,8 @@ StringView to_string_view(PropagationStyle style) { return "B3"; case PropagationStyle::W3C: return "tracecontext"; // for compatibility with OpenTelemetry + case PropagationStyle::BAGGAGE: + return "baggage"; default: assert(style == PropagationStyle::NONE); return "none"; @@ -38,8 +40,6 @@ Optional parse_propagation_style(StringView text) { auto token = std::string{text}; to_lower(token); - // Note: Make sure that these strings are consistent (modulo case) with - // `to_json`, above. if (token == "datadog") { return PropagationStyle::DATADOG; } else if (token == "b3" || token == "b3multi") { @@ -48,6 +48,8 @@ Optional parse_propagation_style(StringView text) { return PropagationStyle::W3C; } else if (token == "none") { return PropagationStyle::NONE; + } else if (token == "baggage") { + return PropagationStyle::BAGGAGE; } return nullopt; diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index c1001844..3302102b 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -435,7 +435,6 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, additional_w3c_tracestate_)); break; default: - assert(style == PropagationStyle::NONE); break; } } diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 7b530258..ffc298ac 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -59,7 +59,10 @@ Tracer::Tracer(const FinalizedTracerConfig& config, injection_styles_(config.injection_styles), extraction_styles_(config.extraction_styles), tags_header_max_size_(config.tags_header_size), - sampling_delegation_enabled_(config.delegate_trace_sampling) { + sampling_delegation_enabled_(config.delegate_trace_sampling), + baggage_opts_(config.baggage_opts), + baggage_injection_enabled_(false), + baggage_extraction_enabled_(false) { if (config.report_hostname) { hostname_ = get_hostname(); } @@ -82,6 +85,20 @@ Tracer::Tracer(const FinalizedTracerConfig& config, } } + for (const auto style : extraction_styles_) { + if (style == PropagationStyle::BAGGAGE) { + baggage_extraction_enabled_ = true; + break; + } + } + + for (const auto style : injection_styles_) { + if (style == PropagationStyle::BAGGAGE) { + baggage_injection_enabled_ = true; + break; + } + } + if (config.log_on_startup) { logger_->log_startup([configuration = this->config()](std::ostream& log) { log << "DATADOG TRACER CONFIGURATION - " << configuration; @@ -102,6 +119,10 @@ std::string Tracer::config() const { {"extraction_styles", extraction_styles_}, {"tags_header_size", tags_header_max_size_}, {"environment_variables", nlohmann::json::parse(environment::to_json())}, + {"baggage", nlohmann::json{ + {"max_bytes", baggage_opts_.max_bytes}, + {"max_items", baggage_opts_.max_items}, + }}, }); // clang-format on @@ -212,7 +233,6 @@ Expected Tracer::extract_span(const DictReader& reader, extract = &extract_w3c; break; default: - assert(style == PropagationStyle::NONE); extract = &extract_none; } audited_reader.entries_found.clear(); @@ -403,5 +423,46 @@ Span Tracer::extract_or_create_span(const DictReader& reader, return create_span(config); } +Baggage Tracer::create_baggage() { return Baggage(baggage_opts_.max_items); } + +Expected Tracer::extract_baggage( + const DictReader& reader) { + if (!baggage_extraction_enabled_) { + return Baggage::Error{Baggage::Error::DISABLED}; + } + + return Baggage::extract(reader); +} + +Baggage Tracer::extract_or_create_baggage(const DictReader& reader) { + auto maybe_baggage = extract_baggage(reader); + if (maybe_baggage) { + return std::move(*maybe_baggage); + } + + return create_baggage(); +} + +Expected Tracer::inject(const Baggage& baggage, DictWriter& writer) { + if (!baggage_injection_enabled_) { + // TODO(@dmehala): update `Expected` to support `` + return Error{Error::Code::OTHER, "Baggage propagation is disabled"}; + } + + auto res = baggage.inject(writer, baggage_opts_); + if (auto err = res.if_error()) { + logger_->log_error( + err->with_prefix("failed to serialize all baggage items: ")); + + if (err->code == Error::Code::BAGGAGE_MAXIMUM_BYTES_REACHED) { + tracer_telemetry_->metrics().tracer.baggage_bytes_exceeded.inc(); + } else if (err->code == Error::Code::BAGGAGE_MAXIMUM_ITEMS_REACHED) { + tracer_telemetry_->metrics().tracer.baggage_items_exceeded.inc(); + } + } + + return {}; +} + } // namespace tracing } // namespace datadog diff --git a/src/datadog/tracer_config.cpp b/src/datadog/tracer_config.cpp index 6d4186b3..0118f102 100644 --- a/src/datadog/tracer_config.cpp +++ b/src/datadog/tracer_config.cpp @@ -131,6 +131,27 @@ Expected load_tracer_env_config(Logger &logger) { env_cfg.generate_128bit_trace_ids = !falsy(*enabled_env); } + // Baggage + if (auto baggage_items_env = + lookup(environment::DD_TRACE_BAGGAGE_MAX_ITEMS)) { + auto maybe_value = parse_uint64(*baggage_items_env, 10); + if (auto *error = maybe_value.if_error()) { + return *error; + } + + env_cfg.baggage_max_items = std::move(*maybe_value); + } + + if (auto baggage_bytes_env = + lookup(environment::DD_TRACE_BAGGAGE_MAX_BYTES)) { + auto maybe_value = parse_uint64(*baggage_bytes_env, 10); + if (auto *error = maybe_value.if_error()) { + return *error; + } + + env_cfg.baggage_max_bytes = std::move(*maybe_value); + } + // PropagationStyle // Print a warning if a questionable combination of environment variables is // defined. @@ -289,7 +310,8 @@ Expected finalize_config(const TracerConfig &user_config, // Extraction Styles const std::vector default_propagation_styles{ - PropagationStyle::DATADOG, PropagationStyle::W3C}; + PropagationStyle::DATADOG, PropagationStyle::W3C, + PropagationStyle::BAGGAGE}; std::tie(origin, final_config.extraction_styles) = pick(env_config->extraction_styles, user_config.extraction_styles, @@ -357,6 +379,32 @@ Expected finalize_config(const TracerConfig &user_config, value_or(env_config->integration_version, user_config.integration_version, tracer_version); + // Baggage + std::tie(origin, final_config.baggage_opts.max_items) = + pick(env_config->baggage_max_items, user_config.baggage_max_items, 64); + final_config.metadata[ConfigName::TRACE_BAGGAGE_MAX_ITEMS] = + ConfigMetadata(ConfigName::TRACE_BAGGAGE_MAX_ITEMS, + to_string(final_config.baggage_opts.max_items), origin); + + std::tie(origin, final_config.baggage_opts.max_bytes) = + pick(env_config->baggage_max_bytes, user_config.baggage_max_bytes, 8192); + final_config.metadata[ConfigName::TRACE_BAGGAGE_MAX_BYTES] = + ConfigMetadata(ConfigName::TRACE_BAGGAGE_MAX_BYTES, + to_string(final_config.baggage_opts.max_bytes), origin); + + if (final_config.baggage_opts.max_items <= 0 || + final_config.baggage_opts.max_bytes < 3) { + auto it = std::remove(final_config.extraction_styles.begin(), + final_config.extraction_styles.end(), + PropagationStyle::BAGGAGE); + final_config.extraction_styles.erase(it); + + it = std::remove(final_config.injection_styles.begin(), + final_config.injection_styles.end(), + PropagationStyle::BAGGAGE); + final_config.injection_styles.erase(it); + } + if (user_config.runtime_id) { final_config.runtime_id = user_config.runtime_id; } diff --git a/src/datadog/tracer_telemetry.cpp b/src/datadog/tracer_telemetry.cpp index 17ffdb79..7aed7da1 100644 --- a/src/datadog/tracer_telemetry.cpp +++ b/src/datadog/tracer_telemetry.cpp @@ -46,6 +46,10 @@ std::string to_string(datadog::tracing::ConfigName name) { return "span_sample_rules"; case ConfigName::TRACE_SAMPLING_RULES: return "trace_sample_rules"; + case ConfigName::TRACE_BAGGAGE_MAX_BYTES: + return "trace_baggage_max_bytes"; + case ConfigName::TRACE_BAGGAGE_MAX_ITEMS: + return "trace_baggage_max_items"; } std::abort(); diff --git a/src/datadog/tracer_telemetry.h b/src/datadog/tracer_telemetry.h index 0c967c85..fc1649bc 100644 --- a/src/datadog/tracer_telemetry.h +++ b/src/datadog/tracer_telemetry.h @@ -77,6 +77,18 @@ class TracerTelemetry { true}; telemetry::CounterMetric trace_segments_closed = { "trace_segments_closed", "tracers", {}, true}; + telemetry::CounterMetric baggage_items_exceeded = { + "context_header.truncated", + "tracers", + {{"truncation_reason:baggage_item_count_exceeded"}}, + true, + }; + telemetry::CounterMetric baggage_bytes_exceeded = { + "context_header.truncated", + "tracers", + {{"truncation_reason:baggage_byte_count_exceeded"}}, + true, + }; } tracer; struct { telemetry::CounterMetric requests = { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index aec6b6ff..e9273d3e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable(tests telemetry/test_metrics.cpp # test cases + test_baggage.cpp test_base64.cpp test_cerr_logger.cpp test_curl.cpp diff --git a/test/mocks/dict_readers.h b/test/mocks/dict_readers.h index fb0147f3..e7a162af 100644 --- a/test/mocks/dict_readers.h +++ b/test/mocks/dict_readers.h @@ -11,11 +11,14 @@ class MockDictReader : public DictReader { const std::unordered_map* map_; public: + MockDictReader() : map_(nullptr){}; explicit MockDictReader( const std::unordered_map& map) : map_(&map) {} Optional lookup(StringView key) const override { + if (map_ == nullptr) return nullopt; + auto found = map_->find(std::string(key)); if (found == map_->end()) { return nullopt; @@ -25,6 +28,8 @@ class MockDictReader : public DictReader { void visit(const std::function& visitor) const override { + if (map_ == nullptr) return; + for (const auto& [key, value] : *map_) { visitor(key, value); } diff --git a/test/test_baggage.cpp b/test/test_baggage.cpp new file mode 100644 index 00000000..4fb0eaa4 --- /dev/null +++ b/test/test_baggage.cpp @@ -0,0 +1,302 @@ +#include + +#include "catch.hpp" +#include "mocks/dict_readers.h" +#include "mocks/dict_writers.h" +#include "random.h" + +#define BAGGAGE_TEST(x) TEST_CASE(x, "[baggage]") + +using namespace datadog::tracing; + +BAGGAGE_TEST("missing baggage header is not an error") { + MockDictReader reader; + auto maybe_baggage = Baggage::extract(reader); + CHECK(!maybe_baggage); +} + +BAGGAGE_TEST("extract") { + SECTION("parsing") { + struct TestCase final { + std::string name; + std::string input; + Expected expected_baggage; + }; + + auto test_case = GENERATE(values({ + { + "empty baggage header", + "", + Baggage(), + }, + { + "only spaces", + " ", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "valid", + "key1=value1,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 1", + " key1=value1,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 2", + " key1 =value1,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 3", + " key1 = value1,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 4", + " key1 = value1 ,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 5", + " key1 = value1 , key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 6", + " key1 = value1 , key2 =value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 7", + " key1 = value1 , key2 = value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 8", + " key1 = value1 , key2 = value2 ", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "leading spaces 9", + "key1 = value1, key2= value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "spaces in key is not allowed", + "key1 foo=value1", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "spaces in value is not allowed", + "key1=value1 value2", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "ignore properties", + "key1=value1;a=b,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "ignore properties 2", + "key1=value1 ;foo=bar,key2=value2", + Baggage({{"key1", "value1"}, {"key2", "value2"}}), + }, + { + "ignore properties 3", + "key1=value1, key2 = value2;property1;property2, key3=value3; " + "propertyKey=propertyValue", + Baggage({ + {"key1", "value1"}, + {"key2", "value2"}, + {"key3", "value3"}, + }), + }, + { + "malformed baggage", + ",k1=v1,k2=v2,", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "malformed baggage 2", + "=", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "malformed baggage 3", + "=,key2=value2", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "malformed baggage 4", + "key1=value1,=", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "malformed baggage 5", + "key1=value1,key2=", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + { + "malformed baggage 6", + "key1=", + Baggage::Error{Baggage::Error::MALFORMED_BAGGAGE_HEADER}, + }, + })); + + CAPTURE(test_case.name, test_case.input); + + const std::unordered_map headers{ + {"baggage", test_case.input}}; + MockDictReader reader(headers); + + auto maybe_baggage = Baggage::extract(reader); + if (maybe_baggage.has_value() && test_case.expected_baggage.has_value()) { + CHECK(*maybe_baggage == *test_case.expected_baggage); + } else if (maybe_baggage.if_error() && + test_case.expected_baggage.if_error()) { + CHECK(maybe_baggage.error().code == + test_case.expected_baggage.error().code); + } else { + FAIL("mistmatch between what is expected and the result"); + } + } +} + +BAGGAGE_TEST("inject") { + SECTION("custom items limit is respected") { + Baggage bag({{"violets", "blue"}, {"roses", "red"}}); + + const Baggage::Options opts_items_limit{ + /*.max_bytes = */ 2048, + /*.max_items =*/1, + }; + + MockDictWriter writer; + auto injected = bag.inject(writer, opts_items_limit); + + REQUIRE(!injected); + REQUIRE(writer.items.count("baggage") == 1); + CHECK((writer.items["baggage"] == "violets=blue" || + writer.items["baggage"] == "roses=red")); + } + + SECTION("custom bytes limit is respected") { + Baggage bag({{"foo", "bar"}, {"a", "b"}, {"hello", "world"}}); + + const std::string expected_baggage{"foo=bar,a=b"}; + const Baggage::Options opts_bytes_limit{ + /*.max_bytes = */ expected_baggage.size(), + /*.max_items =*/1000, + }; + + MockDictWriter writer; + auto injected = bag.inject(writer, opts_bytes_limit); + + REQUIRE(!injected); + REQUIRE(writer.items.count("baggage") == 1); + REQUIRE(writer.items["baggage"].size() <= opts_bytes_limit.max_bytes); + CHECK((writer.items["baggage"] == expected_baggage || + writer.items["baggage"] == "hello=world")); + } + + SECTION("default limits are respected") { + auto default_opts = Baggage::default_options; + SECTION("max items reached") { + std::size_t max_bytes_needed = 0; + + Baggage bag; + for (size_t i = 0; i < default_opts.max_items; ++i) { + auto uuid_value = uuid(); + bag.set(uuid_value, "a"); + max_bytes_needed += + uuid_value.size() + 1 + 2; // +2 are for the separators + } + // NOTE(@dmehala): if that fails, the flackiness is comming from UUIDs + // collision. + REQUIRE(bag.size() == default_opts.max_items); + bag.set("a", "a"); + max_bytes_needed += 4; + + Baggage::Options opts = Baggage::default_options; + opts.max_bytes = max_bytes_needed; + + MockDictWriter writer; + auto injected = bag.inject(writer, opts); + CHECK(!injected); + CHECK(injected.error().code == + Error::Code::BAGGAGE_MAXIMUM_ITEMS_REACHED); + } + + SECTION("max bytes reached") { + std::string v(default_opts.max_bytes, '-'); + Baggage bag({{"a", v}, {"b", v}}); + + MockDictWriter writer; + auto injected = bag.inject(writer); + REQUIRE(!injected); + CHECK(injected.error().code == + Error::Code::BAGGAGE_MAXIMUM_BYTES_REACHED); + } + } +} + +BAGGAGE_TEST("round-trip") { + Baggage bag({ + {"team", "proxy"}, + {"company", "datadog"}, + {"user", "dmehala"}, + }); + + MockDictWriter writer; + REQUIRE(bag.inject(writer)); + + MockDictReader reader(writer.items); + auto extracted_baggage = Baggage::extract(reader); + REQUIRE(extracted_baggage); + + CHECK(*extracted_baggage == bag); +} + +BAGGAGE_TEST("accessors") { + Baggage bag({{"foo", "bar"}, {"answer", "42"}, {"dog", "woof"}}); + + const std::string baggage = "team=proxy,company=datadog,user=dmehala"; + const std::unordered_map headers{ + {"baggage", baggage}}; + MockDictReader reader(headers); + + auto maybe_baggage = Baggage::extract(reader); + REQUIRE(maybe_baggage); + + CHECK(maybe_baggage->size() == 3); + + CHECK(maybe_baggage->get("company") == "datadog"); + CHECK(!maybe_baggage->get("boogaloo")); + CHECK(maybe_baggage->contains("boogaloo") == false); + CHECK(maybe_baggage->contains("team") == true); + + maybe_baggage->set("color", "red"); + /// NOTE: ensure `set` overwrite + maybe_baggage->set("color", "blue"); + CHECK(maybe_baggage->get("color") == "blue"); + CHECK(maybe_baggage->size() == 4); + + maybe_baggage->remove("company"); + CHECK(maybe_baggage->contains("company") == false); + CHECK(maybe_baggage->size() == 3); + + SECTION("visit") { + bag.visit([](StringView key, StringView value) { + (void)key; + (void)value; + }); + } + + SECTION("clear") { + bag.clear(); + CHECK(bag.empty() == true); + } +} diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 60f850db..8863505f 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1434,6 +1434,47 @@ TEST_CASE("span extraction") { } } +TEST_CASE("baggage usage") { + TracerConfig config; + config.logger = std::make_shared(); + config.collector = std::make_shared(); + + SECTION("disabling baggage propagation yield an error") { + config.extraction_styles = {PropagationStyle::DATADOG}; + config.injection_styles = {PropagationStyle::DATADOG}; + + auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + + Tracer tracer(*finalized_config); + + MockDictReader reader; + auto maybe_baggage = tracer.extract_baggage(reader); + CHECK(!maybe_baggage); + + auto baggage = tracer.create_baggage(); + MockDictWriter writer; + CHECK(!tracer.inject(baggage, writer)); + } + + SECTION("feature is enabled") { + auto finalized_config = finalize_config(config); + REQUIRE(finalized_config); + + Tracer tracer(*finalized_config); + + MockDictReader reader; + auto baggage = tracer.extract_or_create_baggage(reader); + + baggage.set("data", "dog"); + MockDictWriter writer; + tracer.inject(baggage, writer); + + REQUIRE(writer.items.count("baggage") == 1); + CHECK(writer.items["baggage"] == "data=dog"); + } +} + TEST_CASE("report hostname") { TracerConfig config; config.service = "testsvc"; diff --git a/test/test_tracer_config.cpp b/test/test_tracer_config.cpp index 0ff3358f..c8056e74 100644 --- a/test/test_tracer_config.cpp +++ b/test/test_tracer_config.cpp @@ -95,7 +95,9 @@ class SomewhatSecureTemporaryFile : public std::fstream { } // namespace -TEST_CASE("TracerConfig::defaults") { +#define TRACER_CONFIG_TEST(x) TEST_CASE(x, "[tracer.config]") + +TRACER_CONFIG_TEST("TracerConfig::defaults") { TracerConfig config; SECTION("service is not required") { @@ -228,7 +230,7 @@ TEST_CASE("TracerConfig::defaults") { } } -TEST_CASE("TracerConfig::log_on_startup") { +TRACER_CONFIG_TEST("TracerConfig::log_on_startup") { TracerConfig config; config.service = "testsvc"; const auto logger = std::make_shared(); @@ -289,7 +291,7 @@ TEST_CASE("TracerConfig::log_on_startup") { } } -TEST_CASE("TracerConfig::report_traces") { +TRACER_CONFIG_TEST("TracerConfig::report_traces") { TracerConfig config; config.service = "testsvc"; const auto collector = std::make_shared(); @@ -358,7 +360,7 @@ TEST_CASE("TracerConfig::report_traces") { } } -TEST_CASE("TracerConfig::agent") { +TRACER_CONFIG_TEST("TracerConfig::agent") { TracerConfig config; config.service = "testsvc"; @@ -548,7 +550,7 @@ TEST_CASE("TracerConfig::agent") { } } -TEST_CASE("TracerConfig::trace_sampler") { +TRACER_CONFIG_TEST("TracerConfig::trace_sampler") { TracerConfig config; config.service = "testsvc"; @@ -817,7 +819,7 @@ TEST_CASE("TracerConfig::trace_sampler") { } } -TEST_CASE("TracerConfig::span_sampler") { +TRACER_CONFIG_TEST("TracerConfig::span_sampler") { TracerConfig config; config.service = "testsvc"; @@ -1051,16 +1053,17 @@ TEST_CASE("TracerConfig::span_sampler") { } } -TEST_CASE("TracerConfig propagation styles") { +TRACER_CONFIG_TEST("TracerConfig propagation styles") { TracerConfig config; config.service = "testsvc"; - SECTION("default style is [Datadog, W3C]") { + SECTION("default style is [Datadog, W3C, Baggage]") { auto finalized = finalize_config(config); REQUIRE(finalized); const std::vector expected_styles = { - PropagationStyle::DATADOG, PropagationStyle::W3C}; + PropagationStyle::DATADOG, PropagationStyle::W3C, + PropagationStyle::BAGGAGE}; REQUIRE(finalized->injection_styles == expected_styles); REQUIRE(finalized->extraction_styles == expected_styles); @@ -1130,7 +1133,9 @@ TEST_CASE("TracerConfig propagation styles") { static const auto x = nullopt; static const auto datadog = PropagationStyle::DATADOG, b3 = PropagationStyle::B3, - none = PropagationStyle::NONE; + none = PropagationStyle::NONE, + baggage = PropagationStyle::BAGGAGE; + // clang-format off auto test_case = GENERATE(values({ {__LINE__, "Datadog", x, {datadog}}, @@ -1151,6 +1156,7 @@ TEST_CASE("TracerConfig propagation styles") { {__LINE__, "b3,datadog,w3c", Error::UNKNOWN_PROPAGATION_STYLE}, {__LINE__, "b3,datadog,datadog", Error::DUPLICATE_PROPAGATION_STYLE}, {__LINE__, " b3 b3 b3, b3 , b3, b3, b3 , b3 b3 b3 ", Error::DUPLICATE_PROPAGATION_STYLE}, + {__LINE__, "baggage", x, {baggage}}, })); // clang-format on @@ -1274,7 +1280,7 @@ TEST_CASE("TracerConfig propagation styles") { } } -TEST_CASE("configure 128-bit trace IDs") { +TRACER_CONFIG_TEST("configure 128-bit trace IDs") { TracerConfig config; config.service = "testsvc"; @@ -1328,3 +1334,119 @@ TEST_CASE("configure 128-bit trace IDs") { REQUIRE(finalized->generate_128bit_trace_ids == test_case.expected_value); } } + +TRACER_CONFIG_TEST("baggage") { + TracerConfig config; + + auto contains_baggage_propagation_style = + [](const std::vector& style) { + return std::find(style.cbegin(), style.cend(), + PropagationStyle::BAGGAGE) != style.cend(); + }; + + SECTION("default") { + auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->baggage_opts.max_items == 64); + CHECK(finalized->baggage_opts.max_bytes == 8192); + + CHECK(contains_baggage_propagation_style(finalized->extraction_styles) == + true); + CHECK(contains_baggage_propagation_style(finalized->injection_styles) == + true); + + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_ITEMS) == + 1); + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_BYTES) == + 1); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_ITEMS].origin == + ConfigMetadata::Origin::DEFAULT); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_BYTES].origin == + ConfigMetadata::Origin::DEFAULT); + } + + SECTION("value overriden by environment variables") { + SECTION("invalid BAGGAGE_MAX_ITEMS is reported") { + EnvGuard guard{"DD_TRACE_BAGGAGE_MAX_ITEMS", "ten"}; + auto finalized = finalize_config(config); + CHECK(!finalized); + } + + SECTION("invalid BAGGAGE_MAX_BYTES is reported") { + EnvGuard guard{"DD_TRACE_BAGGAGE_MAX_BYTES", "2kib"}; + auto finalized = finalize_config(config); + CHECK(!finalized); + } + + EnvGuard guard{"DD_TRACE_BAGGAGE_MAX_ITEMS", "128"}; + EnvGuard guard2{"DD_TRACE_BAGGAGE_MAX_BYTES", "1024"}; + EnvGuard guard3{"DD_TRACE_PROPAGATION_STYLE_EXTRACT", "datadog"}; + EnvGuard guard4{"DD_TRACE_PROPAGATION_STYLE_INJECT", "datadog"}; + + auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->baggage_opts.max_items == 128); + CHECK(finalized->baggage_opts.max_bytes == 1024); + + CHECK(contains_baggage_propagation_style(finalized->extraction_styles) == + false); + CHECK(contains_baggage_propagation_style(finalized->injection_styles) == + false); + + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_ITEMS) == + 1); + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_BYTES) == + 1); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_ITEMS].origin == + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_BYTES].origin == + ConfigMetadata::Origin::ENVIRONMENT_VARIABLE); + } + + SECTION("value overriden by code") { + config.baggage_max_items = 10; + config.baggage_max_bytes = 32; + + auto finalized = finalize_config(config); + REQUIRE(finalized); + CHECK(finalized->baggage_opts.max_items == 10); + CHECK(finalized->baggage_opts.max_bytes == 32); + + CHECK(contains_baggage_propagation_style(finalized->extraction_styles) == + true); + CHECK(contains_baggage_propagation_style(finalized->injection_styles) == + true); + + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_ITEMS) == + 1); + REQUIRE(finalized->metadata.count(ConfigName::TRACE_BAGGAGE_MAX_BYTES) == + 1); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_ITEMS].origin == + ConfigMetadata::Origin::CODE); + CHECK(finalized->metadata[ConfigName::TRACE_BAGGAGE_MAX_BYTES].origin == + ConfigMetadata::Origin::CODE); + } + + SECTION("disabled when `max_bytes` <= 3") { + config.baggage_max_bytes = 2; + auto finalized = finalize_config(config); + REQUIRE(finalized); + + CHECK(contains_baggage_propagation_style(finalized->extraction_styles) == + false); + CHECK(contains_baggage_propagation_style(finalized->injection_styles) == + false); + } + + SECTION("disabled when `max_items` <= 0") { + config.baggage_max_items = 0; + + auto finalized = finalize_config(config); + REQUIRE(finalized); + + CHECK(contains_baggage_propagation_style(finalized->extraction_styles) == + false); + CHECK(contains_baggage_propagation_style(finalized->injection_styles) == + false); + } +}