fluent-plugin-google-cloud 0.5.6 → 0.6.0.v2.alpha.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a79a5500d054c7b1f028bf611a307ad7a369782e
4
- data.tar.gz: 6bd1033baf1f06bcffcddf1e2895a8dc89b7179d
3
+ metadata.gz: 46c9cecb544f4eccc43900d70e03f2e8b50620e8
4
+ data.tar.gz: d31204662ee282da827325657026fdc426d974bb
5
5
  SHA512:
6
- metadata.gz: e926b8821174d19ba9341377d97a31a862021add5f51c0aee4e95d0f93e942920c1734bae2303e1b03c98c6b113f647b54c73ca6774f27a36a241dba5cc3820f
7
- data.tar.gz: 101cdbaee99db245388604f98f52d204b4982b7f56939f26c5c1d04dc58043b4f39d36602f87aeb17a992d141a00f13eb4f8b789a5417d20a5d64c58903610b5
6
+ metadata.gz: 1a058638489313253b731770cad0f296090ca9d43aca84c2514f0f1916aee1a0f9c86cce8128239fd10e40a38501768d7e2b3913313433cf4dc32f8a870782f3
7
+ data.tar.gz: d227ba85e626fc49e62e9554e8bae5283acdc5fa778a3521b116d6dc3b27959b0467e02f12922ccbb39805c0f574c23cb53e4c6a166d689ffa71d6c684960dc7
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluent-plugin-google-cloud (0.5.6)
4
+ fluent-plugin-google-cloud (0.6.0.v2.alpha.1)
5
5
  fluentd (~> 0.10)
6
6
  google-api-client (~> 0.9.0)
7
+ google-cloud-logging (~> 0.23.2)
7
8
  googleapis-common-protos (~> 1.3)
8
9
  googleauth (~> 0.4)
9
10
  grpc (~> 1.0)
@@ -41,6 +42,21 @@ GEM
41
42
  mime-types (>= 1.6)
42
43
  representable (~> 2.3.0)
43
44
  retriable (~> 2.0)
45
+ google-cloud-core (0.21.1)
46
+ googleauth (~> 0.5.1)
47
+ google-cloud-logging (0.23.2)
48
+ google-cloud-core (~> 0.21.1)
49
+ google-gax (~> 0.6.0)
50
+ google-protobuf (~> 3.0)
51
+ googleapis-common-protos (~> 1.3)
52
+ grpc (~> 1.0)
53
+ orderedhash (= 0.0.6)
54
+ stackdriver-core (~> 0.21.0)
55
+ google-gax (0.6.0)
56
+ googleapis-common-protos (~> 1.3.1)
57
+ googleauth (~> 0.5.1)
58
+ grpc (~> 1.0)
59
+ rly (~> 0.2.3)
44
60
  google-protobuf (3.2.0)
45
61
  googleapis-common-protos (1.3.4)
46
62
  google-protobuf (~> 3.0)
@@ -76,6 +92,7 @@ GEM
76
92
  msgpack (1.0.3)
77
93
  multi_json (1.12.1)
78
94
  multipart-post (2.0.0)
95
+ orderedhash (0.0.6)
79
96
  os (0.9.6)
80
97
  parser (2.4.0.0)
81
98
  ast (~> 2.2)
@@ -87,6 +104,7 @@ GEM
87
104
  representable (2.3.0)
88
105
  uber (~> 0.0.7)
89
106
  retriable (2.1.0)
107
+ rly (0.2.3)
90
108
  rubocop (0.35.1)
91
109
  astrolabe (~> 1.3)
92
110
  parser (>= 2.2.3.0, < 3.0)
@@ -104,6 +122,7 @@ GEM
104
122
  faraday (~> 0.9)
105
123
  jwt (~> 1.5)
106
124
  multi_json (~> 1.10)
125
+ stackdriver-core (0.21.0)
107
126
  strptime (0.1.9)
108
127
  test-unit (3.2.3)
109
128
  power_assert
@@ -10,7 +10,7 @@ eos
10
10
  gem.homepage = \
11
11
  'https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud'
12
12
  gem.license = 'Apache-2.0'
13
- gem.version = '0.5.6'
13
+ gem.version = '0.6.0.v2.alpha.1'
14
14
  gem.authors = ['Todd Derr', 'Alex Robinson']
15
15
  gem.email = ['salty@google.com']
16
16
  gem.required_ruby_version = Gem::Requirement.new('>= 2.0')
@@ -22,6 +22,7 @@ eos
22
22
  gem.add_runtime_dependency 'fluentd', '~> 0.10'
23
23
  gem.add_runtime_dependency 'googleapis-common-protos', '~> 1.3'
24
24
  gem.add_runtime_dependency 'google-api-client', '~> 0.9.0'
25
+ gem.add_runtime_dependency 'google-cloud-logging', '~> 0.23.2'
25
26
  gem.add_runtime_dependency 'googleauth', '~> 0.4'
26
27
  gem.add_runtime_dependency 'grpc', '~> 1.0'
27
28
  gem.add_runtime_dependency 'json', '~> 1.8'
@@ -18,10 +18,10 @@ require 'socket'
18
18
  require 'time'
19
19
  require 'yaml'
20
20
  require 'google/apis'
