diff --git a/src/cpp/ext/gcp/BUILD b/src/cpp/ext/gcp/BUILD index fde2c657458..e728e2d216e 100644 --- a/src/cpp/ext/gcp/BUILD +++ b/src/cpp/ext/gcp/BUILD @@ -53,14 +53,26 @@ grpc_cc_library( grpc_cc_library( name = "observability_config", + srcs = [ + "observability_config.cc", + ], hdrs = [ "observability_config.h", ], + external_deps = [ + "absl/status", + "absl/status:statusor", + "absl/strings", + ], language = "c++", visibility = ["//test:__subpackages__"], deps = [ "//:gpr", + "//:grpc_base", + "//:grpc_public_hdrs", + "//:json", "//:json_args", "//:json_object_loader", + "//:slice_refcount", ], ) diff --git a/src/cpp/ext/gcp/observability_config.cc b/src/cpp/ext/gcp/observability_config.cc new file mode 100644 index 00000000000..026b2d9b054 --- /dev/null +++ b/src/cpp/ext/gcp/observability_config.cc @@ -0,0 +1,90 @@ +// +// Copyright 2022 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include + +#include "src/cpp/ext/gcp/observability_config.h" + +#include + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" + +#include +#include + +#include "src/core/lib/gpr/env.h" +#include "src/core/lib/gprpp/memory.h" +#include "src/core/lib/iomgr/error.h" +#include "src/core/lib/iomgr/load_file.h" +#include "src/core/lib/json/json.h" +#include "src/core/lib/slice/slice_internal.h" +#include "src/core/lib/slice/slice_refcount.h" +#include "src/core/lib/transport/error_utils.h" + +namespace grpc { +namespace internal { + +namespace { + +// Loads the contents of the file pointed by env var +// GRPC_OBSERVABILITY_CONFIG_FILE. If unset, falls back to the contents of +// GRPC_OBSERVABILITY_CONFIG. +absl::StatusOr GetGcpObservabilityConfigContents() { + // First, try GRPC_OBSERVABILITY_CONFIG_FILE + std::string contents_str; + grpc_core::UniquePtr path(gpr_getenv("GRPC_OBSERVABILITY_CONFIG_FILE")); + if (path != nullptr) { + grpc_slice contents; + grpc_error_handle error = + grpc_load_file(path.get(), /*add_null_terminator=*/true, &contents); + if (!GRPC_ERROR_IS_NONE(error)) { + return grpc_error_to_absl_status(grpc_error_set_int( + error, GRPC_ERROR_INT_GRPC_STATUS, GRPC_STATUS_FAILED_PRECONDITION)); + } + std::string contents_str(grpc_core::StringViewFromSlice(contents)); + grpc_slice_unref_internal(contents); + return contents_str; + } + // Next, try GRPC_OBSERVABILITY_CONFIG env var. + grpc_core::UniquePtr env_config( + gpr_getenv("GRPC_OBSERVABILITY_CONFIG")); + if (env_config != nullptr) { + return env_config.get(); + } + // No observability config found. + return absl::FailedPreconditionError( + "Environment variables GRPC_OBSERVABILITY_CONFIG_FILE or " + "GRPC_OBSERVABILITY_CONFIG " + "not defined"); +} + +} // namespace + +absl::StatusOr GcpObservabilityConfig::ReadFromEnv() { + auto config_contents = GetGcpObservabilityConfigContents(); + if (!config_contents.ok()) { + return config_contents.status(); + } + auto config_json = grpc_core::Json::Parse(*config_contents); + if (!config_json.ok()) { + return config_json.status(); + } + return grpc_core::LoadFromJson(*config_json); +} + +} // namespace internal +} // namespace grpc diff --git a/src/cpp/ext/gcp/observability_config.h b/src/cpp/ext/gcp/observability_config.h index b6223a40f8c..2ad889eb25c 100644 --- a/src/cpp/ext/gcp/observability_config.h +++ b/src/cpp/ext/gcp/observability_config.h @@ -19,6 +19,8 @@ #include +#include "absl/status/statusor.h" + #include "src/core/lib/json/json_args.h" #include "src/core/lib/json/json_object_loader.h" @@ -85,6 +87,12 @@ struct GcpObservabilityConfig { .Finish(); return loader; } + + // Tries to load the contents of GcpObservabilityConfig from the file located + // by the value of environment variable `GRPC_OBSERVABILITY_CONFIG_FILE`. If + // `GRPC_OBSERVABILITY_CONFIG_FILE` is unset, falls back to + // `GRPC_OBSERVABILITY_CONFIG`. + static absl::StatusOr ReadFromEnv(); }; } // namespace internal diff --git a/test/cpp/ext/gcp/observability_config_test.cc b/test/cpp/ext/gcp/observability_config_test.cc index cac4d455114..cb553003a4e 100644 --- a/test/cpp/ext/gcp/observability_config_test.cc +++ b/test/cpp/ext/gcp/observability_config_test.cc @@ -19,6 +19,10 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include + +#include "src/core/lib/gpr/env.h" +#include "src/core/lib/gpr/tmpfile.h" #include "test/core/util/test_config.h" namespace grpc { @@ -67,6 +71,135 @@ TEST(GcpObservabilityConfigJsonParsingTest, Defaults) { EXPECT_TRUE(config.project_id.empty()); } +TEST(GcpEnvParsingTest, NoEnvironmentVariableSet) { + auto config = GcpObservabilityConfig::ReadFromEnv(); + EXPECT_EQ(config.status(), + absl::FailedPreconditionError( + "Environment variables GRPC_OBSERVABILITY_CONFIG_FILE or " + "GRPC_OBSERVABILITY_CONFIG " + "not defined")); +} + +TEST(GcpEnvParsingTest, ConfigFileDoesNotExist) { + gpr_setenv("GRPC_OBSERVABILITY_CONFIG_FILE", + "/tmp/gcp_observability_config_does_not_exist"); + + auto config = GcpObservabilityConfig::ReadFromEnv(); + + EXPECT_EQ(config.status(), + absl::FailedPreconditionError("Failed to load file")); + + gpr_unsetenv("GRPC_OBSERVABILITY_CONFIG_FILE"); +} + +class EnvParsingTestType { + public: + enum class ConfigSource { + kFile, + kEnvVar, + }; + + EnvParsingTestType& set_config_source(ConfigSource config_source) { + config_source_ = config_source; + return *this; + } + + ConfigSource config_source() const { return config_source_; } + + std::string ToString() const { + std::string ret_val; + if (config_source_ == ConfigSource::kFile) { + absl::StrAppend(&ret_val, "ConfigFromFile"); + } else if (config_source_ == ConfigSource::kEnvVar) { + absl::StrAppend(&ret_val, "ConfigFromEnvVar"); + } + return ret_val; + } + + static std::string Name( + const ::testing::TestParamInfo& info) { + return info.param.ToString(); + } + + private: + ConfigSource config_source_; +}; + +class EnvParsingTest : public ::testing::TestWithParam { + protected: + ~EnvParsingTest() override { + if (GetParam().config_source() == EnvParsingTestType::ConfigSource::kFile) { + if (tmp_file_name != nullptr) { + gpr_unsetenv("GRPC_OBSERVABILITY_CONFIG_FILE"); + remove(tmp_file_name); + gpr_free(tmp_file_name); + } + } else if (GetParam().config_source() == + EnvParsingTestType::ConfigSource::kEnvVar) { + gpr_unsetenv("GRPC_OBSERVABILITY_CONFIG"); + } + } + + void SetConfig(const char* json) { + if (GetParam().config_source() == EnvParsingTestType::ConfigSource::kFile) { + ASSERT_EQ(tmp_file_name, nullptr); + FILE* tmp_config_file = + gpr_tmpfile("gcp_observability_config", &tmp_file_name); + fputs(json, tmp_config_file); + fclose(tmp_config_file); + gpr_setenv("GRPC_OBSERVABILITY_CONFIG_FILE", tmp_file_name); + } else if (GetParam().config_source() == + EnvParsingTestType::ConfigSource::kEnvVar) { + gpr_setenv("GRPC_OBSERVABILITY_CONFIG", json); + } + } + + private: + char* tmp_file_name = nullptr; +}; + +TEST_P(EnvParsingTest, Basic) { + SetConfig(R"json({ + "project_id": "project" + })json"); + auto config = GcpObservabilityConfig::ReadFromEnv(); + + ASSERT_TRUE(config.ok()); + EXPECT_EQ(config->project_id, "project"); +} + +// Test that JSON parsing errors are propagated as expected. +TEST_P(EnvParsingTest, BadJson) { + SetConfig("{"); + auto config = GcpObservabilityConfig::ReadFromEnv(); + + EXPECT_EQ(config.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(config.status().message(), + ::testing::HasSubstr("JSON parsing failed")) + << config.status().message(); +} + +// Make sure that GCP config errors are propagated as expected. +TEST_P(EnvParsingTest, BadGcpConfig) { + SetConfig(R"json({ + "project_id": 123 + })json"); + auto config = GcpObservabilityConfig::ReadFromEnv(); + + EXPECT_EQ(config.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(config.status().message(), + ::testing::HasSubstr("field:project_id error:is not a string")) + << config.status().message(); +} + +INSTANTIATE_TEST_SUITE_P( + GcpObservabilityConfigTest, EnvParsingTest, + ::testing::Values(EnvParsingTestType().set_config_source( + EnvParsingTestType::ConfigSource::kFile), + EnvParsingTestType().set_config_source( + EnvParsingTestType::ConfigSource::kEnvVar)), + &EnvParsingTestType::Name); + } // namespace } // namespace internal } // namespace grpc diff --git a/tools/distrib/fix_build_deps.py b/tools/distrib/fix_build_deps.py index d82f19ae1ed..c386bdef810 100755 --- a/tools/distrib/fix_build_deps.py +++ b/tools/distrib/fix_build_deps.py @@ -395,6 +395,7 @@ args = parser.parse_args() for dirname in [ "", + "src/cpp/ext/gcp", "test/core/uri", "test/core/util", "test/core/end2end",