diff --git a/api/envoy/config/trace/v3/opentelemetry.proto b/api/envoy/config/trace/v3/opentelemetry.proto index e9c7430dcfdd7..8d8cc936be36d 100644 --- a/api/envoy/config/trace/v3/opentelemetry.proto +++ b/api/envoy/config/trace/v3/opentelemetry.proto @@ -2,9 +2,13 @@ syntax = "proto3"; package envoy.config.trace.v3; +import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/grpc_service.proto"; +import "google/protobuf/duration.proto"; + import "udpa/annotations/status.proto"; +import "validate/validate.proto"; option java_package = "io.envoyproxy.envoy.config.trace.v3"; option java_outer_classname = "OpentelemetryProto"; @@ -17,10 +21,41 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration for the OpenTelemetry tracer. // [#extension: envoy.tracers.opentelemetry] message OpenTelemetryConfig { - // The upstream gRPC cluster that will receive OTLP traces. - // Note that the tracer drops traces if the server does not read data fast enough. - // This field can be left empty to disable reporting traces to the collector. - core.v3.GrpcService grpc_service = 1; + // Configuration for sending traces to the collector over HTTP. Note that + // this will only be used if the grpc_service is not set above. + message HttpConfig { + + // The upstream cluster that will receive OTLP traces over HTTP. + string cluster_name = 1; + + // The path to the trace ingest endpoint. The path is appended with the host name configured in the cluster. + // Optional: If omitted, "/v1/traces" will be used as default. + string traces_path = 2; + + // Sets the maximum duration in milliseconds that a response can take to arrive upon request. + google.protobuf.Duration timeout = 3 [(validate.rules).duration = { + required: true + gte {} + }]; + + // Custom header values to be added to the OTLP HTTP request. + repeated config.core.v3.HeaderValue headers = 4; + + // The hostname to include in the Host header of the OTLP HTTP request. + string hostname = 5; + } + + oneof export_protocol { + option (validate.required) = true; + + // The upstream gRPC cluster that will receive OTLP traces. + // Note that the tracer drops traces if the server does not read data fast enough. + // This field can be left empty to disable reporting traces to the collector. + core.v3.GrpcService grpc_service = 1; + + // The configuration to export OTLP traces via HTTP. + HttpConfig http_config = 3; + } // The name for the service. This will be populated in the ResourceSpan Resource attributes. // If it is not provided, it will default to "unknown_service:envoy". diff --git a/source/extensions/tracers/opentelemetry/BUILD b/source/extensions/tracers/opentelemetry/BUILD index 3a36a3f91fd50..246e2b9cdd858 100644 --- a/source/extensions/tracers/opentelemetry/BUILD +++ b/source/extensions/tracers/opentelemetry/BUILD @@ -34,9 +34,10 @@ envoy_cc_library( "span_context.h", "span_context_extractor.h", "tracer.h", + "tracer_stats.h", ], deps = [ - ":grpc_trace_exporter", + ":trace_exporter", "//envoy/thread_local:thread_local_interface", "//source/common/config:utility_lib", "//source/common/tracing:http_tracer_lib", @@ -47,13 +48,28 @@ envoy_cc_library( ) envoy_cc_library( - name = "grpc_trace_exporter", - srcs = ["grpc_trace_exporter.cc"], - hdrs = ["grpc_trace_exporter.h"], + name = "trace_exporter", + srcs = [ + "grpc_trace_exporter.cc", + "http_trace_exporter.cc", + ], + hdrs = [ + "grpc_trace_exporter.h", + "http_trace_exporter.h", + "trace_exporter.h", + "tracer_stats.h", + ], deps = [ "//envoy/grpc:async_client_manager_interface", + "//envoy/upstream:cluster_manager_interface", "//source/common/grpc:typed_async_client_lib", + "//source/common/http:async_client_utility_lib", + "//source/common/http:header_map_lib", + "//source/common/http:message_lib", + "//source/common/http:utility_lib", "//source/common/protobuf", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/trace/v3:pkg_cc_proto", "@opentelemetry_proto//:trace_cc_proto", ], ) diff --git a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h index 2d6ff1be89771..40f70a0b8b0a8 100644 --- a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h +++ b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h @@ -6,6 +6,7 @@ #include "source/common/grpc/typed_async_client.h" #include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" +#include "trace_exporter.h" namespace Envoy { namespace Extensions { @@ -80,18 +81,16 @@ class OpenTelemetryGrpcTraceExporterClient : Logger::Loggable { +class OpenTelemetryGrpcTraceExporter : public OpenTelemetryTraceExporter { public: OpenTelemetryGrpcTraceExporter(const Grpc::RawAsyncClientSharedPtr& client); - bool log(const ExportTraceServiceRequest& request); + bool log(const ExportTraceServiceRequest& request) override; private: OpenTelemetryGrpcTraceExporterClient client_; }; -using OpenTelemetryGrpcTraceExporterPtr = std::unique_ptr; - } // namespace OpenTelemetry } // namespace Tracers } // namespace Extensions diff --git a/source/extensions/tracers/opentelemetry/http_trace_exporter.cc b/source/extensions/tracers/opentelemetry/http_trace_exporter.cc new file mode 100644 index 0000000000000..943ff6c20d75c --- /dev/null +++ b/source/extensions/tracers/opentelemetry/http_trace_exporter.cc @@ -0,0 +1,86 @@ +#include +#include +#include + +#include "http_trace_exporter.h" + +#include "source/common/common/logger.h" +#include "source/common/protobuf/protobuf.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +OpenTelemetryHttpTraceExporter::OpenTelemetryHttpTraceExporter( + Upstream::ClusterManager& cluster_manager, + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config, + OpenTelemetryTracerStats& tracing_stats) + : cluster_manager_(cluster_manager), http_config_(http_config), tracing_stats_(tracing_stats) {} + +bool OpenTelemetryHttpTraceExporter::log(const ExportTraceServiceRequest& request) { + + std::string request_body; + + const auto ok = request.SerializeToString(&request_body); + if (!ok) { + ENVOY_LOG(warn, "Error while serializing the binary proto ExportTraceServiceRequest."); + return false; + } + + Http::RequestMessagePtr message = std::make_unique(); + message->headers().setReferenceMethod(Http::Headers::get().MethodValues.Post); + message->headers().setReferenceContentType(Http::Headers::get().ContentTypeValues.Protobuf); + + // If traces_path is omitted, send to /v1/traces by default + if (http_config_.traces_path().empty()) { + message->headers().setPath(TRACES_PATH); + } else { + message->headers().setPath(http_config_.traces_path()); + } + + // TODO: Can we get the hostname that is configured in the cluster "socker_address" field? + message->headers().setHost(http_config_.hostname()); + + // add all custom headers to the request + for (const envoy::config::core::v3::HeaderValue& header : http_config_.headers()) { + message->headers().setCopy(Http::LowerCaseString(header.key()), header.value()); + } + + message->body().add(request_body); + + const auto thread_local_cluster = + cluster_manager_.getThreadLocalCluster(http_config_.cluster_name()); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(warn, "Thread local cluster not found for collector."); + return false; + } + + std::chrono::milliseconds timeout = std::chrono::duration_cast( + std::chrono::nanoseconds(http_config_.timeout().nanos())); + Http::AsyncClient::Request* http_request = thread_local_cluster->httpAsyncClient().send( + std::move(message), *this, Http::AsyncClient::RequestOptions().setTimeout(timeout)); + tracing_stats_.http_reports_sent_.inc(); + + return http_request; +} + +void OpenTelemetryHttpTraceExporter::onSuccess(const Http::AsyncClient::Request&, + Http::ResponseMessagePtr&& message) { + tracing_stats_.http_reports_success_.inc(); + const auto response_code = message->headers().Status()->value().getStringView(); + if (response_code != "200") { + ENVOY_LOG(warn, "response code: {}", response_code); + } +} + +void OpenTelemetryHttpTraceExporter::onFailure(const Http::AsyncClient::Request&, + Http::AsyncClient::FailureReason) { + ENVOY_LOG(debug, "Request failed."); + tracing_stats_.http_reports_failed_.inc(); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/http_trace_exporter.h b/source/extensions/tracers/opentelemetry/http_trace_exporter.h new file mode 100644 index 0000000000000..8c5b0e7a6fcd2 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/http_trace_exporter.h @@ -0,0 +1,54 @@ + +#pragma once + +#include "envoy/config/core/v3/http_uri.pb.h" +#include "envoy/config/trace/v3/opentelemetry.pb.h" +#include "envoy/upstream/cluster_manager.h" + +#include "source/common/common/logger.h" +#include "source/common/http/async_client_impl.h" +#include "source/common/http/async_client_utility.h" +#include "source/common/http/headers.h" +#include "source/common/http/message_impl.h" +#include "source/common/http/utility.h" + +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" +#include "trace_exporter.h" +#include "tracer_stats.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * Exporter for OTLP traces over HTTP. + */ +class OpenTelemetryHttpTraceExporter : public OpenTelemetryTraceExporter, + public Http::AsyncClient::Callbacks { +public: + OpenTelemetryHttpTraceExporter( + Upstream::ClusterManager& cluster_manager, + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config, + OpenTelemetryTracerStats& tracing_stats); + + // The default path to use when OpenTelemetryConfig::HttpConfig::traces_path is empty + const Http::LowerCaseString TRACES_PATH{"/v1/traces"}; + + bool log(const ExportTraceServiceRequest& request) override; + + // Http::AsyncClient::Callbacks. + void onSuccess(const Http::AsyncClient::Request&, Http::ResponseMessagePtr&&) override; + void onFailure(const Http::AsyncClient::Request&, Http::AsyncClient::FailureReason) override; + void onBeforeFinalizeUpstreamSpan(Tracing::Span&, const Http::ResponseHeaderMap*) override {} + +private: + Upstream::ClusterManager& cluster_manager_; + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config_; + OpenTelemetryTracerStats& tracing_stats_; +}; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc index a3119c187e836..6a2581b2da843 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc @@ -9,10 +9,13 @@ #include "source/common/config/utility.h" #include "source/common/tracing/http_tracer_impl.h" +#include "grpc_trace_exporter.h" +#include "http_trace_exporter.h" #include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" #include "opentelemetry/proto/trace/v1/trace.pb.h" #include "span_context.h" #include "span_context_extractor.h" +#include "trace_exporter.h" #include "tracer.h" namespace Envoy { @@ -28,14 +31,24 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr auto& factory_context = context.serverFactoryContext(); // Create the tracer in Thread Local Storage. tls_slot_ptr_->set([opentelemetry_config, &factory_context, this](Event::Dispatcher& dispatcher) { - OpenTelemetryGrpcTraceExporterPtr exporter; - if (opentelemetry_config.has_grpc_service()) { - Grpc::AsyncClientFactoryPtr&& factory = + OpenTelemetryTraceExporterPtr exporter; + switch (opentelemetry_config.export_protocol_case()) { + case envoy::config::trace::v3::OpenTelemetryConfig::ExportProtocolCase::kGrpcService: { + Grpc::AsyncClientFactoryPtr&& factory = factory_context.clusterManager().grpcAsyncClientManager().factoryForGrpcService( opentelemetry_config.grpc_service(), factory_context.scope(), true); - const Grpc::RawAsyncClientSharedPtr& async_client_shared_ptr = - factory->createUncachedRawAsyncClient(); - exporter = std::make_unique(async_client_shared_ptr); + const Grpc::RawAsyncClientSharedPtr& async_client_shared_ptr = + factory->createUncachedRawAsyncClient(); + exporter = std::make_unique(async_client_shared_ptr); + break; + } + case envoy::config::trace::v3::OpenTelemetryConfig::ExportProtocolCase::kHttpConfig: { + exporter = std::make_unique( + factory_context.clusterManager(), opentelemetry_config.http_config(), tracing_stats_); + break; + } + default: + break; } TracerPtr tracer = std::make_unique( std::move(exporter), factory_context.timeSource(), factory_context.api().randomGenerator(), diff --git a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h index 35f734c87b824..dd9bb68453b88 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h @@ -8,8 +8,8 @@ #include "source/common/common/logger.h" #include "source/common/singleton/const_singleton.h" #include "source/extensions/tracers/common/factory_base.h" -#include "source/extensions/tracers/opentelemetry/grpc_trace_exporter.h" -#include "source/extensions/tracers/opentelemetry/tracer.h" + +#include "tracer.h" namespace Envoy { namespace Extensions { diff --git a/source/extensions/tracers/opentelemetry/trace_exporter.h b/source/extensions/tracers/opentelemetry/trace_exporter.h new file mode 100644 index 0000000000000..f6b49d45e6fcc --- /dev/null +++ b/source/extensions/tracers/opentelemetry/trace_exporter.h @@ -0,0 +1,26 @@ +#pragma once + +#include "source/common/common/logger.h" + +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" + +using opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest; + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +class OpenTelemetryTraceExporter : public Logger::Loggable { +public: + virtual ~OpenTelemetryTraceExporter() = default; + + virtual bool log(const ExportTraceServiceRequest& request) = 0; +}; + +using OpenTelemetryTraceExporterPtr = std::unique_ptr; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/tracer.cc b/source/extensions/tracers/opentelemetry/tracer.cc index a344a253ab31e..82eb830dcba52 100644 --- a/source/extensions/tracers/opentelemetry/tracer.cc +++ b/source/extensions/tracers/opentelemetry/tracer.cc @@ -91,7 +91,7 @@ void Span::setTag(absl::string_view name, absl::string_view value) { *span_.add_attributes() = key_value; } -Tracer::Tracer(OpenTelemetryGrpcTraceExporterPtr exporter, Envoy::TimeSource& time_source, +Tracer::Tracer(OpenTelemetryTraceExporterPtr exporter, Envoy::TimeSource& time_source, Random::RandomGenerator& random, Runtime::Loader& runtime, Event::Dispatcher& dispatcher, OpenTelemetryTracerStats tracing_stats, const std::string& service_name) diff --git a/source/extensions/tracers/opentelemetry/tracer.h b/source/extensions/tracers/opentelemetry/tracer.h index f80cb12fd3f76..8d4573996ac26 100644 --- a/source/extensions/tracers/opentelemetry/tracer.h +++ b/source/extensions/tracers/opentelemetry/tracer.h @@ -14,26 +14,19 @@ #include "absl/strings/escaping.h" #include "span_context.h" +#include "tracer_stats.h" namespace Envoy { namespace Extensions { namespace Tracers { namespace OpenTelemetry { -#define OPENTELEMETRY_TRACER_STATS(COUNTER) \ - COUNTER(spans_sent) \ - COUNTER(timer_flushed) - -struct OpenTelemetryTracerStats { - OPENTELEMETRY_TRACER_STATS(GENERATE_COUNTER_STRUCT) -}; - /** * OpenTelemetry Tracer. It is stored in TLS and contains the exporter. */ class Tracer : Logger::Loggable { public: - Tracer(OpenTelemetryGrpcTraceExporterPtr exporter, Envoy::TimeSource& time_source, + Tracer(OpenTelemetryTraceExporterPtr exporter, Envoy::TimeSource& time_source, Random::RandomGenerator& random, Runtime::Loader& runtime, Event::Dispatcher& dispatcher, OpenTelemetryTracerStats tracing_stats, const std::string& service_name); @@ -55,7 +48,7 @@ class Tracer : Logger::Loggable { */ void flushSpans(); - OpenTelemetryGrpcTraceExporterPtr exporter_; + OpenTelemetryTraceExporterPtr exporter_; Envoy::TimeSource& time_source_; Random::RandomGenerator& random_; std::vector<::opentelemetry::proto::trace::v1::Span> span_buffer_; diff --git a/source/extensions/tracers/opentelemetry/tracer_stats.h b/source/extensions/tracers/opentelemetry/tracer_stats.h new file mode 100644 index 0000000000000..bb14b901ddd57 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/tracer_stats.h @@ -0,0 +1,24 @@ +#pragma once + +#include "envoy/stats/stats_macros.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +#define OPENTELEMETRY_TRACER_STATS(COUNTER) \ + COUNTER(http_reports_failed) \ + COUNTER(http_reports_sent) \ + COUNTER(http_reports_success) \ + COUNTER(spans_sent) \ + COUNTER(timer_flushed) + +struct OpenTelemetryTracerStats { + OPENTELEMETRY_TRACER_STATS(GENERATE_COUNTER_STRUCT) +}; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/BUILD b/test/extensions/tracers/opentelemetry/BUILD index c1ac977443d69..96e7e5d915375 100644 --- a/test/extensions/tracers/opentelemetry/BUILD +++ b/test/extensions/tracers/opentelemetry/BUILD @@ -73,9 +73,23 @@ envoy_extension_cc_test( srcs = ["grpc_trace_exporter_test.cc"], extension_names = ["envoy.tracers.opentelemetry"], deps = [ - "//source/extensions/tracers/opentelemetry:grpc_trace_exporter", + "//source/extensions/tracers/opentelemetry:trace_exporter", "//test/mocks/grpc:grpc_mocks", "//test/mocks/http:http_mocks", "//test/test_common:utility_lib", ], ) + +envoy_extension_cc_test( + name = "http_trace_exporter_test", + srcs = ["http_trace_exporter_test.cc"], + extension_names = ["envoy.tracers.opentelemetry"], + deps = [ + "//source/extensions/tracers/opentelemetry:trace_exporter", + "//test/mocks/http:http_mocks", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/mocks/stats:stats_mocks", + "//test/mocks/upstream:cluster_manager_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/tracers/opentelemetry/config_test.cc b/test/extensions/tracers/opentelemetry/config_test.cc index 72206f07910df..ae9c11970eaaf 100644 --- a/test/extensions/tracers/opentelemetry/config_test.cc +++ b/test/extensions/tracers/opentelemetry/config_test.cc @@ -16,7 +16,7 @@ namespace Extensions { namespace Tracers { namespace OpenTelemetry { -TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracer) { +TEST(OpenTelemetryTracerConfigTest, OpenTelemetryTracerWithGrpcExporter) { NiceMock context; context.server_factory_context_.cluster_manager_.initializeClusters({"fake_cluster"}, {}); OpenTelemetryTracerFactory factory; @@ -41,7 +41,7 @@ TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracer) { EXPECT_NE(nullptr, opentelemetry_tracer); } -TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracerNoExporter) { +TEST(OpenTelemetryTracerConfigTest, OpenTelemetryTracerWithHttpExporter) { NiceMock context; context.server_factory_context_.cluster_manager_.initializeClusters({"fake_cluster"}, {}); OpenTelemetryTracerFactory factory; @@ -51,6 +51,14 @@ TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracerNoExporter) { name: envoy.tracers.opentelemetry typed_config: "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + http_config: + cluster_name: "my_o11y_backend" + traces_path: "/otlp/v1/traces" + hostname: "some-o11y.com" + headers: + - key: "Authorization" + value: "auth-token" + timeout: 0.250s )EOF"; envoy::config::trace::v3::Tracing configuration; TestUtility::loadFromYaml(yaml_string, configuration); @@ -61,6 +69,26 @@ TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracerNoExporter) { EXPECT_NE(nullptr, opentelemetry_tracer); } +TEST(OpenTelemetryTracerConfigTest, OpenTelemetryTracerNoExporter) { + NiceMock context; + context.server_factory_context_.cluster_manager_.initializeClusters({"fake_cluster"}, {}); + OpenTelemetryTracerFactory factory; + + const std::string yaml_string = R"EOF( + http: + name: envoy.tracers.opentelemetry + typed_config: + "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + )EOF"; + envoy::config::trace::v3::Tracing configuration; + TestUtility::loadFromYaml(yaml_string, configuration); + + auto message = Config::Utility::translateToFactoryConfig( + configuration.http(), ProtobufMessage::getStrictValidationVisitor(), factory); + + EXPECT_THROW_WITH_REGEX(factory.createTracerDriver(*message, context), EnvoyException, "field: \"export_protocol\", reason: is required"); +} + } // namespace OpenTelemetry } // namespace Tracers } // namespace Extensions diff --git a/test/extensions/tracers/opentelemetry/http_trace_exporter_test.cc b/test/extensions/tracers/opentelemetry/http_trace_exporter_test.cc new file mode 100644 index 0000000000000..e564deab21687 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/http_trace_exporter_test.cc @@ -0,0 +1,219 @@ +#include + +#include "source/common/buffer/zero_copy_input_stream_impl.h" +#include "source/extensions/tracers/opentelemetry/http_trace_exporter.h" + +#include "test/mocks/common.h" +#include "test/mocks/grpc/mocks.h" +#include "test/mocks/server/tracer_factory_context.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +using testing::_; +using testing::Invoke; +using testing::Return; +using testing::ReturnRef; + +class OpenTelemetryHttpTraceExporterTest : public testing::Test { +public: + OpenTelemetryHttpTraceExporterTest() = default; + + void setup(envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config) { + cluster_manager_.thread_local_cluster_.cluster_.info_->name_ = "my_o11y_backend"; + cluster_manager_.initializeThreadLocalClusters({"my_o11y_backend"}); + ON_CALL(cluster_manager_.thread_local_cluster_, httpAsyncClient()) + .WillByDefault(ReturnRef(cluster_manager_.thread_local_cluster_.async_client_)); + + cluster_manager_.initializeClusters({"my_o11y_backend"}, {}); + + trace_exporter_ = + std::make_unique(cluster_manager_, http_config, stats_); + } + +protected: + NiceMock cluster_manager_; + std::unique_ptr trace_exporter_; + NiceMock context_; + NiceMock& mock_scope_ = context_.server_factory_context_.store_; + OpenTelemetryTracerStats stats_{OpenTelemetryTracerStats{ + OPENTELEMETRY_TRACER_STATS(POOL_COUNTER_PREFIX(mock_scope_, "tracing.opentelemetry."))}}; +}; + +TEST_F(OpenTelemetryHttpTraceExporterTest, CreateExporterAndExportSpan) { + std::string yaml_string = fmt::format(R"EOF( + cluster_name: "my_o11y_backend" + traces_path: "/otlp/v1/traces" + hostname: "some-o11y.com" + headers: + - key: "Authorization" + value: "auth-token" + - key: "x-custom-header" + value: "custom-value" + timeout: 0.250s + )EOF"); + + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config; + TestUtility::loadFromYaml(yaml_string, http_config); + setup(http_config); + + Http::MockAsyncClientRequest request(&cluster_manager_.thread_local_cluster_.async_client_); + Http::AsyncClient::Callbacks* callback; + + EXPECT_CALL( + cluster_manager_.thread_local_cluster_.async_client_, + send_(_, _, Http::AsyncClient::RequestOptions().setTimeout(std::chrono::milliseconds(250)))) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callback = &callbacks; + + EXPECT_EQ(Http::Headers::get().MethodValues.Post, message->headers().getMethodValue()); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.Protobuf, message->headers().getContentTypeValue()); + + EXPECT_EQ("/otlp/v1/traces", message->headers().getPathValue()); + EXPECT_EQ("some-o11y.com", message->headers().getHostValue()); + + // Custom headers provided in the configuration + EXPECT_EQ("auth-token", + message->headers().get(Http::LowerCaseString("authorization"))[0]->value().getStringView()); + EXPECT_EQ("custom-value", + message->headers().get(Http::LowerCaseString("x-custom-header"))[0]->value().getStringView()); + + return &request; + })); + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest + export_trace_service_request; + opentelemetry::proto::trace::v1::Span span; + span.set_name("test"); + *export_trace_service_request.add_resource_spans()->add_scope_spans()->add_spans() = span; + EXPECT_TRUE(trace_exporter_->log(export_trace_service_request)); + + Http::ResponseMessagePtr msg(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "202"}}})); + // onBeforeFinalizeUpstreamSpan is a noop — included for coverage + Tracing::NullSpan null_span; + callback->onBeforeFinalizeUpstreamSpan(null_span, nullptr); + + callback->onSuccess(request, std::move(msg)); + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_sent").value()); + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_success").value()); + EXPECT_EQ(0U, mock_scope_.counter("tracing.opentelemetry.http_reports_failed").value()); + + callback->onFailure(request, Http::AsyncClient::FailureReason::Reset); + + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_failed").value()); +} + +TEST_F(OpenTelemetryHttpTraceExporterTest, UnsuccessfulLogWithoutThreadLocalCluster) { + std::string yaml_string = fmt::format(R"EOF( + cluster_name: "my_o11y_backend" + traces_path: "/otlp/v1/traces" + hostname: "some-o11y.com" + timeout: 0.250s + )EOF"); + + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config; + TestUtility::loadFromYaml(yaml_string, http_config); + setup(http_config); + + Http::MockAsyncClientRequest request(&cluster_manager_.thread_local_cluster_.async_client_); + + ON_CALL(cluster_manager_, getThreadLocalCluster(absl::string_view("my_o11y_backend"))) + .WillByDefault(Return(nullptr)); + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest + export_trace_service_request; + opentelemetry::proto::trace::v1::Span span; + span.set_name("test"); + *export_trace_service_request.add_resource_spans()->add_scope_spans()->add_spans() = span; + EXPECT_FALSE(trace_exporter_->log(export_trace_service_request)); +} + +TEST_F(OpenTelemetryHttpTraceExporterTest, CreateExporterAndExportSpanWithDefaultPath) { + std::string yaml_string = fmt::format(R"EOF( + cluster_name: "my_o11y_backend" + hostname: "some-o11y.com" + timeout: 0.250s + )EOF"); + + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config; + TestUtility::loadFromYaml(yaml_string, http_config); + setup(http_config); + + Http::MockAsyncClientRequest request(&cluster_manager_.thread_local_cluster_.async_client_); + Http::AsyncClient::Callbacks* callback; + + EXPECT_CALL( + cluster_manager_.thread_local_cluster_.async_client_, + send_(_, _, Http::AsyncClient::RequestOptions().setTimeout(std::chrono::milliseconds(250)))) + .WillOnce( + Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& callbacks, + const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { + callback = &callbacks; + + // Default path is used when omitted in the config + EXPECT_EQ("/v1/traces", message->headers().getPathValue()); + + return &request; + })); + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest + export_trace_service_request; + opentelemetry::proto::trace::v1::Span span; + span.set_name("test"); + *export_trace_service_request.add_resource_spans()->add_scope_spans()->add_spans() = span; + EXPECT_TRUE(trace_exporter_->log(export_trace_service_request)); + + Http::ResponseMessagePtr msg(new Http::ResponseMessageImpl( + Http::ResponseHeaderMapPtr{new Http::TestResponseHeaderMapImpl{{":status", "202"}}})); + // onBeforeFinalizeUpstreamSpan is a noop — included for coverage + Tracing::NullSpan null_span; + callback->onBeforeFinalizeUpstreamSpan(null_span, nullptr); + + callback->onSuccess(request, std::move(msg)); + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_sent").value()); + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_success").value()); + EXPECT_EQ(0U, mock_scope_.counter("tracing.opentelemetry.http_reports_failed").value()); + + callback->onFailure(request, Http::AsyncClient::FailureReason::Reset); + + EXPECT_EQ(1U, mock_scope_.counter("tracing.opentelemetry.http_reports_failed").value()); +} + +TEST_F(OpenTelemetryHttpTraceExporterTest, ErrorSerializingOtlpRequest) { + std::string yaml_string = fmt::format(R"EOF( + cluster_name: "my_o11y_backend" + hostname: "some-o11y.com" + timeout: 0.250s + )EOF"); + + envoy::config::trace::v3::OpenTelemetryConfig::HttpConfig http_config; + TestUtility::loadFromYaml(yaml_string, http_config); + setup(http_config); + + Http::MockAsyncClientRequest request(&cluster_manager_.thread_local_cluster_.async_client_); + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest + export_trace_service_request; + + // Fake an invalid OTLP message passed to the exporter + std::string invalid_message = "invalid"; + export_trace_service_request.ParseFromArray(invalid_message.c_str(), invalid_message.size()); + + EXPECT_FALSE(trace_exporter_->log(export_trace_service_request)); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc index 7a427a500955f..bf5f5b91bc2b9 100644 --- a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc +++ b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc @@ -60,6 +60,23 @@ class OpenTelemetryDriverTest : public testing::Test { setup(opentelemetry_config); } + void setupValidDriverWithHttpExporter() { + const std::string yaml_string = R"EOF( + http_config: + cluster_name: "my_o11y_backend" + traces_path: "/otlp/v1/traces" + hostname: "some-o11y.com" + headers: + - key: "Authorization" + value: "auth-token" + timeout: 0.250s + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + setup(opentelemetry_config); + } + protected: const std::string operation_name_{"test"}; NiceMock context_; @@ -80,6 +97,11 @@ TEST_F(OpenTelemetryDriverTest, InitializeDriverValidConfig) { EXPECT_NE(driver_, nullptr); } +TEST_F(OpenTelemetryDriverTest, InitializeDriverValidConfigHttpExporter) { + setupValidDriverWithHttpExporter(); + EXPECT_NE(driver_, nullptr); +} + TEST_F(OpenTelemetryDriverTest, ParseSpanContextFromHeadersTest) { // Set up driver setupValidDriver(); @@ -511,6 +533,38 @@ TEST_F(OpenTelemetryDriverTest, ExportSpanWithCustomServiceName) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanHTTP) { + context_.server_factory_context_.cluster_manager_.thread_local_cluster_.cluster_.info_->name_ = + "my_o11y_backend"; + context_.server_factory_context_.cluster_manager_.initializeThreadLocalClusters( + {"my_o11y_backend"}); + ON_CALL(context_.server_factory_context_.cluster_manager_.thread_local_cluster_, + httpAsyncClient()) + .WillByDefault(ReturnRef( + context_.server_factory_context_.cluster_manager_.thread_local_cluster_.async_client_)); + context_.server_factory_context_.cluster_manager_.initializeClusters({"my_o11y_backend"}, + {}); + setupValidDriverWithHttpExporter(); + + Http::TestRequestHeaderMapImpl request_headers{ + {":authority", "test.com"}, {":path", "/"}, {":method", "GET"}}; + Tracing::SpanPtr span = driver_->startSpan(mock_tracing_config_, request_headers, stream_info_, + operation_name_, {Tracing::Reason::Sampling, true}); + EXPECT_NE(span.get(), nullptr); + + // Flush after a single span. + EXPECT_CALL(runtime_.snapshot_, getInteger("tracing.opentelemetry.min_flush_spans", 5U)) + .Times(1) + .WillRepeatedly(Return(1)); + // We should see a call to the async client to export that single span. + EXPECT_CALL(context_.server_factory_context_.cluster_manager_.thread_local_cluster_.async_client_, + send_(_, _, _)); + + span->finishSpan(); + + EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); +} + } // namespace OpenTelemetry } // namespace Tracers } // namespace Extensions