21
- require 'google/apis/logging_v1beta3'
22
- require 'google/logging/v1/logging_pb'
23
- require 'google/logging/v1/logging_services_pb'
24
- require 'google/logging/v1/log_entry_pb'
21
+ require 'google/apis/logging_v2beta1'
22
+ require 'google/logging/v2/logging_pb'
23
+ require 'google/logging/v2/logging_services_pb'
24
+ require 'google/logging/v2/log_entry_pb'
25
25
  require 'googleauth'
26
26
 
27
27
  module Google
@@ -36,17 +36,44 @@ end
36
36
  module Fluent
37
37
  # fluentd output plugin for the Stackdriver Logging API
38
38
  class GoogleCloudOutput < BufferedOutput
39
+ # Constants for service names and resource types.
40
+ module Constants
41
+ APPENGINE_CONSTANTS = {
42
+ service: 'appengine.googleapis.com',
43
+ resource_type: 'gae_app'
44
+ }
45
+ CLOUDFUNCTIONS_CONSTANTS = {
46
+ service: 'cloudfunctions.googleapis.com',
47
+ resource_type: 'cloud_function'
48
+ }
49
+ COMPUTE_CONSTANTS = {
50
+ service: 'compute.googleapis.com',
51
+ resource_type: 'gce_instance'
52
+ }
53
+ CONTAINER_CONSTANTS = {
54
+ service: 'container.googleapis.com',
55
+ resource_type: 'container'
56
+ }
57
+ DATAFLOW_CONSTANTS = {
58
+ service: 'dataflow.googleapis.com',
59
+ resource_type: 'dataflow_step'
60
+ }
61
+ EC2_CONSTANTS = {
62
+ service: 'ec2.amazonaws.com',
63
+ resource_type: 'aws_ec2_instance'
64
+ }
65
+ ML_CONSTANTS = {
66
+ service: 'ml.googleapis.com',
67
+ resource_type: 'ml_job'
68
+ }
69
+ end
70
+
71
+ include self::Constants
72
+
39
73
  Fluent::Plugin.register_output('google_cloud', self)
40
74
 
41
75
  PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
42
- PLUGIN_VERSION = '0.5.6'
43
-
44
- # Constants for service names.
45
- APPENGINE_SERVICE = 'appengine.googleapis.com'
46
- CLOUDFUNCTIONS_SERVICE = 'cloudfunctions.googleapis.com'
47
- COMPUTE_SERVICE = 'compute.googleapis.com'
48
- CONTAINER_SERVICE = 'container.googleapis.com'
49
- EC2_SERVICE = 'ec2.amazonaws.com'
76
+ PLUGIN_VERSION = '0.6.0.v2.alpha.1'
50
77
 
51
78
  # Name of the the Google cloud logging write scope.
52
79
  LOGGING_SCOPE = 'https://www.googleapis.com/auth/logging.write'
@@ -167,7 +194,7 @@ module Fluent
167
194
  attr_reader :running_on_managed_vm
168
195
  attr_reader :gae_backend_name
169
196
  attr_reader :gae_backend_version
170
- attr_reader :service_name
197
+ attr_reader :resource
171
198
  attr_reader :common_labels
172
199
 
173
200
  def initialize
@@ -198,6 +225,11 @@ module Fluent
198
225
  @common_labels = {}
199
226
  @common_labels.merge!(@labels) if @labels
200
227
 
228
+ # TODO: Construct Google::Api::MonitoredResource when @use_grpc is
229
+ # true after the protobuf map corruption issue is fixed.
230
+ @resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
231
+ labels: {})
232
+
201
233
  @compiled_kubernetes_tag_regexp = nil
202
234
  if @kubernetes_tag_regexp
203
235
  @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
@@ -235,7 +267,7 @@ module Fluent
235
267
  @vm_id = metadata['instanceId']
236
268
  end
237
269
  if metadata.key?('accountId')
238
- common_labels["#{EC2_SERVICE}/account_id"] = metadata['accountId']
270
+ @resource.labels['aws_account'] = metadata['accountId']
239
271
  end
240
272
  when Platform::OTHER
241
273
  # do nothing
@@ -268,12 +300,19 @@ module Fluent
268
300
  # Functions.
269
301
  @running_cloudfunctions = false
270
302
 
271
- # Set labels, etc. based on the config
303
+ # Set up the MonitoredResource, labels, etc. based on the config.
272
304
  case @platform
273
305
  when Platform::GCE
274
- @service_name = COMPUTE_SERVICE
306
+ @resource.type = COMPUTE_CONSTANTS[:resource_type]
307
+ # TODO: introduce a new MonitoredResource-centric configuration and
308
+ # deprecate subservice-name; for now, translate known uses.
275
309
  if @subservice_name
276
- @service_name = @subservice_name
310
+ # TODO: what should we do if we encounter an unknown value?
311
+ if @subservice_name == DATAFLOW_CONSTANTS[:service]
312
+ @resource.type = DATAFLOW_CONSTANTS[:resource_type]
313
+ elsif @subservice_name == ML_CONSTANTS[:service]
314
+ @resource.type = ML_CONSTANTS[:resource_type]
315
+ end
277
316
  elsif @detect_subservice
278
317
  # Check for specialized GCE environments.
279
318
  # TODO: Add config options for these to allow for running outside GCE?
@@ -287,41 +326,56 @@ module Fluent
287
326
  fetch_gce_metadata('instance/attributes/gae_backend_name')
288
327
  @gae_backend_version =
289
328
  fetch_gce_metadata('instance/attributes/gae_backend_version')
