From 3400b8e66001d312338d6285d73ca28b21386bdc Mon Sep 17 00:00:00 2001 From: Thomas Ebner <96168670+samohte@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:51:46 +0100 Subject: [PATCH] Feat/dynatrace sampler internal (#11) Signed-off-by: thomas.ebner --- .../samplers/v3/dynatrace_sampler.proto | 20 +++ source/extensions/extensions_build_config.bzl | 1 + source/extensions/extensions_metadata.yaml | 7 + .../opentelemetry/samplers/dynatrace/BUILD | 42 +++++ .../samplers/dynatrace/config.cc | 34 +++++ .../opentelemetry/samplers/dynatrace/config.h | 41 +++++ .../samplers/dynatrace/dynatrace_sampler.cc | 88 +++++++++++ .../samplers/dynatrace/dynatrace_sampler.h | 45 ++++++ .../dynatrace/dynatrace_tracestate.cc | 46 ++++++ .../samplers/dynatrace/dynatrace_tracestate.h | 53 +++++++ .../samplers/dynatrace/tracestate.cc | 45 ++++++ .../samplers/dynatrace/tracestate.h | 47 ++++++ .../opentelemetry/samplers/dynatrace/BUILD | 55 +++++++ .../samplers/dynatrace/config_test.cc | 37 +++++ .../dynatrace_sampler_integration_test.cc | 143 ++++++++++++++++++ .../dynatrace/dynatrace_sampler_test.cc | 55 +++++++ .../dynatrace/dynatrace_tracestate_test.cc | 21 +++ .../samplers/dynatrace/tracestate_test.cc | 132 ++++++++++++++++ 18 files changed, 912 insertions(+) create mode 100644 api/envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.proto create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/config.h create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.cc create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.cc create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.cc create mode 100644 source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/config_test.cc create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_integration_test.cc create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_test.cc create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate_test.cc create mode 100644 test/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate_test.cc diff --git a/api/envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.proto b/api/envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.proto new file mode 100644 index 000000000000..e0c37a7e51a4 --- /dev/null +++ b/api/envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package envoy.extensions.tracers.opentelemetry.samplers.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.tracers.opentelemetry.samplers.v3"; +option java_outer_classname = "DynatraceSamplerProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/tracers/opentelemetry/samplers/v3;samplersv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Dynatrace Sampler config] +// [#extension: envoy.tracers.opentelemetry.samplers.dynatrace] + +message DynatraceSamplerConfig { + string tenant_id = 1; + + string cluster_id = 2; +} diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index efa1de486e61..b6e40d872f80 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -278,6 +278,7 @@ EXTENSIONS = { # "envoy.tracers.opentelemetry.samplers.always_on": "//source/extensions/tracers/opentelemetry/samplers/always_on:config", + "envoy.tracers.opentelemetry.samplers.dynatrace": "//source/extensions/tracers/opentelemetry/samplers/dynatrace:config", # # Transport sockets diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 739f8a5003d3..aac5fe99df4f 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1162,6 +1162,13 @@ envoy.tracers.opentelemetry.samplers.always_on: status: wip type_urls: - envoy.extensions.tracers.opentelemetry.samplers.v3.AlwaysOnSamplerConfig +envoy.tracers.opentelemetry.samplers.dynatrace: + categories: + - envoy.tracers.opentelemetry.samplers + security_posture: unknown + status: wip + type_urls: + - envoy.extensions.tracers.opentelemetry.samplers.v3.DynatraceSamplerConfig envoy.tracers.skywalking: categories: - envoy.tracers diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD b/source/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD new file mode 100644 index 000000000000..2d772e01cb9f --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD @@ -0,0 +1,42 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":dynatrace_sampler_lib", + "//envoy/registry", + "//source/common/config:utility_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/samplers/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "dynatrace_sampler_lib", + srcs = [ + "dynatrace_sampler.cc", + "dynatrace_tracestate.cc", + "tracestate.cc", + ], + hdrs = [ + "dynatrace_sampler.h", + "dynatrace_tracestate.h", + "tracestate.h", + ], + deps = [ + "//source/common/config:datasource_lib", + "//source/extensions/tracers/opentelemetry:opentelemetry_tracer_lib", + "//source/extensions/tracers/opentelemetry/samplers:sampler_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/samplers/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc new file mode 100644 index 000000000000..9e410e75ea0c --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.cc @@ -0,0 +1,34 @@ +#include "config.h" + +#include "envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.pb.validate.h" + +#include "source/common/config/utility.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +SamplerSharedPtr +DynatraceSamplerFactory::createSampler(const Protobuf::Message& config, + Server::Configuration::TracerFactoryContext& context) { + auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( + dynamic_cast(config), context.messageValidationVisitor(), *this); + return std::make_shared( + MessageUtil::downcastAndValidate< + const envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig&>( + *mptr, context.messageValidationVisitor()), + context); +} + +/** + * Static registration for the Env sampler factory. @see RegisterFactory. + */ +REGISTER_FACTORY(DynatraceSamplerFactory, SamplerFactory); + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.h b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.h new file mode 100644 index 000000000000..bc1baf6c3451 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/config.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.pb.h" +#include "envoy/registry/registry.h" + +#include "source/extensions/tracers/opentelemetry/samplers/sampler.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * Config registration for the DynatraceSampler. @see SamplerFactory. + */ +class DynatraceSamplerFactory : public SamplerFactory { +public: + /** + * @brief Create a Sampler which samples every span + * + * @param context + * @return SamplerSharedPtr + */ + SamplerSharedPtr createSampler(const Protobuf::Message& config, + Server::Configuration::TracerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique< + envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig>(); + } + std::string name() const override { return "envoy.tracers.opentelemetry.samplers.dynatrace"; } +}; + +DECLARE_FACTORY(DynatraceSamplerFactory); + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.cc b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.cc new file mode 100644 index 000000000000..a76a76810ecb --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.cc @@ -0,0 +1,88 @@ +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h" + +#include +#include +#include + +#include "source/common/config/datasource.h" +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h" +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h" +#include "source/extensions/tracers/opentelemetry/samplers/sampler.h" +#include "source/extensions/tracers/opentelemetry/span_context.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +static const char* SAMPLING_EXTRAPOLATION_SPAN_ATTRIBUTE_NAME = + "sampling_extrapolation_set_in_sampler"; + +FW4Tag DynatraceSampler::getFW4Tag(const Tracestate& tracestate) { + for (auto const& entry : tracestate.entries()) { + if (dt_tracestate_entry_.keyMatches( + entry.key)) { // found a tracestate entry with key matching our tenant/cluster + return FW4Tag::create(entry.value); + } + } + return FW4Tag::createInvalid(); +} + +DynatraceSampler::DynatraceSampler( + const envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig& config, + Server::Configuration::TracerFactoryContext& /*context*/) + : tenant_id_(config.tenant_id()), cluster_id_(config.cluster_id()), + dt_tracestate_entry_(tenant_id_, cluster_id_), counter_(0) {} + +SamplingResult DynatraceSampler::shouldSample(const absl::optional parent_context, + const std::string& /*trace_id*/, + const std::string& /*name*/, OTelSpanKind /*kind*/, + OptRef /*trace_context*/, + const std::vector& /*links*/) { + + SamplingResult result; + std::map att; + // search for an existing forward tag in the tracestate + Tracestate tracestate; + tracestate.parse(parent_context.has_value() ? parent_context->tracestate() : ""); + + if (FW4Tag fw4_tag = getFW4Tag(tracestate); + fw4_tag.isValid()) { // we found a trace decision in tracestate header + result.decision = fw4_tag.isIgnored() ? Decision::DROP : Decision::RECORD_AND_SAMPLE; + att[SAMPLING_EXTRAPOLATION_SPAN_ATTRIBUTE_NAME] = std::to_string(fw4_tag.getSamplingExponent()); + result.tracestate = parent_context->tracestate(); + + } else { // make a sampling decision + // this is just a demo, we sample every second request here + uint32_t current_counter = ++counter_; + bool sample; + int sampling_exponent; + if (current_counter % 2) { + sample = true; + sampling_exponent = 1; + } else { + sample = false; + sampling_exponent = 0; + } + + att[SAMPLING_EXTRAPOLATION_SPAN_ATTRIBUTE_NAME] = std::to_string(sampling_exponent); + + result.decision = sample ? Decision::RECORD_AND_SAMPLE : Decision::DROP; + // create new forward tag and add it to tracestate + FW4Tag new_tag = FW4Tag::create(!sample, sampling_exponent); + tracestate.add(dt_tracestate_entry_.getKey(), new_tag.asString()); + result.tracestate = tracestate.asString(); + } + + if (!att.empty()) { + result.attributes = std::make_unique>(std::move(att)); + } + return result; +} + +std::string DynatraceSampler::getDescription() const { return "DynatraceSampler"; } + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h new file mode 100644 index 000000000000..33ae48fc9c55 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h @@ -0,0 +1,45 @@ +#pragma once + +#include "envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.pb.h" +#include "envoy/server/factory_context.h" + +#include "source/common/common/logger.h" +#include "source/common/config/datasource.h" +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h" +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h" +#include "source/extensions/tracers/opentelemetry/samplers/sampler.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * @brief A Dynatrace specific sampler * + */ +class DynatraceSampler : public Sampler, Logger::Loggable { +public: + DynatraceSampler( + const envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig& config, + Server::Configuration::TracerFactoryContext& context); + + SamplingResult shouldSample(const absl::optional parent_context, + const std::string& trace_id, const std::string& name, + OTelSpanKind spankind, + OptRef trace_context, + const std::vector& links) override; + + std::string getDescription() const override; + +private: + std::string tenant_id_; + std::string cluster_id_; + DtTracestateEntry dt_tracestate_entry_; + std::atomic counter_; // request counter for dummy sampling + FW4Tag getFW4Tag(const Tracestate& tracestate); +}; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.cc b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.cc new file mode 100644 index 000000000000..25c9db3c6a94 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.cc @@ -0,0 +1,46 @@ +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h" + +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +FW4Tag FW4Tag::createInvalid() { return {false, false, 0}; } + +FW4Tag FW4Tag::create(bool ignored, int sampling_exponent) { + return {true, ignored, sampling_exponent}; +} + +FW4Tag FW4Tag::create(const std::string& value) { + std::vector tracestate_components = + absl::StrSplit(value, ';', absl::AllowEmpty()); + if (tracestate_components.size() < 7) { + return createInvalid(); + } + + if (tracestate_components[0] != "fw4") { + return createInvalid(); + } + bool ignored = tracestate_components[5] == "1"; + int sampling_exponent = std::stoi(std::string(tracestate_components[6])); + return {true, ignored, sampling_exponent}; +} + +std::string FW4Tag::asString() const { + std::string ret = + absl::StrCat("fw4;0;0;0;0;", ignored_ ? "1" : "0", ";", sampling_exponent_, ";0"); + return ret; +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h new file mode 100644 index 000000000000..eb2935fc8166 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +class FW4Tag { +public: + static FW4Tag createInvalid(); + + static FW4Tag create(bool ignored, int sampling_exponent); + + static FW4Tag create(const std::string& value); + + std::string asString() const; + + bool isValid() const { return valid_; }; + bool isIgnored() const { return ignored_; }; + int getSamplingExponent() const { return sampling_exponent_; }; + +private: + FW4Tag(bool valid, bool ignored, int sampling_exponent) + : valid_(valid), ignored_(ignored), sampling_exponent_(sampling_exponent) {} + + bool valid_; + bool ignored_; + int sampling_exponent_; +}; + +// -@dt=fw4;0;0;0;0;;; +class DtTracestateEntry { +public: + DtTracestateEntry(const std::string& tenant_id, const std::string& cluster_id) { + key_ = absl::StrCat(absl::string_view(tenant_id), "-", absl::string_view(cluster_id), "@dt"); + } + + std::string getKey() const { return key_; }; + + bool keyMatches(const std::string& key) { return (key_.compare(key) == 0); } + +private: + std::string key_; +}; +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.cc b/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.cc new file mode 100644 index 000000000000..688ce69898e2 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.cc @@ -0,0 +1,45 @@ +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h" + +#include "source/common/common/utility.h" + +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +void Tracestate::parse(const std::string& tracestate) { + clear(); + raw_trace_state_ = tracestate; + std::vector list_members = absl::StrSplit(tracestate, ',', absl::SkipEmpty()); + for (auto const& list_member : list_members) { + std::vector kv = absl::StrSplit(list_member, '=', absl::SkipEmpty()); + if (kv.size() != 2) { + // unexpected entry, ignore + continue; + } + absl::string_view key = StringUtil::ltrim(kv[0]); + absl::string_view val = StringUtil::rtrim(kv[1]); + TracestateEntry entry{{key.data(), key.size()}, {val.data(), val.size()}}; + entries_.push_back(std::move(entry)); + } +} + +void Tracestate::add(const std::string& key, const std::string& value) { + std::string tracestate = + absl::StrCat(absl::string_view(key), "=", absl::string_view(value), + raw_trace_state_.empty() > 0 ? "" : ",", absl::string_view(raw_trace_state_)); + parse(tracestate); +} + +void Tracestate::clear() { + entries_.clear(); + raw_trace_state_.clear(); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h b/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h new file mode 100644 index 000000000000..be9cdeeb7072 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +// TODO: +// file should be moved outside "dynatrace" folder, can be useful for other implementations +// should be aligned to open telemetry implementation: +// https://github.com/open-telemetry/opentelemetry-cpp/blob/main/api/include/opentelemetry/trace/trace_state.h + +class TracestateEntry { + // TODO: could be string_views to Tracestate::raw_trace_state_ +public: + std::string key; + std::string value; +}; + +/** + * @brief parses and manipultes W3C tracestate header + * see https://www.w3.org/TR/trace-context/#tracestate-header + * + */ +class Tracestate { +public: + void parse(const std::string& tracestate); + + void add(const std::string& key, const std::string& value); + + std::vector entries() const { return entries_; } + + std::string asString() const { return raw_trace_state_; } + +private: + std::vector entries_; + std::string raw_trace_state_; + + void clear(); +}; +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD b/test/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD new file mode 100644 index 000000000000..e1d099cfc05d --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/BUILD @@ -0,0 +1,55 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.tracers.opentelemetry.samplers.dynatrace"], + deps = [ + "//envoy/registry", + "//source/extensions/tracers/opentelemetry/samplers/dynatrace:config", + "//source/extensions/tracers/opentelemetry/samplers/dynatrace:dynatrace_sampler_lib", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "dynatrace_sampler_test", + srcs = [ + "dynatrace_sampler_test.cc", + "dynatrace_tracestate_test.cc", + "tracestate_test.cc", + ], + extension_names = ["envoy.tracers.opentelemetry.samplers.dynatrace"], + deps = [ + "//source/extensions/tracers/opentelemetry/samplers/dynatrace:dynatrace_sampler_lib", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/samplers/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "dynatrace_sampler_integration_test", + srcs = [ + "dynatrace_sampler_integration_test.cc", + ], + extension_names = ["envoy.tracers.opentelemetry.samplers.dynatrace"], + deps = [ + "//source/exe:main_common_lib", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/config_test.cc b/test/extensions/tracers/opentelemetry/samplers/dynatrace/config_test.cc new file mode 100644 index 000000000000..f4aa70929603 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/config_test.cc @@ -0,0 +1,37 @@ +#include "envoy/registry/registry.h" + +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/config.h" + +#include "test/mocks/server/tracer_factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +TEST(DynatraceSamplerFactoryTest, Test) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.tracers.opentelemetry.samplers.dynatrace"); + ASSERT_NE(factory, nullptr); + EXPECT_STREQ(factory->name().c_str(), "envoy.tracers.opentelemetry.samplers.dynatrace"); + EXPECT_NE(factory->createEmptyConfigProto(), nullptr); + + envoy::config::core::v3::TypedExtensionConfig typed_config; + const std::string yaml = R"EOF( + name: envoy.tracers.opentelemetry.samplers.dynatrace + typed_config: + "@type": type.googleapis.com/envoy.extensions.tracers.opentelemetry.samplers.v3.DynatraceSamplerConfig + )EOF"; + TestUtility::loadFromYaml(yaml, typed_config); + NiceMock context; + EXPECT_NE(factory->createSampler(typed_config.typed_config(), context), nullptr); + EXPECT_STREQ(factory->name().c_str(), "envoy.tracers.opentelemetry.samplers.dynatrace"); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_integration_test.cc b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_integration_test.cc new file mode 100644 index 000000000000..998ce7fab4e5 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_integration_test.cc @@ -0,0 +1,143 @@ +#include +#include + +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" + +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "absl/strings/match.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { +namespace { + +const char* TRACEPARENT_VALUE = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; +const char* TRACEPARENT_VALUE_START = "00-0af7651916cd43dd8448eb211c80319c"; + +class DynatraceSamplerIntegrationTest : public Envoy::HttpIntegrationTest, + public testing::TestWithParam { +public: + DynatraceSamplerIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) { + const std::string yaml_string = R"EOF( + provider: + name: envoy.tracers.opentelemetry + typed_config: + "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + grpc_service: + envoy_grpc: + cluster_name: opentelemetry_collector + timeout: 0.250s + service_name: "a_service_name" + sampler: + name: envoy.tracers.opentelemetry.samplers.dynatrace + typed_config: + "@type": type.googleapis.com/envoy.extensions.tracers.opentelemetry.samplers.v3.DynatraceSamplerConfig + tenant_id: "9712ad40" + cluster_id: "980df25c" + )EOF"; + + auto tracing_config = + std::make_unique<::envoy::extensions::filters::network::http_connection_manager::v3:: + HttpConnectionManager_Tracing>(); + TestUtility::loadFromYaml(yaml_string, *tracing_config.get()); + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { hcm.set_allocated_tracing(tracing_config.release()); }); + + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, DynatraceSamplerIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Sends a request with traceparent and tracestate header. +TEST_P(DynatraceSamplerIntegrationTest, TestWithTraceparentAndTracestate) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test/long/url"}, {":scheme", "http"}, + {":authority", "host"}, {"tracestate", "key=value"}, {"traceparent", TRACEPARENT_VALUE}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().getStatusValue(), "200"); + + // traceparent should be set: traceid should be re-used, span id should be different + absl::string_view traceparent_value = upstream_request_->headers() + .get(Http::LowerCaseString("traceparent"))[0] + ->value() + .getStringView(); + EXPECT_TRUE(absl::StartsWith(traceparent_value, TRACEPARENT_VALUE_START)); + EXPECT_NE(TRACEPARENT_VALUE, traceparent_value); + // tracestate should be forwarded + absl::string_view tracestate_value = upstream_request_->headers() + .get(Http::LowerCaseString("tracestate"))[0] + ->value() + .getStringView(); + EXPECT_EQ("9712ad40-980df25c@dt=fw4;0;0;0;0;0;1;0,key=value", tracestate_value); +} + +// Sends a request with traceparent but no tracestate header. +TEST_P(DynatraceSamplerIntegrationTest, TestWithTraceparentOnly) { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"traceparent", TRACEPARENT_VALUE}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().getStatusValue(), "200"); + + // traceparent should be set: traceid should be re-used, span id should be different + absl::string_view traceparent_value = upstream_request_->headers() + .get(Http::LowerCaseString("traceparent"))[0] + ->value() + .getStringView(); + EXPECT_TRUE(absl::StartsWith(traceparent_value, TRACEPARENT_VALUE_START)); + EXPECT_NE(TRACEPARENT_VALUE, traceparent_value); + // OTLP tracer adds an empty tracestate + absl::string_view tracestate_value = upstream_request_->headers() + .get(Http::LowerCaseString("tracestate"))[0] + ->value() + .getStringView(); + EXPECT_EQ("9712ad40-980df25c@dt=fw4;0;0;0;0;0;1;0", tracestate_value); +} + +// Sends a request without traceparent and tracestate header. +TEST_P(DynatraceSamplerIntegrationTest, TestWithoutTraceparentAndTracestate) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test/long/url"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ(response->headers().getStatusValue(), "200"); + + // traceparent will be added, trace_id and span_id will be generated, so there is nothing we can + // assert + EXPECT_EQ(upstream_request_->headers().get(::Envoy::Http::LowerCaseString("traceparent")).size(), + 1); + // OTLP tracer adds an empty tracestate + absl::string_view tracestate_value = upstream_request_->headers() + .get(Http::LowerCaseString("tracestate"))[0] + ->value() + .getStringView(); + EXPECT_EQ("9712ad40-980df25c@dt=fw4;0;0;0;0;0;1;0", tracestate_value); +} + +} // namespace +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_test.cc b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_test.cc new file mode 100644 index 000000000000..d3e8422c773d --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler_test.cc @@ -0,0 +1,55 @@ +#include +#include + +#include "envoy/extensions/tracers/opentelemetry/samplers/v3/dynatrace_sampler.pb.h" + +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_sampler.h" +#include "source/extensions/tracers/opentelemetry/span_context.h" + +#include "test/mocks/server/tracer_factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +class DynatraceSamplerTest : public testing::Test { + + const std::string yaml_string = R"EOF( + tenant_id: "9712ad40" + cluster_id: "980df25c" + )EOF"; + +public: + DynatraceSamplerTest() { + TestUtility::loadFromYaml(yaml_string, config_); + NiceMock context; + sampler_ = std::make_unique(config_, context); + EXPECT_STREQ(sampler_->getDescription().c_str(), "DynatraceSampler"); + } + +protected: + envoy::extensions::tracers::opentelemetry::samplers::v3::DynatraceSamplerConfig config_; + std::unique_ptr sampler_; +}; + +// Verify sampler being invoked with an invalid span context +TEST_F(DynatraceSamplerTest, TestWithoutParentContext) { + + auto sampling_result = + sampler_->shouldSample(absl::nullopt, "operation_name", "12345", + ::opentelemetry::proto::trace::v1::Span::SPAN_KIND_SERVER, {}, {}); + EXPECT_EQ(sampling_result.decision, Decision::RECORD_AND_SAMPLE); + EXPECT_EQ(sampling_result.attributes->size(), 1); + EXPECT_STREQ(sampling_result.tracestate.c_str(), "9712ad40-980df25c@dt=fw4;0;0;0;0;0;1;0"); + EXPECT_TRUE(sampling_result.isRecording()); + EXPECT_TRUE(sampling_result.isSampled()); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate_test.cc b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate_test.cc new file mode 100644 index 000000000000..928235e58416 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate_test.cc @@ -0,0 +1,21 @@ +#include + +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/dynatrace_tracestate.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +TEST(DynatraceTracestateTest, TestKey) { + DtTracestateEntry tracestate("98812ad49", "980df25c"); + EXPECT_STREQ(tracestate.getKey().c_str(), "98812ad49-980df25c@dt"); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate_test.cc b/test/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate_test.cc new file mode 100644 index 000000000000..f8f72542281d --- /dev/null +++ b/test/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate_test.cc @@ -0,0 +1,132 @@ +#include + +#include "source/extensions/tracers/opentelemetry/samplers/dynatrace/tracestate.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +// Test parsing an empty string +TEST(TracestateTest, TestEmpty) { + Tracestate tracestate; + tracestate.parse(""); + EXPECT_EQ(0, tracestate.entries().size()); + EXPECT_STREQ(tracestate.asString().c_str(), ""); +} + +// Test parsing a tracestate string with one entry +TEST(TracestateTest, TestSingleEntry) { + Tracestate tracestate; + tracestate.parse("key0=value0"); + auto entries = tracestate.entries(); + EXPECT_EQ(1, entries.size()); + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); +} + +// Test parsing a tracestate string an invalid entry +TEST(TracestateTest, TestInvalidEntry) { + Tracestate tracestate; + tracestate.parse("key0=value0,key1="); + auto entries = tracestate.entries(); + EXPECT_EQ(1, entries.size()); + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); + + tracestate.parse("key0=value0,=value1"); + entries = tracestate.entries(); + EXPECT_EQ(1, entries.size()); + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); + + tracestate.parse("key0=value0,something"); + entries = tracestate.entries(); + EXPECT_EQ(1, entries.size()); + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); +} + +// Test parsing a tracestate string with multiple entries +TEST(TracestateTest, TestMultiEntries) { + Tracestate tracestate; + tracestate.parse("key0=value0,key1=value1,key2=value2,key3=value3"); + auto entries = tracestate.entries(); + EXPECT_EQ(4, entries.size()); + + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); + + EXPECT_STREQ(entries[1].key.c_str(), "key1"); + EXPECT_STREQ(entries[1].value.c_str(), "value1"); + + EXPECT_STREQ(entries[2].key.c_str(), "key2"); + EXPECT_STREQ(entries[2].value.c_str(), "value2"); + + EXPECT_STREQ(entries[3].key.c_str(), "key3"); + EXPECT_STREQ(entries[3].value.c_str(), "value3"); +} + +// Test parsing a tracestate string with optional white spaces +TEST(TracestateTest, TestWithOWS) { + Tracestate tracestate; + // whitespace before and after ',' should be removed + // whitespace inside value is allowed + const char* c = + "key0=value0,key1=value1, key2=val ue2 , key3=value3 ,key4=value4 , key5=value5"; + tracestate.parse(c); + EXPECT_STREQ(tracestate.asString().c_str(), c); + auto entries = tracestate.entries(); + EXPECT_EQ(6, entries.size()); + + EXPECT_STREQ(entries[0].key.c_str(), "key0"); + EXPECT_STREQ(entries[0].value.c_str(), "value0"); + + EXPECT_STREQ(entries[1].key.c_str(), "key1"); + EXPECT_STREQ(entries[1].value.c_str(), "value1"); + + EXPECT_STREQ(entries[2].key.c_str(), "key2"); + EXPECT_STREQ(entries[2].value.c_str(), "val ue2"); + + EXPECT_STREQ(entries[3].key.c_str(), "key3"); + EXPECT_STREQ(entries[3].value.c_str(), "value3"); + + EXPECT_STREQ(entries[4].key.c_str(), "key4"); + EXPECT_STREQ(entries[4].value.c_str(), "value4"); + + EXPECT_STREQ(entries[5].key.c_str(), "key5"); + EXPECT_STREQ(entries[5].value.c_str(), "value5"); +} + +// Test appending to an empty tracestate +TEST(TracestateTest, TestAppendToEmpty) { + Tracestate tracestate; + tracestate.add("new_key", "new_value"); + auto entries = tracestate.entries(); + EXPECT_EQ(1, entries.size()); + EXPECT_STREQ(entries[0].key.c_str(), "new_key"); + EXPECT_STREQ(entries[0].value.c_str(), "new_value"); + EXPECT_STREQ(tracestate.asString().c_str(), "new_key=new_value"); +} + +// Test appending to an existing tracestate +TEST(TracestateTest, TestAppend) { + Tracestate tracestate; + tracestate.parse("key1=value1,key2=value2,key3=value3"); + tracestate.add("new_key", "new_value"); + + auto entries = tracestate.entries(); + EXPECT_EQ(4, entries.size()); + EXPECT_STREQ(tracestate.asString().c_str(), + "new_key=new_value,key1=value1,key2=value2,key3=value3"); + EXPECT_STREQ(entries[0].key.c_str(), "new_key"); + EXPECT_STREQ(entries[0].value.c_str(), "new_value"); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy