diff --git a/api/envoy/config/core/v3/http_service.proto b/api/envoy/config/core/v3/http_service.proto new file mode 100644 index 000000000000..426994c033ca --- /dev/null +++ b/api/envoy/config/core/v3/http_service.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package envoy.config.core.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/http_uri.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.core.v3"; +option java_outer_classname = "HttpServiceProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/config/core/v3;corev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: HTTP services] + +// HTTP service configuration. +message HttpService { + // The service's HTTP URI. For example: + // + // .. code-block:: yaml + // + // http_uri: + // uri: https://www.myserviceapi.com/v1/data + // cluster: www.myserviceapi.com|443 + // + HttpUri http_uri = 1; + + // Specifies a list of HTTP headers that should be added to each request + // handled by this virtual host. + repeated HeaderValueOption request_headers_to_add = 2 + [(validate.rules).repeated = {max_items: 1000}]; +} diff --git a/api/envoy/config/trace/v3/opentelemetry.proto b/api/envoy/config/trace/v3/opentelemetry.proto index e9c7430dcfdd..7ae6a964bd72 100644 --- a/api/envoy/config/trace/v3/opentelemetry.proto +++ b/api/envoy/config/trace/v3/opentelemetry.proto @@ -3,7 +3,9 @@ syntax = "proto3"; package envoy.config.trace.v3; import "envoy/config/core/v3/grpc_service.proto"; +import "envoy/config/core/v3/http_service.proto"; +import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; option java_package = "io.envoyproxy.envoy.config.trace.v3"; @@ -19,8 +21,24 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; 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; + // This field can be left empty to disable reporting traces to the gRPC service. + // Only one of ``grpc_service``, ``http_service`` may be used. + core.v3.GrpcService grpc_service = 1 + [(udpa.annotations.field_migrate).oneof_promotion = "otlp_exporter"]; + + // The upstream HTTP cluster that will receive OTLP traces. + // This field can be left empty to disable reporting traces to the HTTP service. + // Only one of ``grpc_service``, ``http_service`` may be used. + // + // .. note:: + // + // Note: The ``request_headers_to_add`` property in the OTLP HTTP exporter service + // does not support the :ref:`format specifier ` as used for + // :ref:`HTTP access logging `. + // The values configured are added as HTTP headers on the OTLP export request + // without any formatting applied. + core.v3.HttpService http_service = 3 + [(udpa.annotations.field_migrate).oneof_promotion = "otlp_exporter"]; // 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/changelogs/current.yaml b/changelogs/current.yaml index 4fe2257ecdb4..e2c07d0eb8eb 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -416,6 +416,9 @@ new_features: change: | Added ``metadata`` support for :ref:`virtual host ` and :ref:`route configuration `. +- area: tracing + change: | + Added support for exporting spans via HTTP on the OpenTelemetry tracer. deprecated: - area: tracing diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index 6fc045c73e47..a1f3488a86d8 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -23,6 +23,7 @@ Common messages ../extensions/filters/common/dependency/v3/dependency.proto ../extensions/regex_engines/v3/google_re2.proto ../config/core/v3/grpc_method_list.proto + ../config/core/v3/http_service.proto ../config/core/v3/grpc_service.proto ../extensions/key_value/file_based/v3/config.proto ../config/common/key_value/v3/config.proto diff --git a/source/extensions/tracers/opentelemetry/BUILD b/source/extensions/tracers/opentelemetry/BUILD index 3a36a3f91fd5..58d0a20ba5b7 100644 --- a/source/extensions/tracers/opentelemetry/BUILD +++ b/source/extensions/tracers/opentelemetry/BUILD @@ -36,7 +36,7 @@ envoy_cc_library( "tracer.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 +47,26 @@ 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", + ], 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", "@opentelemetry_proto//:trace_cc_proto", ], ) diff --git a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.cc b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.cc index ac418f33ff86..5b7e6bda0669 100644 --- a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.cc +++ b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.cc @@ -1,4 +1,3 @@ -#include "grpc_trace_exporter.h" #include "source/extensions/tracers/opentelemetry/grpc_trace_exporter.h" #include "source/common/common/logger.h" diff --git a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h index 2d6ff1be8977..ecfc4baaa27e 100644 --- a/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h +++ b/source/extensions/tracers/opentelemetry/grpc_trace_exporter.h @@ -4,6 +4,7 @@ #include "source/common/common/logger.h" #include "source/common/grpc/typed_async_client.h" +#include "source/extensions/tracers/opentelemetry/trace_exporter.h" #include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" @@ -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 000000000000..b1fb4efdc9c1 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/http_trace_exporter.cc @@ -0,0 +1,94 @@ +#include "source/extensions/tracers/opentelemetry/http_trace_exporter.h" + +#include +#include +#include +#include + +#include "source/common/common/enum_to_int.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, + const envoy::config::core::v3::HttpService& http_service) + : cluster_manager_(cluster_manager), http_service_(http_service) { + + // Prepare and store headers to be used later on each export request + for (const auto& header_value_option : http_service_.request_headers_to_add()) { + parsed_headers_to_add_.push_back({Http::LowerCaseString(header_value_option.header().key()), + header_value_option.header().value()}); + } +} + +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; + } + + const auto thread_local_cluster = + cluster_manager_.getThreadLocalCluster(http_service_.http_uri().cluster()); + if (thread_local_cluster == nullptr) { + ENVOY_LOG(error, "OTLP HTTP exporter failed: [cluster = {}] is not configured", + http_service_.http_uri().cluster()); + return false; + } + + Http::RequestMessagePtr message = Http::Utility::prepareHeaders(http_service_.http_uri()); + + // The request follows the OTLP HTTP specification: + // https://github.com/open-telemetry/opentelemetry-proto/blob/v1.0.0/docs/specification.md#otlphttp. + message->headers().setReferenceMethod(Http::Headers::get().MethodValues.Post); + message->headers().setReferenceContentType(Http::Headers::get().ContentTypeValues.Protobuf); + + // Add all custom headers to the request. + for (const auto& header_pair : parsed_headers_to_add_) { + message->headers().setReference(header_pair.first, header_pair.second); + } + message->body().add(request_body); + + const auto options = Http::AsyncClient::RequestOptions().setTimeout(std::chrono::milliseconds( + DurationUtil::durationToMilliseconds(http_service_.http_uri().timeout()))); + + Http::AsyncClient::Request* in_flight_request = + thread_local_cluster->httpAsyncClient().send(std::move(message), *this, options); + + if (in_flight_request == nullptr) { + return false; + } + + active_requests_.add(*in_flight_request); + return true; +} + +void OpenTelemetryHttpTraceExporter::onSuccess(const Http::AsyncClient::Request& request, + Http::ResponseMessagePtr&& http_response) { + active_requests_.remove(request); + const auto response_code = Http::Utility::getResponseStatus(http_response->headers()); + if (response_code != enumToInt(Http::Code::OK)) { + ENVOY_LOG(error, + "OTLP HTTP exporter received a non-success status code: {} while exporting the OTLP " + "message", + response_code); + } +} + +void OpenTelemetryHttpTraceExporter::onFailure(const Http::AsyncClient::Request& request, + Http::AsyncClient::FailureReason reason) { + active_requests_.remove(request); + ENVOY_LOG(debug, "The OTLP export request failed. Reason {}", enumToInt(reason)); +} + +} // 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 000000000000..ee5a5cf36564 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/http_trace_exporter.h @@ -0,0 +1,48 @@ +#pragma once + +#include "envoy/config/core/v3/http_service.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 "source/extensions/tracers/opentelemetry/trace_exporter.h" + +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.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, + const envoy::config::core::v3::HttpService& http_service); + + 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::core::v3::HttpService http_service_; + // Track active HTTP requests to be able to cancel them on destruction. + Http::AsyncClientRequestTracker active_requests_; + std::vector> parsed_headers_to_add_; +}; + +} // 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 4ff15757b468..52e40c5cffbc 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc @@ -8,12 +8,15 @@ #include "source/common/common/logger.h" #include "source/common/config/utility.h" #include "source/common/tracing/http_tracer_impl.h" +#include "source/extensions/tracers/opentelemetry/grpc_trace_exporter.h" +#include "source/extensions/tracers/opentelemetry/http_trace_exporter.h" +#include "source/extensions/tracers/opentelemetry/span_context.h" +#include "source/extensions/tracers/opentelemetry/span_context_extractor.h" +#include "source/extensions/tracers/opentelemetry/trace_exporter.h" +#include "source/extensions/tracers/opentelemetry/tracer.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 "tracer.h" namespace Envoy { namespace Extensions { @@ -26,9 +29,16 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr tracing_stats_{OPENTELEMETRY_TRACER_STATS( POOL_COUNTER_PREFIX(context.serverFactoryContext().scope(), "tracing.opentelemetry"))} { auto& factory_context = context.serverFactoryContext(); + + if (opentelemetry_config.has_grpc_service() && opentelemetry_config.has_http_service()) { + throw EnvoyException( + "OpenTelemetry Tracer cannot have both gRPC and HTTP exporters configured. " + "OpenTelemetry tracer will be disabled."); + } + // Create the tracer in Thread Local Storage. tls_slot_ptr_->set([opentelemetry_config, &factory_context, this](Event::Dispatcher& dispatcher) { - OpenTelemetryGrpcTraceExporterPtr exporter; + OpenTelemetryTraceExporterPtr exporter; if (opentelemetry_config.has_grpc_service()) { Grpc::AsyncClientFactoryPtr&& factory = factory_context.clusterManager().grpcAsyncClientManager().factoryForGrpcService( @@ -36,6 +46,9 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr const Grpc::RawAsyncClientSharedPtr& async_client_shared_ptr = factory->createUncachedRawAsyncClient(); exporter = std::make_unique(async_client_shared_ptr); + } else if (opentelemetry_config.has_http_service()) { + exporter = std::make_unique( + factory_context.clusterManager(), opentelemetry_config.http_service()); } 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 bbdc94b43f16..5083cff22f6e 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h @@ -8,7 +8,6 @@ #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" namespace Envoy { diff --git a/source/extensions/tracers/opentelemetry/span_context_extractor.cc b/source/extensions/tracers/opentelemetry/span_context_extractor.cc index 0b8b3e535b8f..fb9cb9e977f3 100644 --- a/source/extensions/tracers/opentelemetry/span_context_extractor.cc +++ b/source/extensions/tracers/opentelemetry/span_context_extractor.cc @@ -1,4 +1,4 @@ -#include "span_context_extractor.h" +#include "source/extensions/tracers/opentelemetry/span_context_extractor.h" #include "envoy/tracing/tracer.h" diff --git a/source/extensions/tracers/opentelemetry/span_context_extractor.h b/source/extensions/tracers/opentelemetry/span_context_extractor.h index 961119dc470d..ddac0f55024e 100644 --- a/source/extensions/tracers/opentelemetry/span_context_extractor.h +++ b/source/extensions/tracers/opentelemetry/span_context_extractor.h @@ -5,8 +5,7 @@ #include "source/common/common/statusor.h" #include "source/common/http/header_map_impl.h" - -#include "span_context.h" +#include "source/extensions/tracers/opentelemetry/span_context.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 000000000000..df4ac67d8617 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/trace_exporter.h @@ -0,0 +1,38 @@ +#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 { + +/** + * @brief Base class for all OpenTelemetry Protocol (OTLP) exporters. + * @see + * https://github.com/open-telemetry/opentelemetry-proto/blob/v1.0.0/docs/specification.md#otlphttp + */ +class OpenTelemetryTraceExporter : public Logger::Loggable { +public: + virtual ~OpenTelemetryTraceExporter() = default; + + /** + * @brief Exports the trace request to the configured OTLP service. + * + * @param request The protobuf-encoded OTLP trace request. + * @return true When the request was sent. + * @return false When sending the request failed. + */ + 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 ca52280ed359..683d5ea87d5f 100644 --- a/source/extensions/tracers/opentelemetry/tracer.cc +++ b/source/extensions/tracers/opentelemetry/tracer.cc @@ -107,7 +107,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 7f0d547e356b..07d38ef22c8e 100644 --- a/source/extensions/tracers/opentelemetry/tracer.h +++ b/source/extensions/tracers/opentelemetry/tracer.h @@ -11,9 +11,9 @@ #include "source/common/common/logger.h" #include "source/extensions/tracers/common/factory_base.h" #include "source/extensions/tracers/opentelemetry/grpc_trace_exporter.h" +#include "source/extensions/tracers/opentelemetry/span_context.h" #include "absl/strings/escaping.h" -#include "span_context.h" namespace Envoy { namespace Extensions { @@ -33,7 +33,7 @@ struct OpenTelemetryTracerStats { */ 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); @@ -57,7 +57,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/test/extensions/tracers/opentelemetry/BUILD b/test/extensions/tracers/opentelemetry/BUILD index c1ac977443d6..96e7e5d91537 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 72206f07910d..839107f24e0e 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,15 @@ TEST(OpenTelemetryTracerConfigTest, OpenTelemetryHttpTracerNoExporter) { name: envoy.tracers.opentelemetry typed_config: "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + http_service: + http_uri: + uri: "https://some-o11y.com//otlp/v1/traces" + cluster: "my_o11y_backend" + timeout: 0.250s + request_headers_to_add: + - header: + key: "Authorization" + value: "auth-token" )EOF"; envoy::config::trace::v3::Tracing configuration; TestUtility::loadFromYaml(yaml_string, configuration); @@ -61,6 +70,27 @@ 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); + + auto opentelemetry_tracer = factory.createTracerDriver(*message, context); + EXPECT_NE(nullptr, opentelemetry_tracer); +} + } // 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 000000000000..b6d5702c27e5 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/http_trace_exporter_test.cc @@ -0,0 +1,146 @@ +#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::core::v3::HttpService http_service) { + 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_service); + } + +protected: + NiceMock cluster_manager_; + std::unique_ptr trace_exporter_; + NiceMock context_; + NiceMock& mock_scope_ = context_.server_factory_context_.store_; +}; + +// Test exporting an OTLP message via HTTP containing one span +TEST_F(OpenTelemetryHttpTraceExporterTest, CreateExporterAndExportSpan) { + std::string yaml_string = fmt::format(R"EOF( + http_uri: + uri: "https://some-o11y.com/otlp/v1/traces" + cluster: "my_o11y_backend" + timeout: 0.250s + request_headers_to_add: + - header: + key: "Authorization" + value: "auth-token" + - header: + key: "x-custom-header" + value: "custom-value" + )EOF"); + + envoy::config::core::v3::HttpService http_service; + TestUtility::loadFromYaml(yaml_string, http_service); + setup(http_service); + + 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)); + callback->onFailure(request, Http::AsyncClient::FailureReason::Reset); +} + +// Test export is aborted when cluster is not found +TEST_F(OpenTelemetryHttpTraceExporterTest, UnsuccessfulLogWithoutThreadLocalCluster) { + std::string yaml_string = fmt::format(R"EOF( + http_uri: + uri: "https://some-o11y.com/otlp/v1/traces" + cluster: "my_o11y_backend" + timeout: 10s + )EOF"); + + envoy::config::core::v3::HttpService http_service; + TestUtility::loadFromYaml(yaml_string, http_service); + setup(http_service); + + 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)); +} + +} // 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 bdf680898539..4954854efd3d 100644 --- a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc +++ b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc @@ -60,6 +60,24 @@ class OpenTelemetryDriverTest : public testing::Test { setup(opentelemetry_config); } + void setupValidDriverWithHttpExporter() { + const std::string yaml_string = R"EOF( + http_service: + http_uri: + cluster: "my_o11y_backend" + uri: "https://some-o11y.com/otlp/v1/traces" + timeout: 0.250s + request_headers_to_add: + - header: + key: "Authorization" + value: "auth-token" + )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_; @@ -75,11 +93,41 @@ class OpenTelemetryDriverTest : public testing::Test { Stats::Scope& scope_{*stats_.rootScope()}; }; +// Tests the tracer initialization with the gRPC exporter TEST_F(OpenTelemetryDriverTest, InitializeDriverValidConfig) { setupValidDriver(); EXPECT_NE(driver_, nullptr); } +// Tests the tracer initialization with the HTTP exporter +TEST_F(OpenTelemetryDriverTest, InitializeDriverValidConfigHttpExporter) { + setupValidDriverWithHttpExporter(); + EXPECT_NE(driver_, nullptr); +} + +// Verifies that the tracer cannot be configured with two exporters at the same time +TEST_F(OpenTelemetryDriverTest, BothGrpcAndHttpExportersConfigured) { + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + http_service: + http_uri: + cluster: "my_o11y_backend" + uri: "https://some-o11y.com/otlp/v1/traces" + timeout: 0.250s + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + EXPECT_THROW_WITH_MESSAGE(setup(opentelemetry_config), EnvoyException, + "OpenTelemetry Tracer cannot have both gRPC and HTTP exporters " + "configured. OpenTelemetry tracer will be disabled."); + EXPECT_EQ(driver_, nullptr); +} + +// Verifies traceparent/tracestate headers are properly parsed and propagated TEST_F(OpenTelemetryDriverTest, ParseSpanContextFromHeadersTest) { // Set up driver setupValidDriver(); @@ -167,6 +215,7 @@ TEST_F(OpenTelemetryDriverTest, ParseSpanContextFromHeadersTest) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies span is properly created when the incoming request has no traceparent/tracestate headers TEST_F(OpenTelemetryDriverTest, GenerateSpanContextWithoutHeadersTest) { // Set up driver setupValidDriver(); @@ -206,6 +255,7 @@ TEST_F(OpenTelemetryDriverTest, GenerateSpanContextWithoutHeadersTest) { "00-00000000000000010000000000000002-0000000000000003-01"); } +// Verifies a span it not created when an invalid traceparent header is received TEST_F(OpenTelemetryDriverTest, NullSpanWithPropagationHeaderError) { setupValidDriver(); // Add an invalid OTLP header to the request headers. @@ -221,6 +271,7 @@ TEST_F(OpenTelemetryDriverTest, NullSpanWithPropagationHeaderError) { EXPECT_EQ(typeid(null_span).name(), typeid(Tracing::NullSpan).name()); } +// Verifies the export happens after one span is created TEST_F(OpenTelemetryDriverTest, ExportOTLPSpan) { // Set up driver setupValidDriver(); @@ -248,6 +299,7 @@ TEST_F(OpenTelemetryDriverTest, ExportOTLPSpan) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies the export happens only when a second span is created TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithBuffer) { // Set up driver setupValidDriver(); @@ -275,6 +327,7 @@ TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithBuffer) { EXPECT_EQ(2U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies the export happens after a timeout TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithFlushTimeout) { timer_ = new NiceMock(&context_.server_factory_context_.thread_local_.dispatcher_); @@ -307,6 +360,7 @@ TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithFlushTimeout) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.timer_flushed").value()); } +// Verifies child span is related to parent span TEST_F(OpenTelemetryDriverTest, SpawnChildSpan) { // Set up driver setupValidDriver(); @@ -347,6 +401,7 @@ TEST_F(OpenTelemetryDriverTest, SpawnChildSpan) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies the span types TEST_F(OpenTelemetryDriverTest, SpanType) { // Set up driver setupValidDriver(); @@ -464,6 +519,7 @@ TEST_F(OpenTelemetryDriverTest, SpanType) { } } +// Verifies spans are exported with their attributes TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithAttributes) { setupValidDriver(); Http::TestRequestHeaderMapImpl request_headers{ @@ -529,6 +585,7 @@ TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithAttributes) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Not sampled spans are ignored TEST_F(OpenTelemetryDriverTest, IgnoreNotSampledSpan) { setupValidDriver(); Http::TestRequestHeaderMapImpl request_headers{ @@ -545,6 +602,7 @@ TEST_F(OpenTelemetryDriverTest, IgnoreNotSampledSpan) { EXPECT_EQ(0U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies tracer is "disabled" when no exporter is configured TEST_F(OpenTelemetryDriverTest, NoExportWithoutGrpcService) { const std::string yaml_string = "{}"; envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; @@ -568,6 +626,7 @@ TEST_F(OpenTelemetryDriverTest, NoExportWithoutGrpcService) { EXPECT_EQ(0U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies a custom service name is properly set on exported spans TEST_F(OpenTelemetryDriverTest, ExportSpanWithCustomServiceName) { const std::string yaml_string = R"EOF( grpc_service: @@ -628,6 +687,38 @@ TEST_F(OpenTelemetryDriverTest, ExportSpanWithCustomServiceName) { EXPECT_EQ(1U, stats_.counter("tracing.opentelemetry.spans_sent").value()); } +// Verifies the export using the HTTP exporter +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