290
- @service_name = APPENGINE_SERVICE
291
- common_labels["#{APPENGINE_SERVICE}/module_id"] = @gae_backend_name
292
- common_labels["#{APPENGINE_SERVICE}/version_id"] =
293
- @gae_backend_version
329
+ @resource.type = APPENGINE_CONSTANTS[:resource_type]
330
+ @resource.labels['module_id'] = @gae_backend_name
331
+ @resource.labels['version_id'] = @gae_backend_version
294
332
  elsif attributes.include?('kube-env')
295
333
  # Kubernetes/Container Engine
296
- @service_name = CONTAINER_SERVICE
297
- common_labels["#{CONTAINER_SERVICE}/instance_id"] = @vm_id
334
+ @resource.type = CONTAINER_CONSTANTS[:resource_type]
298
335
  @raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
299
336
  @kube_env = YAML.load(@raw_kube_env)
300
- common_labels["#{CONTAINER_SERVICE}/cluster_name"] =
337
+ @resource.labels['cluster_name'] =
301
338
  cluster_name_from_kube_env(@kube_env)
302
339
  detect_cloudfunctions(attributes)
303
340
  end
304
341
  end
305
- common_labels["#{COMPUTE_SERVICE}/resource_type"] = 'instance'
306
- common_labels["#{COMPUTE_SERVICE}/resource_id"] = @vm_id
307
- common_labels["#{COMPUTE_SERVICE}/resource_name"] = @vm_name
342
+ # Some services have the GCE instance_id and zone as MonitoredResource
343
+ # labels; for other services we send them as entry labels.
344
+ if @resource.type == COMPUTE_CONSTANTS[:resource_type] ||
345
+ @resource.type == CONTAINER_CONSTANTS[:resource_type]
346
+ @resource.labels['instance_id'] = @vm_id
347
+ @resource.labels['zone'] = @zone
348
+ else
349
+ common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
350
+ common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
351
+ end
352
+ common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
308
353
  when Platform::EC2
309
- @service_name = EC2_SERVICE
310
- common_labels["#{EC2_SERVICE}/resource_type"] = 'instance'
311
- common_labels["#{EC2_SERVICE}/resource_id"] = @vm_id
312
- common_labels["#{EC2_SERVICE}/resource_name"] = @vm_name
354
+ @resource.type = EC2_CONSTANTS[:resource_type]
355
+ @resource.labels['instance_id'] = @vm_id
356
+ @resource.labels['region'] = @zone
357
+ # the aws_account label is populated above.
358
+ common_labels["#{EC2_CONSTANTS[:service]}/resource_name"] = @vm_name
313
359
  when Platform::OTHER
314
- # Use COMPUTE_SERVICE as the default environment.
315
- @service_name = COMPUTE_SERVICE
316
- common_labels["#{COMPUTE_SERVICE}/resource_type"] = 'instance'
317
- common_labels["#{COMPUTE_SERVICE}/resource_id"] = @vm_id
318
- common_labels["#{COMPUTE_SERVICE}/resource_name"] = @vm_name
360
+ # Use GCE as the default environment.
361
+ @resource.type = COMPUTE_CONSTANTS[:resource_type]
362
+ @resource.labels['instance_id'] = @vm_id
363
+ @resource.labels['zone'] = @zone
364
+ common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
319
365
  end
366
+ @resource.labels.merge!(
367
+ extract_resource_labels(@resource.type, common_labels))
368
+
369
+ # The resource and labels are now set up; ensure they can't be modified
370
+ # without first duping them.
371
+ @resource.freeze
372
+ @resource.labels.freeze
373
+ @common_labels.freeze
320
374
 
321
375
  # Log an informational message containing the Logs viewer URL
322
- @log.info 'Logs viewer address: ',
323
- 'https://console.developers.google.com/project/', @project_id,
324
- '/logs?service=', @service_name, '&key1=instance&key2=', @vm_id
376
+ @log.info 'Logs viewer address: https://console.cloud.google.com/logs/',
377
+ "viewer?project=#{@project_id}&resource=#{@resource_type}/",
378
+ "instance_id/#{@vm_id}"
325
379
  end
326
380
 
327
381
  def start
@@ -353,6 +407,111 @@ module Fluent
353
407
  tag
354
408
  end
355
409
 
