vmik-fluent-plugin-google-cloud 0.5.5.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1534 @@
1
+ # Copyright 2016 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'google/apis'
16
+ require 'helper'
17
+ require 'mocha/test_unit'
18
+ require 'webmock/test_unit'
19
+
20
+ # Unit tests for Google Cloud Logging plugin
21
+ module BaseTest
22
+ def setup
23
+ Fluent::Test.setup
24
+ # delete environment variables that googleauth uses to find credentials.
25
+ ENV.delete('GOOGLE_APPLICATION_CREDENTIALS')
26
+ # service account env.
27
+ ENV.delete('PRIVATE_KEY_VAR')
28
+ ENV.delete('CLIENT_EMAIL_VAR')
29
+ # authorized_user env.
30
+ ENV.delete('CLIENT_ID_VAR')
31
+ ENV.delete('CLIENT_SECRET_VAR')
32
+ ENV.delete('REFRESH_TOKEN_VAR')
33
+ # home var, which is used to find $HOME/.gcloud/...
34
+ ENV.delete('HOME')
35
+
36
+ setup_auth_stubs
37
+ @logs_sent = []
38
+ end
39
+
40
+ # generic attributes
41
+ HOSTNAME = Socket.gethostname
42
+
43
+ # attributes used for the GCE metadata service
44
+ PROJECT_ID = 'test-project-id'
45
+ ZONE = 'us-central1-b'
46
+ FULLY_QUALIFIED_ZONE = 'projects/' + PROJECT_ID + '/zones/' + ZONE
47
+ VM_ID = '9876543210'
48
+
49
+ # attributes used for custom (overridden) configs
50
+ CUSTOM_PROJECT_ID = 'test-custom-project-id'
51
+ CUSTOM_ZONE = 'us-custom-central1-b'
52
+ CUSTOM_FULLY_QUALIFIED_ZONE = 'projects/' + PROJECT_ID + '/zones/' + ZONE
53
+ CUSTOM_VM_ID = 'C9876543210'
54
+ CUSTOM_HOSTNAME = 'custom.hostname.org'
55
+
56
+ # attributes used for the EC2 metadata service
57
+ EC2_PROJECT_ID = 'test-ec2-project-id'
58
+ EC2_ZONE = 'us-west-2b'
59
+ EC2_PREFIXED_ZONE = 'aws:' + EC2_ZONE
60
+ EC2_VM_ID = 'i-81c16767'
61
+ EC2_ACCOUNT_ID = '123456789012'
62
+
63
+ # The formatting here matches the format used on the VM.
64
+ EC2_IDENTITY_DOCUMENT = %({
65
+ "accountId" : "#{EC2_ACCOUNT_ID}",
66
+ "availabilityZone" : "#{EC2_ZONE}",
67
+ "instanceId" : "#{EC2_VM_ID}"
68
+ })
69
+
70
+ # Managed VMs specific labels
71
+ MANAGED_VM_BACKEND_NAME = 'default'
72
+ MANAGED_VM_BACKEND_VERSION = 'guestbook2.0'
73
+
74
+ # Container Engine / Kubernetes specific labels
75
+ CONTAINER_CLUSTER_NAME = 'cluster-1'
76
+ CONTAINER_NAMESPACE_ID = '898268c8-4a36-11e5-9d81-42010af0194c'
77
+ CONTAINER_NAMESPACE_NAME = 'kube-system'
78
+ CONTAINER_POD_ID = 'cad3c3c4-4b9c-11e5-9d81-42010af0194c'
79
+ CONTAINER_POD_NAME = 'redis-master-c0l82.foo.bar'
80
+ CONTAINER_CONTAINER_NAME = 'redis'
81
+ CONTAINER_LABEL_KEY = 'component'
82
+ CONTAINER_LABEL_VALUE = 'redis-component'
83
+ CONTAINER_STREAM = 'stdout'
84
+ CONTAINER_SEVERITY = 'INFO'
85
+ # Timestamp for 1234567890 seconds and 987654321 nanoseconds since epoch
86
+ CONTAINER_TIMESTAMP = '2009-02-13T23:31:30.987654321Z'
87
+ CONTAINER_SECONDS_EPOCH = 1_234_567_890
88
+ CONTAINER_NANOS = 987_654_321
89
+
90
+ # Cloud Functions specific labels
91
+ CLOUDFUNCTIONS_FUNCTION_NAME = '$My_Function.Name-@1'
92
+ CLOUDFUNCTIONS_REGION = 'us-central1'
93
+ CLOUDFUNCTIONS_EXECUTION_ID = '123-0'
94
+ CLOUDFUNCTIONS_CLUSTER_NAME = 'cluster-1'
95
+ CLOUDFUNCTIONS_NAMESPACE_NAME = 'default'
96
+ CLOUDFUNCTIONS_POD_NAME = 'd.dc.myu.uc.functionp.pc.name-a.a1.987-c0l82'
97
+ CLOUDFUNCTIONS_CONTAINER_NAME = 'worker'
98
+
99
+ # Parameters used for authentication
100
+ AUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
101
+ FAKE_AUTH_TOKEN = 'abc123'
102
+
103
+ # Information about test credentials files.
104
+ # path: Path to the credentials file.
105
+ # project_id: ID of the project, which must correspond to the file contents.
106
+ IAM_CREDENTIALS = {
107
+ path: 'test/plugin/data/iam-credentials.json',
108
+ project_id: 'fluent-test-project'
109
+ }
110
+ LEGACY_CREDENTIALS = {
111
+ path: 'test/plugin/data/credentials.json',
112
+ project_id: '847859579879'
113
+ }
114
+ INVALID_CREDENTIALS = {
115
+ path: 'test/plugin/data/invalid_credentials.json',
116
+ project_id: ''
117
+ }
118
+
119
+ # Configuration files for various test scenarios
120
+ APPLICATION_DEFAULT_CONFIG = %(
121
+ )
122
+
123
+ # rubocop:disable Metrics/LineLength
124
+ PRIVATE_KEY_CONFIG = %(
125
+ auth_method private_key
126
+ private_key_email 271661262351-ft99kc9kjro9rrihq3k2n3s2inbplu0q@developer.gserviceaccount.com
127
+ private_key_path test/plugin/data/c31e573fd7f62ed495c9ca3821a5a85cb036dee1-privatekey.p12
128
+ )
129
+ # rubocop:enable Metrics/LineLength
130
+
131
+ REQUIRE_VALID_TAGS_CONFIG = %(
132
+ require_valid_tags true
133
+ )
134
+
135
+ NO_METADATA_SERVICE_CONFIG = %(
136
+ use_metadata_service false
137
+ )
138
+
139
+ NO_DETECT_SUBSERVICE_CONFIG = %(
140
+ detect_subservice false
141
+ )
142
+
143
+ CUSTOM_METADATA_CONFIG = %(
144
+ project_id #{CUSTOM_PROJECT_ID}
145
+ zone #{CUSTOM_ZONE}
146
+ vm_id #{CUSTOM_VM_ID}
147
+ vm_name #{CUSTOM_HOSTNAME}
148
+ )
149
+
150
+ CONFIG_MISSING_METADATA_PROJECT_ID = %(
151
+ zone #{CUSTOM_ZONE}
152
+ vm_id #{CUSTOM_VM_ID}
153
+ )
154
+ CONFIG_MISSING_METADATA_ZONE = %(
155
+ project_id #{CUSTOM_PROJECT_ID}
156
+ vm_id #{CUSTOM_VM_ID}
157
+ )
158
+ CONFIG_MISSING_METADATA_VM_ID = %(
159
+ project_id #{CUSTOM_PROJECT_ID}
160
+ zone #{CUSTOM_ZONE}
161
+ )
162
+ CONFIG_MISSING_METADATA_ALL = %(
163
+ )
164
+
165
+ CONFIG_EC2_PROJECT_ID = %(
166
+ project_id #{EC2_PROJECT_ID}
167
+ )
168
+
169
+ CONFIG_EC2_PROJECT_ID_AND_CUSTOM_VM_ID = %(
170
+ project_id #{EC2_PROJECT_ID}
171
+ vm_id #{CUSTOM_VM_ID}
172
+ )
173
+
174
+ # Service configurations for various services
175
+ COMPUTE_SERVICE_NAME = 'compute.googleapis.com'
176
+ APPENGINE_SERVICE_NAME = 'appengine.googleapis.com'
177
+ CONTAINER_SERVICE_NAME = 'container.googleapis.com'
178
+ CLOUDFUNCTIONS_SERVICE_NAME = 'cloudfunctions.googleapis.com'
179
+ EC2_SERVICE_NAME = 'ec2.amazonaws.com'
180
+
181
+ COMPUTE_PARAMS = {
182
+ service_name: COMPUTE_SERVICE_NAME,
183
+ log_name: 'test',
184
+ project_id: PROJECT_ID,
185
+ zone: ZONE,
186
+ labels: {
187
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
188
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
189
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
190
+ }
191
+ }
192
+
193
+ VMENGINE_PARAMS = {
194
+ service_name: APPENGINE_SERVICE_NAME,
195
+ log_name: "#{APPENGINE_SERVICE_NAME}%2Ftest",
196
+ project_id: PROJECT_ID,
197
+ zone: ZONE,
198
+ labels: {
199
+ "#{APPENGINE_SERVICE_NAME}/module_id" => MANAGED_VM_BACKEND_NAME,
200
+ "#{APPENGINE_SERVICE_NAME}/version_id" => MANAGED_VM_BACKEND_VERSION,
201
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
202
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
203
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
204
+ }
205
+ }
206
+
207
+ CONTAINER_TAG = "kubernetes.#{CONTAINER_POD_NAME}_" \
208
+ "#{CONTAINER_NAMESPACE_NAME}_#{CONTAINER_CONTAINER_NAME}"
209
+
210
+ CONTAINER_FROM_METADATA_PARAMS = {
211
+ service_name: CONTAINER_SERVICE_NAME,
212
+ log_name: CONTAINER_CONTAINER_NAME,
213
+ project_id: PROJECT_ID,
214
+ zone: ZONE,
215
+ labels: {
216
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
217
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CONTAINER_CLUSTER_NAME,
218
+ "#{CONTAINER_SERVICE_NAME}/namespace_name" => CONTAINER_NAMESPACE_NAME,
219
+ "#{CONTAINER_SERVICE_NAME}/namespace_id" => CONTAINER_NAMESPACE_ID,
220
+ "#{CONTAINER_SERVICE_NAME}/pod_name" => CONTAINER_POD_NAME,
221
+ "#{CONTAINER_SERVICE_NAME}/pod_id" => CONTAINER_POD_ID,
222
+ "#{CONTAINER_SERVICE_NAME}/container_name" => CONTAINER_CONTAINER_NAME,
223
+ "#{CONTAINER_SERVICE_NAME}/stream" => CONTAINER_STREAM,
224
+ "label/#{CONTAINER_LABEL_KEY}" => CONTAINER_LABEL_VALUE,
225
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
226
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
227
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
228
+ }
229
+ }
230
+
231
+ # Almost the same as from metadata, but missing namespace_id and pod_id.
232
+ CONTAINER_FROM_TAG_PARAMS = {
233
+ service_name: CONTAINER_SERVICE_NAME,
234
+ log_name: CONTAINER_CONTAINER_NAME,
235
+ project_id: PROJECT_ID,
236
+ zone: ZONE,
237
+ labels: {
238
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
239
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CONTAINER_CLUSTER_NAME,
240
+ "#{CONTAINER_SERVICE_NAME}/namespace_name" => CONTAINER_NAMESPACE_NAME,
241
+ "#{CONTAINER_SERVICE_NAME}/pod_name" => CONTAINER_POD_NAME,
242
+ "#{CONTAINER_SERVICE_NAME}/container_name" => CONTAINER_CONTAINER_NAME,
243
+ "#{CONTAINER_SERVICE_NAME}/stream" => CONTAINER_STREAM,
244
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
245
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
246
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
247
+ }
248
+ }
249
+
250
+ CLOUDFUNCTIONS_TAG = "kubernetes.#{CLOUDFUNCTIONS_POD_NAME}_" \
251
+ "#{CLOUDFUNCTIONS_NAMESPACE_NAME}_" \
252
+ "#{CLOUDFUNCTIONS_CONTAINER_NAME}"
253
+
254
+ CLOUDFUNCTIONS_PARAMS = {
255
+ service_name: CLOUDFUNCTIONS_SERVICE_NAME,
256
+ log_name: 'cloud-functions',
257
+ project_id: PROJECT_ID,
258
+ zone: ZONE,
259
+ labels: {
260
+ 'execution_id' => CLOUDFUNCTIONS_EXECUTION_ID,
261
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/function_name" =>
262
+ CLOUDFUNCTIONS_FUNCTION_NAME,
263
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/region" => CLOUDFUNCTIONS_REGION,
264
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
265
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CLOUDFUNCTIONS_CLUSTER_NAME,
266
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
267
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
268
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
269
+ }
270
+ }
271
+
272
+ CLOUDFUNCTIONS_TEXT_NOT_MATCHED_PARAMS = {
273
+ service_name: CLOUDFUNCTIONS_SERVICE_NAME,
274
+ log_name: 'cloud-functions',
275
+ project_id: PROJECT_ID,
276
+ zone: ZONE,
277
+ labels: {
278
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/function_name" =>
279
+ CLOUDFUNCTIONS_FUNCTION_NAME,
280
+ "#{CLOUDFUNCTIONS_SERVICE_NAME}/region" => CLOUDFUNCTIONS_REGION,
281
+ "#{CONTAINER_SERVICE_NAME}/instance_id" => VM_ID,
282
+ "#{CONTAINER_SERVICE_NAME}/cluster_name" => CLOUDFUNCTIONS_CLUSTER_NAME,
283
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
284
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => VM_ID,
285
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => HOSTNAME
286
+ }
287
+ }
288
+
289
+ CUSTOM_PARAMS = {
290
+ service_name: COMPUTE_SERVICE_NAME,
291
+ log_name: 'test',
292
+ project_id: CUSTOM_PROJECT_ID,
293
+ zone: CUSTOM_ZONE,
294
+ labels: {
295
+ "#{COMPUTE_SERVICE_NAME}/resource_type" => 'instance',
296
+ "#{COMPUTE_SERVICE_NAME}/resource_id" => CUSTOM_VM_ID,
297
+ "#{COMPUTE_SERVICE_NAME}/resource_name" => CUSTOM_HOSTNAME
298
+ }
299
+ }
300
+
301
+ EC2_PARAMS = {
302
+ service_name: EC2_SERVICE_NAME,
303
+ log_name: 'test',
304
+ project_id: EC2_PROJECT_ID,
305
+ zone: EC2_PREFIXED_ZONE,
306
+ labels: {
307
+ "#{EC2_SERVICE_NAME}/resource_type" => 'instance',
308
+ "#{EC2_SERVICE_NAME}/resource_id" => EC2_VM_ID,
309
+ "#{EC2_SERVICE_NAME}/account_id" => EC2_ACCOUNT_ID,
310
+ "#{EC2_SERVICE_NAME}/resource_name" => HOSTNAME
311
+ }
312
+ }
313
+
314
+ HTTP_REQUEST_MESSAGE = {
315
+ 'requestMethod' => 'POST',
316
+ 'requestUrl' => 'http://example/',
317
+ 'requestSize' => 210,
318
+ 'status' => 200,
319
+ 'responseSize' => 65,
320
+ 'userAgent' => 'USER AGENT 1.0',
321
+ 'remoteIp' => '55.55.55.55',
322
+ 'referer' => 'http://referer/',
323
+ 'cacheHit' => true,
324
+ 'cacheValidatedWithOriginServer' => true
325
+ }
326
+
327
+ # Tags and their sanitized and encoded version.
328
+ VALID_TAGS = {
329
+ 'test' => 'test',
330
+ 'germanß' => 'german%C3%9F',
331
+ 'chinese中' => 'chinese%E4%B8%AD',
332
+ 'specialCharacter/_-.' => 'specialCharacter%2F_-.',
333
+ 'abc@&^$*' => 'abc%40%26%5E%24%2A',
334
+ '@&^$*' => '%40%26%5E%24%2A'
335
+ }
336
+ INVALID_TAGS = {
337
+ # Non-string tags.
338
+ 123 => '123',
339
+ 1.23 => '1.23',
340
+ [1, 2, 3] => '%5B1%2C%202%2C%203%5D',
341
+ { key: 'value' } => '%7B%22key%22%3D%3E%22value%22%7D',
342
+ # Non-utf8 string tags.
343
+ "nonutf8#{[0x92].pack('C*')}" => 'nonutf8%20',
344
+ "abc#{[0x92].pack('C*')}" => 'abc%20',
345
+ "#{[0x92].pack('C*')}" => '%20',
346
+ # Empty string tag.
347
+ '' => '_'
348
+ }
349
+ ALL_TAGS = VALID_TAGS.merge(INVALID_TAGS)
350
+
351
+ # Shared tests.
352
+
353
+ def test_configure_service_account_application_default
354
+ setup_gce_metadata_stubs
355
+ d = create_driver
356
+ assert_equal HOSTNAME, d.instance.vm_name
357
+ end
358
+
359
+ def test_configure_service_account_private_key
360
+ # Using out-of-date config method.
361
+ exception_count = 0
362
+ begin
363
+ create_driver(PRIVATE_KEY_CONFIG)
364
+ rescue Fluent::ConfigError => error
365
+ assert error.message.include? 'Please remove configuration parameters'
366
+ exception_count += 1
367
+ end
368
+ assert_equal 1, exception_count
369
+ end
370
+
371
+ def test_configure_custom_metadata
372
+ setup_no_metadata_service_stubs
373
+ d = create_driver(CUSTOM_METADATA_CONFIG)
374
+ assert_equal CUSTOM_PROJECT_ID, d.instance.project_id
375
+ assert_equal CUSTOM_ZONE, d.instance.zone
376
+ assert_equal CUSTOM_VM_ID, d.instance.vm_id
377
+ end
378
+
379
+ def test_configure_invalid_metadata_missing_parts
380
+ setup_no_metadata_service_stubs
381
+ Fluent::GoogleCloudOutput::CredentialsInfo.stubs(:project_id).returns(nil)
382
+ { CONFIG_MISSING_METADATA_PROJECT_ID => ['project_id'],
383
+ CONFIG_MISSING_METADATA_ZONE => ['zone'],
384
+ CONFIG_MISSING_METADATA_VM_ID => ['vm_id'],
385
+ CONFIG_MISSING_METADATA_ALL => %w(project_id zone vm_id)
386
+ }.each_with_index do |(config, parts), index|
387
+ exception_count = 0
388
+ begin
389
+ create_driver(config)
390
+ rescue Fluent::ConfigError => error
391
+ assert error.message.include?('Unable to obtain metadata parameters:'),
392
+ "Index #{index} failed."
393
+ parts.each do |part|
394
+ assert error.message.include?(part), "Index #{index} failed."
395
+ end
396
+ exception_count += 1
397
+ end
398
+ assert_equal 1, exception_count, "Index #{index} failed."
399
+ end
400
+ end
401
+
402
+ def test_metadata_loading
403
+ setup_gce_metadata_stubs
404
+ d = create_driver
405
+ d.run
406
+ assert_equal PROJECT_ID, d.instance.project_id
407
+ assert_equal ZONE, d.instance.zone
408
+ assert_equal VM_ID, d.instance.vm_id
409
+ assert_equal false, d.instance.running_on_managed_vm
410
+ end
411
+
412
+ def test_managed_vm_metadata_loading
413
+ setup_gce_metadata_stubs
414
+ setup_managed_vm_metadata_stubs
415
+ d = create_driver
416
+ d.run
417
+ assert_equal PROJECT_ID, d.instance.project_id
418
+ assert_equal ZONE, d.instance.zone
419
+ assert_equal VM_ID, d.instance.vm_id
420
+ assert_equal true, d.instance.running_on_managed_vm
421
+ assert_equal MANAGED_VM_BACKEND_NAME, d.instance.gae_backend_name
422
+ assert_equal MANAGED_VM_BACKEND_VERSION, d.instance.gae_backend_version
423
+ end
424
+
425
+ def test_gce_metadata_does_not_load_when_use_metadata_service_is_false
426
+ Fluent::GoogleCloudOutput.any_instance.expects(:fetch_metadata).never
427
+ d = create_driver(NO_METADATA_SERVICE_CONFIG + CUSTOM_METADATA_CONFIG)
428
+ d.run
429
+ assert_equal CUSTOM_PROJECT_ID, d.instance.project_id
430
+ assert_equal CUSTOM_ZONE, d.instance.zone
431
+ assert_equal CUSTOM_VM_ID, d.instance.vm_id
432
+ assert_equal false, d.instance.running_on_managed_vm
433
+ end
434
+
435
+ def test_gce_used_when_detect_subservice_is_false
436
+ setup_gce_metadata_stubs
437
+ # This would cause the service to be container.googleapis.com if not for the
438
+ # detect_subservice=false config.
439
+ setup_container_metadata_stubs
440
+ d = create_driver(NO_DETECT_SUBSERVICE_CONFIG)
441
+ d.run
442
+ assert_equal COMPUTE_SERVICE_NAME, d.instance.service_name
443
+ end
444
+
445
+ def test_metadata_overrides
446
+ {
447
+ # In this case we are overriding all configured parameters so we should
448
+ # see all "custom" values rather than the ones from the metadata server.
449
+ CUSTOM_METADATA_CONFIG =>
450
+ ['gce', CUSTOM_PROJECT_ID, CUSTOM_ZONE, CUSTOM_VM_ID],
451
+ # Similar to above, but we are not overriding project_id in this config so
452
+ # we should see the metadata value for project_id and "custom" otherwise.
453
+ CONFIG_MISSING_METADATA_PROJECT_ID =>
454
+ ['gce', PROJECT_ID, CUSTOM_ZONE, CUSTOM_VM_ID],
455
+ CONFIG_EC2_PROJECT_ID =>
456
+ ['ec2', EC2_PROJECT_ID, EC2_PREFIXED_ZONE, EC2_VM_ID],
457
+ CONFIG_EC2_PROJECT_ID_AND_CUSTOM_VM_ID =>
458
+ ['ec2', EC2_PROJECT_ID, EC2_PREFIXED_ZONE, CUSTOM_VM_ID]
459
+ }.each_with_index do |(config, parts), index|
460
+ send("setup_#{parts[0]}_metadata_stubs")
461
+ d = create_driver(config)
462
+ d.run
463
+ assert_equal parts[1], d.instance.project_id, "Index #{index} failed."
464
+ assert_equal parts[2], d.instance.zone, "Index #{index} failed."
465
+ assert_equal parts[3], d.instance.vm_id, "Index #{index} failed."
466
+ assert_equal false, d.instance.running_on_managed_vm,
467
+ "Index #{index} failed."
468
+ end
469
+ end
470
+
471
+ def test_ec2_metadata_requires_project_id
472
+ setup_ec2_metadata_stubs
473
+ exception_count = 0
474
+ Fluent::GoogleCloudOutput::CredentialsInfo.stubs(:project_id).returns(nil)
475
+ begin
476
+ create_driver
477
+ rescue Fluent::ConfigError => error
478
+ assert error.message.include? 'Unable to obtain metadata parameters:'
479
+ assert error.message.include? 'project_id'
480
+ exception_count += 1
481
+ end
482
+ assert_equal 1, exception_count
483
+ end
484
+
485
+ def test_ec2_metadata_project_id_from_credentials
486
+ setup_ec2_metadata_stubs
487
+ [IAM_CREDENTIALS, LEGACY_CREDENTIALS].each do |creds|
488
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = creds[:path]
489
+ d = create_driver
490
+ d.run
491
+ assert_equal creds[:project_id], d.instance.project_id
492
+ end
493
+ end
494
+
495
+ def test_one_log
496
+ setup_gce_metadata_stubs
497
+ setup_logging_stubs do
498
+ d = create_driver
499
+ d.emit('message' => log_entry(0))
500
+ d.run
501
+ end
502
+ verify_log_entries(1, COMPUTE_PARAMS)
503
+ end
504
+
505
+ def test_one_log_with_json_credentials
506
+ setup_gce_metadata_stubs
507
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
508
+ setup_logging_stubs do
509
+ d = create_driver
510
+ d.emit('message' => log_entry(0))
511
+ d.run
512
+ end
513
+ verify_log_entries(1, COMPUTE_PARAMS)
514
+ end
515
+
516
+ def test_one_log_with_invalid_json_credentials
517
+ setup_gce_metadata_stubs
518
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = INVALID_CREDENTIALS[:path]
519
+ setup_logging_stubs do
520
+ d = create_driver
521
+ d.emit('message' => log_entry(0))
522
+ exception_count = 0
523
+ begin
524
+ d.run
525
+ rescue RuntimeError => error
526
+ assert error.message.include? 'Unable to read the credential file'
527
+ exception_count += 1
528
+ end
529
+ assert_equal 1, exception_count
530
+ end
531
+ end
532
+
533
+ def test_one_log_custom_metadata
534
+ # don't set up any metadata stubs, so the test will fail if we try to
535
+ # fetch metadata (and explicitly check this as well).
536
+ Fluent::GoogleCloudOutput.any_instance.expects(:fetch_metadata).never
537
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
538
+ setup_logging_stubs do
539
+ d = create_driver(NO_METADATA_SERVICE_CONFIG + CUSTOM_METADATA_CONFIG)
540
+ d.emit('message' => log_entry(0))
541
+ d.run
542
+ end
543
+ verify_log_entries(1, CUSTOM_PARAMS)
544
+ end
545
+
546
+ def test_one_log_ec2
547
+ ENV['GOOGLE_APPLICATION_CREDENTIALS'] = IAM_CREDENTIALS[:path]
548
+ setup_ec2_metadata_stubs
549
+ setup_logging_stubs do
550
+ d = create_driver(CONFIG_EC2_PROJECT_ID)
551
+ d.emit('message' => log_entry(0))
552
+ d.run
553
+ end
554
+ verify_log_entries(1, EC2_PARAMS)
555
+ end
556
+
557
+ def test_struct_payload_log
558
+ setup_gce_metadata_stubs
559
+ setup_logging_stubs do
560
+ d = create_driver
561
+ d.emit('msg' => log_entry(0), 'tag2' => 'test', 'data' => 5000,
562
+ 'some_null_field' => nil)
563
+ d.run
564
+ end
565
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
566
+ fields = get_fields(entry['structPayload'])
567
+ assert_equal 4, fields.size, entry
568
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
569
+ assert_equal 'test', get_string(fields['tag2']), entry
570
+ assert_equal 5000, get_number(fields['data']), entry
571
+ assert_equal null_value, fields['some_null_field'], entry
572
+ end
573
+ end
574
+
575
+ def test_struct_payload_malformatted_log
576
+ setup_gce_metadata_stubs
577
+ message = 'test message'
578
+ setup_logging_stubs do
579
+ d = create_driver
580
+ d.emit(
581
+ 'int_key' => { 1 => message },
582
+ 'int_array_key' => { [1, 2, 3, 4] => message },
583
+ 'string_array_key' => { %w(a b c) => message },
584
+ 'hash_key' => { { 'some_key' => 'some_value' } => message },
585
+ 'mixed_key' => { { 'some_key' => %w(a b c) } => message },
586
+ 'symbol_key' => { some_symbol: message },
587
+ 'nil_key' => { nil => message }
588
+ )
589
+ d.run
590
+ end
591
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
592
+ fields = get_fields(entry['structPayload'])
593
+ assert_equal 7, fields.size, entry
594
+ assert_equal message, get_string(get_fields(get_struct(fields \
595
+ ['int_key']))['1']), entry
596
+ assert_equal message, get_string(get_fields(get_struct(fields \
597
+ ['int_array_key']))['[1, 2, 3, 4]']), entry
598
+ assert_equal message, get_string(get_fields(get_struct(fields \
599
+ ['string_array_key']))['["a", "b", "c"]']), entry
600
+ assert_equal message, get_string(get_fields(get_struct(fields \
601
+ ['hash_key']))['{"some_key"=>"some_value"}']), entry
602
+ assert_equal message, get_string(get_fields(get_struct(fields \
603
+ ['mixed_key']))['{"some_key"=>["a", "b", "c"]}']), entry
604
+ assert_equal message, get_string(get_fields(get_struct(fields \
605
+ ['symbol_key']))['some_symbol']), entry
606
+ assert_equal message, get_string(get_fields(get_struct(fields \
607
+ ['nil_key']))['']), entry
608
+ end
609
+ end
610
+
611
+ def test_struct_payload_json_log
612
+ setup_gce_metadata_stubs
613
+ setup_logging_stubs do
614
+ d = create_driver
615
+ json_string = '{"msg": "test log entry 0", "tag2": "test", "data": 5000}'
616
+ d.emit('message' => 'notJSON ' + json_string)
617
+ d.emit('message' => json_string)
618
+ d.emit('message' => "\t" + json_string)
619
+ d.emit('message' => ' ' + json_string)
620
+ d.run
621
+ end
622
+ verify_log_entries(4, COMPUTE_PARAMS, '') do |entry|
623
+ assert entry.key?('textPayload'), 'Entry did not have textPayload'
624
+ end
625
+ end
626
+
627
+ def test_struct_payload_json_container_log
628
+ setup_gce_metadata_stubs
629
+ setup_container_metadata_stubs
630
+ setup_logging_stubs do
631
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
632
+ json_string = '{"msg": "test log entry 0", "tag2": "test", ' \
633
+ '"data": 5000, "some_null_field": null}'
634
+ d.emit(container_log_entry_with_metadata('notJSON' + json_string))
635
+ d.emit(container_log_entry_with_metadata(json_string))
636
+ d.emit(container_log_entry_with_metadata(" \r\n \t" + json_string))
637
+ d.run
638
+ end
639
+ log_index = 0
640
+ verify_log_entries(
641
+ 3, CONTAINER_FROM_METADATA_PARAMS, '') do |entry|
642
+ log_index += 1
643
+ if log_index == 1
644
+ assert entry.key?('textPayload'), 'Entry did not have textPayload'
645
+ else
646
+ assert entry.key?('structPayload'), 'Entry did not have structPayload'
647
+ fields = get_fields(entry['structPayload'])
648
+ assert_equal 4, fields.size, entry
649
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
650
+ assert_equal 'test', get_string(fields['tag2']), entry
651
+ assert_equal 5000, get_number(fields['data']), entry
652
+ assert_equal null_value, fields['some_null_field'], entry
653
+ end
654
+ end
655
+ end
656
+
657
+ # Verify that we drop the log entries when 'require_valid_tags' is true and
658
+ # any non-string tags or tags with non-utf8 characters are detected.
659
+ def test_reject_invalid_tags_with_require_valid_tags_true
660
+ setup_gce_metadata_stubs
661
+ INVALID_TAGS.keys.each do |tag|
662
+ setup_logging_stubs do
663
+ @logs_sent = []
664
+ d = create_driver(REQUIRE_VALID_TAGS_CONFIG, tag)
665
+ d.emit('msg' => log_entry(0))
666
+ d.run
667
+ end
668
+ verify_log_entries(0, COMPUTE_PARAMS, 'structPayload')
669
+ end
670
+ end
671
+
672
+ # Verify that empty string container name should fail the kubernetes regex
673
+ # match, thus the original tag is used as the log name.
674
+ def test_handle_empty_container_name
675
+ setup_gce_metadata_stubs
676
+ setup_container_metadata_stubs
677
+ container_name = ''
678
+ # This tag will not match the kubernetes regex because it requires a
679
+ # non-empty container name.
680
+ tag = container_tag_with_container_name(container_name)
681
+ params = CONTAINER_FROM_METADATA_PARAMS.merge(
682
+ labels: CONTAINER_FROM_METADATA_PARAMS[:labels].merge(
683
+ "#{CONTAINER_SERVICE_NAME}/container_name" => container_name),
684
+ log_name: tag)
685
+ setup_logging_stubs([params]) do
686
+ @logs_sent = []
687
+ d = create_driver(REQUIRE_VALID_TAGS_CONFIG, tag)
688
+ d.emit(container_log_entry_with_metadata(log_entry(0), container_name))
689
+ d.run
690
+ end
691
+ verify_log_entries(1, params, 'textPayload')
692
+ assert_equal "projects/#{PROJECT_ID}/logs/#{tag}", @logs_sent[0]['logName']
693
+ end
694
+
695
+ # Verify that container names with non-utf8 characters should be rejected when
696
+ # 'require_valid_tags' is true.
697
+ def test_reject_non_utf8_container_name_with_require_valid_tags_true
698
+ setup_gce_metadata_stubs
699
+ setup_container_metadata_stubs
700
+ non_utf8_tags = INVALID_TAGS.select do |tag, _|
701
+ tag.is_a?(String) && !tag.empty?
702
+ end
703
+ non_utf8_tags.each do |container_name, encoded_name|
704
+ params = CONTAINER_FROM_METADATA_PARAMS.merge(
705
+ labels: CONTAINER_FROM_METADATA_PARAMS[:labels].merge(
706
+ "#{CONTAINER_SERVICE_NAME}/container_name" =>
707
+ URI.decode(encoded_name)),
708
+ log_name: encoded_name)
709
+ setup_logging_stubs([params]) do
710
+ @logs_sent = []
711
+ d = create_driver(REQUIRE_VALID_TAGS_CONFIG,
712
+ container_tag_with_container_name(container_name))
713
+ d.emit(container_log_entry_with_metadata(log_entry(0), container_name))
714
+ d.run
715
+ end
716
+ verify_log_entries(0, params, 'textPayload')
717
+ end
718
+ end
719
+
720
+ # Verify that tags are properly encoded. When 'require_valid_tags' is true, we
721
+ # only accept string tags with utf8 characters.
722
+ def test_encode_tags_with_require_valid_tags_true
723
+ setup_gce_metadata_stubs
724
+ VALID_TAGS.each do |tag, encoded_tag|
725
+ setup_logging_stubs([COMPUTE_PARAMS.merge(log_name: encoded_tag)]) do
726
+ @logs_sent = []
727
+ d = create_driver(REQUIRE_VALID_TAGS_CONFIG, tag)
728
+ d.emit('msg' => log_entry(0))
729
+ d.run
730
+ end
731
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload')
732
+ assert_equal "projects/#{PROJECT_ID}/logs/#{encoded_tag}",
733
+ @logs_sent[0]['logName']
734
+ end
735
+ end
736
+
737
+ # Verify that tags extracted from container names are properly encoded.
738
+ def test_encode_tags_from_container_name_with_require_valid_tags_true
739
+ setup_gce_metadata_stubs
740
+ setup_container_metadata_stubs
741
+ VALID_TAGS.each do |tag, encoded_tag|
742
+ params = CONTAINER_FROM_METADATA_PARAMS.merge(
743
+ labels: CONTAINER_FROM_METADATA_PARAMS[:labels].merge(
744
+ "#{CONTAINER_SERVICE_NAME}/container_name" => tag),
745
+ log_name: encoded_tag)
746
+ setup_logging_stubs([params]) do
747
+ @logs_sent = []
748
+ d = create_driver(REQUIRE_VALID_TAGS_CONFIG,
749
+ container_tag_with_container_name(tag))
750
+ d.emit(container_log_entry_with_metadata(log_entry(0), tag))
751
+ d.run
752
+ end
753
+ verify_log_entries(1, params, 'textPayload')
754
+ assert_equal "projects/#{PROJECT_ID}/logs/#{encoded_tag}",
755
+ @logs_sent[0]['logName']
756
+ end
757
+ end
758
+
759
+ # Verify that tags are properly encoded and sanitized. When
760
+ # 'require_valid_tags' is false, we try to convert any non-string tags to
761
+ # strings, and replace non-utf8 characters with a replacement string.
762
+ def test_sanitize_tags_with_require_valid_tags_false
763
+ setup_gce_metadata_stubs
764
+ ALL_TAGS.each do |tag, sanitized_tag|
765
+ setup_logging_stubs([COMPUTE_PARAMS.merge(log_name: sanitized_tag)]) do
766
+ @logs_sent = []
767
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, tag)
768
+ d.emit('msg' => log_entry(0))
769
+ d.run
770
+ end
771
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload')
772
+ assert_equal "projects/#{PROJECT_ID}/logs/#{sanitized_tag}",
773
+ @logs_sent[0]['logName']
774
+ end
775
+ end
776
+
777
+ # Verify that tags extracted from container names are properly encoded and
778
+ # sanitized.
779
+ def test_sanitize_tags_from_container_name_with_require_valid_tags_false
780
+ setup_gce_metadata_stubs
781
+ setup_container_metadata_stubs
782
+ # Log names are derived from container names for containers. And container
783
+ # names are extracted from the tag based on a regex match pattern. As a
784
+ # prerequisite, the tag should already be a string, thus we only test
785
+ # non-empty string cases here.
786
+ string_tags = ALL_TAGS.select { |tag, _| tag.is_a?(String) && !tag.empty? }
787
+ string_tags.each do |container_name, encoded_container_name|
788
+ # Container name in the label is sanitized but not encoded, while the log
789
+ # name is encoded.
790
+ params = CONTAINER_FROM_METADATA_PARAMS.merge(
791
+ labels: CONTAINER_FROM_METADATA_PARAMS[:labels].merge(
792
+ "#{CONTAINER_SERVICE_NAME}/container_name" =>
793
+ URI.decode(encoded_container_name)),
794
+ log_name: encoded_container_name)
795
+ setup_logging_stubs([params]) do
796
+ @logs_sent = []
797
+ d = create_driver(APPLICATION_DEFAULT_CONFIG,
798
+ container_tag_with_container_name(container_name))
799
+ d.emit(container_log_entry_with_metadata(log_entry(0), container_name))
800
+ d.run
801
+ end
802
+ verify_log_entries(1, params, 'textPayload')
803
+ assert_equal "projects/#{PROJECT_ID}/logs/#{encoded_container_name}",
804
+ @logs_sent[0]['logName']
805
+ end
806
+ end
807
+
808
+ def test_timestamps
809
+ setup_gce_metadata_stubs
810
+ expected_ts = []
811
+ emit_index = 0
812
+ setup_logging_stubs do
813
+ [Time.at(123_456.789), Time.at(0), Time.now].each do |ts|
814
+ d = create_driver
815
+ # Test the "native" fluentd timestamp as well as our nanosecond tags.
816
+ d.emit({ 'message' => log_entry(emit_index) }, ts.to_f)
817
+ expected_ts.push(ts)
818
+ emit_index += 1
819
+ d.emit('message' => log_entry(emit_index),
820
+ 'timeNanos' => ts.tv_sec * 1_000_000_000 + ts.tv_nsec)
821
+ expected_ts.push(ts)
822
+ emit_index += 1
823
+ d.emit('message' => log_entry(emit_index),
824
+ 'timestamp' => { 'seconds' => ts.tv_sec, 'nanos' => ts.tv_nsec })
825
+ expected_ts.push(ts)
826
+ emit_index += 1
827
+ d.emit('message' => log_entry(emit_index),
828
+ 'timestampSeconds' => ts.tv_sec, 'timestampNanos' => ts.tv_nsec)
829
+ expected_ts.push(ts)
830
+ emit_index += 1
831
+ d.run
832
+ end
833
+ end
834
+ verify_index = 0
835
+ verify_log_entries(emit_index, COMPUTE_PARAMS) do |entry|
836
+ assert_equal_with_default entry['metadata']['timestamp']['seconds'],
837
+ expected_ts[verify_index].tv_sec, 0, entry
838
+ assert_equal_with_default entry['metadata']['timestamp']['nanos'],
839
+ expected_ts[verify_index].tv_nsec, 0, entry do
840
+ # Fluentd v0.14 onwards supports nanosecond timestamp values.
841
+ # Added in 600 ns delta to avoid flaky tests introduced
842
+ # due to rounding error in double-precision floating-point numbers
843
+ # (to account for the missing 9 bits of precision ~ 512 ns).
844
+ # See http://wikipedia.org/wiki/Double-precision_floating-point_format
845
+ assert_in_delta expected_ts[verify_index].tv_nsec,
846
+ entry['metadata']['timestamp']['nanos'], 600, entry
847
+ end
848
+ verify_index += 1
849
+ end
850
+ end
851
+
852
+ def test_malformed_timestamp
853
+ setup_gce_metadata_stubs
854
+ setup_logging_stubs do
855
+ d = create_driver
856
+ # if timestamp is not a hash it is passed through to the struct payload.
857
+ d.emit('message' => log_entry(0), 'timestamp' => 'not-a-hash')
858
+ d.run
859
+ end
860
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
861
+ fields = get_fields(entry['structPayload'])
862
+ assert_equal 2, fields.size, entry
863
+ assert_equal 'not-a-hash', get_string(fields['timestamp']), entry
864
+ end
865
+ end
866
+
867
+ # Make parse_severity public so we can test it.
868
+ class Fluent::GoogleCloudOutput # rubocop:disable Style/ClassAndModuleChildren
869
+ public :parse_severity
870
+ end
871
+
872
+ def test_label_map_without_field_present
873
+ setup_gce_metadata_stubs
874
+ setup_logging_stubs do
875
+ config = %(label_map { "label_field": "sent_label" })
876
+ d = create_driver(config)
877
+ d.emit('message' => log_entry(0))
878
+ d.run
879
+ # No additional labels should be present
880
+ end
881
+ verify_log_entries(1, COMPUTE_PARAMS)
882
+ end
883
+
884
+ def test_label_map_with_field_present
885
+ setup_gce_metadata_stubs
886
+ setup_logging_stubs do
887
+ config = %(label_map { "label_field": "sent_label" })
888
+ d = create_driver(config)
889
+ d.emit('message' => log_entry(0), 'label_field' => 'label_value')
890
+ d.run
891
+ end
892
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
893
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
894
+ params[:labels]['sent_label'] = 'label_value'
895
+ verify_log_entries(1, params)
896
+ end
897
+
898
+ def test_label_map_with_numeric_field
899
+ setup_gce_metadata_stubs
900
+ setup_logging_stubs do
901
+ config = %(label_map { "label_field": "sent_label" })
902
+ d = create_driver(config)
903
+ d.emit('message' => log_entry(0), 'label_field' => 123_456_789)
904
+ d.run
905
+ end
906
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
907
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
908
+ params[:labels]['sent_label'] = '123456789'
909
+ verify_log_entries(1, params)
910
+ end
911
+
912
+ def test_label_map_with_hash_field
913
+ setup_gce_metadata_stubs
914
+ setup_logging_stubs do
915
+ config = %(label_map { "label_field": "sent_label" })
916
+ d = create_driver(config)
917
+ # I'm not sure this actually makes sense for a user to do, but make
918
+ # sure that it works if they try it.
919
+ d.emit('message' => log_entry(0),
920
+ 'label_field' => { 'k1' => 10, 'k2' => 'val' })
921
+ d.run
922
+ end
923
+ # make a deep copy of COMPUTE_PARAMS and add the parsed label.
924
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
925
+ params[:labels]['sent_label'] = '{"k1"=>10, "k2"=>"val"}'
926
+ verify_log_entries(1, params)
927
+ end
928
+
929
+ def test_label_map_with_multiple_fields
930
+ setup_gce_metadata_stubs
931
+ setup_logging_stubs do
932
+ config = %(
933
+ label_map {
934
+ "label1": "sent_label_1",
935
+ "label_number_two": "foo.googleapis.com/bar",
936
+ "label3": "label3"
937
+ }
938
+ )
939
+ d = create_driver(config)
940
+ # not_a_label passes through to the struct payload
941
+ d.emit('message' => log_entry(0),
942
+ 'label1' => 'value1',
943
+ 'label_number_two' => 'value2',
944
+ 'not_a_label' => 'value4',
945
+ 'label3' => 'value3')
946
+ d.run
947
+ end
948
+ # make a deep copy of COMPUTE_PARAMS and add the parsed labels.
949
+ params = Marshal.load(Marshal.dump(COMPUTE_PARAMS))
950
+ params[:labels]['sent_label_1'] = 'value1'
951
+ params[:labels]['foo.googleapis.com/bar'] = 'value2'
952
+ params[:labels]['label3'] = 'value3'
953
+ verify_log_entries(1, params, 'structPayload') do |entry|
954
+ fields = get_fields(entry['structPayload'])
955
+ assert_equal 2, fields.size, entry
956
+ assert_equal 'test log entry 0', get_string(fields['message']), entry
957
+ assert_equal 'value4', get_string(fields['not_a_label']), entry
958
+ end
959
+ end
960
+
961
+ def test_multiple_logs
962
+ setup_gce_metadata_stubs
963
+ # Only test a few values because otherwise the test can take minutes.
964
+ [2, 3, 5, 11, 50].each do |n|
965
+ setup_logging_stubs do
966
+ d = create_driver
967
+ # The test driver doesn't clear its buffer of entries after running, so
968
+ # do it manually here.
969
+ d.instance_variable_get('@entries').clear
970
+ @logs_sent = []
971
+ n.times { |i| d.emit('message' => log_entry(i)) }
972
+ d.run
973
+ end
974
+ verify_log_entries(n, COMPUTE_PARAMS)
975
+ end
976
+ end
977
+
978
+ def test_malformed_log
979
+ setup_gce_metadata_stubs
980
+ setup_logging_stubs do
981
+ d = create_driver
982
+ # if the entry is not a hash, the plugin should silently drop it.
983
+ d.emit('a string is not a valid message')
984
+ d.run
985
+ end
986
+ assert @logs_sent.empty?
987
+ end
988
+
989
+ def test_one_managed_vm_log
990
+ setup_gce_metadata_stubs
991
+ setup_managed_vm_metadata_stubs
992
+ setup_logging_stubs do
993
+ d = create_driver
994
+ d.emit('message' => log_entry(0))
995
+ d.run
996
+ end
997
+ verify_log_entries(1, VMENGINE_PARAMS)
998
+ end
999
+
1000
+ def test_multiple_managed_vm_logs
1001
+ setup_gce_metadata_stubs
1002
+ setup_managed_vm_metadata_stubs
1003
+ [2, 3, 5, 11, 50].each do |n|
1004
+ setup_logging_stubs do
1005
+ d = create_driver
1006
+ # The test driver doesn't clear its buffer of entries after running, so
1007
+ # do it manually here.
1008
+ d.instance_variable_get('@entries').clear
1009
+ @logs_sent = []
1010
+ n.times { |i| d.emit('message' => log_entry(i)) }
1011
+ d.run
1012
+ end
1013
+ verify_log_entries(n, VMENGINE_PARAMS)
1014
+ end
1015
+ end
1016
+
1017
+ def test_one_container_log_metadata_from_plugin
1018
+ setup_gce_metadata_stubs
1019
+ setup_container_metadata_stubs
1020
+ setup_logging_stubs do
1021
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1022
+ d.emit(container_log_entry_with_metadata(log_entry(0)))
1023
+ d.run
1024
+ end
1025
+ verify_log_entries(1, CONTAINER_FROM_METADATA_PARAMS) do |entry|
1026
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1027
+ entry['metadata']['timestamp']['seconds'], entry
1028
+ assert_equal CONTAINER_NANOS, \
1029
+ entry['metadata']['timestamp']['nanos'], entry
1030
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
1031
+ end
1032
+ end
1033
+
1034
+ def test_multiple_container_logs_metadata_from_plugin
1035
+ setup_gce_metadata_stubs
1036
+ setup_container_metadata_stubs
1037
+ [2, 3, 5, 11, 50].each do |n|
1038
+ @logs_sent = []
1039
+ setup_logging_stubs do
1040
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1041
+ # The test driver doesn't clear its buffer of entries after running, so
1042
+ # do it manually here.
1043
+ d.instance_variable_get('@entries').clear
1044
+ n.times { |i| d.emit(container_log_entry_with_metadata(log_entry(i))) }
1045
+ d.run
1046
+ end
1047
+ verify_log_entries(n, CONTAINER_FROM_METADATA_PARAMS) do |entry|
1048
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1049
+ entry['metadata']['timestamp']['seconds'], entry
1050
+ assert_equal CONTAINER_NANOS, \
1051
+ entry['metadata']['timestamp']['nanos'], entry
1052
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ def test_multiple_container_logs_metadata_from_tag
1058
+ setup_gce_metadata_stubs
1059
+ setup_container_metadata_stubs
1060
+ [2, 3, 5, 11, 50].each do |n|
1061
+ @logs_sent = []
1062
+ setup_logging_stubs do
1063
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1064
+ # The test driver doesn't clear its buffer of entries after running, so
1065
+ # do it manually here.
1066
+ d.instance_variable_get('@entries').clear
1067
+ n.times { |i| d.emit(container_log_entry(log_entry(i))) }
1068
+ d.run
1069
+ end
1070
+ verify_log_entries(n, CONTAINER_FROM_TAG_PARAMS) do |entry|
1071
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1072
+ entry['metadata']['timestamp']['seconds'], entry
1073
+ assert_equal CONTAINER_NANOS, \
1074
+ entry['metadata']['timestamp']['nanos'], entry
1075
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
1076
+ end
1077
+ end
1078
+ end
1079
+
1080
+ def test_one_container_log_metadata_from_tag
1081
+ setup_gce_metadata_stubs
1082
+ setup_container_metadata_stubs
1083
+ setup_logging_stubs do
1084
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1085
+ d.emit(container_log_entry(log_entry(0)))
1086
+ d.run
1087
+ end
1088
+ verify_log_entries(1, CONTAINER_FROM_TAG_PARAMS) do |entry|
1089
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1090
+ entry['metadata']['timestamp']['seconds'], entry
1091
+ assert_equal CONTAINER_NANOS, \
1092
+ entry['metadata']['timestamp']['nanos'], entry
1093
+ assert_equal CONTAINER_SEVERITY, entry['metadata']['severity'], entry
1094
+ end
1095
+ end
1096
+
1097
+ def test_one_container_log_from_tag_stderr
1098
+ setup_gce_metadata_stubs
1099
+ setup_container_metadata_stubs
1100
+ setup_logging_stubs do
1101
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1102
+ d.emit(container_log_entry(log_entry(0), 'stderr'))
1103
+ d.run
1104
+ end
1105
+ expected_params = CONTAINER_FROM_TAG_PARAMS.merge(
1106
+ labels: { "#{CONTAINER_SERVICE_NAME}/stream" => 'stderr' }
1107
+ ) { |_, oldval, newval| oldval.merge(newval) }
1108
+ verify_log_entries(1, expected_params) do |entry|
1109
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1110
+ entry['metadata']['timestamp']['seconds'], entry
1111
+ assert_equal CONTAINER_NANOS, \
1112
+ entry['metadata']['timestamp']['nanos'], entry
1113
+ assert_equal 'ERROR', entry['metadata']['severity'], entry
1114
+ end
1115
+ end
1116
+
1117
+ def test_struct_container_log_metadata_from_plugin
1118
+ setup_gce_metadata_stubs
1119
+ setup_container_metadata_stubs
1120
+ setup_logging_stubs do
1121
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1122
+ d.emit(container_log_entry_with_metadata('{"msg": "test log entry 0", ' \
1123
+ '"tag2": "test", "data": ' \
1124
+ '5000, "severity": "WARNING"}'))
1125
+ d.run
1126
+ end
1127
+ verify_log_entries(1, CONTAINER_FROM_METADATA_PARAMS,
1128
+ 'structPayload') do |entry|
1129
+ fields = get_fields(entry['structPayload'])
1130
+ assert_equal 3, fields.size, entry
1131
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
1132
+ assert_equal 'test', get_string(fields['tag2']), entry
1133
+ assert_equal 5000, get_number(fields['data']), entry
1134
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1135
+ entry['metadata']['timestamp']['seconds'], entry
1136
+ assert_equal CONTAINER_NANOS, \
1137
+ entry['metadata']['timestamp']['nanos'], entry
1138
+ assert_equal 'WARNING', entry['metadata']['severity'], entry
1139
+ end
1140
+ end
1141
+
1142
+ def test_struct_container_log_metadata_from_tag
1143
+ setup_gce_metadata_stubs
1144
+ setup_container_metadata_stubs
1145
+ setup_logging_stubs do
1146
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1147
+ d.emit(container_log_entry('{"msg": "test log entry 0", ' \
1148
+ '"tag2": "test", "data": 5000, ' \
1149
+ '"severity": "W"}'))
1150
+ d.run
1151
+ end
1152
+ verify_log_entries(1, CONTAINER_FROM_TAG_PARAMS,
1153
+ 'structPayload') do |entry|
1154
+ fields = get_fields(entry['structPayload'])
1155
+ assert_equal 3, fields.size, entry
1156
+ assert_equal 'test log entry 0', get_string(fields['msg']), entry
1157
+ assert_equal 'test', get_string(fields['tag2']), entry
1158
+ assert_equal 5000, get_number(fields['data']), entry
1159
+ assert_equal CONTAINER_SECONDS_EPOCH, \
1160
+ entry['metadata']['timestamp']['seconds'], entry
1161
+ assert_equal CONTAINER_NANOS, \
1162
+ entry['metadata']['timestamp']['nanos'], entry
1163
+ assert_equal 'WARNING', entry['metadata']['severity'], entry
1164
+ end
1165
+ end
1166
+
1167
+ def test_cloudfunctions_log
1168
+ setup_gce_metadata_stubs
1169
+ setup_cloudfunctions_metadata_stubs
1170
+ [1, 2, 3, 5, 11, 50].each do |n|
1171
+ setup_logging_stubs do
1172
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CLOUDFUNCTIONS_TAG)
1173
+ # The test driver doesn't clear its buffer of entries after running, so
1174
+ # do it manually here.
1175
+ d.instance_variable_get('@entries').clear
1176
+ @logs_sent = []
1177
+ n.times { |i| d.emit(cloudfunctions_log_entry(i)) }
1178
+ d.run
1179
+ end
1180
+ verify_log_entries(n, CLOUDFUNCTIONS_PARAMS) do |entry|
1181
+ assert_equal 'DEBUG', entry['metadata']['severity'],
1182
+ "Test with #{n} logs failed. \n#{entry}"
1183
+ end
1184
+ end
1185
+ end
1186
+
1187
+ def test_cloudfunctions_logs_text_not_matched
1188
+ setup_gce_metadata_stubs
1189
+ setup_cloudfunctions_metadata_stubs
1190
+ [1, 2, 3, 5, 11, 50].each do |n|
1191
+ @logs_sent = []
1192
+ setup_logging_stubs do
1193
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CLOUDFUNCTIONS_TAG)
1194
+ # The test driver doesn't clear its buffer of entries after running, so
1195
+ # do it manually here.
1196
+ d.instance_variable_get('@entries').clear
1197
+ n.times { |i| d.emit(cloudfunctions_log_entry_text_not_matched(i)) }
1198
+ d.run
1199
+ end
1200
+ verify_log_entries(
1201
+ n, CLOUDFUNCTIONS_TEXT_NOT_MATCHED_PARAMS) do |entry|
1202
+ assert_equal 'INFO', entry['metadata']['severity'],
1203
+ "Test with #{n} logs failed. \n#{entry}"
1204
+ end
1205
+ end
1206
+ end
1207
+
1208
+ def test_multiple_cloudfunctions_logs_tag_not_matched
1209
+ setup_gce_metadata_stubs
1210
+ setup_cloudfunctions_metadata_stubs
1211
+ [1, 2, 3, 5, 11, 50].each do |n|
1212
+ @logs_sent = []
1213
+ setup_logging_stubs do
1214
+ d = create_driver(APPLICATION_DEFAULT_CONFIG, CONTAINER_TAG)
1215
+ # The test driver doesn't clear its buffer of entries after running, so
1216
+ # do it manually here.
1217
+ d.instance_variable_get('@entries').clear
1218
+ n.times { |i| d.emit(cloudfunctions_log_entry(i)) }
1219
+ d.run
1220
+ end
1221
+ i = 0
1222
+ verify_log_entries(n, CONTAINER_FROM_TAG_PARAMS, '') do |entry|
1223
+ assert_equal '[D][2015-09-25T12:34:56.789Z][123-0] test log entry ' \
1224
+ "#{i}", entry['textPayload'],
1225
+ "Test with #{n} logs failed. \n#{entry}"
1226
+ i += 1
1227
+ end
1228
+ end
1229
+ end
1230
+
1231
+ def test_http_request_from_record
1232
+ setup_gce_metadata_stubs
1233
+ setup_logging_stubs do
1234
+ d = create_driver
1235
+ d.emit('httpRequest' => http_request_message)
1236
+ d.run
1237
+ end
1238
+ verify_log_entries(1, COMPUTE_PARAMS, 'httpRequest') do |entry|
1239
+ assert_equal http_request_message, entry['httpRequest'], entry
1240
+ assert_nil get_fields(entry['structPayload'])['httpRequest'], entry
1241
+ end
1242
+ end
1243
+
1244
+ def test_http_request_partial_from_record
1245
+ setup_gce_metadata_stubs
1246
+ setup_logging_stubs do
1247
+ d = create_driver
1248
+ d.emit('httpRequest' => http_request_message.merge(
1249
+ 'otherKey' => 'value'))
1250
+ d.run
1251
+ end
1252
+ verify_log_entries(1, COMPUTE_PARAMS, 'httpRequest') do |entry|
1253
+ assert_equal http_request_message, entry['httpRequest'], entry
1254
+ fields = get_fields(entry['structPayload'])
1255
+ request = get_fields(get_struct(fields['httpRequest']))
1256
+ assert_equal 'value', get_string(request['otherKey']), entry
1257
+ end
1258
+ end
1259
+
1260
+ def test_http_request_when_not_hash
1261
+ setup_gce_metadata_stubs
1262
+ setup_logging_stubs do
1263
+ d = create_driver
1264
+ d.emit('httpRequest' => 'a_string')
1265
+ d.run
1266
+ end
1267
+ verify_log_entries(1, COMPUTE_PARAMS, 'structPayload') do |entry|
1268
+ fields = get_fields(entry['structPayload'])
1269
+ assert_equal 'a_string', get_string(fields['httpRequest']), entry
1270
+ assert_nil entry['httpRequest'], entry
1271
+ end
1272
+ end
1273
+
1274
+ private
1275
+
1276
+ def uri_for_log(params)
1277
+ 'https://logging.googleapis.com/v1beta3/projects/' + params[:project_id] +
1278
+ '/logs/' + params[:log_name] + '/entries:write'
1279
+ end
1280
+
1281
+ def stub_metadata_request(metadata_path, response_body)
1282
+ stub_request(:get, 'http://169.254.169.254/computeMetadata/v1/' +
1283
+ metadata_path)
1284
+ .to_return(body: response_body, status: 200,
1285
+ headers: { 'Content-Length' => response_body.length })
1286
+ end
1287
+
1288
+ def setup_no_metadata_service_stubs
1289
+ # Simulate a machine with no metadata service present
1290
+ stub_request(:any, %r{http://169.254.169.254/.*})
1291
+ .to_raise(Errno::EHOSTUNREACH)
1292
+ end
1293
+
1294
+ def setup_gce_metadata_stubs
1295
+ # Stub the root, used for platform detection by the plugin and 'googleauth'.
1296
+ stub_request(:get, 'http://169.254.169.254')
1297
+ .to_return(status: 200, headers: { 'Metadata-Flavor' => 'Google' })
1298
+
1299
+ # Create stubs for all the GCE metadata lookups the agent needs to make.
1300
+ stub_metadata_request('project/project-id', PROJECT_ID)
1301
+ stub_metadata_request('instance/zone', FULLY_QUALIFIED_ZONE)
1302
+ stub_metadata_request('instance/id', VM_ID)
1303
+ stub_metadata_request('instance/attributes/',
1304
+ "attribute1\nattribute2\nattribute3")
1305
+
1306
+ # Used by 'googleauth' to fetch the default service account credentials.
1307
+ stub_request(:get, 'http://169.254.169.254/computeMetadata/v1/' \
1308
+ 'instance/service-accounts/default/token')
1309
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1310
+ status: 200,
1311
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1312
+ 'Content-Type' => 'application/json' })
1313
+ end
1314
+
1315
+ def setup_ec2_metadata_stubs
1316
+ # Stub the root, used for platform detection.
1317
+ stub_request(:get, 'http://169.254.169.254')
1318
+ .to_return(status: 200, headers: { 'Server' => 'EC2ws' })
1319
+
1320
+ # Stub the identity document lookup made by the agent.
1321
+ stub_request(:get, 'http://169.254.169.254/latest/dynamic/' \
1322
+ 'instance-identity/document')
1323
+ .to_return(body: EC2_IDENTITY_DOCUMENT, status: 200,
1324
+ headers: { 'Content-Length' => EC2_IDENTITY_DOCUMENT.length })
1325
+ end
1326
+
1327
+ def setup_auth_stubs
1328
+ # Used when loading credentials from a JSON file.
1329
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
1330
+ .with(body: hash_including(grant_type: AUTH_GRANT_TYPE))
1331
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1332
+ status: 200,
1333
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1334
+ 'Content-Type' => 'application/json' })
1335
+
1336
+ stub_request(:post, 'https://www.googleapis.com/oauth2/v3/token')
1337
+ .with(body: hash_including(grant_type: 'refresh_token'))
1338
+ .to_return(body: %({"access_token": "#{FAKE_AUTH_TOKEN}"}),
1339
+ status: 200,
1340
+ headers: { 'Content-Length' => FAKE_AUTH_TOKEN.length,
1341
+ 'Content-Type' => 'application/json' })
1342
+ end
1343
+
1344
+ def setup_managed_vm_metadata_stubs
1345
+ stub_metadata_request(
1346
+ 'instance/attributes/',
1347
+ "attribute1\ngae_backend_name\ngae_backend_version\nlast_attribute")
1348
+ stub_metadata_request('instance/attributes/gae_backend_name',
1349
+ MANAGED_VM_BACKEND_NAME)
1350
+ stub_metadata_request('instance/attributes/gae_backend_version',
1351
+ MANAGED_VM_BACKEND_VERSION)
1352
+ end
1353
+
1354
+ def setup_container_metadata_stubs
1355
+ stub_metadata_request(
1356
+ 'instance/attributes/',
1357
+ "attribute1\nkube-env\nlast_attribute")
1358
+ stub_metadata_request('instance/attributes/kube-env',
1359
+ "ENABLE_NODE_LOGGING: \"true\"\n"\
1360
+ 'INSTANCE_PREFIX: '\
1361
+ "gke-#{CONTAINER_CLUSTER_NAME}-740fdafa\n"\
1362
+ 'KUBE_BEARER_TOKEN: AoQiMuwkNP2BMT0S')
1363
+ end
1364
+
1365
+ def setup_cloudfunctions_metadata_stubs
1366
+ stub_metadata_request(
1367
+ 'instance/attributes/',
1368
+ "attribute1\nkube-env\ngcf_region\nlast_attribute")
1369
+ stub_metadata_request('instance/attributes/kube-env',
1370
+ "ENABLE_NODE_LOGGING: \"true\"\n"\
1371
+ 'INSTANCE_PREFIX: '\
1372
+ "gke-#{CLOUDFUNCTIONS_CLUSTER_NAME}-740fdafa\n"\
1373
+ 'KUBE_BEARER_TOKEN: AoQiMuwkNP2BMT0S')
1374
+ stub_metadata_request('instance/attributes/gcf_region',
1375
+ CLOUDFUNCTIONS_REGION)
1376
+ end
1377
+
1378
+ def container_tag_with_container_name(container_name)
1379
+ "kubernetes.#{CONTAINER_POD_NAME}_#{CONTAINER_NAMESPACE_NAME}_" \
1380
+ "#{container_name}"
1381
+ end
1382
+
1383
+ def container_log_entry_with_metadata(
1384
+ log, container_name = CONTAINER_CONTAINER_NAME)
1385
+ {
1386
+ log: log,
1387
+ stream: CONTAINER_STREAM,
1388
+ time: CONTAINER_TIMESTAMP,
1389
+ kubernetes: {
1390
+ namespace_id: CONTAINER_NAMESPACE_ID,
1391
+ namespace_name: CONTAINER_NAMESPACE_NAME,
1392
+ pod_id: CONTAINER_POD_ID,
1393
+ pod_name: CONTAINER_POD_NAME,
1394
+ container_name: container_name,
1395
+ labels: {
1396
+ CONTAINER_LABEL_KEY => CONTAINER_LABEL_VALUE
1397
+ }
1398
+ }
1399
+ }
1400
+ end
1401
+
1402
+ def container_log_entry(log, stream = CONTAINER_STREAM)
1403
+ {
1404
+ log: log,
1405
+ stream: stream,
1406
+ time: CONTAINER_TIMESTAMP
1407
+ }
1408
+ end
1409
+
1410
+ def cloudfunctions_log_entry(i)
1411
+ {
1412
+ stream: 'stdout',
1413
+ log: '[D][2015-09-25T12:34:56.789Z][123-0] ' + log_entry(i)
1414
+ }
1415
+ end
1416
+
1417
+ def cloudfunctions_log_entry_text_not_matched(i)
1418
+ {
1419
+ stream: 'stdout',
1420
+ log: log_entry(i)
1421
+ }
1422
+ end
1423
+
1424
+ def log_entry(i)
1425
+ 'test log entry ' + i.to_s
1426
+ end
1427
+
1428
+ def check_labels(entry, common_labels, expected_labels)
1429
+ # TODO(salty) test/handle overlap between common_labels and entry labels
1430
+ all_labels ||= common_labels
1431
+ all_labels.merge!(entry['metadata']['labels'] || {})
1432
+ all_labels.each do |key, value|
1433
+ assert value.is_a?(String), "Value #{value} for label #{key} " \
1434
+ 'is not a string: ' + value.class.name
1435
+ assert expected_labels.key?(key), "Unexpected label #{key} => #{value}"
1436
+ assert_equal expected_labels[key], value, 'Value mismatch - expected ' \
1437
+ "#{expected_labels[key]} in #{key} => #{value}"
1438
+ end
1439
+ assert_equal expected_labels.length, all_labels.length, 'Expected ' \
1440
+ "#{expected_labels.length} labels, got #{all_labels.length}"
1441
+ end
1442
+
1443
+ # The caller can optionally provide a block which is called for each entry.
1444
+ def verify_json_log_entries(n, params, payload_type = 'textPayload')
1445
+ i = 0
1446
+ @logs_sent.each do |batch|
1447
+ batch['entries'].each do |entry|
1448
+ unless payload_type.empty?
1449
+ assert entry.key?(payload_type), 'Entry did not contain expected ' \
1450
+ "#{payload_type} key: " + entry.to_s
1451
+ # Check the payload for textPayload, otherwise it's up to the caller.
1452
+ if payload_type == 'textPayload'
1453
+ assert_equal "test log entry #{i}", entry['textPayload'], batch
1454
+ end
1455
+ end
1456
+
1457
+ assert_equal params[:zone], entry['metadata']['zone']
1458
+ assert_equal params[:service_name], entry['metadata']['serviceName']
1459
+ check_labels entry, batch['commonLabels'], params[:labels]
1460
+ yield(entry) if block_given?
1461
+ i += 1
1462
+ assert i <= n, "Number of entries #{i} exceeds expected number #{n}"
1463
+ end
1464
+ end
1465
+ assert i == n, "Number of entries #{i} does not match expected number #{n}"
1466
+ end
1467
+
1468
+ # The http request message to test against.
1469
+ def http_request_message
1470
+ HTTP_REQUEST_MESSAGE
1471
+ end
1472
+
1473
+ # Replace the 'referer' field with nil.
1474
+ def http_request_message_with_nil_referer
1475
+ http_request_message.merge('referer' => nil)
1476
+ end
1477
+
1478
+ # This module expects the methods below to be overridden.
1479
+
1480
+ # Create a Fluentd output test driver with the Google Cloud Output plugin.
1481
+ def create_driver(_conf = APPLICATION_DEFAULT_CONFIG, _tag = 'test')
1482
+ _undefined
1483
+ end
1484
+
1485
+ # Set up http or grpc stubs to mock the external calls.
1486
+ def setup_logging_stubs
1487
+ _undefined
1488
+ end
1489
+
1490
+ # Verify the number and the content of the log entries match the expectation.
1491
+ # The caller can optionally provide a block which is called for each entry.
1492
+ def verify_log_entries(_n, _params, _payload_type = 'textPayload', &_block)
1493
+ _undefined
1494
+ end
1495
+
1496
+ # For an optional field with default values, Protobuf omits the field when it
1497
+ # is deserialized to json. So we need to add an extra check for gRPC which
1498
+ # uses Protobuf.
1499
+ #
1500
+ # An optional block can be passed in if we need to assert something other than
1501
+ # a plain equal. e.g. assert_in_delta.
1502
+ def assert_equal_with_default(_field, _expected_value, _default_value, _entry)
1503
+ _undefined
1504
+ end
1505
+
1506
+ # Get the fields of the struct payload.
1507
+ def get_fields(_struct_payload)
1508
+ _undefined
1509
+ end
1510
+
1511
+ # Get the value of a struct field.
1512
+ def get_struct(_field)
1513
+ _undefined
1514
+ end
1515
+
1516
+ # Get the value of a string field.
1517
+ def get_string(_field)
1518
+ _undefined
1519
+ end
1520
+
1521
+ # Get the value of a number field.
1522
+ def get_number(_field)
1523
+ _undefined
1524
+ end
1525
+
1526
+ # The null value.
1527
+ def null_value(_field)
1528
+ _undefined
1529
+ end
1530
+
1531
+ def _undefined
1532
+ fail "Method #{__callee__} is unimplemented and needs to be overridden."
1533
+ end
1534
+ end