xref: /aosp_15_r20/external/grpc-grpc/test/cpp/ext/gcp/observability_config_test.cc (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1 //
2 // Copyright 2022 gRPC authors.
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //     http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 
17 #include "src/cpp/ext/gcp/observability_config.h"
18 
19 #include "gmock/gmock.h"
20 #include "gtest/gtest.h"
21 
22 #include <grpc/support/alloc.h>
23 
24 #include "src/core/lib/config/core_configuration.h"
25 #include "src/core/lib/gpr/tmpfile.h"
26 #include "src/core/lib/gprpp/env.h"
27 #include "src/core/lib/json/json_reader.h"
28 #include "test/core/util/test_config.h"
29 
30 namespace grpc {
31 namespace internal {
32 namespace {
33 
TEST(GcpObservabilityConfigJsonParsingTest,Basic)34 TEST(GcpObservabilityConfigJsonParsingTest, Basic) {
35   const char* json_str = R"json({
36       "cloud_logging": {
37         "client_rpc_events": [
38           {
39             "methods": ["google.pubsub.v1.Subscriber/Acknowledge", "google.pubsub.v1.Publisher/CreateTopic"],
40             "exclude": true
41           },
42           {
43             "methods": ["google.pubsub.v1.Subscriber/*", "google.pubsub.v1.Publisher/*"],
44             "max_metadata_bytes": 4096,
45             "max_message_bytes": 4096
46           }],
47         "server_rpc_events": [
48           {
49             "methods": ["*"],
50             "max_metadata_bytes": 4096,
51             "max_message_bytes": 4096
52           }
53         ]
54       },
55       "cloud_monitoring": {},
56       "cloud_trace": {
57         "sampling_rate": 0.05
58       },
59       "project_id": "project",
60       "labels": {
61         "SOURCE_VERSION": "v1",
62         "SERVICE_NAME": "payment-service",
63         "DATA_CENTER": "us-west1-a"
64       }
65     })json";
66   auto json = grpc_core::JsonParse(json_str);
67   ASSERT_TRUE(json.ok()) << json.status();
68   grpc_core::ValidationErrors errors;
69   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
70       *json, grpc_core::JsonArgs(), &errors);
71   ASSERT_TRUE(errors.ok()) << errors.status(absl::StatusCode::kInvalidArgument,
72                                             "unexpected errors");
73   ASSERT_TRUE(config.cloud_logging.has_value());
74   ASSERT_EQ(config.cloud_logging->client_rpc_events.size(), 2);
75   EXPECT_THAT(config.cloud_logging->client_rpc_events[0].qualified_methods,
76               ::testing::ElementsAre("google.pubsub.v1.Subscriber/Acknowledge",
77                                      "google.pubsub.v1.Publisher/CreateTopic"));
78   EXPECT_TRUE(config.cloud_logging->client_rpc_events[0].exclude);
79   EXPECT_EQ(config.cloud_logging->client_rpc_events[0].max_metadata_bytes, 0);
80   EXPECT_EQ(config.cloud_logging->client_rpc_events[0].max_message_bytes, 0);
81   EXPECT_THAT(config.cloud_logging->client_rpc_events[1].qualified_methods,
82               ::testing::ElementsAre("google.pubsub.v1.Subscriber/*",
83                                      "google.pubsub.v1.Publisher/*"));
84   EXPECT_FALSE(config.cloud_logging->client_rpc_events[1].exclude);
85   EXPECT_EQ(config.cloud_logging->client_rpc_events[1].max_metadata_bytes,
86             4096);
87   EXPECT_EQ(config.cloud_logging->client_rpc_events[1].max_message_bytes, 4096);
88   ASSERT_EQ(config.cloud_logging->server_rpc_events.size(), 1);
89   EXPECT_THAT(config.cloud_logging->server_rpc_events[0].qualified_methods,
90               ::testing::ElementsAre("*"));
91   EXPECT_EQ(config.cloud_logging->server_rpc_events[0].max_metadata_bytes,
92             4096);
93   EXPECT_EQ(config.cloud_logging->server_rpc_events[0].max_message_bytes, 4096);
94   EXPECT_TRUE(config.cloud_monitoring.has_value());
95   EXPECT_TRUE(config.cloud_trace.has_value());
96   EXPECT_FLOAT_EQ(config.cloud_trace->sampling_rate, 0.05);
97   EXPECT_EQ(config.project_id, "project");
98   EXPECT_THAT(config.labels,
99               ::testing::UnorderedElementsAre(
100                   ::testing::Pair("SOURCE_VERSION", "v1"),
101                   ::testing::Pair("SERVICE_NAME", "payment-service"),
102                   ::testing::Pair("DATA_CENTER", "us-west1-a")));
103 }
104 
105 TEST(GcpObservabilityConfigJsonParsingTest, Defaults) {
106   const char* json_str = R"json({
107     })json";
108   auto json = grpc_core::JsonParse(json_str);
109   ASSERT_TRUE(json.ok()) << json.status();
110   grpc_core::ValidationErrors errors;
111   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
112       *json, grpc_core::JsonArgs(), &errors);
113   ASSERT_TRUE(errors.ok()) << errors.status(absl::StatusCode::kInvalidArgument,
114                                             "unexpected errors");
115   EXPECT_FALSE(config.cloud_logging.has_value());
116   EXPECT_FALSE(config.cloud_monitoring.has_value());
117   EXPECT_FALSE(config.cloud_trace.has_value());
118   EXPECT_TRUE(config.project_id.empty());
119   EXPECT_TRUE(config.labels.empty());
120 }
121 
122 TEST(GcpObservabilityConfigJsonParsingTest, LoggingConfigMethodIllegalSlashes) {
123   const char* json_str = R"json({
124       "cloud_logging": {
125         "client_rpc_events": [
126           {
127             "methods": ["servicemethod", "service/method/foo"]
128           }
129         ]
130       }
131     })json";
132   auto json = grpc_core::JsonParse(json_str);
133   ASSERT_TRUE(json.ok()) << json.status();
134   grpc_core::ValidationErrors errors;
135   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
136       *json, grpc_core::JsonArgs(), &errors);
137   EXPECT_THAT(errors.status(absl::StatusCode::kInvalidArgument, "Parsing error")
138                   .ToString(),
139               ::testing::AllOf(
140                   ::testing::HasSubstr(
141                       "field:cloud_logging.client_rpc_events[0].methods[0]"
142                       " error:Illegal methods[] configuration"),
143                   ::testing::HasSubstr(
144                       "field:cloud_logging.client_rpc_events[0].methods[1] "
145                       "error:methods[] can have at most a single '/'")));
146 }
147 
148 TEST(GcpObservabilityConfigJsonParsingTest, LoggingConfigEmptyMethod) {
149   const char* json_str = R"json({
150       "cloud_logging": {
151         "client_rpc_events": [
152           {
153             "methods": [""]
154           }
155         ]
156       }
157     })json";
158   auto json = grpc_core::JsonParse(json_str);
159   ASSERT_TRUE(json.ok()) << json.status();
160   grpc_core::ValidationErrors errors;
161   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
162       *json, grpc_core::JsonArgs(), &errors);
163   EXPECT_THAT(
164       errors.status(absl::StatusCode::kInvalidArgument, "Parsing error")
165           .ToString(),
166       ::testing::HasSubstr("field:cloud_logging.client_rpc_events[0].methods[0]"
167                            " error:Empty configuration"));
168 }
169 
170 TEST(GcpObservabilityConfigJsonParsingTest, LoggingConfigWildcardEntries) {
171   const char* json_str = R"json({
172       "cloud_logging": {
173         "client_rpc_events": [
174           {
175             "methods": ["*", "service/*"]
176           }
177         ],
178         "server_rpc_events": [
179           {
180             "methods": ["*", "service/*"]
181           }
182         ]
183       }
184     })json";
185   auto json = grpc_core::JsonParse(json_str);
186   ASSERT_TRUE(json.ok()) << json.status();
187   grpc_core::ValidationErrors errors;
188   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
189       *json, grpc_core::JsonArgs(), &errors);
190   ASSERT_TRUE(errors.ok()) << errors.status(absl::StatusCode::kInvalidArgument,
191                                             "unexpected errors");
192   ASSERT_TRUE(config.cloud_logging.has_value());
193   ASSERT_EQ(config.cloud_logging->client_rpc_events.size(), 1);
194   EXPECT_THAT(config.cloud_logging->client_rpc_events[0].qualified_methods,
195               ::testing::ElementsAre("*", "service/*"));
196   ASSERT_EQ(config.cloud_logging->server_rpc_events.size(), 1);
197   EXPECT_THAT(config.cloud_logging->server_rpc_events[0].qualified_methods,
198               ::testing::ElementsAre("*", "service/*"));
199 }
200 
201 TEST(GcpObservabilityConfigJsonParsingTest,
202      LoggingConfigIncorrectWildcardSpecs) {
203   const char* json_str = R"json({
204       "cloud_logging": {
205         "client_rpc_events": [
206           {
207             "methods": ["*"],
208             "exclude": true
209           },
210           {
211             "methods": ["*/method", "service/*blah"],
212             "exclude": true
213           }
214         ]
215       }
216     })json";
217   auto json = grpc_core::JsonParse(json_str);
218   ASSERT_TRUE(json.ok()) << json.status();
219   grpc_core::ValidationErrors errors;
220   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
221       *json, grpc_core::JsonArgs(), &errors);
222   EXPECT_THAT(
223       errors.status(absl::StatusCode::kInvalidArgument, "Parsing error")
224           .ToString(),
225       ::testing::AllOf(
226           ::testing::HasSubstr(
227               "field:cloud_logging.client_rpc_events[0].methods[0]"
228               " error:Wildcard match '*' not allowed when 'exclude' is set"),
229           ::testing::HasSubstr(
230               "field:cloud_logging.client_rpc_events[1].methods[0] "
231               "error:Configuration of type '*/method' not allowed"),
232           ::testing::HasSubstr(
233               "field:cloud_logging.client_rpc_events[1].methods[1] "
234               "error:Wildcard specified for method in incorrect manner")));
235 }
236 
237 TEST(GcpObservabilityConfigJsonParsingTest, SamplingRateDefaults) {
238   const char* json_str = R"json({
239       "cloud_trace": {
240         "sampling_rate": 0.05
241       }
242     })json";
243   auto json = grpc_core::JsonParse(json_str);
244   ASSERT_TRUE(json.ok()) << json.status();
245   grpc_core::ValidationErrors errors;
246   auto config = grpc_core::LoadFromJson<GcpObservabilityConfig>(
247       *json, grpc_core::JsonArgs(), &errors);
248   ASSERT_TRUE(errors.ok()) << errors.status(absl::StatusCode::kInvalidArgument,
249                                             "unexpected errors");
250   ASSERT_TRUE(config.cloud_trace.has_value());
251   EXPECT_FLOAT_EQ(config.cloud_trace->sampling_rate, 0.05);
252 }
253 
254 TEST(GcpEnvParsingTest, NoEnvironmentVariableSet) {
255   auto config = GcpObservabilityConfig::ReadFromEnv();
256   EXPECT_EQ(config.status(),
257             absl::FailedPreconditionError(
258                 "Environment variables GRPC_GCP_OBSERVABILITY_CONFIG_FILE or "
259                 "GRPC_GCP_OBSERVABILITY_CONFIG "
260                 "not defined"));
261 }
262 
263 TEST(GcpEnvParsingTest, ConfigFileDoesNotExist) {
264   const char* kPath = "/tmp/gcp_observability_config_does_not_exist";
265   grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG_FILE", kPath);
266 
267   auto config = GcpObservabilityConfig::ReadFromEnv();
268 
269   EXPECT_EQ(config.status().code(), absl::StatusCode::kFailedPrecondition);
270   EXPECT_THAT(
271       std::string(config.status().message()),
272       ::testing::StartsWith(absl::StrCat("error loading file ", kPath)));
273 
274   grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG_FILE");
275 }
276 
277 TEST(GcpEnvParsingTest, ProjectIdNotSet) {
278   grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG", "{}");
279 
280   auto config = GcpObservabilityConfig::ReadFromEnv();
281   EXPECT_EQ(config.status(),
282             absl::FailedPreconditionError("GCP Project ID not found."));
283 
284   grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG");
285   grpc_core::CoreConfiguration::Reset();
286 }
287 
288 TEST(GcpEnvParsingTest, ProjectIdFromGcpProjectEnvVar) {
289   grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG", "{}");
290   grpc_core::SetEnv("GCP_PROJECT", "gcp_project");
291 
292   auto config = GcpObservabilityConfig::ReadFromEnv();
293   EXPECT_TRUE(config.ok());
294   EXPECT_EQ(config->project_id, "gcp_project");
295 
296   grpc_core::UnsetEnv("GCP_PROJECT");
297   grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG");
298   grpc_core::CoreConfiguration::Reset();
299 }
300 
301 TEST(GcpEnvParsingTest, ProjectIdFromGcloudProjectEnvVar) {
302   grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG", "{}");
303   grpc_core::SetEnv("GCLOUD_PROJECT", "gcloud_project");
304 
305   auto config = GcpObservabilityConfig::ReadFromEnv();
306   EXPECT_TRUE(config.ok());
307   EXPECT_EQ(config->project_id, "gcloud_project");
308 
309   grpc_core::UnsetEnv("GCLOUD_PROJECT");
310   grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG");
311   grpc_core::CoreConfiguration::Reset();
312 }
313 
314 TEST(GcpEnvParsingTest, ProjectIdFromGoogleCloudProjectEnvVar) {
315   grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG", "{}");
316   grpc_core::SetEnv("GOOGLE_CLOUD_PROJECT", "google_cloud_project");
317 
318   auto config = GcpObservabilityConfig::ReadFromEnv();
319   EXPECT_TRUE(config.ok());
320   EXPECT_EQ(config->project_id, "google_cloud_project");
321 
322   grpc_core::UnsetEnv("GOOGLE_CLOUD_PROJECT");
323   grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG");
324   grpc_core::CoreConfiguration::Reset();
325 }
326 
327 class EnvParsingTestType {
328  public:
329   enum class ConfigSource {
330     kFile,
331     kEnvVar,
332   };
333 
334   EnvParsingTestType& set_config_source(ConfigSource config_source) {
335     config_source_ = config_source;
336     return *this;
337   }
338 
339   ConfigSource config_source() const { return config_source_; }
340 
341   std::string ToString() const {
342     std::string ret_val;
343     if (config_source_ == ConfigSource::kFile) {
344       absl::StrAppend(&ret_val, "ConfigFromFile");
345     } else if (config_source_ == ConfigSource::kEnvVar) {
346       absl::StrAppend(&ret_val, "ConfigFromEnvVar");
347     }
348     return ret_val;
349   }
350 
351   static std::string Name(
352       const ::testing::TestParamInfo<EnvParsingTestType>& info) {
353     return info.param.ToString();
354   }
355 
356  private:
357   ConfigSource config_source_;
358 };
359 
360 class EnvParsingTest : public ::testing::TestWithParam<EnvParsingTestType> {
361  protected:
362   ~EnvParsingTest() override {
363     if (GetParam().config_source() == EnvParsingTestType::ConfigSource::kFile) {
364       if (tmp_file_name != nullptr) {
365         grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG_FILE");
366         remove(tmp_file_name);
367         gpr_free(tmp_file_name);
368       }
369     } else if (GetParam().config_source() ==
370                EnvParsingTestType::ConfigSource::kEnvVar) {
371       grpc_core::UnsetEnv("GRPC_GCP_OBSERVABILITY_CONFIG");
372     }
373   }
374 
375   void SetConfig(const char* json) {
376     if (GetParam().config_source() == EnvParsingTestType::ConfigSource::kFile) {
377       ASSERT_EQ(tmp_file_name, nullptr);
378       FILE* tmp_config_file =
379           gpr_tmpfile("gcp_observability_config", &tmp_file_name);
380       fputs(json, tmp_config_file);
381       fclose(tmp_config_file);
382       grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG_FILE", tmp_file_name);
383     } else if (GetParam().config_source() ==
384                EnvParsingTestType::ConfigSource::kEnvVar) {
385       grpc_core::SetEnv("GRPC_GCP_OBSERVABILITY_CONFIG", json);
386     }
387   }
388 
389  private:
390   char* tmp_file_name = nullptr;
391 };
392 
393 TEST_P(EnvParsingTest, Basic) {
394   SetConfig(R"json({
395       "project_id": "project"
396     })json");
397   auto config = GcpObservabilityConfig::ReadFromEnv();
398 
399   ASSERT_TRUE(config.ok());
400   EXPECT_EQ(config->project_id, "project");
401 }
402 
403 // Test that JSON parsing errors are propagated as expected.
404 TEST_P(EnvParsingTest, BadJson) {
405   SetConfig("{");
406   auto config = GcpObservabilityConfig::ReadFromEnv();
407 
408   EXPECT_EQ(config.status().code(), absl::StatusCode::kInvalidArgument);
409   EXPECT_THAT(config.status().message(),
410               ::testing::HasSubstr("JSON parsing failed"))
411       << config.status().message();
412 }
413 
414 TEST_P(EnvParsingTest, BadJsonEmptyString) {
415   SetConfig("");
416   auto config = GcpObservabilityConfig::ReadFromEnv();
417   if (GetParam().config_source() == EnvParsingTestType::ConfigSource::kFile) {
418     EXPECT_EQ(config.status().code(), absl::StatusCode::kInvalidArgument);
419     EXPECT_THAT(config.status().message(),
420                 ::testing::HasSubstr("JSON parsing failed"))
421         << config.status().message();
422   } else {
423     EXPECT_EQ(config.status(),
424               absl::FailedPreconditionError(
425                   "Environment variables GRPC_GCP_OBSERVABILITY_CONFIG_FILE or "
426                   "GRPC_GCP_OBSERVABILITY_CONFIG not defined"));
427   }
428 }
429 
430 // Make sure that GCP config errors are propagated as expected.
431 TEST_P(EnvParsingTest, BadGcpConfig) {
432   SetConfig(R"json({
433       "project_id": 123
434     })json");
435   auto config = GcpObservabilityConfig::ReadFromEnv();
436 
437   EXPECT_EQ(config.status().code(), absl::StatusCode::kInvalidArgument);
438   EXPECT_THAT(config.status().message(),
439               ::testing::HasSubstr("field:project_id error:is not a string"))
440       << config.status().message();
441 }
442 
443 INSTANTIATE_TEST_SUITE_P(
444     GcpObservabilityConfigTest, EnvParsingTest,
445     ::testing::Values(EnvParsingTestType().set_config_source(
446                           EnvParsingTestType::ConfigSource::kFile),
447                       EnvParsingTestType().set_config_source(
448                           EnvParsingTestType::ConfigSource::kEnvVar)),
449     &EnvParsingTestType::Name);
450 
451 }  // namespace
452 }  // namespace internal
453 }  // namespace grpc
454 
455 int main(int argc, char** argv) {
456   grpc::testing::TestEnvironment env(&argc, argv);
457   ::testing::InitGoogleTest(&argc, argv);
458   return RUN_ALL_TESTS();
459 }
460