410
+ # Compute the monitored resource and common labels shared by a collection of
411
+ # entries.
412
+ def compute_group_resource_and_labels(tag)
413
+ # Note that we assume that labels added to group_common_labels below are
414
+ # not 'service' labels (i.e. we do not call extract_resource_labels
415
+ # again).
416
+ group_resource = @resource.dup
417
+ group_common_labels = @common_labels.dup
418
+
419
+ if @running_cloudfunctions
420
+ # If the current group of entries is coming from a Cloud Functions
421
+ # function, the function name can be extracted from the tag.
422
+ match_data = @cloudfunctions_tag_regexp.match(tag)
423
+ if match_data
424
+ # Resource type is set to Cloud Functions only for logs actually
425
+ # coming from a function, otherwise we leave it as Container.
426
+ group_resource.type = CLOUDFUNCTIONS_CONSTANTS[:resource_type]
427
+ group_resource.labels['region'] = @gcf_region
428
+ group_resource.labels['function_name'] =
429
+ decode_cloudfunctions_function_name(
430
+ match_data['encoded_function_name'])
431
+ # Move GKE container labels from the MonitoredResource to the
432
+ # LogEntry.
433
+ instance_id = group_resource.labels.delete('instance_id')
434
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/cluster_name"] =
435
+ group_resource.labels.delete('cluster_name')
436
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/instance_id"] =
437
+ instance_id
438
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
439
+ instance_id
440
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] =
441
+ group_resource.labels.delete('zone')
442
+ end
443
+ end
444
+ if group_resource.type == CONTAINER_CONSTANTS[:resource_type] &&
445
+ @compiled_kubernetes_tag_regexp
446
+ # Container logs in Kubernetes are tagged based on where they came
447
+ # from, so we can extract useful metadata from the tag.
448
+ # Do this here to avoid having to repeat it for each record.
449
+ match_data = @compiled_kubernetes_tag_regexp.match(tag)
450
+ if match_data
451
+ group_resource.labels['container_name'] =
452
+ match_data['container_name']
453
+ %w(namespace_name pod_name).each do |field|
454
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/#{field}"] =
455
+ match_data[field]
456
+ end
457
+ end
458
+ end
459
+
460
+ # Freeze the per-request state. Any further changes must be made on a
461
+ # per-entry basis.
462
+ group_resource.freeze
463
+ group_resource.labels.freeze
464
+ group_common_labels.freeze
465
+
466
+ [group_resource, group_common_labels]
467
+ end
468
+
469
+ # Extract entry resource and common labels that should be applied to
470
+ # individual entries from the group resource.
471
+ def extract_entry_labels(group_resource, record)
472
+ resource_labels = {}
473
+ common_labels = {}
474
+
475
+ if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
476
+ record.key?('log')
477
+ @cloudfunctions_log_match =
478
+ @cloudfunctions_log_regexp.match(record['log'])
479
+ end
480
+
481
+ if group_resource.type == CONTAINER_CONSTANTS[:resource_type]
482
+ # Move the stdout/stderr annotation from the record into a label
483
+ common_labels.merge!(
484
+ fields_to_labels(
485
+ record, 'stream' => "#{CONTAINER_CONSTANTS[:service]}/stream"))
486
+
487
+ # If the record has been annotated by the kubernetes_metadata_filter
488
+ # plugin, then use that metadata. Otherwise, rely on commonLabels
489
+ # populated at the grouped_entries level from the group's tag.
490
+ if record.key?('kubernetes')
491
+ extracted_resource_labels, extracted_common_labels = \
492
+ extract_container_metadata(record)
493
+ resource_labels.merge!(extracted_resource_labels)
494
+ common_labels.merge!(extracted_common_labels)
495
+ end
496
+ end
497
+
498
+ # If a field is present in the label_map, send its value as a label
499
+ # (mapping the field name to label name as specified in the config)
500
+ # and do not send that field as part of the payload.
501
+ common_labels.merge!(fields_to_labels(record, @label_map))
502
+
503
+ if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
504
+ @cloudfunctions_log_match &&
505
+ @cloudfunctions_log_match['execution_id']
506
+ common_labels['execution_id'] =
507
+ @cloudfunctions_log_match['execution_id']
508
+ end
509
+ resource_labels.merge!(
510
+ extract_resource_labels(group_resource.type, common_labels))
511
+
512
+ [resource_labels, common_labels]
513
+ end
514
+
356
515
  def write(chunk)
357
516
  # Group the entries since we have to make one call per tag.
358
517
  grouped_entries = {}
@@ -369,74 +528,20 @@ module Fluent
369
528
 
370
529
  grouped_entries.each do |tag, arr|
371
530
  entries = []
372
- labels = @common_labels.clone
373
-
374
- if @running_cloudfunctions
375
- # If the current group of entries is coming from a Cloud Functions
376
- # function, the function name can be extracted from the tag.
377
- match_data = @cloudfunctions_tag_regexp.match(tag)
378
- if match_data
379
- # Service name is set to Cloud Functions only for logs actually
380
- # coming from a function.
381
- @service_name = CLOUDFUNCTIONS_SERVICE
382
- labels["#{CLOUDFUNCTIONS_SERVICE}/region"] = @gcf_region
383
- labels["#{CLOUDFUNCTIONS_SERVICE}/function_name"] =
384
- decode_cloudfunctions_function_name(
385
- match_data['encoded_function_name'])
386
- else
387
- # Other logs are considered as coming from the Container Engine
388
- # service.
389
- @service_name = CONTAINER_SERVICE
390
- end
391
- end
392
- if @service_name == CONTAINER_SERVICE && @compiled_kubernetes_tag_regexp
393
- # Container logs in Kubernetes are tagged based on where they came
394
- # from, so we can extract useful metadata from the tag.
395
- # Do this here to avoid having to repeat it for each record.
396
- match_data = @compiled_kubernetes_tag_regexp.match(tag)
397
- if match_data
398
- %w(namespace_name pod_name container_name).each do |field|
399
- labels["#{CONTAINER_SERVICE}/#{field}"] = match_data[field]
400
- end
401
- end
402
- end
531
+ group_resource, group_common_labels = compute_group_resource_and_labels(
532
+ tag)
403
533
 
404
534
  arr.each do |time, record|
405
535
  next unless record.is_a?(Hash)
406
536
 
