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