diff --git a/source/common/orca/BUILD b/source/common/orca/BUILD new file mode 100644 index 000000000000..9337ab05ecaf --- /dev/null +++ b/source/common/orca/BUILD @@ -0,0 +1,25 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "orca_parser", + srcs = ["orca_parser.cc"], + hdrs = ["orca_parser.h"], + external_deps = [ + "abseil_strings", + "abseil_statusor", + "fmtlib", + ], + deps = [ + "//envoy/http:header_map_interface", + "//source/common/common:base64_lib", + "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", + ], +) diff --git a/source/common/orca/orca_parser.cc b/source/common/orca/orca_parser.cc new file mode 100644 index 000000000000..73ba57da26d9 --- /dev/null +++ b/source/common/orca/orca_parser.cc @@ -0,0 +1,45 @@ +#include "source/common/orca/orca_parser.h" + +#include + +#include "envoy/http/header_map.h" + +#include "source/common/common/base64.h" +#include "source/common/common/fmt.h" + +#include "absl/strings/string_view.h" + +using ::Envoy::Http::HeaderMap; +using xds::data::orca::v3::OrcaLoadReport; + +namespace Envoy { +namespace Orca { + +namespace { + +const Http::LowerCaseString& endpointLoadMetricsHeaderBin() { + CONSTRUCT_ON_FIRST_USE(Http::LowerCaseString, kEndpointLoadMetricsHeaderBin); +} + +} // namespace + +absl::StatusOr parseOrcaLoadReportHeaders(const HeaderMap& headers) { + OrcaLoadReport load_report; + + // Binary protobuf format. + if (const auto header_bin = headers.get(endpointLoadMetricsHeaderBin()); !header_bin.empty()) { + const auto header_value = header_bin[0]->value().getStringView(); + const std::string decoded_value = Envoy::Base64::decode(header_value); + if (!load_report.ParseFromString(decoded_value)) { + return absl::InvalidArgumentError( + fmt::format("unable to parse binaryheader to OrcaLoadReport: {}", header_value)); + } + } else { + return absl::NotFoundError("no ORCA data sent from the backend"); + } + + return load_report; +} + +} // namespace Orca +} // namespace Envoy diff --git a/source/common/orca/orca_parser.h b/source/common/orca/orca_parser.h new file mode 100644 index 000000000000..86fd23944017 --- /dev/null +++ b/source/common/orca/orca_parser.h @@ -0,0 +1,19 @@ +#pragma once + +#include "envoy/http/header_map.h" + +#include "absl/status/statusor.h" +#include "xds/data/orca/v3/orca_load_report.pb.h" + +namespace Envoy { +namespace Orca { + +// Header used to send ORCA load metrics from the backend. +static constexpr absl::string_view kEndpointLoadMetricsHeaderBin = "endpoint-load-metrics-bin"; + +// Parses ORCA load metrics from a header map into an OrcaLoadReport proto. +// Supports serialized binary formats. +absl::StatusOr +parseOrcaLoadReportHeaders(const Envoy::Http::HeaderMap& headers); +} // namespace Orca +} // namespace Envoy diff --git a/test/common/orca/BUILD b/test/common/orca/BUILD new file mode 100644 index 000000000000..9122593cf921 --- /dev/null +++ b/test/common/orca/BUILD @@ -0,0 +1,26 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "orca_parser_test", + srcs = ["orca_parser_test.cc"], + external_deps = [ + "abseil_status", + "abseil_strings", + "fmtlib", + ], + deps = [ + "//source/common/common:base64_lib", + "//source/common/orca:orca_parser", + "//test/test_common:status_utility_lib", + "//test/test_common:utility_lib", + "@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto", + ], +) diff --git a/test/common/orca/orca_parser_test.cc b/test/common/orca/orca_parser_test.cc new file mode 100644 index 000000000000..84debabd85ed --- /dev/null +++ b/test/common/orca/orca_parser_test.cc @@ -0,0 +1,72 @@ +#include + +#include "source/common/common/base64.h" +#include "source/common/orca/orca_parser.h" + +#include "test/test_common/status_utility.h" +#include "test/test_common/utility.h" + +#include "absl/status/status.h" +#include "xds/data/orca/v3/orca_load_report.pb.h" + +namespace Envoy { +namespace Orca { +namespace { + +// Returns an example OrcaLoadReport proto with all fields populated. +static xds::data::orca::v3::OrcaLoadReport exampleOrcaLoadReport() { + xds::data::orca::v3::OrcaLoadReport orca_load_report; + orca_load_report.set_cpu_utilization(0.7); + orca_load_report.set_application_utilization(0.8); + orca_load_report.set_mem_utilization(0.9); + orca_load_report.set_eps(2); + orca_load_report.set_rps_fractional(1000); + orca_load_report.mutable_named_metrics()->insert({"foo", 123}); + orca_load_report.mutable_named_metrics()->insert({"bar", 0.2}); + return orca_load_report; +} + +TEST(OrcaParserUtilTest, NoHeaders) { + Http::TestRequestHeaderMapImpl headers{}; + // parseOrcaLoadReport returns error when no ORCA data is sent from + // the backend. + EXPECT_THAT(parseOrcaLoadReportHeaders(headers), + StatusHelpers::HasStatus(absl::NotFoundError("no ORCA data sent from the backend"))); +} + +TEST(OrcaParserUtilTest, MissingOrcaHeaders) { + Http::TestRequestHeaderMapImpl headers{{"wrong-header", "wrong-value"}}; + // parseOrcaLoadReport returns error when no ORCA data is sent from + // the backend. + EXPECT_THAT(parseOrcaLoadReportHeaders(headers), + StatusHelpers::HasStatus(absl::NotFoundError("no ORCA data sent from the backend"))); +} + +TEST(OrcaParserUtilTest, BinaryHeader) { + const std::string proto_string = + TestUtility::getProtobufBinaryStringFromMessage(exampleOrcaLoadReport()); + const auto orca_load_report_header_bin = + Envoy::Base64::encode(proto_string.c_str(), proto_string.length()); + Http::TestRequestHeaderMapImpl headers{ + {std::string(kEndpointLoadMetricsHeaderBin), orca_load_report_header_bin}}; + EXPECT_THAT(parseOrcaLoadReportHeaders(headers), + StatusHelpers::IsOkAndHolds(ProtoEq(exampleOrcaLoadReport()))); +} + +TEST(OrcaParserUtilTest, InvalidBinaryHeader) { + const std::string proto_string = + TestUtility::getProtobufBinaryStringFromMessage(exampleOrcaLoadReport()); + // Force a bad base64 encoding by shortening the length of the output. + const auto orca_load_report_header_bin = + Envoy::Base64::encode(proto_string.c_str(), proto_string.length() / 2); + Http::TestRequestHeaderMapImpl headers{ + {std::string(kEndpointLoadMetricsHeaderBin), orca_load_report_header_bin}}; + EXPECT_THAT(parseOrcaLoadReportHeaders(headers), + StatusHelpers::HasStatus( + absl::StatusCode::kInvalidArgument, + testing::HasSubstr("unable to parse binaryheader to OrcaLoadReport"))); +} + +} // namespace +} // namespace Orca +} // namespace Envoy