407
- if @use_grpc
408
- entry = Google::Logging::V1::LogEntry.new(
409
- metadata: Google::Logging::V1::LogEntryMetadata.new(
410
- service_name: convert_to_utf8(@service_name),
411
- project_id: convert_to_utf8(@project_id),
412
- zone: convert_to_utf8(@zone),
413
- labels: {}
414
- ))
415
- else
416
- entry = Google::Apis::LoggingV1beta3::LogEntry.new(
417
- metadata: Google::Apis::LoggingV1beta3::LogEntryMetadata.new(
418
- service_name: @service_name,
419
- project_id: @project_id,
420
- zone: @zone,
421
- labels: {}
422
- ))
423
- end
424
-
425
- if @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
426
- @cloudfunctions_log_match =
427
- @cloudfunctions_log_regexp.match(record['log'])
428
- end
429
- if @service_name == CONTAINER_SERVICE
430
- # Move the stdout/stderr annotation from the record into a label.
431
- field_to_label(record, 'stream', entry.metadata.labels,
432
- "#{CONTAINER_SERVICE}/stream")
433
- # If the record has been annotated by the kubernetes_metadata_filter
434
- # plugin, then use that metadata. Otherwise, rely on commonLabels
435
- # populated at the grouped_entries level from the group's tag.
436
- if record.key?('kubernetes')
437
- handle_container_metadata(record, entry)
438
- end
537
+ extracted_resource_labels, extracted_common_labels = \
538
+ extract_entry_labels(group_resource, record)
539
+ entry_resource = group_resource.dup
540
+ entry_resource.labels.merge!(extracted_resource_labels)
541
+ entry_common_labels = \
542
+ group_common_labels.merge(extracted_common_labels)
439
543
 
544
+ if entry_resource.type == CONTAINER_CONSTANTS[:resource_type]
440
545
  # Save the timestamp if available, then clear it out to allow for
441
546
  # determining whether we should parse the log or message field.
442
547
  timestamp = record.key?('time') ? record['time'] : nil
@@ -460,57 +565,47 @@ module Fluent
460
565
  end
461
566
  end
462
567
 
463
- ts_secs, ts_nanos = compute_timestamp(record, time)
568
+ ts_secs, ts_nanos = compute_timestamp(
569
+ entry_resource.type, record, time)
570
+ severity = compute_severity(
571
+ entry_resource.type, record, entry_common_labels)
572
+
464
573
  if @use_grpc
574
+ entry = Google::Logging::V2::LogEntry.new(
575
+ labels: entry_common_labels,
576
+ resource: Google::Api::MonitoredResource.new(
577
+ type: entry_resource.type,
578
+ labels: entry_resource.labels.to_h
579
+ ),
580
+ severity: grpc_severity(severity)
581
+ )
465
582
  # If "seconds" is null or not an integer, we will omit the timestamp
466
583
  # field and defer the decision on how to handle it to the downstream
467
584
  # Logging API. If "nanos" is null or not an integer, it will be set
468
585
  # to 0.
469
586
  if ts_secs.is_a?(Integer)
470
587
  ts_nanos = 0 unless ts_nanos.is_a?(Integer)
471
- entry.metadata.timestamp = Google::Protobuf::Timestamp.new(
588
+ entry.timestamp = Google::Protobuf::Timestamp.new(
472
589
  seconds: ts_secs,
473
590
  nanos: ts_nanos
474
591
  )
475
592
  end
476
-
477
- entry.metadata.severity =
478
- grpc_severity(compute_severity(record, entry))
479
-
480
- set_http_request_grpc(record, entry) # FIXME
593
+ set_http_request_grpc(record, entry)
594
+ set_payload_grpc(entry_resource.type, record, entry, is_json)
481
595
  else
482
- entry.metadata.timestamp = {
483
- seconds: ts_secs,
484
- nanos: ts_nanos
485
- }
486
-
487
- entry.metadata.severity =
488
- compute_severity(record, entry)
489
-
596
+ # Remove the labels if we didn't populate them with anything.
597
+ entry_resource.labels = nil if entry_resource.labels.empty?
598
+ entry = Google::Apis::LoggingV2beta1::LogEntry.new(
599
+ labels: entry_common_labels,
600
+ resource: entry_resource,
601
+ severity: severity,
602
+ timestamp: {
603
+ seconds: ts_secs,
604
+ nanos: ts_nanos
605
+ }
606
+ )
490
607
  set_http_request(record, entry)
491
- end
492
-
493
- # If a field is present in the label_map, send its value as a label
494
- # (mapping the field name to label name as specified in the config)
495
- # and do not send that field as part of the payload.
496
- if @label_map
497
- @label_map.each do |field, label|
498
- field_to_label(record, field, entry.metadata.labels, label)
499
- end
500
- end
501
-
502
- if @service_name == CLOUDFUNCTIONS_SERVICE &&
503
- @cloudfunctions_log_match &&
504
- @cloudfunctions_log_match['execution_id']
505
- entry.metadata.labels['execution_id'] =
506
- @cloudfunctions_log_match['execution_id']
507
- end
508
-
509
- if @use_grpc
510
- set_payload_grpc(record, entry, is_json)
511
- else
512
- set_payload(record, entry, is_json)
513
- entry.metadata.labels = nil if entry.metadata.labels.empty?
608
+ set_payload(entry_resource.type, record, entry, is_json)
514
609
  end
515
610
 
516
611
  entries.push(entry)
@@ -518,21 +613,24 @@ module Fluent
518
613
  # Don't send an empty request if we rejected all the entries.
519
614
  next if entries.empty?
520
615
 
521
- log_name = log_name(tag, labels)
616
+ log_name = "projects/#{@project_id}/logs/#{log_name(
617
+ tag, group_resource)}"
522
618
 
