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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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