Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added utility methods to parse ORCA response headers from backends. #35422

Merged
merged 10 commits into from
Aug 8, 2024
30 changes: 30 additions & 0 deletions source/common/orca/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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_flat_hash_set",
"abseil_status",
"abseil_strings",
"abseil_statusor",
"fmtlib",
],
deps = [
"//envoy/common:exception_lib",
"//envoy/http:header_map_interface",
"//source/common/common:base64_lib",
"//source/common/http:header_utility_lib",
"//source/common/protobuf:utility_lib_header",
"@com_github_cncf_xds//xds/data/orca/v3:pkg_cc_proto",
],
)
163 changes: 163 additions & 0 deletions source/common/orca/orca_parser.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include "source/common/orca/orca_parser.h"

#include <cmath>
#include <cstddef>
#include <string>
#include <utility>
#include <vector>

#include "envoy/common/exception.h"
#include "envoy/http/header_map.h"

#include "source/common/common/base64.h"
#include "source/common/common/fmt.h"
#include "source/common/http/header_utility.h"
#include "source/common/protobuf/utility.h"

#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/strings/strip.h"

using ::Envoy::Http::HeaderMap;
using ::Envoy::Http::LowerCaseString;
using xds::data::orca::v3::OrcaLoadReport;

namespace Envoy {
namespace Orca {

namespace {

absl::Status tryCopyNamedMetricToOrcaLoadReport(absl::string_view metric_name, double metric_value,
OrcaLoadReport& orca_load_report) {
if (metric_name.empty()) {
return absl::InvalidArgumentError("named metric key is empty.");
}

orca_load_report.mutable_named_metrics()->insert({std::string(metric_name), metric_value});
return absl::OkStatus();
}

std::vector<absl::string_view> parseCommaDelimitedHeader(const HeaderMap::GetResult& entry) {
std::vector<absl::string_view> values;
blake-snyder marked this conversation as resolved.
Show resolved Hide resolved
values.reserve(entry.size());
for (size_t i = 0; i < entry.size(); ++i) {
std::vector<absl::string_view> tokens =
Envoy::Http::HeaderUtility::parseCommaDelimitedHeader(entry[i]->value().getStringView());
values.insert(values.end(), tokens.begin(), tokens.end());
}
return values;
}

absl::Status tryCopyMetricToOrcaLoadReport(absl::string_view metric_name,
absl::string_view metric_value,
OrcaLoadReport& orca_load_report) {
if (metric_name.empty()) {
return absl::InvalidArgumentError("metric names cannot be empty strings");
}

if (metric_value.empty()) {
return absl::InvalidArgumentError("metric values cannot be empty strings");
}

double value;
if (!absl::SimpleAtod(metric_value, &value)) {
return absl::InvalidArgumentError(fmt::format(
"unable to parse custom backend load metric value({}): {}", metric_name, metric_value));
}

if (std::isnan(value)) {
return absl::InvalidArgumentError(
fmt::format("custom backend load metric value({}) cannot be NaN.", metric_name));
}

if (std::isinf(value)) {
return absl::InvalidArgumentError(
fmt::format("custom backend load metric value({}) cannot be infinity.", metric_name));
}

if (absl::StartsWith(metric_name, kNamedMetricsFieldPrefix)) {
auto metric_name_without_prefix = absl::StripPrefix(metric_name, kNamedMetricsFieldPrefix);
return tryCopyNamedMetricToOrcaLoadReport(metric_name_without_prefix, value, orca_load_report);
}

if (metric_name == kCpuUtilizationField) {
orca_load_report.set_cpu_utilization(value);
} else if (metric_name == kMemUtilizationField) {
orca_load_report.set_mem_utilization(value);
} else if (metric_name == kApplicationUtilizationField) {
orca_load_report.set_application_utilization(value);
} else if (metric_name == kEpsField) {
orca_load_report.set_eps(value);
} else if (metric_name == kRpsFractionalField) {
orca_load_report.set_rps_fractional(value);
} else {
return absl::InvalidArgumentError(absl::StrCat("unsupported metric name: ", metric_name));
}
return absl::OkStatus();
}

absl::Status tryParseNativeHttpEncoded(const HeaderMap::GetResult& header,
OrcaLoadReport& orca_load_report) {
const std::vector<absl::string_view> values = parseCommaDelimitedHeader(header);

// Check for duplicate metric names here because OrcaLoadReport fields are not
// marked as optional and therefore don't differentiate between unset and
// default values.
absl::flat_hash_set<absl::string_view> metric_names;
for (const auto value : values) {
std::pair<absl::string_view, absl::string_view> entry =
absl::StrSplit(value, absl::MaxSplits(':', 1), absl::SkipWhitespace());
if (metric_names.contains(entry.first)) {
return absl::AlreadyExistsError(
absl::StrCat(kEndpointLoadMetricsHeader, " contains duplicate metric: ", entry.first));
}
RETURN_IF_NOT_OK(tryCopyMetricToOrcaLoadReport(entry.first, entry.second, orca_load_report));
metric_names.insert(entry.first);
}
return absl::OkStatus();
}

} // namespace

absl::StatusOr<OrcaLoadReport> parseOrcaLoadReportHeaders(const HeaderMap& headers) {
OrcaLoadReport load_report;

// Binary protobuf format.
if (const auto header_bin = headers.get(LowerCaseString(kEndpointLoadMetricsHeaderBin));
blake-snyder marked this conversation as resolved.
Show resolved Hide resolved
!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 if (const auto header_native_http =
headers.get(LowerCaseString(kEndpointLoadMetricsHeader));
!header_native_http.empty()) {
// Native HTTP format.
RETURN_IF_NOT_OK(tryParseNativeHttpEncoded(header_native_http, load_report));
} else if (const auto header_json = headers.get(LowerCaseString(kEndpointLoadMetricsHeaderJson));
!header_json.empty()) {
// JSON format.
#if defined(ENVOY_ENABLE_FULL_PROTOS) && defined(ENVOY_ENABLE_YAML)
bool has_unknown_field = false;
const std::string json_string = std::string(header_json[0]->value().getStringView());
RETURN_IF_ERROR(
Envoy::MessageUtil::loadFromJsonNoThrow(json_string, load_report, has_unknown_field));
#else
IS_ENVOY_BUG("JSON formatted ORCA header support not implemented for this build");
#endif // !ENVOY_ENABLE_FULL_PROTOS || !ENVOY_ENABLE_YAML
} else {
return absl::NotFoundError("no ORCA data sent from the backend");
}

return load_report;
blake-snyder marked this conversation as resolved.
Show resolved Hide resolved
}

} // namespace Orca
} // namespace Envoy
29 changes: 29 additions & 0 deletions source/common/orca/orca_parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#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 {

// Headers used to send ORCA load metrics from the backend.
static constexpr absl::string_view kEndpointLoadMetricsHeader = "endpoint-load-metrics";
static constexpr absl::string_view kEndpointLoadMetricsHeaderBin = "endpoint-load-metrics-bin";
static constexpr absl::string_view kEndpointLoadMetricsHeaderJson = "endpoint-load-metrics-json";
// 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.";

// Parses ORCA load metrics from a header map into an OrcaLoadReport proto.
// Supports native HTTP, JSON and serialized binary formats.
absl::StatusOr<xds::data::orca::v3::OrcaLoadReport>
parseOrcaLoadReportHeaders(const Envoy::Http::HeaderMap& headers);
} // namespace Orca
} // namespace Envoy
26 changes: 26 additions & 0 deletions test/common/orca/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
Loading
Loading