619
+ # Does the actual write to the cloud logging api.
620
+ client = api_client
523
621
  if @use_grpc
524
622
  begin
525
- # Does the actual write to the cloud logging api.
526
-
527
- client = api_client
528
-
529
- labels_utf8_pairs = labels.map do |k, v|
623
+ labels_utf8_pairs = group_common_labels.map do |k, v|
530
624
  [k.encode('utf-8'), convert_to_utf8(v)]
531
625
  end
532
626
 
533
- write_request = Google::Logging::V1::WriteLogEntriesRequest.new(
534
- log_name: "projects/#{@project_id}/logs/#{log_name}",
535
- common_labels: Hash[labels_utf8_pairs],
627
+ write_request = Google::Logging::V2::WriteLogEntriesRequest.new(
628
+ log_name: log_name,
629
+ resource: Google::Api::MonitoredResource.new(
630
+ type: group_resource.type,
631
+ labels: group_resource.labels.to_h
632
+ ),
633
+ labels: labels_utf8_pairs.to_h,
536
634
  entries: entries
537
635
  )
538
636
 
@@ -584,21 +682,15 @@ module Fluent
584
682
  end
585
683
  else
586
684
  begin
587
- # Does the actual write to the cloud logging api.
588
-
589
- client = api_client
590
-
591
- # The URI of the write is constructed by the Google::Api request;
592
- # it is equivalent to this URL:
593
- # 'https://logging.googleapis.com/v1beta3/projects/' \
594
- # "#{@project_id}/logs/#{log_name}/entries:write"
595
685
  write_request = \
596
- Google::Apis::LoggingV1beta3::WriteLogEntriesRequest.new(
597
- common_labels: labels,
686
+ Google::Apis::LoggingV2beta1::WriteLogEntriesRequest.new(
687
+ log_name: log_name,
688
+ resource: group_resource,
689
+ labels: group_common_labels,
598
690
  entries: entries)
599
691
 
600
692
  # TODO: RequestOptions
601
- client.write_log_entries(@project_id, log_name, write_request)
693
+ client.write_entry_log_entries(write_request)
602
694
 
603
695
  # Let the user explicitly know when the first call succeeded,
604
696
  # to aid with verification and troubleshooting.
@@ -759,7 +851,7 @@ module Fluent
759
851
  instance_prefix
760
852
  end
761
853
 
762
- def compute_timestamp(record, time)
854
+ def compute_timestamp(resource_type, record, time)
763
855
  if record.key?('timestamp') &&
764
856
  record['timestamp'].is_a?(Hash) &&
765
857
  record['timestamp'].key?('seconds') &&
@@ -783,7 +875,7 @@ module Fluent
783
875
  @log.warn 'timeNanos is deprecated - please use ' \
784
876
  'timestampSeconds and timestampNanos instead.'
785
877
  end
786
- elsif @service_name == CLOUDFUNCTIONS_SERVICE &&
878
+ elsif resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
787
879
  @cloudfunctions_log_match
788
880
  timestamp = DateTime.parse(@cloudfunctions_log_match['timestamp'])
789
881
  ts_secs = timestamp.strftime('%s').to_i
@@ -805,8 +897,8 @@ module Fluent
805
897
  [ts_secs, ts_nanos]
806
898
  end
807
899
 
808
- def compute_severity(record, entry)
809
- if @service_name == CLOUDFUNCTIONS_SERVICE
900
+ def compute_severity(resource_type, record, entry_common_labels)
901
+ if resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type]
810
902
  if @cloudfunctions_log_match && @cloudfunctions_log_match['severity']
811
903
  return parse_severity(@cloudfunctions_log_match['severity'])
812
904
  elsif record.key?('stream') && record['stream'] == 'stdout'
@@ -820,9 +912,9 @@ module Fluent
820
912
  end
821
913
  elsif record.key?('severity')
822
914
  return parse_severity(record.delete('severity'))
823
- elsif @service_name == CONTAINER_SERVICE && \
824
- entry.metadata.labels.key?("#{CONTAINER_SERVICE}/stream")
825
- stream = entry.metadata.labels["#{CONTAINER_SERVICE}/stream"]
915
+ elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
916
+ entry_common_labels.key?("#{CONTAINER_CONSTANTS[:service]}/stream")
917
+ stream = entry_common_labels["#{CONTAINER_CONSTANTS[:service]}/stream"]
826
918
  if stream == 'stdout'
827
919
  return 'INFO'
828
920
  elsif stream == 'stderr'
@@ -838,7 +930,7 @@ module Fluent
838
930
  def set_http_request(record, entry)
839
931
  return nil unless record['httpRequest'].is_a?(Hash)
840
932
  input = record['httpRequest']
841
- output = Google::Apis::LoggingV1beta3::HttpRequest.new
933
+ output = Google::Apis::LoggingV2beta1::HttpRequest.new
842
934
  output.request_method = input.delete('requestMethod')
843
935
  output.request_url = input.delete('requestUrl')
844
936
  output.request_size = input.delete('requestSize')
@@ -848,8 +940,8 @@ module Fluent
848
940
  output.remote_ip = input.delete('remoteIp')
849
941
  output.referer = input.delete('referer')
850
942
  output.cache_hit = input.delete('cacheHit')
851
- output.validated_with_origin_server = \
852
- input.delete('validatedWithOriginServer')
943
+ output.cache_validated_with_origin_server = \
944
+ input.delete('cacheValidatedWithOriginServer')
853
945
  record.delete('httpRequest') if input.empty?
854
946
  entry.http_request = output
855
947
  end
@@ -989,16 +1081,23 @@ module Fluent
989
1081
  end
990
1082
 
991
1083
  # Requires that record has a 'kubernetes' field.
992
- def handle_container_metadata(record, entry)
993
- fields = %w(namespace_id namespace_name pod_id pod_name container_name)
994
- fields.each do |field|
995
- field_to_label(record['kubernetes'], field, entry.metadata.labels,
996
- "#{CONTAINER_SERVICE}/#{field}")
1084
+ def extract_container_metadata(record)
1085
+ resource_labels = {}
1086
+ common_labels = {}
1087
+ %w(namespace_id pod_id container_name).each do |field|
1088
+ resource_labels.merge!(
1089
+ fields_to_labels(record['kubernetes'], field => field))
1090
+ end
1091
+ %w(namespace_name pod_name).each do |field|
1092
+ common_labels.merge!(
1093
+ fields_to_labels(
1094
+ record['kubernetes'],
1095
+ field => "#{CONTAINER_CONSTANTS[:service]}/#{field}"))
997
1096
  end
998
1097
  # Prepend label/ to all user-defined labels' keys.
999
1098
  if record['kubernetes'].key?('labels')
1000
1099
  record['kubernetes']['labels'].each do |key, value|
1001
- entry.metadata.labels["label/#{key}"] = value
1100
+ common_labels["label/#{key}"] = value
1002
1101
  end
1003
1102
  end
1004
1103
  # We've explicitly consumed all the fields we care about -- don't litter
@@ -1006,33 +1105,43 @@ module Fluent
1006
1105
  # filter plugin includes (or an empty 'kubernetes' field).
1007
1106
  record.delete('kubernetes')
1008
1107
  record.delete('docker')
1108
+ [resource_labels, common_labels]
1009
1109
  end
1010
1110
 
1011
- def field_to_label(record, field, labels, label)
1012
- return unless record.key?(field)
1013
- labels[label] = convert_to_utf8(record[field].to_s)
1014
- record.delete(field)
1111
+ # For every original_label => new_label pair in the label_map, delete the
1112
+ # original_label from the record if it exists, and extract the value to form
1113
+ # a map with the new_label as the key.
1114
+ def fields_to_labels(record, label_map)
1115
+ return {} if label_map.nil? || !label_map.is_a?(Hash)
1116
+ label_map.each_with_object({}) \
1117
+ do |(original_label, new_label), extracted_labels|
1118
+ extracted_labels[new_label] = convert_to_utf8(
1119
+ record.delete(original_label).to_s) if record.key?(original_label)
1120
+ end
1015
1121
  end
1016
1122
 
1017
- def set_payload(record, entry, is_json)
1123
+ def set_payload(resource_type, record, entry, is_json)
1018
1124
  # If this is a Cloud Functions log that matched the expected regexp,
1019
1125
  # use text payload. Otherwise, use JSON if we found valid JSON, or text
1020
1126
  # payload in the following cases:
1021
1127
  # 1. This is a Cloud Functions log and the 'log' key is available
1022
1128
  # 2. This is an unstructured Container log and the 'log' key is available
1023
1129
  # 3. The only remaining key is 'message'
1024
- if @service_name == CLOUDFUNCTIONS_SERVICE && @cloudfunctions_log_match
1130
+ if resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1131
+ @cloudfunctions_log_match
1025
1132
  entry.text_payload = @cloudfunctions_log_match['text']
1026
- elsif @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
1133
+ elsif resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1134
+ record.key?('log')
1027
1135
  entry.text_payload = record['log']
1028
1136
  elsif is_json
1029
- entry.struct_payload = record
1030
- elsif @service_name == CONTAINER_SERVICE && record.key?('log')
1137
+ entry.json_payload = record
1138
+ elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1139
+ record.key?('log')
1031
1140
  entry.text_payload = record['log']
1032
1141
  elsif record.size == 1 && record.key?('message')
1033
1142
  entry.text_payload = record['message']
1034
1143
  else
1035
- entry.struct_payload = record
1144
+ entry.json_payload = record
1036
1145
  end
1037
1146
  end
1038
1147
 
@@ -1080,56 +1189,82 @@ module Fluent
1080
1189
  ret
1081
1190
  end
1082
1191
 
1083
- def set_payload_grpc(record, entry, is_json)
1192
+ def set_payload_grpc(resource_type, record, entry, is_json)
1084
1193
  # If this is a Cloud Functions log that matched the expected regexp,
1085
1194
  # use text payload. Otherwise, use JSON if we found valid JSON, or text
1086
1195
  # payload in the following cases:
1087
1196
  # 1. This is a Cloud Functions log and the 'log' key is available
1088
1197
  # 2. This is an unstructured Container log and the 'log' key is available
1089
1198
  # 3. The only remaining key is 'message'
1090
- if @service_name == CLOUDFUNCTIONS_SERVICE && @cloudfunctions_log_match
1199
+ if resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1200
+ @cloudfunctions_log_match
1091
1201
  entry.text_payload = convert_to_utf8(
1092
1202
  @cloudfunctions_log_match['text'])
1093
- elsif @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
1203
+ elsif resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1204
+ record.key?('log')
1094
1205
  entry.text_payload = convert_to_utf8(record['log'])
1095
1206
  elsif is_json
1096
- entry.struct_payload = struct_from_ruby(record)
1097
- elsif @service_name == CONTAINER_SERVICE && record.key?('log')
1207
+ entry.json_payload = struct_from_ruby(record)
1208
+ elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1209
+ record.key?('log')
1098
1210
  entry.text_payload = convert_to_utf8(record['log'])
1099
1211
  elsif record.size == 1 && record.key?('message')
1100
1212
  entry.text_payload = convert_to_utf8(record['message'])
1101
1213
  else
1102
- entry.struct_payload = struct_from_ruby(record)
1214
+ entry.json_payload = struct_from_ruby(record)
1103
1215
  end
1104
1216
  end
1105
1217
 
1106
- def log_name(tag, common_labels)
1107
- if @service_name == CLOUDFUNCTIONS_SERVICE
1218
+ def log_name(tag, resource)
1219
+ if resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type]
1108
1220
  tag = 'cloud-functions'
