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