diff --git a/api/envoy/config/cluster/v3/cluster.proto b/api/envoy/config/cluster/v3/cluster.proto index 5f347ade6d3d..ef6bbe121b4c 100644 --- a/api/envoy/config/cluster/v3/cluster.proto +++ b/api/envoy/config/cluster/v3/cluster.proto @@ -1154,6 +1154,8 @@ message Cluster { // [#not-implemented-hide:] // A list of metric names from ORCA load reports to propagate to LRS. // + // If not specified, then ORCA load reports will not be propagated to LRS. + // // For map fields in the ORCA proto, the string will be of the form ``.``. // For example, the string ``named_metrics.foo`` will mean to look for the key ``foo`` in the ORCA // ``named_metrics`` field. diff --git a/envoy/upstream/upstream.h b/envoy/upstream/upstream.h index 45fd1a7cc58f..95a454abc799 100644 --- a/envoy/upstream/upstream.h +++ b/envoy/upstream/upstream.h @@ -1235,6 +1235,11 @@ class ClusterInfo : public Http::FilterChainFactory { virtual OptRef happyEyeballsConfig() const PURE; + /** + * @return Reference to the optional config for LRS endpoint metric reporting. + */ + virtual OptRef> lrsReportMetricNames() const PURE; + protected: /** * Invoked by extensionProtocolOptionsTyped. diff --git a/source/common/orca/BUILD b/source/common/orca/BUILD index 9337ab05ecaf..fab411692e2b 100644 --- a/source/common/orca/BUILD +++ b/source/common/orca/BUILD @@ -23,3 +23,23 @@ envoy_cc_library( "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", ], ) + +envoy_cc_library( + name = "orca_load_metrics_lib", + srcs = ["orca_load_metrics.cc"], + hdrs = ["orca_load_metrics.h"], + external_deps = [ + "abseil_flat_hash_set", + "abseil_status", + "abseil_strings", + "abseil_statusor", + "fmtlib", + ], + deps = [ + ":orca_parser", + "//envoy/http:header_map_interface", + "//source/common/http:header_utility_lib", + "//source/common/protobuf:utility_lib_header", + "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", + ], +) diff --git a/source/common/orca/orca_load_metrics.cc b/source/common/orca/orca_load_metrics.cc new file mode 100644 index 000000000000..9b931ff26380 --- /dev/null +++ b/source/common/orca/orca_load_metrics.cc @@ -0,0 +1,74 @@ +#include "source/common/orca/orca_load_metrics.h" + +#include + +#include "source/common/orca/orca_parser.h" + +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Orca { +namespace { +// The following fields are the names of the metrics tracked in the ORCA load +// report proto. +static constexpr absl::string_view kApplicationUtilizationField = "application_utilization"; +static constexpr absl::string_view kCpuUtilizationField = "cpu_utilization"; +static constexpr absl::string_view kMemUtilizationField = "mem_utilization"; +static constexpr absl::string_view kEpsField = "eps"; +static constexpr absl::string_view kRpsFractionalField = "rps_fractional"; +static constexpr absl::string_view kNamedMetricsFieldPrefix = "named_metrics."; +static constexpr absl::string_view kRequestCostFieldPrefix = "request_cost."; +static constexpr absl::string_view kUtilizationFieldPrefix = "utilization."; +} // namespace + +void addOrcaNamedMetricToLoadMetricStats(const Protobuf::Map& metrics_map, + const absl::string_view metric_name, + const absl::string_view metric_name_prefix, + Upstream::LoadMetricStats& stats) { + absl::string_view metric_name_without_prefix = absl::StripPrefix(metric_name, metric_name_prefix); + // If the metric name is "*", add all metrics from the map. + if (metric_name_without_prefix == "*") { + for (const auto& [key, value] : metrics_map) { + stats.add(absl::StrCat(metric_name_prefix, key), value); + } + } else { + // Add the metric if it exists in the map. + const auto metric_it = metrics_map.find(metric_name_without_prefix); + if (metric_it != metrics_map.end()) { + stats.add(metric_name, metric_it->second); + } + } +} + +void addOrcaLoadReportToLoadMetricStats(const LrsReportMetricNames& metric_names, + const xds::data::orca::v3::OrcaLoadReport& report, + Upstream::LoadMetricStats& stats) { + // TODO(efimki): Use InlineMap to speed up this loop. + for (const std::string& metric_name : metric_names) { + if (metric_name == kCpuUtilizationField) { + stats.add(metric_name, report.cpu_utilization()); + } else if (metric_name == kMemUtilizationField) { + stats.add(metric_name, report.mem_utilization()); + } else if (metric_name == kApplicationUtilizationField) { + stats.add(metric_name, report.application_utilization()); + } else if (metric_name == kEpsField) { + stats.add(metric_name, report.eps()); + } else if (metric_name == kRpsFractionalField) { + stats.add(metric_name, report.rps_fractional()); + } else if (absl::StartsWith(metric_name, kNamedMetricsFieldPrefix)) { + addOrcaNamedMetricToLoadMetricStats(report.named_metrics(), metric_name, + kNamedMetricsFieldPrefix, stats); + } else if (absl::StartsWith(metric_name, kUtilizationFieldPrefix)) { + addOrcaNamedMetricToLoadMetricStats(report.utilization(), metric_name, + kUtilizationFieldPrefix, stats); + } else if (absl::StartsWith(metric_name, kRequestCostFieldPrefix)) { + addOrcaNamedMetricToLoadMetricStats(report.request_cost(), metric_name, + kRequestCostFieldPrefix, stats); + } + } +} + +} // namespace Orca +} // namespace Envoy diff --git a/source/common/orca/orca_load_metrics.h b/source/common/orca/orca_load_metrics.h new file mode 100644 index 000000000000..90c213ba1bf8 --- /dev/null +++ b/source/common/orca/orca_load_metrics.h @@ -0,0 +1,18 @@ +#pragma once + +#include "envoy/upstream/host_description.h" + +#include "xds/data/orca/v3/orca_load_report.pb.h" + +namespace Envoy { +namespace Orca { + +// List of metric names to report to the LRS. +typedef std::vector LrsReportMetricNames; + +void addOrcaLoadReportToLoadMetricStats(const LrsReportMetricNames& metric_names, + const xds::data::orca::v3::OrcaLoadReport& report, + Upstream::LoadMetricStats& stats); + +} // namespace Orca +} // namespace Envoy diff --git a/source/common/router/BUILD b/source/common/router/BUILD index 65458851c98a..70200e05eaff 100644 --- a/source/common/router/BUILD +++ b/source/common/router/BUILD @@ -366,6 +366,8 @@ envoy_cc_library( "//source/common/network:socket_option_factory_lib", "//source/common/network:transport_socket_options_lib", "//source/common/network:upstream_socket_options_filter_state_lib", + "//source/common/orca:orca_load_metrics_lib", + "//source/common/orca:orca_parser", "//source/common/stream_info:stream_info_lib", "//source/common/stream_info:uint32_accessor_lib", "//source/common/tracing:http_tracer_lib", diff --git a/source/common/router/router.cc b/source/common/router/router.cc index 02930fe93365..05ad0e6430c4 100644 --- a/source/common/router/router.cc +++ b/source/common/router/router.cc @@ -35,6 +35,8 @@ #include "source/common/network/upstream_server_name.h" #include "source/common/network/upstream_socket_options_filter_state.h" #include "source/common/network/upstream_subject_alt_names.h" +#include "source/common/orca/orca_load_metrics.h" +#include "source/common/orca/orca_parser.h" #include "source/common/router/config_impl.h" #include "source/common/router/debug_config.h" #include "source/common/router/retry_state_impl.h" @@ -1568,6 +1570,8 @@ void Filter::onUpstreamHeaders(uint64_t response_code, Http::ResponseHeaderMapPt } } + maybeProcessOrcaLoadReport(*headers, upstream_request); + if (grpc_status.has_value()) { upstream_request.upstreamHost()->outlierDetector().putHttpResponseCode(grpc_to_http_status); } else { @@ -1737,6 +1741,8 @@ void Filter::onUpstreamTrailers(Http::ResponseTrailerMapPtr&& trailers, } } + maybeProcessOrcaLoadReport(*trailers, upstream_request); + onUpstreamComplete(upstream_request); callbacks_->encodeTrailers(std::move(trailers)); @@ -2100,6 +2106,38 @@ bool Filter::checkDropOverload(Upstream::ThreadLocalCluster& cluster, return false; } +void Filter::maybeProcessOrcaLoadReport(const Envoy::Http::HeaderMap& headers_or_trailers, + UpstreamRequest& upstream_request) { + // Process the load report only once, so if response has report in headers, + // then don't process it in trailers. + if (orca_load_report_received_) { + return; + } + // Check whether we need to send the load report to the LRS or invoke the ORCA + // callbacks. + auto host = upstream_request.upstreamHost(); + const bool need_to_send_load_report = + (host != nullptr) && cluster_->lrsReportMetricNames().has_value(); + if (!need_to_send_load_report) { + return; + } + + absl::StatusOr orca_load_report = + Envoy::Orca::parseOrcaLoadReportHeaders(headers_or_trailers); + if (!orca_load_report.ok()) { + ENVOY_STREAM_LOG(trace, "Headers don't have orca load report: {}", *callbacks_, + orca_load_report.status().message()); + return; + } + + orca_load_report_received_ = true; + + ENVOY_STREAM_LOG(trace, "Adding ORCA load report {} to load metrics", *callbacks_, + orca_load_report->DebugString()); + Envoy::Orca::addOrcaLoadReportToLoadMetricStats( + cluster_->lrsReportMetricNames().value(), orca_load_report.value(), host->loadMetricStats()); +} + RetryStatePtr ProdFilter::createRetryState(const RetryPolicy& policy, Http::RequestHeaderMap& request_headers, const Upstream::ClusterInfo& cluster, const VirtualCluster* vcluster, diff --git a/source/common/router/router.h b/source/common/router/router.h index 4b93247e29fd..6e92950fbe1d 100644 --- a/source/common/router/router.h +++ b/source/common/router/router.h @@ -308,7 +308,7 @@ class Filter : Logger::Loggable, downstream_response_started_(false), downstream_end_stream_(false), is_retry_(false), request_buffer_overflowed_(false), streaming_shadows_(Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.streaming_shadow")), - upstream_request_started_(false) {} + upstream_request_started_(false), orca_load_report_received_(false) {} ~Filter() override; @@ -559,6 +559,9 @@ class Filter : Logger::Loggable, Http::Context& httpContext() { return config_->http_context_; } bool checkDropOverload(Upstream::ThreadLocalCluster& cluster, std::function& modify_headers); + // Process Orca Load Report if necessary (e.g. cluster has lrsReportMetricNames). + void maybeProcessOrcaLoadReport(const Envoy::Http::HeaderMap& headers_or_trailers, + UpstreamRequest& upstream_request); RetryStatePtr retry_state_; const FilterConfigSharedPtr config_; @@ -611,6 +614,9 @@ class Filter : Logger::Loggable, bool request_buffer_overflowed_ : 1; const bool streaming_shadows_ : 1; bool upstream_request_started_ : 1; + // Indicate that ORCA report is received to process it only once in either response headers or + // trailers. + bool orca_load_report_received_ : 1; }; class ProdFilter : public Filter { diff --git a/source/common/upstream/BUILD b/source/common/upstream/BUILD index 1def579c48c7..011eec2f43a1 100644 --- a/source/common/upstream/BUILD +++ b/source/common/upstream/BUILD @@ -457,6 +457,7 @@ envoy_cc_library( "//source/common/http/http2:codec_stats_lib", "//source/common/http/http3:codec_stats_lib", "//source/common/init:manager_lib", + "//source/common/orca:orca_load_metrics_lib", "//source/common/shared_pool:shared_pool_lib", "//source/common/stats:deferred_creation", "//source/common/stats:isolated_store_lib", diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 6c368861692d..3b75fb478f5c 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -1208,6 +1208,11 @@ ClusterInfoImpl::ClusterInfoImpl( envoy::config::cluster::v3::UpstreamConnectionOptions::HappyEyeballsConfig>( config.upstream_connection_options().happy_eyeballs_config()) : nullptr), + lrs_report_metric_names_(!config.lrs_report_endpoint_metrics().empty() + ? std::make_unique( + config.lrs_report_endpoint_metrics().begin(), + config.lrs_report_endpoint_metrics().end()) + : nullptr), per_connection_buffer_limit_bytes_( PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, per_connection_buffer_limit_bytes, 1024 * 1024)), max_response_headers_count_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 70edb9c49d98..e7573e4f50fd 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -55,6 +55,7 @@ #include "source/common/http/http3/codec_stats.h" #include "source/common/init/manager_impl.h" #include "source/common/network/utility.h" +#include "source/common/orca/orca_load_metrics.h" #include "source/common/shared_pool/shared_pool.h" #include "source/common/stats/isolated_store_impl.h" #include "source/common/upstream/edf_scheduler.h" @@ -1023,6 +1024,13 @@ class ClusterInfoImpl : public ClusterInfo, return *happy_eyeballs_config_; } + OptRef lrsReportMetricNames() const override { + if (lrs_report_metric_names_ == nullptr) { + return absl::nullopt; + } + return *lrs_report_metric_names_; + } + protected: // Gets the retry budget percent/concurrency from the circuit breaker thresholds. If the retry // budget message is specified, defaults will be filled in if either params are unspecified. @@ -1111,6 +1119,7 @@ class ClusterInfoImpl : public ClusterInfo, UpstreamFactoryContextImpl upstream_context_; std::unique_ptr happy_eyeballs_config_; + const std::unique_ptr lrs_report_metric_names_; // Keep small values like bools and enums at the end of the class to reduce // overhead via alignment diff --git a/test/common/orca/BUILD b/test/common/orca/BUILD index 9122593cf921..ea012a118e6b 100644 --- a/test/common/orca/BUILD +++ b/test/common/orca/BUILD @@ -8,6 +8,23 @@ licenses(["notice"]) # Apache 2 envoy_package() +envoy_cc_test( + name = "orca_load_metrics_test", + srcs = ["orca_load_metrics_test.cc"], + external_deps = [ + "abseil_status", + "abseil_strings", + "fmtlib", + ], + deps = [ + "//source/common/orca:orca_load_metrics_lib", + "//source/common/upstream:upstream_lib", + "//test/test_common:status_utility_lib", + "//test/test_common:utility_lib", + "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "orca_parser_test", srcs = ["orca_parser_test.cc"], diff --git a/test/common/orca/orca_load_metrics_test.cc b/test/common/orca/orca_load_metrics_test.cc new file mode 100644 index 000000000000..cce62960437e --- /dev/null +++ b/test/common/orca/orca_load_metrics_test.cc @@ -0,0 +1,136 @@ +#include "source/common/orca/orca_load_metrics.h" +#include "source/common/upstream/upstream_impl.h" + +#include "test/test_common/status_utility.h" +#include "test/test_common/utility.h" + +using ::Envoy::Upstream::LoadMetricStats; +using ::Envoy::Upstream::LoadMetricStatsImpl; +using ::testing::DoubleEq; +using ::testing::Field; +using ::testing::ReturnRef; + +namespace Envoy { +namespace Orca { +namespace { + +xds::data::orca::v3::OrcaLoadReport makeOrcaReport() { + xds::data::orca::v3::OrcaLoadReport report; + report.mutable_named_metrics()->insert({"nm_foo", 0.1}); + report.mutable_named_metrics()->insert({"nm_bar", 0.2}); + report.mutable_request_cost()->insert({"rc_foo", 0.4}); + report.mutable_request_cost()->insert({"rc_bar", 0.5}); + report.mutable_utilization()->insert({"ut_foo", 0.6}); + report.mutable_utilization()->insert({"ut_bar", 0.7}); + report.set_application_utilization(0.8); + report.set_cpu_utilization(0.9); + report.set_mem_utilization(1.0); + report.set_eps(10); + report.set_rps_fractional(11); + return report; +} + +TEST(OrcaLoadMetricsTest, AddCpuUtilization) { + Envoy::Orca::LrsReportMetricNames metric_names; + metric_names.push_back("cpu_utilization"); + + Envoy::Upstream::LoadMetricStatsImpl stats; + Envoy::Orca::addOrcaLoadReportToLoadMetricStats(metric_names, makeOrcaReport(), stats); + auto load_stats_map = stats.latch(); + ASSERT_NE(load_stats_map, nullptr); + EXPECT_EQ(load_stats_map->size(), 1); + + EXPECT_EQ(load_stats_map->at("cpu_utilization").total_metric_value, 0.9); + EXPECT_EQ(load_stats_map->at("cpu_utilization").num_requests_with_metric, 1); +} + +TEST(OrcaLoadMetricsTest, AddSpecificNamedMetrics) { + Envoy::Orca::LrsReportMetricNames metric_names; + metric_names.push_back("named_metrics.foo"); + metric_names.push_back("named_metrics.not-in-report"); + + xds::data::orca::v3::OrcaLoadReport report; + report.mutable_named_metrics()->insert({"foo", 0.7}); + report.mutable_named_metrics()->insert({"not-in-config", 0.3}); + + Envoy::Upstream::LoadMetricStatsImpl stats; + Envoy::Orca::addOrcaLoadReportToLoadMetricStats(metric_names, report, stats); + auto load_stats_map = stats.latch(); + ASSERT_NE(load_stats_map, nullptr); + EXPECT_EQ(load_stats_map->size(), 1); + EXPECT_EQ(load_stats_map->at("named_metrics.foo").total_metric_value, 0.7); +} + +TEST(OrcaLoadMetricsTest, AddWildcardUtilization) { + Envoy::Orca::LrsReportMetricNames metric_names; + metric_names.push_back("utilization.*"); + + Envoy::Upstream::LoadMetricStatsImpl stats; + Envoy::Orca::addOrcaLoadReportToLoadMetricStats(metric_names, makeOrcaReport(), stats); + auto load_stats_map = stats.latch(); + ASSERT_NE(load_stats_map, nullptr); + EXPECT_THAT(*load_stats_map, + UnorderedElementsAre( + Pair("utilization.ut_foo", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.6)))), + Pair("utilization.ut_bar", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.7)))))); +} + +TEST(OrcaLoadMetricsTest, AddAllReportedMetrics) { + Envoy::Orca::LrsReportMetricNames metric_names; + metric_names.push_back("application_utilization"); + metric_names.push_back("cpu_utilization"); + metric_names.push_back("mem_utilization"); + metric_names.push_back("eps"); + metric_names.push_back("rps_fractional"); + metric_names.push_back("named_metrics.*"); + metric_names.push_back("utilization.*"); + metric_names.push_back("request_cost.*"); + + Envoy::Upstream::LoadMetricStatsImpl stats; + Envoy::Orca::addOrcaLoadReportToLoadMetricStats(metric_names, makeOrcaReport(), stats); + auto load_stats_map = stats.latch(); + ASSERT_NE(load_stats_map, nullptr); + EXPECT_THAT( + *load_stats_map, + UnorderedElementsAre( + Pair("named_metrics.nm_foo", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.1)))), + Pair("named_metrics.nm_bar", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.2)))), + Pair("request_cost.rc_foo", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.4)))), + Pair("request_cost.rc_bar", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.5)))), + Pair("utilization.ut_foo", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.6)))), + Pair("utilization.ut_bar", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.7)))), + Pair("application_utilization", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.8)))), + Pair("cpu_utilization", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(0.9)))), + Pair("mem_utilization", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(1.0)))), + Pair("eps", AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(10)))), + Pair("rps_fractional", + AllOf(Field(&LoadMetricStats::Stat::num_requests_with_metric, 1), + Field(&LoadMetricStats::Stat::total_metric_value, DoubleEq(11)))))); +} + +} // namespace +} // namespace Orca +} // namespace Envoy diff --git a/test/common/router/router_test.cc b/test/common/router/router_test.cc index 1ccd9ef87620..ecc13f1b1e24 100644 --- a/test/common/router/router_test.cc +++ b/test/common/router/router_test.cc @@ -12,6 +12,7 @@ #include "envoy/type/v3/percent.pb.h" #include "source/common/buffer/buffer_impl.h" +#include "source/common/common/base64.h" #include "source/common/common/empty_string.h" #include "source/common/config/metadata.h" #include "source/common/config/well_known_names.h" @@ -6675,5 +6676,74 @@ TEST_F(RouterTest, OverwriteSchemeWithUpstreamTransportProtocol) { callbacks_.route_->virtual_host_.virtual_cluster_.stats().upstream_rq_total_.value()); } +TEST_F(RouterTest, OrcaLoadReport) { + EXPECT_CALL(callbacks_.route_->route_entry_, timeout()) + .WillOnce(Return(std::chrono::milliseconds(0))); + EXPECT_CALL(callbacks_.dispatcher_, createTimer_(_)).Times(0); + + NiceMock encoder; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, true); + + // Create LRS endpoint metric reporting config with three metrics. + Envoy::Orca::LrsReportMetricNames metric_names; + metric_names.push_back("cpu_utilization"); + metric_names.push_back("named_metrics.good"); + metric_names.push_back("named_metrics.not-in-report"); + ON_CALL(*cm_.thread_local_cluster_.cluster_.info_, lrsReportMetricNames()) + .WillByDefault(Return(makeOptRef(metric_names))); + // Send three metrics, one of which is not in the config. + xds::data::orca::v3::OrcaLoadReport orca_load_report; + orca_load_report.set_cpu_utilization(0.5); + orca_load_report.mutable_named_metrics()->insert({"not-in-config", 0.1}); + orca_load_report.mutable_named_metrics()->insert({"good", 0.7}); + std::string proto_string = TestUtility::getProtobufBinaryStringFromMessage(orca_load_report); + std::string orca_load_report_header_bin = + Envoy::Base64::encode(proto_string.c_str(), proto_string.length()); + Http::ResponseHeaderMapPtr response_headers(new Http::TestResponseHeaderMapImpl{ + {":status", "200"}, {"endpoint-load-metrics-bin", orca_load_report_header_bin}}); + response_decoder->decodeHeaders(std::move(response_headers), true); + auto load_metric_stats_map = + cm_.thread_local_cluster_.conn_pool_.host_->loadMetricStats().latch(); + ASSERT_NE(load_metric_stats_map, nullptr); + EXPECT_EQ(load_metric_stats_map->size(), 2); + EXPECT_EQ(load_metric_stats_map->at("cpu_utilization").total_metric_value, 0.5); + EXPECT_EQ(load_metric_stats_map->at("cpu_utilization").num_requests_with_metric, 1); + EXPECT_EQ(load_metric_stats_map->at("named_metrics.good").total_metric_value, 0.7); + EXPECT_EQ(load_metric_stats_map->at("named_metrics.good").num_requests_with_metric, 1); +} + +TEST_F(RouterTest, OrcaLoadReport_NoConfiguredMetricNames) { + EXPECT_CALL(callbacks_.route_->route_entry_, timeout()) + .WillOnce(Return(std::chrono::milliseconds(0))); + EXPECT_CALL(callbacks_.dispatcher_, createTimer_(_)).Times(0); + + NiceMock encoder; + Http::ResponseDecoder* response_decoder = nullptr; + expectNewStreamWithImmediateEncoder(encoder, &response_decoder, Http::Protocol::Http10); + + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + router_->decodeHeaders(headers, true); + + // Verify that no load metric stats are added when there are no configured metric names. + xds::data::orca::v3::OrcaLoadReport orca_load_report; + orca_load_report.set_cpu_utilization(0.5); + orca_load_report.mutable_named_metrics()->insert({"good", 0.7}); + std::string proto_string = TestUtility::getProtobufBinaryStringFromMessage(orca_load_report); + std::string orca_load_report_header_bin = + Envoy::Base64::encode(proto_string.c_str(), proto_string.length()); + Http::ResponseHeaderMapPtr response_headers(new Http::TestResponseHeaderMapImpl{ + {":status", "200"}, {"endpoint-load-metrics-bin", orca_load_report_header_bin}}); + response_decoder->decodeHeaders(std::move(response_headers), true); + auto load_metric_stats_map = + cm_.thread_local_cluster_.conn_pool_.host_->loadMetricStats().latch(); + ASSERT_EQ(load_metric_stats_map, nullptr); +} + } // namespace Router } // namespace Envoy diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index 4038e57bd8c1..380c7a7c8bcd 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -171,6 +171,11 @@ MockClusterInfo::MockClusterInfo() protocol, codecStats(protocol)) : nullptr; })); + ON_CALL(*this, lrsReportMetricNames()) + .WillByDefault(Invoke([this]() -> OptRef { + return makeOptRefFromPtr( + lrs_report_metric_names_.get()); + })); } MockClusterInfo::~MockClusterInfo() = default; diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index d9ec12addef6..8f5a72973f49 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -172,6 +172,7 @@ class MockClusterInfo : public ClusterInfo { MOCK_METHOD( OptRef, happyEyeballsConfig, (), (const)); + MOCK_METHOD(OptRef>, lrsReportMetricNames, (), (const)); ::Envoy::Http::HeaderValidatorStats& codecStats(Http::Protocol protocol) const; Http::Http1::CodecStats& http1CodecStats() const override; Http::Http2::CodecStats& http2CodecStats() const override; @@ -235,6 +236,7 @@ class MockClusterInfo : public ClusterInfo { Http::HeaderValidatorFactoryPtr header_validator_factory_; absl::optional happy_eyeballs_config_; + const std::unique_ptr lrs_report_metric_names_; }; class MockIdleTimeEnabledClusterInfo : public MockClusterInfo {