diff --git a/source/extensions/tracers/datadog/BUILD b/source/extensions/tracers/datadog/BUILD index f21618260f50b..cee4cd93feddb 100644 --- a/source/extensions/tracers/datadog/BUILD +++ b/source/extensions/tracers/datadog/BUILD @@ -16,11 +16,13 @@ envoy_cc_library( srcs = [ "datadog_tracer_impl.cc", "dict_util.cc", + "span.cc", "time_util.cc", ], hdrs = [ "datadog_tracer_impl.h", "dict_util.h", + "span.h", "time_util.h", "tracer_stats.h", ], @@ -30,8 +32,8 @@ envoy_cc_library( "-DDD_USE_ABSEIL_FOR_ENVOY", ], external_deps = [ - "dd_opentracing_cpp", "dd_trace_cpp", + "dd_opentracing_cpp", ], deps = [ "//source/common/config:utility_lib", diff --git a/source/extensions/tracers/datadog/datadog_tracer_impl.h b/source/extensions/tracers/datadog/datadog_tracer_impl.h index d48253b9a4e96..bcdb6ba442c64 100644 --- a/source/extensions/tracers/datadog/datadog_tracer_impl.h +++ b/source/extensions/tracers/datadog/datadog_tracer_impl.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "envoy/config/trace/v3/datadog.pb.h" #include "envoy/local_info/local_info.h" #include "envoy/runtime/runtime.h" @@ -14,6 +12,7 @@ #include "source/common/upstream/cluster_update_tracker.h" #include "source/extensions/tracers/common/ot/opentracing_driver_impl.h" +#include "datadog/opentracing.h" #include "fmt/ostream.h" namespace Envoy { diff --git a/source/extensions/tracers/datadog/dict_util.h b/source/extensions/tracers/datadog/dict_util.h index a844a5cce1adb..518e4ab164928 100644 --- a/source/extensions/tracers/datadog/dict_util.h +++ b/source/extensions/tracers/datadog/dict_util.h @@ -10,11 +10,11 @@ * headers, or writing HTTP request headers. */ -#include -#include - #include +#include "datadog/dict_reader.h" +#include "datadog/dict_writer.h" + namespace Envoy { namespace Tracing { class TraceContext; diff --git a/source/extensions/tracers/datadog/span.cc b/source/extensions/tracers/datadog/span.cc new file mode 100644 index 0000000000000..45922a20e620f --- /dev/null +++ b/source/extensions/tracers/datadog/span.cc @@ -0,0 +1,117 @@ +#include "source/extensions/tracers/datadog/span.h" + +#include + +#include "source/common/tracing/null_span_impl.h" +#include "source/extensions/tracers/datadog/time_util.h" + +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "datadog/dict_writer.h" +#include "datadog/sampling_priority.h" +#include "datadog/span_config.h" +#include "datadog/trace_segment.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { +namespace { + +class TraceContextWriter : public datadog::tracing::DictWriter { +public: + explicit TraceContextWriter(Tracing::TraceContext& context) : context_(context) {} + + void set(datadog::tracing::StringView key, datadog::tracing::StringView value) override { + context_.setByKey(key, value); + } + +private: + Tracing::TraceContext& context_; +}; + +} // namespace + +Span::Span(datadog::tracing::Span&& span) : span_(std::move(span)) {} + +const datadog::tracing::Optional& Span::impl() const { return span_; } + +void Span::setOperation(absl::string_view operation) { + if (!span_) { + return; + } + + span_->set_name(operation); +} + +void Span::setTag(absl::string_view name, absl::string_view value) { + if (!span_) { + return; + } + + span_->set_tag(name, value); +} + +void Span::log(SystemTime, const std::string&) { + // Datadog spans don't have in-bound "events" or "logs". +} + +void Span::finishSpan() { span_.reset(); } + +void Span::injectContext(Tracing::TraceContext& trace_context, + const Upstream::HostDescriptionConstSharedPtr&) { + if (!span_) { + return; + } + + TraceContextWriter writer{trace_context}; + span_->inject(writer); +} + +Tracing::SpanPtr Span::spawnChild(const Tracing::Config&, const std::string& name, + SystemTime start_time) { + if (!span_) { + // I don't expect this to happen. This means that `spawnChild` was called + // after `finishSpan`. + return std::make_unique(); + } + + // The OpenTracing implementation ignored the `Tracing::Config` argument, + // so we will as well. + datadog::tracing::SpanConfig config; + config.name = name; + config.start = estimateTime(start_time); + + return std::make_unique(span_->create_child(config)); +} + +void Span::setSampled(bool sampled) { + if (!span_) { + return; + } + + auto priority = static_cast(sampled ? datadog::tracing::SamplingPriority::USER_KEEP + : datadog::tracing::SamplingPriority::USER_DROP); + span_->trace_segment().override_sampling_priority(priority); +} + +std::string Span::getBaggage(absl::string_view) { + // not implemented + return std::string{}; +} + +void Span::setBaggage(absl::string_view, absl::string_view) { + // not implemented +} + +std::string Span::getTraceIdAsHex() const { + if (!span_) { + return std::string{}; + } + return absl::StrCat(absl::Hex(span_->id())); +} + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/datadog/span.h b/source/extensions/tracers/datadog/span.h new file mode 100644 index 0000000000000..85f3ed47a4d56 --- /dev/null +++ b/source/extensions/tracers/datadog/span.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include "envoy/common/time.h" +#include "envoy/tracing/trace_driver.h" + +#include "datadog/span.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { + +/** + * Tracing::Span implementation for use in Datadog tracing. This class contains + * an optional and forwards its member functions to the + * corresponding member functions of datadog::tracing::Span. + * + * datadog::tracing::Span is the span type used in Datadog's core tracing + * library, dd-trace-cpp. It's wrapped in an optional<> because the lifetime + * of a datadog::tracing::Span is tied to the scope of the object itself, + * whereas the Tracing::Span implemented here has a finishSpan() member + * function that allows the span's lifetime to end without destroying the + * object. + * + * For the same reason, this class has two states: one when the + * optional is not empty and member functions are + * forwarded to it, and another state when the optional + * is empty and member functions have no effect. + */ +class Span : public Tracing::Span { +public: + explicit Span(datadog::tracing::Span&& span); + + const datadog::tracing::Optional& impl() const; + + // Envoy::Tracing::Span + void setOperation(absl::string_view operation) override; + void setTag(absl::string_view name, absl::string_view value) override; + void log(SystemTime, const std::string&) override; + void finishSpan() override; + void injectContext(Tracing::TraceContext& trace_context, + const Upstream::HostDescriptionConstSharedPtr& upstream) override; + Tracing::SpanPtr spawnChild(const Tracing::Config& config, const std::string& name, + SystemTime start_time) override; + void setSampled(bool sampled) override; + std::string getBaggage(absl::string_view key) override; + void setBaggage(absl::string_view key, absl::string_view value) override; + std::string getTraceIdAsHex() const override; + +private: + datadog::tracing::Optional span_; +}; + +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/datadog/time_util.h b/source/extensions/tracers/datadog/time_util.h index 723c4ded82d2a..d37b9818915e2 100644 --- a/source/extensions/tracers/datadog/time_util.h +++ b/source/extensions/tracers/datadog/time_util.h @@ -3,10 +3,10 @@ /** * This file contains functions related to time points and durations. * - * Envoy has a time type that contains both a system time point and a steady - * ("monotonic") time point. However, only the system time is exposed to the - * tracing subsystem. This may be remedied in the future, but for now we work - * with the system time. + * Envoy has a TimeSource abstraction that provides both a system time point and + * a steady ("monotonic") time point. However, only the system time is exposed + * to the tracing subsystem. This may be remedied in the future, but for now we + * work with the system time. * * This is problematic for the Datadog core tracing library (dd-trace-cpp), * because it uses the steady time to calculate the duration of a span @@ -19,10 +19,10 @@ * estimateTime function. */ -#include - #include "envoy/common/time.h" +#include "datadog/clock.h" + namespace Envoy { namespace Extensions { namespace Tracers { diff --git a/test/extensions/tracers/datadog/BUILD b/test/extensions/tracers/datadog/BUILD index 737dc0a6c1635..97698df9b2c63 100644 --- a/test/extensions/tracers/datadog/BUILD +++ b/test/extensions/tracers/datadog/BUILD @@ -16,6 +16,7 @@ envoy_extension_cc_test( srcs = [ "datadog_tracer_impl_test.cc", "dict_util_test.cc", + "span_test.cc", "time_util_test.cc", "tracer_stats_test.cc", ], diff --git a/test/extensions/tracers/datadog/dict_util_test.cc b/test/extensions/tracers/datadog/dict_util_test.cc index d20e669c101b4..855744fde7aec 100644 --- a/test/extensions/tracers/datadog/dict_util_test.cc +++ b/test/extensions/tracers/datadog/dict_util_test.cc @@ -1,5 +1,3 @@ -#include - #include #include @@ -10,6 +8,7 @@ #include "test/mocks/http/mocks.h" #include "test/test_common/utility.h" +#include "datadog/optional.h" #include "gtest/gtest.h" namespace Envoy { diff --git a/test/extensions/tracers/datadog/span_test.cc b/test/extensions/tracers/datadog/span_test.cc new file mode 100644 index 0000000000000..15e754dfceb3b --- /dev/null +++ b/test/extensions/tracers/datadog/span_test.cc @@ -0,0 +1,305 @@ +#include +#include +#include +#include +#include +#include + +#include "source/common/tracing/null_span_impl.h" +#include "source/extensions/tracers/datadog/span.h" +#include "source/extensions/tracers/datadog/time_util.h" + +#include "test/mocks/tracing/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "datadog/clock.h" +#include "datadog/collector.h" +#include "datadog/expected.h" +#include "datadog/id_generator.h" +#include "datadog/json.hpp" +#include "datadog/logger.h" +#include "datadog/sampling_priority.h" +#include "datadog/span_data.h" +#include "datadog/tags.h" +#include "datadog/tracer.h" +#include "gtest/gtest.h" + +namespace datadog { +namespace tracing { + +bool operator==(const TimePoint& left, const TimePoint& right) { + return left.wall == right.wall && left.tick == right.tick; +} + +} // namespace tracing +} // namespace datadog + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace Datadog { +namespace { + +class NullLogger : public datadog::tracing::Logger { +public: + ~NullLogger() override = default; + + void log_error(const LogFunc&) override {} + void log_startup(const LogFunc&) override {} + + void log_error(const datadog::tracing::Error&) override {} + void log_error(datadog::tracing::StringView) override {} +}; + +struct MockCollector : public datadog::tracing::Collector { + datadog::tracing::Expected + send(std::vector>&& spans, + const std::shared_ptr&) override { + chunks.push_back(std::move(spans)); + return {}; + } + + nlohmann::json config_json() const override { + return nlohmann::json::object({{"type", "Envoy::Extensions::Tracers::Datadog::MockCollector"}}); + } + + ~MockCollector() override = default; + + std::vector>> chunks; +}; + +class DatadogTracerSpanTest : public testing::Test { +public: + DatadogTracerSpanTest() + : id_(0xcafebabe), collector_(std::make_shared()), + config_(makeConfig(collector_)), + tracer_( + // Override the tracer's ID generator so that all trace IDs and span + // IDs are 0xcafebabe. + *datadog::tracing::finalize_config(config_), [this]() { return id_; }, + datadog::tracing::default_clock), + span_(tracer_.create_span()) {} + +private: + static datadog::tracing::TracerConfig + makeConfig(const std::shared_ptr& collector) { + datadog::tracing::TracerConfig config; + config.defaults.service = "testsvc"; + config.collector = collector; + config.logger = std::make_shared(); + // Drop all spans. Equivalently, we could keep all spans. + datadog::tracing::TraceSamplerConfig::Rule rule; + rule.sample_rate = 0; + config.trace_sampler.rules.push_back(std::move(rule)); + return config; + } + +protected: + const std::uint64_t id_; + const std::shared_ptr collector_; + const datadog::tracing::TracerConfig config_; + datadog::tracing::Tracer tracer_; + datadog::tracing::Span span_; + Event::SimulatedTimeSystem time_; +}; + +TEST_F(DatadogTracerSpanTest, SetOperation) { + Span span{std::move(span_)}; + span.setOperation("gastric bypass"); + span.finishSpan(); + + ASSERT_EQ(1, collector_->chunks.size()); + const auto& chunk = collector_->chunks[0]; + ASSERT_EQ(1, chunk.size()); + const auto& data_ptr = chunk[0]; + ASSERT_NE(nullptr, data_ptr); + const datadog::tracing::SpanData& data = *data_ptr; + + EXPECT_EQ("gastric bypass", data.name); +} + +TEST_F(DatadogTracerSpanTest, SetTag) { + Span span{std::move(span_)}; + span.setTag("foo", "bar"); + span.setTag("boom", "bam"); + span.setTag("foo", "new"); + span.finishSpan(); + + ASSERT_EQ(1, collector_->chunks.size()); + const auto& chunk = collector_->chunks[0]; + ASSERT_EQ(1, chunk.size()); + const auto& data_ptr = chunk[0]; + ASSERT_NE(nullptr, data_ptr); + const datadog::tracing::SpanData& data = *data_ptr; + + auto found = data.tags.find("foo"); + ASSERT_NE(data.tags.end(), found); + EXPECT_EQ("new", found->second); + + found = data.tags.find("boom"); + ASSERT_NE(data.tags.end(), found); + EXPECT_EQ("bam", found->second); +} + +TEST_F(DatadogTracerSpanTest, InjectContext) { + Span span{std::move(span_)}; + + Tracing::TestTraceContextImpl context{}; + span.injectContext(context, nullptr); + // Span::injectContext doesn't modify any of named fields. + EXPECT_EQ("", context.context_protocol_); + EXPECT_EQ("", context.context_host_); + EXPECT_EQ("", context.context_path_); + EXPECT_EQ("", context.context_method_); + + // Span::injectContext inserts propagation headers that depend on the + // propagation style configured (i.e. the DD_TRACE_PROPAGATION_STYLE_INJECT + // environment variable). The default style includes Datadog propagation + // headers, so we check those here. + auto found = context.context_map_.find("x-datadog-trace-id"); + ASSERT_NE(context.context_map_.end(), found); + EXPECT_EQ(std::to_string(id_), found->second); + found = context.context_map_.find("x-datadog-parent-id"); + ASSERT_NE(context.context_map_.end(), found); + EXPECT_EQ(std::to_string(id_), found->second); + found = context.context_map_.find("x-datadog-sampling-priority"); + ASSERT_NE(context.context_map_.end(), found); + // USER_DROP because we set a rule that keeps nothing. + EXPECT_EQ(std::to_string(int(datadog::tracing::SamplingPriority::USER_DROP)), found->second); +} + +TEST_F(DatadogTracerSpanTest, SpawnChild) { + const auto child_start = time_.timeSystem().systemTime(); + { + Span parent{std::move(span_)}; + auto child = parent.spawnChild(Tracing::MockConfig{}, "child", child_start); + child->finishSpan(); + parent.finishSpan(); + } + + EXPECT_EQ(1, collector_->chunks.size()); + const auto& spans = collector_->chunks[0]; + EXPECT_EQ(2, spans.size()); + const auto& child_ptr = spans[1]; + EXPECT_NE(nullptr, child_ptr); + const datadog::tracing::SpanData& child = *child_ptr; + EXPECT_EQ(estimateTime(child_start).wall, child.start.wall); + EXPECT_EQ("child", child.name); + EXPECT_EQ(id_, child.trace_id); + EXPECT_EQ(id_, child.span_id); + EXPECT_EQ(id_, child.parent_id); +} + +TEST_F(DatadogTracerSpanTest, SetSampledTrue) { + // `Span::setSampled(bool)` on any span causes the entire group (chunk) of + // spans to take that sampling override. In terms of dd-trace-cpp, this means + // that the local root of the chunk will have its + // `datadog::tracing::tags::internal::sampling_priority` tag set to either -1 + // (hard drop) or 2 (hard keep). + { + Span local_root{std::move(span_)}; + auto child = + local_root.spawnChild(Tracing::MockConfig{}, "child", time_.timeSystem().systemTime()); + child->setSampled(true); + child->finishSpan(); + local_root.finishSpan(); + } + EXPECT_EQ(1, collector_->chunks.size()); + const auto& spans = collector_->chunks[0]; + EXPECT_EQ(2, spans.size()); + const auto& local_root_ptr = spans[0]; + EXPECT_NE(nullptr, local_root_ptr); + const datadog::tracing::SpanData& local_root = *local_root_ptr; + const auto found = + local_root.numeric_tags.find(datadog::tracing::tags::internal::sampling_priority); + EXPECT_NE(local_root.numeric_tags.end(), found); + EXPECT_EQ(2, found->second); +} + +TEST_F(DatadogTracerSpanTest, SetSampledFalse) { + // `Span::setSampled(bool)` on any span causes the entire group (chunk) of + // spans to take that sampling override. In terms of dd-trace-cpp, this means + // that the local root of the chunk will have its + // `datadog::tracing::tags::internal::sampling_priority` tag set to either -1 + // (hard drop) or 2 (hard keep). + { + Span local_root{std::move(span_)}; + auto child = + local_root.spawnChild(Tracing::MockConfig{}, "child", time_.timeSystem().systemTime()); + child->setSampled(false); + child->finishSpan(); + local_root.finishSpan(); + } + EXPECT_EQ(1, collector_->chunks.size()); + const auto& spans = collector_->chunks[0]; + EXPECT_EQ(2, spans.size()); + const auto& local_root_ptr = spans[0]; + EXPECT_NE(nullptr, local_root_ptr); + const datadog::tracing::SpanData& local_root = *local_root_ptr; + const auto found = + local_root.numeric_tags.find(datadog::tracing::tags::internal::sampling_priority); + EXPECT_NE(local_root.numeric_tags.end(), found); + EXPECT_EQ(-1, found->second); +} + +TEST_F(DatadogTracerSpanTest, Baggage) { + // Baggage is not supported by dd-trace-cpp, so `Span::getBaggage` and + // `Span::setBaggage` do nothing. + Span span{std::move(span_)}; + EXPECT_EQ("", span.getBaggage("foo")); + span.setBaggage("foo", "bar"); + EXPECT_EQ("", span.getBaggage("foo")); +} + +TEST_F(DatadogTracerSpanTest, GetTraceIdAsHex) { + Span span{std::move(span_)}; + EXPECT_EQ("cafebabe", span.getTraceIdAsHex()); +} + +TEST_F(DatadogTracerSpanTest, NoOpMode) { + // `Span::finishSpan` destroys its `datadog::tracing::Span` member. + // Subsequently, methods called on the `Span` do nothing. + // + // I don't expect that Envoy will call methods on a finished span, and it's + // hard to verify that the operations are no-ops, so this test just exercises + // the code paths to verify that they don't trip any memory violations. + Span span{std::move(span_)}; + span.finishSpan(); + + // `Span::finishSpan` is idempotent. + span.finishSpan(); + + // Inner `datadog::tracing::Span` really is destroyed. + const datadog::tracing::Optional& impl = span.impl(); + EXPECT_EQ(datadog::tracing::nullopt, impl); + + // Other methods. + span.setOperation("foo"); + span.setTag("foo", "bar"); + // `Span::log` doesn't do anything in any case. + span.log(time_.timeSystem().systemTime(), "ignored"); + Tracing::TestTraceContextImpl context{}; + span.injectContext(context, nullptr); + EXPECT_EQ("", context.context_protocol_); + EXPECT_EQ("", context.context_host_); + EXPECT_EQ("", context.context_path_); + EXPECT_EQ("", context.context_method_); + EXPECT_EQ(0, context.context_map_.size()); + const Tracing::SpanPtr child = + span.spawnChild(Tracing::MockConfig{}, "child", time_.timeSystem().systemTime()); + EXPECT_NE(nullptr, child); + const Tracing::Span& child_span = *child; + EXPECT_EQ(typeid(Tracing::NullSpan), typeid(child_span)); + span.setSampled(true); + span.setSampled(false); + EXPECT_EQ("", span.getBaggage("foo")); + span.setBaggage("foo", "bar"); + EXPECT_EQ("", span.getTraceIdAsHex()); +} + +} // namespace +} // namespace Datadog +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/datadog/time_util_test.cc b/test/extensions/tracers/datadog/time_util_test.cc index 472677f6dde95..21efa6ab8d1f6 100644 --- a/test/extensions/tracers/datadog/time_util_test.cc +++ b/test/extensions/tracers/datadog/time_util_test.cc @@ -1,9 +1,8 @@ -#include - #include "envoy/common/time.h" #include "source/extensions/tracers/datadog/time_util.h" +#include "datadog/clock.h" #include "gtest/gtest.h" namespace Envoy {