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