From 4eaa1f866164eee5d51e46406bcb31d8dec99bdd Mon Sep 17 00:00:00 2001 From: Joao Grassi Date: Fri, 27 Oct 2023 17:14:36 +0200 Subject: [PATCH] opentelemetry tracer: add support for environment resource detector (#29547) Commit Message: Allow specifying resource detectors for the OpenTelemetry tracer via a new configuration resource_detectors. The resource detector reads from the env variable OTEL_RESOURCE_ATTRIBUTES which is defined by the OTel specification. The detector returns a resource object populated with the detected attributes, which is sent as part of the OTLP request. Additional Description: This PR adds the "foundation" for building other resource detectors in Envoy. It is based on the OTel collector implementation. Users can configure multiple resource detectors, and they work together to "merge" all the detected attributes into a single resource object, which is then part of the OTLP message exported. Risk Level: Low Testing: Multiple unit tests, that cover all new code/scenarios. I also did manual testing, running Envoy locally with the OTel tracer + env resource detector enabled. Resource attributes detected from my environment is successfully exported as seen in the Jaeger screenshot. resource-detectors-env-jaeger Docs Changes: Not sure if I should add/where. Happy to do it. Release Notes: N/A Platform Specific Features: N/A [Optional Runtime guard:] N/A [Optional Fixes #28929] Here is how the new config is used: tracing: provider: name: envoy.tracers.opentelemetry typed_config: "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig grpc_service: envoy_grpc: cluster_name: opentelemetry_collector timeout: 0.250s service_name: envoy-gRPC-exporter resource_detectors: # --> NEW CONFIG - name: envoy.tracers.opentelemetry.resource_detectors.environment typed_config: "@type": type.googleapis.com/envoy.extensions.tracers.opentelemetry.resource_detectors.v3.EnvironmentResourceDetectorConfig Signed-off-by: Joao Grassi --- api/BUILD | 1 + api/envoy/config/trace/v3/opentelemetry.proto | 5 + .../opentelemetry/resource_detectors/v3/BUILD | 9 + .../v3/environment_resource_detector.proto | 25 ++ api/versioning/BUILD | 1 + changelogs/current.yaml | 4 +- .../opentelemetry/resource_detectors.rst | 10 + docs/root/api-v3/config/trace/trace.rst | 1 + source/extensions/extensions_build_config.bzl | 6 + source/extensions/extensions_metadata.yaml | 7 + source/extensions/tracers/opentelemetry/BUILD | 1 + .../opentelemetry_tracer_impl.cc | 15 +- .../opentelemetry/opentelemetry_tracer_impl.h | 6 + .../opentelemetry/resource_detectors/BUILD | 27 ++ .../resource_detectors/environment/BUILD | 33 ++ .../resource_detectors/environment/config.cc | 35 ++ .../resource_detectors/environment/config.h | 46 ++ .../environment_resource_detector.cc | 60 +++ .../environment_resource_detector.h | 38 ++ .../resource_detectors/resource_detector.h | 80 ++++ .../resource_detectors/resource_provider.cc | 110 +++++ .../resource_detectors/resource_provider.h | 44 ++ .../tracers/opentelemetry/tracer.cc | 31 +- .../extensions/tracers/opentelemetry/tracer.h | 5 +- .../opentelemetry_tracer_impl_test.cc | 25 +- .../opentelemetry/resource_detectors/BUILD | 21 + .../resource_detectors/environment/BUILD | 37 ++ .../environment/config_test.cc | 36 ++ .../environment_resource_detector_test.cc | 109 +++++ .../resource_provider_test.cc | 424 ++++++++++++++++++ tools/extensions/extensions_schema.yaml | 1 + tools/spelling/spelling_dictionary.txt | 1 + 32 files changed, 1233 insertions(+), 21 deletions(-) create mode 100644 api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/BUILD create mode 100644 api/envoy/extensions/tracers/opentelemetry/resource_detectors/v3/environment_resource_detector.proto create mode 100644 docs/root/api-v3/config/trace/opentelemetry/resource_detectors.rst create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/BUILD create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/environment/config.cc create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/environment/config.h create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.cc create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector.h create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/resource_detector.h create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.cc create mode 100644 source/extensions/tracers/opentelemetry/resource_detectors/resource_provider.h create mode 100644 test/extensions/tracers/opentelemetry/resource_detectors/BUILD create mode 100644 test/extensions/tracers/opentelemetry/resource_detectors/environment/BUILD create mode 100644 test/extensions/tracers/opentelemetry/resource_detectors/environment/config_test.cc create mode 100644 test/extensions/tracers/opentelemetry/resource_detectors/environment/environment_resource_detector_test.cc create mode 100644 test/extensions/tracers/opentelemetry/resource_detectors/resource_provider_test.cc 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