diff --git a/api/BUILD b/api/BUILD index 76facfe2dda1..d40a6c4d7470 100644 --- a/api/BUILD +++ b/api/BUILD @@ -306,6 +306,7 @@ proto_library( "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", + "//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", "//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg", "//envoy/extensions/transport_sockets/internal_upstream/v3:pkg", diff --git a/api/envoy/config/trace/v3/opentelemetry.proto b/api/envoy/config/trace/v3/opentelemetry.proto index 7ae6a964bd72..5d9c9202cb5a 100644 --- a/api/envoy/config/trace/v3/opentelemetry.proto +++ b/api/envoy/config/trace/v3/opentelemetry.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.config.trace.v3; +import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/grpc_service.proto"; import "envoy/config/core/v3/http_service.proto"; @@ -43,4 +44,8 @@ message OpenTelemetryConfig { // 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". string service_name = 2; + + // An ordered list of resource detectors + // [#extension-category: envoy.tracers.opentelemetry.resource_detectors] + repeated core.v3.TypedExtensionConfig resource_detectors = 4; } diff --git a/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/BUILD b/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.proto b/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.proto new file mode 100644 index 000000000000..df62fc2d9e42 --- /dev/null +++ b/api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package envoy.extensions.tracers.opentelemetry.resource_detectors.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.tracers.opentelemetry.resource_detectors.v3"; +option java_outer_classname = "EnvironmentResourceDetectorProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/tracers/opentelemetry/resource_detectors/v3;resource_detectorsv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Environment Resource Detector config] + +// Configuration for the Environment Resource detector extension. +// The resource detector reads from the ``OTEL_RESOURCE_ATTRIBUTES`` +// environment variable, as per the OpenTelemetry specification. +// +// See: +// +// `OpenTelemetry specification `_ +// +// [#extension: envoy.tracers.opentelemetry.resource_detectors.environment] +message EnvironmentResourceDetectorConfig { +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 9f8638e33ee2..9ad67e06e99c 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -245,6 +245,7 @@ proto_library( "//envoy/extensions/stat_sinks/graphite_statsd/v3:pkg", "//envoy/extensions/stat_sinks/open_telemetry/v3:pkg", "//envoy/extensions/stat_sinks/wasm/v3:pkg", + "//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", "//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg", "//envoy/extensions/transport_sockets/internal_upstream/v3:pkg", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index cb74cc6886e8..cff26a84b048 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -73,7 +73,6 @@ new_features: change: | added :ref:`per_endpoint_stats ` to get some metrics for each endpoint in a cluster. - - area: jwt change: | The jwt filter can now serialize non-primitive custom claims when maping claims to headers. @@ -90,5 +89,8 @@ new_features: returns an error or cannot be reached with :ref:`status_on_error ` configuration flag. +- area: tracing + change: | + Added support for configuring resource detectors on the OpenTelemetry tracer. deprecated: diff --git a/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst b/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst new file mode 100644 index 000000000000..87790ac145ec --- /dev/null +++ b/docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst @@ -0,0 +1,10 @@ +OpenTelemetry Resource Detectors +================================ + +Resource detectors that can be configured with the OpenTelemetry Tracer: + +.. toctree:: + :glob: + :maxdepth: 3 + + ../../../extensions/tracers/opentelemetry/resource_detectors/v3/* diff --git a/docs/root/api-v3/config/trace/trace.rst b/docs/root/api-v3/config/trace/trace.rst index 8f8d039a18d8..6cccb4f67d1b 100644 --- a/docs/root/api-v3/config/trace/trace.rst +++ b/docs/root/api-v3/config/trace/trace.rst @@ -12,3 +12,4 @@ HTTP tracers :maxdepth: 2 v3/* + opentelemetry/resource_detectors diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index f73bf64356c9..a75696fbf8c8 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -265,6 +265,12 @@ EXTENSIONS = { "envoy.tracers.skywalking": "//source/extensions/tracers/skywalking:config", "envoy.tracers.opentelemetry": "//source/extensions/tracers/opentelemetry:config", + # + # OpenTelemetry Resource Detectors + # + + "envoy.tracers.opentelemetry.resource_detectors.environment": "//source/extensions/tracers/opentelemetry/resource_detectors/environment:config", + # # Transport sockets # diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 7098afed83ad..2471dc695903 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1676,3 +1676,10 @@ envoy.filters.network.set_filter_state: status: alpha type_urls: - envoy.extensions.filters.network.set_filter_state.v3.Config +envoy.tracers.opentelemetry.resource_detectors.environment: + categories: + - envoy.tracers.opentelemetry.resource_detectors + security_posture: unknown + status: wip + type_urls: + - envoy.extensions.tracers.opentelemetry.resource_detectors.v3.EnvironmentResourceDetectorConfig diff --git a/source/extensions/tracers/opentelemetry/BUILD b/source/extensions/tracers/opentelemetry/BUILD index 58d0a20ba5b7..6122aa34d05a 100644 --- a/source/extensions/tracers/opentelemetry/BUILD +++ b/source/extensions/tracers/opentelemetry/BUILD @@ -41,6 +41,7 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/tracing:http_tracer_lib", "//source/extensions/tracers/common:factory_base_lib", + "//source/extensions/tracers/opentelemetry/resource_detectors:resource_detector_lib", "@envoy_api//envoy/config/trace/v3:pkg_cc_proto", "@opentelemetry_proto//:trace_cc_proto", ], diff --git a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc index 52e40c5cffbc..54f41ca2da12 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.cc @@ -10,6 +10,8 @@ #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/resource_detectors/resource_detector.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.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" @@ -25,11 +27,19 @@ namespace OpenTelemetry { Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, Server::Configuration::TracerFactoryContext& context) + : Driver(opentelemetry_config, context, ResourceProviderImpl{}) {} + +Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context, + const ResourceProvider& resource_provider) : tls_slot_ptr_(context.serverFactoryContext().threadLocal().allocateSlot()), tracing_stats_{OPENTELEMETRY_TRACER_STATS( POOL_COUNTER_PREFIX(context.serverFactoryContext().scope(), "tracing.opentelemetry"))} { auto& factory_context = context.serverFactoryContext(); + Resource resource = resource_provider.getResource(opentelemetry_config, context); + ResourceConstSharedPtr resource_ptr = std::make_shared(std::move(resource)); + if (opentelemetry_config.has_grpc_service() && opentelemetry_config.has_http_service()) { throw EnvoyException( "OpenTelemetry Tracer cannot have both gRPC and HTTP exporters configured. " @@ -37,7 +47,8 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr } // Create the tracer in Thread Local Storage. - tls_slot_ptr_->set([opentelemetry_config, &factory_context, this](Event::Dispatcher& dispatcher) { + tls_slot_ptr_->set([opentelemetry_config, &factory_context, this, + resource_ptr](Event::Dispatcher& dispatcher) { OpenTelemetryTraceExporterPtr exporter; if (opentelemetry_config.has_grpc_service()) { Grpc::AsyncClientFactoryPtr&& factory = @@ -52,7 +63,7 @@ Driver::Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetr } TracerPtr tracer = std::make_unique( std::move(exporter), factory_context.timeSource(), factory_context.api().randomGenerator(), - factory_context.runtime(), dispatcher, tracing_stats_, opentelemetry_config.service_name()); + factory_context.runtime(), dispatcher, tracing_stats_, resource_ptr); return std::make_shared(std::move(tracer)); }); diff --git a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h index 5083cff22f6e..d197ba2d5f97 100644 --- a/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h +++ b/source/extensions/tracers/opentelemetry/opentelemetry_tracer_impl.h @@ -8,6 +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/resource_detectors/resource_provider.h" #include "source/extensions/tracers/opentelemetry/tracer.h" namespace Envoy { @@ -31,6 +33,10 @@ class Driver : Logger::Loggable, public Tracing::Driver { Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, Server::Configuration::TracerFactoryContext& context); + Driver(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context, + const ResourceProvider& resource_provider); + // Tracing::Driver Tracing::SpanPtr startSpan(const Tracing::Config& config, Tracing::TraceContext& trace_context, const StreamInfo::StreamInfo& stream_info, diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/BUILD b/source/extensions/tracers/opentelemetry/resource_detectors/BUILD new file mode 100644 index 000000000000..c8b064de43e4 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/BUILD @@ -0,0 +1,27 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "resource_detector_lib", + srcs = [ + "resource_provider.cc", + ], + hdrs = [ + "resource_detector.h", + "resource_provider.h", + ], + deps = [ + "//envoy/config:typed_config_interface", + "//envoy/server:tracer_config_interface", + "//source/common/common:logger_lib", + "//source/common/config:utility_lib", + "@envoy_api//envoy/config/trace/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD b/source/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD new file mode 100644 index 000000000000..3a0026dbd0dd --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD @@ -0,0 +1,33 @@ +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 = [ + ":environment_resource_detector_lib", + "//envoy/registry", + "//source/common/config:utility_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "environment_resource_detector_lib", + srcs = ["environment_resource_detector.cc"], + hdrs = ["environment_resource_detector.h"], + deps = [ + "//source/common/config:datasource_lib", + "//source/extensions/tracers/opentelemetry/resource_detectors:resource_detector_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.cc b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.cc new file mode 100644 index 000000000000..5216a959ce1d --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.cc @@ -0,0 +1,35 @@ +#include "source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h" + +#include "envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.pb.h" +#include "envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.pb.validate.h" + +#include "source/common/config/utility.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +ResourceDetectorPtr EnvironmentResourceDetectorFactory::createResourceDetector( + const Protobuf::Message& message, Server::Configuration::TracerFactoryContext& context) { + + auto mptr = Envoy::Config::Utility::translateAnyToFactoryConfig( + dynamic_cast(message), context.messageValidationVisitor(), *this); + + const auto& proto_config = MessageUtil::downcastAndValidate< + const envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig&>(*mptr, context.messageValidationVisitor()); + + return std::make_unique(proto_config, context); +} + +/** + * Static registration for the Env resource detector factory. @see RegisterFactory. + */ +REGISTER_FACTORY(EnvironmentResourceDetectorFactory, ResourceDetectorFactory); + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h new file mode 100644 index 000000000000..a2bf1f72025f --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.pb.h" + +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * Config registration for the Environment resource detector. @see ResourceDetectorFactory. + */ +class EnvironmentResourceDetectorFactory : public ResourceDetectorFactory { +public: + /** + * @brief Create a Resource Detector that reads from the OTEL_RESOURCE_ATTRIBUTES + * environment variable. + * + * @param message The resource detector configuration. + * @param context The tracer factory context. + * @return ResourceDetectorPtr + */ + ResourceDetectorPtr + createResourceDetector(const Protobuf::Message& message, + Server::Configuration::TracerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { + return "envoy.tracers.opentelemetry.resource_detectors.environment"; + } +}; + +DECLARE_FACTORY(EnvironmentResourceDetectorFactory); + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.cc b/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.cc new file mode 100644 index 000000000000..3c69e32b76f3 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.cc @@ -0,0 +1,60 @@ +#include "environment_resource_detector.h" + +#include +#include + +#include "source/common/config/datasource.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +constexpr absl::string_view kOtelResourceAttributesEnv = "OTEL_RESOURCE_ATTRIBUTES"; + +/** + * @brief Detects a resource from the OTEL_RESOURCE_ATTRIBUTES environment variable + * Based on the OTel C++ SDK: + * https://github.com/open-telemetry/opentelemetry-cpp/blob/v1.11.0/sdk/src/resource/resource_detector.cc + * + * @return Resource A resource with the attributes from the OTEL_RESOURCE_ATTRIBUTES environment + * variable. + */ +Resource EnvironmentResourceDetector::detect() { + envoy::config::core::v3::DataSource ds; + ds.set_environment_variable(kOtelResourceAttributesEnv); + + Resource resource; + resource.schemaUrl_ = ""; + std::string attributes_str = ""; + + attributes_str = Config::DataSource::read(ds, true, context_.serverFactoryContext().api()); + + if (attributes_str.empty()) { + throw EnvoyException( + fmt::format("The OpenTelemetry environment resource detector is configured but the '{}'" + " environment variable is empty.", + kOtelResourceAttributesEnv)); + } + + for (const auto& pair : StringUtil::splitToken(attributes_str, ",")) { + const auto keyValue = StringUtil::splitToken(pair, "="); + if (keyValue.size() != 2) { + throw EnvoyException( + fmt::format("The OpenTelemetry environment resource detector is configured but the '{}'" + " environment variable has an invalid format.", + kOtelResourceAttributesEnv)); + } + + const std::string key = std::string(keyValue[0]); + const std::string value = std::string(keyValue[1]); + resource.attributes_[key] = value; + } + return resource; +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h b/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h new file mode 100644 index 000000000000..78327b047840 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h @@ -0,0 +1,38 @@ +#pragma once + +#include "envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.pb.h" +#include "envoy/server/factory_context.h" + +#include "source/common/common/logger.h" +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * @brief A resource detector that extracts attributes from the OTEL_RESOURCE_ATTRIBUTES environment + * variable. + * @see + * https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/resource/sdk.md#detecting-resource-information-from-the-environment + * + */ +class EnvironmentResourceDetector : public ResourceDetector, Logger::Loggable { +public: + EnvironmentResourceDetector(const envoy::extensions::tracers::opentelemetry::resource_detectors:: + v3::EnvironmentResourceDetectorConfig& config, + Server::Configuration::TracerFactoryContext& context) + : config_(config), context_(context) {} + Resource detect() override; + +private: + const envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig config_; + Server::Configuration::TracerFactoryContext& context_; +}; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h b/source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h new file mode 100644 index 000000000000..69894b917680 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/typed_config.h" +#include "envoy/server/tracer_config.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +/** + * @brief A string key-value map that stores the resource attributes. + */ +using ResourceAttributes = std::map; + +/** + * @brief A Resource represents the entity producing telemetry as Attributes. + * For example, a process producing telemetry that is running in a container on Kubernetes + * has a Pod name, it is in a namespace and possibly is part of a Deployment which also has a name. + * See: + * https://github.com/open-telemetry/opentelemetry-specification/blob/v1.26.0/specification/resource/sdk.md + */ +struct Resource { + std::string schemaUrl_{""}; + ResourceAttributes attributes_{}; + + virtual ~Resource() = default; +}; + +using ResourceConstSharedPtr = std::shared_ptr; + +/** + * @brief The base type for all resource detectors + * + */ +class ResourceDetector { +public: + virtual ~ResourceDetector() = default; + + /** + * @brief Load attributes and returns a Resource object + * populated with them and a possible SchemaUrl. + * @return Resource + */ + virtual Resource detect() PURE; +}; + +using ResourceDetectorPtr = std::unique_ptr; + +/* + * A factory for creating resource detectors. + */ +class ResourceDetectorFactory : public Envoy::Config::TypedFactory { +public: + ~ResourceDetectorFactory() override = default; + + /** + * @brief Creates a resource detector based on the configuration type provided. + * + * @param message The resource detector configuration. + * @param context The tracer factory context. + * @return ResourceDetectorPtr A resource detector based on the configuration type provided. + */ + virtual ResourceDetectorPtr + createResourceDetector(const Protobuf::Message& message, + Server::Configuration::TracerFactoryContext& context) PURE; + + std::string category() const override { return "envoy.tracers.opentelemetry.resource_detectors"; } +}; + +using ResourceDetectorTypedFactoryPtr = std::unique_ptr; + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc new file mode 100644 index 000000000000..a8f106cc3729 --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc @@ -0,0 +1,110 @@ +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h" + +#include + +#include "source/common/common/logger.h" +#include "source/common/config/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +namespace { +bool isEmptyResource(const Resource& resource) { return resource.attributes_.empty(); } + +Resource createInitialResource(const std::string& service_name) { + Resource resource{}; + + // Creates initial resource with the static service.name attribute. + resource.attributes_[std::string(kServiceNameKey.data(), kServiceNameKey.size())] = + service_name.empty() ? std::string{kDefaultServiceName} : service_name; + + return resource; +} + +/** + * @brief Resolves the new schema url when merging two resources. + * This function implements the algorithm as defined in the OpenTelemetry Resource SDK + * specification. @see + * https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/resource/sdk.md#merge + * + * @param old_schema_url The old resource's schema URL. + * @param updating_schema_url The updating resource's schema URL. + * @return std::string The calculated schema URL. + */ +std::string resolveSchemaUrl(const std::string& old_schema_url, + const std::string& updating_schema_url) { + if (old_schema_url.empty()) { + return updating_schema_url; + } + if (updating_schema_url.empty()) { + return old_schema_url; + } + if (old_schema_url == updating_schema_url) { + return old_schema_url; + } + // The OTel spec leaves this case (when both have value but are different) unspecified. + ENVOY_LOG_MISC(info, "Resource schemaUrl conflict. Fall-back to old schema url: {}", + old_schema_url); + return old_schema_url; +} + +/** + * @brief Updates an old resource with a new one. This function implements + * the Merge operation defined in the OpenTelemetry Resource SDK specification. + * @see + * https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/resource/sdk.md#merge + * + * @param old_resource The old resource. + * @param updating_resource The new resource. + */ +void mergeResource(Resource& old_resource, const Resource& updating_resource) { + // The schemaUrl is merged, regardless if the resources being merged + // have attributes or not. This behavior is compliant with the OTel spec. + // see: https://github.com/envoyproxy/envoy/pull/29547#discussion_r1344540427 + old_resource.schemaUrl_ = resolveSchemaUrl(old_resource.schemaUrl_, updating_resource.schemaUrl_); + + if (isEmptyResource(updating_resource)) { + return; + } + for (auto const& attr : updating_resource.attributes_) { + old_resource.attributes_.insert_or_assign(attr.first, attr.second); + } +} +} // namespace + +Resource ResourceProviderImpl::getResource( + const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context) const { + + Resource resource = createInitialResource(opentelemetry_config.service_name()); + + const auto& detectors_configs = opentelemetry_config.resource_detectors(); + + for (const auto& detector_config : detectors_configs) { + ResourceDetectorPtr detector; + auto* factory = Envoy::Config::Utility::getFactory(detector_config); + + if (!factory) { + throw EnvoyException( + fmt::format("Resource detector factory not found: '{}'", detector_config.name())); + } + + detector = factory->createResourceDetector(detector_config.typed_config(), context); + + if (!detector) { + throw EnvoyException( + fmt::format("Resource detector could not be created: '{}'", detector_config.name())); + } + + Resource detected_resource = detector->detect(); + mergeResource(resource, detected_resource); + } + return resource; +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h new file mode 100644 index 000000000000..9ecf6420c31d --- /dev/null +++ b/source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/config/trace/v3/opentelemetry.pb.h" + +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h" + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +constexpr absl::string_view kServiceNameKey = "service.name"; +constexpr absl::string_view kDefaultServiceName = "unknown_service:envoy"; + +class ResourceProvider : public Logger::Loggable { +public: + virtual ~ResourceProvider() = default; + + /** + * @brief Iterates through all loaded resource detectors and merge all the returned + * resources into one. Resource merging is done according to the OpenTelemetry + * resource SDK specification. @see + * https://github.com/open-telemetry/opentelemetry-specification/blob/v1.24.0/specification/resource/sdk.md#merge. + * + * @param opentelemetry_config The OpenTelemetry configuration, which contains the configured + * resource detectors. + * @param context The tracer factory context. + * @return Resource const The merged resource. + */ + virtual Resource + getResource(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context) const PURE; +}; + +class ResourceProviderImpl : public ResourceProvider { +public: + Resource getResource(const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context) const override; +}; + +} // 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 683d5ea87d5f..5d55725906d2 100644 --- a/source/extensions/tracers/opentelemetry/tracer.cc +++ b/source/extensions/tracers/opentelemetry/tracer.cc @@ -19,8 +19,6 @@ namespace OpenTelemetry { constexpr absl::string_view kTraceParent = "traceparent"; constexpr absl::string_view kTraceState = "tracestate"; constexpr absl::string_view kDefaultVersion = "00"; -constexpr absl::string_view kServiceNameKey = "service.name"; -constexpr absl::string_view kDefaultServiceName = "unknown_service:envoy"; using opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest; @@ -110,12 +108,9 @@ void Span::setTag(absl::string_view name, absl::string_view value) { 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) + const ResourceConstSharedPtr resource) : exporter_(std::move(exporter)), time_source_(time_source), random_(random), runtime_(runtime), - tracing_stats_(tracing_stats), service_name_(service_name) { - if (service_name.empty()) { - service_name_ = std::string{kDefaultServiceName}; - } + tracing_stats_(tracing_stats), resource_(resource) { flush_timer_ = dispatcher.createTimer([this]() -> void { tracing_stats_.timer_flushed_.inc(); flushSpans(); @@ -134,14 +129,20 @@ void Tracer::flushSpans() { ExportTraceServiceRequest request; // A request consists of ResourceSpans. ::opentelemetry::proto::trace::v1::ResourceSpans* resource_span = request.add_resource_spans(); - opentelemetry::proto::common::v1::KeyValue key_value = - opentelemetry::proto::common::v1::KeyValue(); - opentelemetry::proto::common::v1::AnyValue value_proto = - opentelemetry::proto::common::v1::AnyValue(); - value_proto.set_string_value(std::string{service_name_}); - key_value.set_key(std::string{kServiceNameKey}); - *key_value.mutable_value() = value_proto; - (*resource_span->mutable_resource()->add_attributes()) = key_value; + resource_span->set_schema_url(resource_->schemaUrl_); + + // add resource attributes + for (auto const& att : resource_->attributes_) { + opentelemetry::proto::common::v1::KeyValue key_value = + opentelemetry::proto::common::v1::KeyValue(); + opentelemetry::proto::common::v1::AnyValue value_proto = + opentelemetry::proto::common::v1::AnyValue(); + value_proto.set_string_value(std::string{att.second}); + key_value.set_key(std::string{att.first}); + *key_value.mutable_value() = value_proto; + (*resource_span->mutable_resource()->add_attributes()) = key_value; + } + ::opentelemetry::proto::trace::v1::ScopeSpans* scope_span = resource_span->add_scope_spans(); for (const auto& pending_span : span_buffer_) { (*scope_span->add_spans()) = pending_span; diff --git a/source/extensions/tracers/opentelemetry/tracer.h b/source/extensions/tracers/opentelemetry/tracer.h index 07d38ef22c8e..74bdb55952b8 100644 --- a/source/extensions/tracers/opentelemetry/tracer.h +++ b/source/extensions/tracers/opentelemetry/tracer.h @@ -11,6 +11,7 @@ #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/resource_detectors/resource_detector.h" #include "source/extensions/tracers/opentelemetry/span_context.h" #include "absl/strings/escaping.h" @@ -35,7 +36,7 @@ class Tracer : Logger::Loggable { public: Tracer(OpenTelemetryTraceExporterPtr exporter, Envoy::TimeSource& time_source, Random::RandomGenerator& random, Runtime::Loader& runtime, Event::Dispatcher& dispatcher, - OpenTelemetryTracerStats tracing_stats, const std::string& service_name); + OpenTelemetryTracerStats tracing_stats, const ResourceConstSharedPtr resource); void sendSpan(::opentelemetry::proto::trace::v1::Span& span); @@ -64,7 +65,7 @@ class Tracer : Logger::Loggable { Runtime::Loader& runtime_; Event::TimerPtr flush_timer_; OpenTelemetryTracerStats tracing_stats_; - std::string service_name_; + const ResourceConstSharedPtr resource_; }; /** diff --git a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc index 4954854efd3d..300e92cf8d5a 100644 --- a/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc +++ b/test/extensions/tracers/opentelemetry/opentelemetry_tracer_impl_test.cc @@ -27,6 +27,14 @@ using testing::NiceMock; using testing::Return; using testing::ReturnRef; +class MockResourceProvider : public ResourceProvider { +public: + MOCK_METHOD(Resource, getResource, + (const envoy::config::trace::v3::OpenTelemetryConfig& opentelemetry_config, + Server::Configuration::TracerFactoryContext& context), + (const)); +}; + class OpenTelemetryDriverTest : public testing::Test { public: OpenTelemetryDriverTest() = default; @@ -44,7 +52,13 @@ class OpenTelemetryDriverTest : public testing::Test { .WillByDefault(Return(ByMove(std::move(mock_client_factory)))); ON_CALL(factory_context, scope()).WillByDefault(ReturnRef(scope_)); - driver_ = std::make_unique(opentelemetry_config, context_); + Resource resource; + resource.attributes_.insert(std::pair("key1", "val1")); + + auto mock_resource_provider = NiceMock(); + EXPECT_CALL(mock_resource_provider, getResource(_, _)).WillRepeatedly(Return(resource)); + + driver_ = std::make_unique(opentelemetry_config, context_, mock_resource_provider); } void setupValidDriver() { @@ -183,6 +197,9 @@ TEST_F(OpenTelemetryDriverTest, ParseSpanContextFromHeadersTest) { key: "service.name" value: string_value: "unknown_service:envoy" + key: "key1" + value: + string_value: "val1" scope_spans: spans: trace_id: "AAA" @@ -550,6 +567,9 @@ TEST_F(OpenTelemetryDriverTest, ExportOTLPSpanWithAttributes) { key: "service.name" value: string_value: "unknown_service:envoy" + key: "key1" + value: + string_value: "val1" scope_spans: spans: trace_id: "AAA" @@ -659,6 +679,9 @@ TEST_F(OpenTelemetryDriverTest, ExportSpanWithCustomServiceName) { key: "service.name" value: string_value: "test-service-name" + key: "key1" + value: + string_value: "val1" scope_spans: spans: trace_id: "AAA" diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/BUILD b/test/extensions/tracers/opentelemetry/resource_detectors/BUILD new file mode 100644 index 000000000000..b91bdda9b2e2 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/resource_detectors/BUILD @@ -0,0 +1,21 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "resource_provider_test", + srcs = ["resource_provider_test.cc"], + deps = [ + "//envoy/registry", + "//source/extensions/tracers/opentelemetry/resource_detectors:resource_detector_lib", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD b/test/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD new file mode 100644 index 000000000000..2e6598200c38 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD @@ -0,0 +1,37 @@ +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.resource_detectors.environment"], + deps = [ + "//envoy/registry", + "//source/extensions/tracers/opentelemetry/resource_detectors/environment:config", + "//source/extensions/tracers/opentelemetry/resource_detectors/environment:environment_resource_detector_lib", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "environment_resource_detector_test", + srcs = ["environment_resource_detector_test.cc"], + extension_names = ["envoy.tracers.opentelemetry.resource_detectors.environment"], + deps = [ + "//source/extensions/tracers/opentelemetry/resource_detectors/environment:environment_resource_detector_lib", + "//test/mocks/server:tracer_factory_context_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/tracers/opentelemetry/resource_detectors/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc new file mode 100644 index 000000000000..7e9ada0850eb --- /dev/null +++ b/test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc @@ -0,0 +1,36 @@ +#include "envoy/registry/registry.h" + +#include "source/extensions/tracers/opentelemetry/resource_detectors/environment/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 create resource detector via factory +TEST(EnvironmentResourceDetectorFactoryTest, Basic) { + auto* factory = Registry::FactoryRegistry::getFactory( + "envoy.tracers.opentelemetry.resource_detectors.environment"); + ASSERT_NE(factory, nullptr); + + envoy::config::core::v3::TypedExtensionConfig typed_config; + const std::string yaml = R"EOF( + name: envoy.tracers.opentelemetry.resource_detectors.environment + typed_config: + "@type": type.googleapis.com/envoy.extensions.tracers.opentelemetry.resource_detectors.v3.EnvironmentResourceDetectorConfig + )EOF"; + TestUtility::loadFromYaml(yaml, typed_config); + + NiceMock context; + EXPECT_NE(factory->createResourceDetector(typed_config.typed_config(), context), nullptr); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc new file mode 100644 index 000000000000..e88f0dd5e72a --- /dev/null +++ b/test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc @@ -0,0 +1,109 @@ +#include + +#include "envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.pb.h" +#include "envoy/registry/registry.h" + +#include "source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h" + +#include "test/mocks/server/tracer_factory_context.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { + +const std::string kOtelResourceAttributesEnv = "OTEL_RESOURCE_ATTRIBUTES"; + +// Test detector when env variable is not present +TEST(EnvironmentResourceDetectorTest, EnvVariableNotPresent) { + NiceMock context; + + envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig config; + + auto detector = std::make_unique(config, context); + EXPECT_THROW_WITH_MESSAGE(detector->detect(), EnvoyException, + "Environment variable doesn't exist: OTEL_RESOURCE_ATTRIBUTES"); +} + +// Test detector when env variable is present but contains an empty value +TEST(EnvironmentResourceDetectorTest, EnvVariablePresentButEmpty) { + NiceMock context; + TestEnvironment::setEnvVar(kOtelResourceAttributesEnv, "", 1); + Envoy::Cleanup cleanup([]() { TestEnvironment::unsetEnvVar(kOtelResourceAttributesEnv); }); + + envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig config; + + auto detector = std::make_unique(config, context); + +#ifdef WIN32 + EXPECT_THROW_WITH_MESSAGE(detector->detect(), EnvoyException, + "Environment variable doesn't exist: OTEL_RESOURCE_ATTRIBUTES"); +#else + EXPECT_THROW_WITH_MESSAGE(detector->detect(), EnvoyException, + "The OpenTelemetry environment resource detector is configured but the " + "'OTEL_RESOURCE_ATTRIBUTES'" + " environment variable is empty."); +#endif +} + +// Test detector with valid values in the env variable +TEST(EnvironmentResourceDetectorTest, EnvVariablePresentAndWithAttributes) { + NiceMock context; + TestEnvironment::setEnvVar(kOtelResourceAttributesEnv, "key1=val1,key2=val2", 1); + Envoy::Cleanup cleanup([]() { TestEnvironment::unsetEnvVar(kOtelResourceAttributesEnv); }); + ResourceAttributes expected_attributes = {{"key1", "val1"}, {"key2", "val2"}}; + + Api::ApiPtr api = Api::createApiForTest(); + EXPECT_CALL(context.server_factory_context_, api()).WillRepeatedly(ReturnRef(*api)); + + envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig config; + + auto detector = std::make_unique(config, context); + Resource resource = detector->detect(); + + EXPECT_EQ(resource.schemaUrl_, ""); + EXPECT_EQ(2, resource.attributes_.size()); + + for (auto& actual : resource.attributes_) { + auto expected = expected_attributes.find(actual.first); + + EXPECT_TRUE(expected != expected_attributes.end()); + EXPECT_EQ(expected->second, actual.second); + } +} + +// Test detector with invalid values mixed with valid ones in the env variable +TEST(EnvironmentResourceDetectorTest, EnvVariablePresentAndWithAttributesWrongFormat) { + NiceMock context; + TestEnvironment::setEnvVar(kOtelResourceAttributesEnv, "key1=val1,key2val2,key3/val3, , key", 1); + Envoy::Cleanup cleanup([]() { TestEnvironment::unsetEnvVar(kOtelResourceAttributesEnv); }); + ResourceAttributes expected_attributes = {{"key1", "val"}}; + + Api::ApiPtr api = Api::createApiForTest(); + EXPECT_CALL(context.server_factory_context_, api()).WillRepeatedly(ReturnRef(*api)); + + envoy::extensions::tracers::opentelemetry::resource_detectors::v3:: + EnvironmentResourceDetectorConfig config; + + auto detector = std::make_unique(config, context); + + EXPECT_THROW_WITH_MESSAGE(detector->detect(), EnvoyException, + "The OpenTelemetry environment resource detector is configured but the " + "'OTEL_RESOURCE_ATTRIBUTES'" + " environment variable has an invalid format."); +} + +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc b/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc new file mode 100644 index 000000000000..8f49c4d93ca7 --- /dev/null +++ b/test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc @@ -0,0 +1,424 @@ +#include + +#include "envoy/registry/registry.h" + +#include "source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h" + +#include "test/mocks/server/tracer_factory_context.h" +#include "test/test_common/environment.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::Return; + +namespace Envoy { +namespace Extensions { +namespace Tracers { +namespace OpenTelemetry { +namespace { + +class SampleDetector : public ResourceDetector { +public: + MOCK_METHOD(Resource, detect, ()); +}; + +class DetectorFactoryA : public ResourceDetectorFactory { +public: + MOCK_METHOD(ResourceDetectorPtr, createResourceDetector, + (const Protobuf::Message& message, + Server::Configuration::TracerFactoryContext& context)); + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return "envoy.tracers.opentelemetry.resource_detectors.a"; } +}; + +class DetectorFactoryB : public ResourceDetectorFactory { +public: + MOCK_METHOD(ResourceDetectorPtr, createResourceDetector, + (const Protobuf::Message& message, + Server::Configuration::TracerFactoryContext& context)); + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return "envoy.tracers.opentelemetry.resource_detectors.b"; } +}; + +const std::string kOtelResourceAttributesEnv = "OTEL_RESOURCE_ATTRIBUTES"; + +class ResourceProviderTest : public testing::Test { +public: + ResourceProviderTest() { + resource_a_.attributes_.insert(std::pair("key1", "val1")); + resource_b_.attributes_.insert(std::pair("key2", "val2")); + } + NiceMock context_; + Resource resource_a_; + Resource resource_b_; +}; + +// Verifies a resource with the static service name is returned when no detectors are configured +TEST_F(ResourceProviderTest, NoResourceDetectorsConfigured) { + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + EXPECT_EQ(resource.schemaUrl_, ""); + + // Only the service name was added to the resource + EXPECT_EQ(1, resource.attributes_.size()); +} + +// Verifies a resource with the default service name is returned when no detectors + static service +// name are configured +TEST_F(ResourceProviderTest, ServiceNameNotProvided) { + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + EXPECT_EQ(resource.schemaUrl_, ""); + + // service.name receives the unknown value when not configured + EXPECT_EQ(1, resource.attributes_.size()); + auto service_name = resource.attributes_.find("service.name"); + EXPECT_EQ("unknown_service:envoy", service_name->second); +} + +// Verifies it is possible to configure multiple resource detectors +TEST_F(ResourceProviderTest, MultipleResourceDetectorsConfigured) { + auto detector_a = std::make_unique>(); + EXPECT_CALL(*detector_a, detect()).WillOnce(Return(resource_a_)); + + auto detector_b = std::make_unique>(); + EXPECT_CALL(*detector_b, detect()).WillOnce(Return(resource_b_)); + + DetectorFactoryA factory_a; + Registry::InjectFactory factory_a_registration(factory_a); + + DetectorFactoryB factory_b; + Registry::InjectFactory factory_b_registration(factory_b); + + EXPECT_CALL(factory_a, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_a)))); + EXPECT_CALL(factory_b, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_b)))); + + // Expected merged attributes from all detectors + ResourceAttributes expected_attributes = { + {"service.name", "my-service"}, {"key1", "val1"}, {"key2", "val2"}}; + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + - name: envoy.tracers.opentelemetry.resource_detectors.b + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + EXPECT_EQ(resource.schemaUrl_, ""); + + // The resource should contain all 3 merged attributes + // service.name + 1 for each detector + EXPECT_EQ(3, resource.attributes_.size()); + + for (auto& actual : resource.attributes_) { + auto expected = expected_attributes.find(actual.first); + + EXPECT_TRUE(expected != expected_attributes.end()); + EXPECT_EQ(expected->second, actual.second); + } +} + +// Verifies Envoy fails when an unknown resource detector is configured +TEST_F(ResourceProviderTest, UnknownResourceDetectors) { + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.UnkownResourceDetector + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + EXPECT_THROW_WITH_MESSAGE( + resource_provider.getResource(opentelemetry_config, context_), EnvoyException, + "Resource detector factory not found: " + "'envoy.tracers.opentelemetry.resource_detectors.UnkownResourceDetector'"); +} + +// Verifies Envoy fails when an error occurs while instantiating a resource detector +TEST_F(ResourceProviderTest, ProblemCreatingResourceDetector) { + DetectorFactoryA factory; + Registry::InjectFactory factory_registration(factory); + + // Simulating having a problem when creating the resource detector + EXPECT_CALL(factory, createResourceDetector(_, _)).WillOnce(Return(testing::ByMove(nullptr))); + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-clusterdetector_a + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + )EOF"; + + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + EXPECT_THROW_WITH_MESSAGE(resource_provider.getResource(opentelemetry_config, context_), + EnvoyException, + "Resource detector could not be created: " + "'envoy.tracers.opentelemetry.resource_detectors.a'"); +} + +// Test merge when old schema url is empty but updating is not +TEST_F(ResourceProviderTest, OldSchemaEmptyUpdatingSet) { + std::string expected_schema_url = "my.schema/v1"; + Resource old_resource = resource_a_; + + // Updating resource is empty (no attributes) + Resource updating_resource; + updating_resource.schemaUrl_ = expected_schema_url; + + auto detector_a = std::make_unique>(); + EXPECT_CALL(*detector_a, detect()).WillOnce(Return(old_resource)); + + auto detector_b = std::make_unique>(); + EXPECT_CALL(*detector_b, detect()).WillOnce(Return(updating_resource)); + + DetectorFactoryA factory_a; + Registry::InjectFactory factory_a_registration(factory_a); + + DetectorFactoryB factory_b; + Registry::InjectFactory factory_b_registration(factory_b); + + EXPECT_CALL(factory_a, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_a)))); + EXPECT_CALL(factory_b, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_b)))); + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + - name: envoy.tracers.opentelemetry.resource_detectors.b + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + // OTel spec says the updating schema should be used + EXPECT_EQ(expected_schema_url, resource.schemaUrl_); +} + +// Test merge when old schema url is not empty but updating is +TEST_F(ResourceProviderTest, OldSchemaSetUpdatingEmpty) { + std::string expected_schema_url = "my.schema/v1"; + Resource old_resource = resource_a_; + old_resource.schemaUrl_ = expected_schema_url; + + Resource updating_resource = resource_b_; + updating_resource.schemaUrl_ = ""; + + auto detector_a = std::make_unique>(); + EXPECT_CALL(*detector_a, detect()).WillOnce(Return(old_resource)); + + auto detector_b = std::make_unique>(); + EXPECT_CALL(*detector_b, detect()).WillOnce(Return(updating_resource)); + + DetectorFactoryA factory_a; + Registry::InjectFactory factory_a_registration(factory_a); + + DetectorFactoryB factory_b; + Registry::InjectFactory factory_b_registration(factory_b); + + EXPECT_CALL(factory_a, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_a)))); + EXPECT_CALL(factory_b, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_b)))); + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + - name: envoy.tracers.opentelemetry.resource_detectors.b + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + // OTel spec says the updating schema should be used + EXPECT_EQ(expected_schema_url, resource.schemaUrl_); +} + +// Test merge when both old and updating schema url are set and equal +TEST_F(ResourceProviderTest, OldAndUpdatingSchemaAreEqual) { + std::string expected_schema_url = "my.schema/v1"; + Resource old_resource = resource_a_; + old_resource.schemaUrl_ = expected_schema_url; + + Resource updating_resource = resource_b_; + updating_resource.schemaUrl_ = expected_schema_url; + + auto detector_a = std::make_unique>(); + EXPECT_CALL(*detector_a, detect()).WillOnce(Return(old_resource)); + + auto detector_b = std::make_unique>(); + EXPECT_CALL(*detector_b, detect()).WillOnce(Return(updating_resource)); + + DetectorFactoryA factory_a; + Registry::InjectFactory factory_a_registration(factory_a); + + DetectorFactoryB factory_b; + Registry::InjectFactory factory_b_registration(factory_b); + + EXPECT_CALL(factory_a, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_a)))); + EXPECT_CALL(factory_b, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_b)))); + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + - name: envoy.tracers.opentelemetry.resource_detectors.b + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + EXPECT_EQ(expected_schema_url, resource.schemaUrl_); +} + +// Test merge when both old and updating schema url are set but different +TEST_F(ResourceProviderTest, OldAndUpdatingSchemaAreDifferent) { + std::string expected_schema_url = "my.schema/v1"; + Resource old_resource = resource_a_; + old_resource.schemaUrl_ = expected_schema_url; + + Resource updating_resource = resource_b_; + updating_resource.schemaUrl_ = "my.schema/v2"; + + auto detector_a = std::make_unique>(); + EXPECT_CALL(*detector_a, detect()).WillOnce(Return(old_resource)); + + auto detector_b = std::make_unique>(); + EXPECT_CALL(*detector_b, detect()).WillOnce(Return(updating_resource)); + + DetectorFactoryA factory_a; + Registry::InjectFactory factory_a_registration(factory_a); + + DetectorFactoryB factory_b; + Registry::InjectFactory factory_b_registration(factory_b); + + EXPECT_CALL(factory_a, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_a)))); + EXPECT_CALL(factory_b, createResourceDetector(_, _)) + .WillOnce(Return(testing::ByMove(std::move(detector_b)))); + + const std::string yaml_string = R"EOF( + grpc_service: + envoy_grpc: + cluster_name: fake-cluster + timeout: 0.250s + service_name: my-service + resource_detectors: + - name: envoy.tracers.opentelemetry.resource_detectors.a + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + - name: envoy.tracers.opentelemetry.resource_detectors.b + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + )EOF"; + envoy::config::trace::v3::OpenTelemetryConfig opentelemetry_config; + TestUtility::loadFromYaml(yaml_string, opentelemetry_config); + + ResourceProviderImpl resource_provider; + Resource resource = resource_provider.getResource(opentelemetry_config, context_); + + // OTel spec says Old schema should be used + EXPECT_EQ(expected_schema_url, resource.schemaUrl_); +} + +} // namespace +} // namespace OpenTelemetry +} // namespace Tracers +} // namespace Extensions +} // namespace Envoy diff --git a/tools/extensions/extensions_schema.yaml b/tools/extensions/extensions_schema.yaml index 36181fb61786..e45938acb534 100644 --- a/tools/extensions/extensions_schema.yaml +++ b/tools/extensions/extensions_schema.yaml @@ -136,6 +136,7 @@ categories: - envoy.http.early_header_mutation - envoy.http.custom_response - envoy.router.cluster_specifier_plugin +- envoy.tracers.opentelemetry.resource_detectors status_values: - name: stable diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 3a73591424f9..ba9fe8bc3477 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -38,6 +38,7 @@ DOM Gasd GiB IPTOS +OTEL Repick Reserializer SION