1109
1221
  elsif @running_on_managed_vm
1110
1222
  # Add a prefix to Managed VM logs to prevent namespace collisions.
1111
- tag = "#{APPENGINE_SERVICE}/#{tag}"
1112
- elsif @service_name == CONTAINER_SERVICE
1223
+ tag = "#{APPENGINE_CONSTANTS[:service]}/#{tag}"
1224
+ elsif resource.type == CONTAINER_CONSTANTS[:resource_type]
1113
1225
  # For Kubernetes logs, use just the container name as the log name
1114
1226
  # if we have it.
1115
- container_name_key = "#{CONTAINER_SERVICE}/container_name"
1116
- if common_labels && common_labels.key?(container_name_key)
1117
- sanitized_log_name = sanitize_tag(common_labels[container_name_key])
1118
- tag = sanitized_log_name unless sanitized_log_name.nil?
1227
+ if resource.labels && resource.labels.key?('container_name')
1228
+ sanitized_tag = sanitize_tag(resource.labels['container_name'])
1229
+ tag = sanitized_tag unless sanitized_tag.nil?
1119
1230
  end
1120
1231
  end
1121
- # Only encode the log name for the grpc path, since the non-grpc client
1122
- # lib already handles encoding.
1123
- tag = ERB::Util.url_encode(tag) if @use_grpc
1232
+ tag = ERB::Util.url_encode(tag)
1124
1233
  tag
1125
1234
  end
1126
1235
 
1236
+ # Some services set labels (via configuring 'labels' or 'label_map') which
1237
+ # are now MonitoredResource labels in v2.
1238
+ # For these services, remove resource labels from 'labels' and return a
1239
+ # Hash of labels to be merged into the MonitoredResource labels.
1240
+ # Otherwise, return an empty hash and leave 'labels' unmodified.
1241
+ def extract_resource_labels(resource_type, labels)
1242
+ extracted_labels = {}
1243
+ return extracted_labels if labels.nil? || !labels.is_a?(Hash)
1244
+
1245
+ if resource_type == DATAFLOW_CONSTANTS[:resource_type]
1246
+ label_prefix = DATAFLOW_CONSTANTS[:service]
1247
+ labels_to_extract = %w(region job_name job_id step_id)
1248
+ elsif resource_type == ML_CONSTANTS[:resource_type]
1249
+ label_prefix = ML_CONSTANTS[:service]
1250
+ labels_to_extract = %w(job_id task_name)
1251
+ else
1252
+ return extracted_labels
1253
+ end
1254
+
1255
+ labels_to_extract.each do |label|
1256
+ extracted_labels[label] = labels.delete("#{label_prefix}/#{label}") if
1257
+ labels.key?("#{label_prefix}/#{label}")
1258
+ end
1259
+ extracted_labels
1260
+ end
1261
+
1127
1262
  def init_api_client
1128
1263
  return if @use_grpc
1129
1264
  # TODO: Use a non-default ClientOptions object.
1130
1265
  Google::Apis::ClientOptions.default.application_name = PLUGIN_NAME
1131
1266
  Google::Apis::ClientOptions.default.application_version = PLUGIN_VERSION
1132
- @client = Google::Apis::LoggingV1beta3::LoggingService.new
1267
+ @client = Google::Apis::LoggingV2beta1::LoggingService.new
1133
1268
  @client.authorization = Google::Auth.get_application_default(
1134
1269
  LOGGING_SCOPE)
1135
1270
  end
@@ -1140,7 +1275,7 @@ module Fluent
1140
1275
  authentication = Google::Auth.get_application_default
1141
1276
  creds = GRPC::Core::CallCredentials.new(authentication.updater_proc)
1142
1277
  creds = ssl_creds.compose(creds)
1143
- @client = Google::Logging::V1::LoggingService::Stub.new(
1278
+ @client = Google::Logging::V2::LoggingServiceV2::Stub.new(
1144
1279
  'logging.googleapis.com', creds)
1145
1280
  else
1146
1281
  unless @client.authorization.expired?
@@ -1182,3 +1317,18 @@ module Fluent
1182
1317
  end
1183
1318
  end
1184
1319
  end
1320
+
1321
+ module Google
1322
+ module Apis
1323
+ module LoggingV2beta1
1324
+ # Override MonitoredResource::dup to make a deep copy.
1325
+ class MonitoredResource
1326
+ def dup
1327
+ ret = super
1328
+ ret.labels = labels.dup
1329
+ ret
1330
+ end
1331
+ end
1332
+ end
1333
+ end
1334
+ end