fluent-plugin-google-cloud 0.5.2 → 0.5.3.grpc.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: 8109f72f6f16a0756b3a30c911e9343392051b96
4
- data.tar.gz: e76c44a9c3e8cb63c6125f1b621e7985e3050918
3
+ metadata.gz: 4289702253d986d7711b6e0a97712b13702901c3
4
+ data.tar.gz: aac7a196ff97e4c70234772ad5755861d80549aa
5
5
  SHA512:
6
- metadata.gz: aad0d625ddb42768a112e887bae953d601bd646d9003d7a7308a19a3e9e4afdbd61725bd56b844cefc7626a99dad067ea3ef6d68e44f06bc324ab7abe937bcd2
7
- data.tar.gz: c880a81905b4ce2535f247a7bd1938e46082461f8c9f48a03716908219e6fe95234397a09ee4975ed21dc5c196b782d1efd1ebf499617c7fb989aa234efbf60e
6
+ metadata.gz: 4eb63be9e1eb8492ff627270ba13ab5b2d1cbe4dc1c01a0e2b7b14cc3192084d3d477ee9338c25c3af3ab8875e2203ee8e0d7fac2ddbda52fbbc6c5983959a16
7
+ data.tar.gz: 8fd692a696fec2163b7c1935cf68e488d3eb4d780ebe90bd5e22f53d4e06cab5d2dbc8dedca419a8218019ce13eacf9d15eebe76a71af40ad02514dec80887dc
@@ -1,10 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluent-plugin-google-cloud (0.5.2)
4
+ fluent-plugin-google-cloud (0.5.3.grpc.alpha.1)
5
5
  fluentd (~> 0.10, <= 0.13)
6
6
  google-api-client (> 0.9)
7
+ googleapis-common-protos (~> 1.1)
7
8
  googleauth (~> 0.4)
9
+ grpc (~> 0.15)
8
10
  json (~> 1.8)
9
11
 
10
12
  GEM
@@ -39,6 +41,9 @@ GEM
39
41
  representable (~> 2.3.0)
40
42
  retriable (~> 2.0)
41
43
  thor (~> 0.19)
44
+ google-protobuf (3.0.0.alpha.5.0.5.1)
45
+ googleapis-common-protos (1.1.1)
46
+ google-protobuf (~> 3.0.0.alpha.5.0)
42
47
  googleauth (0.5.1)
43
48
  faraday (~> 0.9)
44
49
  jwt (~> 1.4)
@@ -47,6 +52,9 @@ GEM
47
52
  multi_json (~> 1.11)
48
53
  os (~> 0.9)
49
54
  signet (~> 0.7)
55
+ grpc (0.15.0)
56
+ google-protobuf (~> 3.0.0.alpha.5.0.3)
57
+ googleauth (~> 0.5.1)
50
58
  hashdiff (0.3.0)
51
59
  http_parser.rb (0.6.0)
52
60
  httpclient (2.8.0)
data/Rakefile CHANGED
@@ -18,15 +18,25 @@ end
18
18
 
19
19
  # Building the gem will use the local file mode, so ensure it's world-readable.
20
20
  # https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud/issues/53
21
- desc 'Check plugin file permissions'
22
- task :check_perms do
23
- plugin = 'lib/fluent/plugin/out_google_cloud.rb'
24
- mode = File.stat(plugin).mode & 0777
25
- fail "Unexpected mode #{mode.to_s(8)} for #{plugin}" unless
26
- mode & 0444 == 0444
21
+ desc 'Fix file permissions'
22
+ task :fix_perms do
23
+ files = [
24
+ 'lib/fluent/plugin/out_google_cloud.rb',
25
+ 'lib/google/**/*.rb'
26
+ ].flat_map do |file|
27
+ file.include?('*') ? Dir.glob(file) : [file]
28
+ end
29
+
30
+ files.each do |file|
31
+ mode = File.stat(file).mode & 0777
32
+ next unless mode & 0444 != 0444
33
+ puts "Changing mode of #{file} from #{mode.to_s(8)} to "\
34
+ "#{(mode | 0444).to_s(8)}"
35
+ chmod mode | 0444, file
36
+ end
27
37
  end
28
38
 
29
39
  desc 'Run unit tests and RuboCop to check for style violations'
30
- task all: [:test, :rubocop, :check_perms]
40
+ task all: [:test, :rubocop, :fix_perms]
31
41
 
32
42
  task default: :all
@@ -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.2'
13
+ gem.version = '0.5.3.grpc.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')
@@ -20,8 +20,10 @@ eos
20
20
  gem.require_paths = ['lib']
21
21
 
22
22
  gem.add_runtime_dependency 'fluentd', '~> 0.10', '<= 0.13'
23
+ gem.add_runtime_dependency 'googleapis-common-protos', '~> 1.1'
23
24
  gem.add_runtime_dependency 'google-api-client', '> 0.9'
24
25
  gem.add_runtime_dependency 'googleauth', '~> 0.4'
26
+ gem.add_runtime_dependency 'grpc', '~> 0.15'
25
27
  gem.add_runtime_dependency 'json', '~> 1.8'
26
28
 
27
29
  gem.add_development_dependency 'mocha', '~> 1.1'
@@ -11,6 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ require 'grpc'
14
15
  require 'json'
15
16
  require 'open-uri'
16
17
  require 'socket'
@@ -18,6 +19,9 @@ require 'time'
18
19
  require 'yaml'
19
20
  require 'google/apis'
20
21
  require 'google/apis/logging_v1beta3'
22
+ require 'google/logging/v1/logging'
23
+ require 'google/logging/v1/logging_services'
24
+ require 'google/logging/v1/log_entry'
21
25
  require 'googleauth'
22
26
 
23
27
  module Fluent
@@ -26,7 +30,7 @@ module Fluent
26
30
  Fluent::Plugin.register_output('google_cloud', self)
27
31
 
28
32
  PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
29
- PLUGIN_VERSION = '0.5.2'
33
+ PLUGIN_VERSION = '0.5.3.grpc.alpha.1'
30
34
 
31
35
  # Constants for service names.
32
36
  APPENGINE_SERVICE = 'appengine.googleapis.com'
@@ -114,6 +118,10 @@ module Fluent
114
118
  # }
115
119
  config_param :labels, :hash, :default => nil
116
120
 
121
+ # Whether to use gRPC instead of REST/JSON to communicate to the
122
+ # Cloud Logging API.
123
+ config_param :use_grpc, :bool, :default => false
124
+
117
125
  # DEPRECATED: The following parameters, if present in the config
118
126
  # indicate that the plugin configuration must be updated.
119
127
  config_param :auth_method, :string, :default => nil
@@ -348,23 +356,34 @@ module Fluent
348
356
  end
349
357
  end
350
358
  end
359
+
351
360
  arr.each do |time, record|
352
361
  next unless record.is_a?(Hash)
353
362
 
354
- entry = Google::Apis::LoggingV1beta3::LogEntry.new(
355
- metadata: Google::Apis::LoggingV1beta3::LogEntryMetadata.new(
356
- service_name: @service_name,
357
- project_id: @project_id,
358
- zone: @zone,
359
- labels: {}
360
- ))
363
+ if @use_grpc
364
+ entry = Google::Logging::V1::LogEntry.new(
365
+ metadata: Google::Logging::V1::LogEntryMetadata.new(
366
+ service_name: @service_name.encode('utf-8'),
367
+ project_id: @project_id.encode('utf-8'),
368
+ zone: @zone.encode('utf-8'),
369
+ labels: {}
370
+ ))
371
+ else
372
+ entry = Google::Apis::LoggingV1beta3::LogEntry.new(
373
+ metadata: Google::Apis::LoggingV1beta3::LogEntryMetadata.new(
374
+ service_name: @service_name,
375
+ project_id: @project_id,
376
+ zone: @zone,
377
+ labels: {}
378
+ ))
379
+ end
361
380
 
362
381
  if @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
363
382
  @cloudfunctions_log_match =
364
383
  @cloudfunctions_log_regexp.match(record['log'])
365
384
  end
366
385
  if @service_name == CONTAINER_SERVICE
367
- # Move the stdout/stderr annotation from the record into a label
386
+ # Move the stdout/stderr annotation from the record into a label.
368
387
  field_to_label(record, 'stream', entry.metadata.labels,
369
388
  "#{CONTAINER_SERVICE}/stream")
370
389
  # If the record has been annotated by the kubernetes_metadata_filter
@@ -397,9 +416,28 @@ module Fluent
397
416
  end
398
417
  end
399
418
 
400
- set_timestamp(record, entry, time)
401
- set_severity(record, entry)
402
- set_http_request(record, entry)
419
+ ts_secs, ts_nanos = compute_timestamp(record, time)
420
+ if @use_grpc
421
+ entry.metadata.timestamp = Google::Protobuf::Timestamp.new(
422
+ seconds: ts_secs,
423
+ nanos: ts_nanos
424
+ )
425
+
426
+ entry.metadata.severity =
427
+ grpc_severity(compute_severity(record, entry))
428
+
429
+ set_http_request_grpc(record, entry) # FIXME
430
+ else
431
+ entry.metadata.timestamp = {
432
+ seconds: ts_secs,
433
+ nanos: ts_nanos
434
+ }
435
+
436
+ entry.metadata.severity =
437
+ compute_severity(record, entry)
438
+
439
+ set_http_request(record, entry)
440
+ end
403
441
 
404
442
  # If a field is present in the label_map, send its value as a label
405
443
  # (mapping the field name to label name as specified in the config)
@@ -417,8 +455,12 @@ module Fluent
417
455
  @cloudfunctions_log_match['execution_id']
418
456
  end
419
457
 
420
- set_payload(record, entry, is_json)
421
- entry.metadata.labels = nil if entry.metadata.labels.empty?
458
+ if @use_grpc
459
+ set_payload_grpc(record, entry, is_json)
460
+ else
461
+ set_payload(record, entry, is_json)
462
+ entry.metadata.labels = nil if entry.metadata.labels.empty?
463
+ end
422
464
 
423
465
  entries.push(entry)
424
466
  end
@@ -427,52 +469,449 @@ module Fluent
427
469
 
428
470
  log_name = log_name(tag, labels)
429
471
 
430
- begin
431
- # Does the actual write to the cloud logging api.
432
- # The URI of the write is constructed by the Google::Api request;
433
- # it is equivalent to this URL:
434
- # 'https://logging.googleapis.com/v1beta3/projects/' \
435
- # "#{@project_id}/logs/#{log_name}/entries:write"
436
-
437
- client = api_client
438
-
439
- write_request = \
440
- Google::Apis::LoggingV1beta3::WriteLogEntriesRequest.new(
441
- common_labels: labels,
442
- entries: entries)
443
-
444
- # TODO: RequestOptions
445
- client.write_log_entries(@project_id, log_name, write_request)
446
-
447
- # Let the user explicitly know when the first call succeeded,
448
- # to aid with verification and troubleshooting.
449
- unless @successful_call
450
- @successful_call = true
451
- @log.info 'Successfully sent to Google Cloud Logging API.'
472
+ if @use_grpc
473
+ begin
474
+ # Does the actual write to the cloud logging api.
475
+
476
+ client = api_client
477
+
478
+ labels_utf8_pairs = labels.map do |k, v|
479
+ [k.encode('utf-8'), v.encode('utf-8')]
480
+ end
481
+ utf8_log_name = log_name.encode('utf-8')
482
+
483
+ write_request = Google::Logging::V1::WriteLogEntriesRequest.new(
484
+ log_name: "projects/#{@project_id}/logs/#{utf8_log_name}",
485
+ common_labels: Hash[labels_utf8_pairs],
486
+ entries: entries
487
+ )
488
+
489
+ client.write_log_entries(write_request)
490
+
491
+ # Let the user explicitly know when the first call succeeded,
492
+ # to aid with verification and troubleshooting.
493
+ unless @successful_call
494
+ @successful_call = true
495
+ @log.info 'Successfully sent gRPC to Google Cloud Logging API.'
496
+ end
497
+
498
+ rescue GRPC::Cancelled => error
499
+ # RPC cancelled, so retry via re-raising the error.
500
+ raise error
501
+
502
+ rescue GRPC::BadStatus => error
503
+ case error.code
504
+ when GRPC::Core::StatusCodes::CANCELLED,
505
+ GRPC::Core::StatusCodes::UNAVAILABLE,
506
+ GRPC::Core::StatusCodes::DEADLINE_EXCEEDED,
507
+ GRPC::Core::StatusCodes::INTERNAL,
508
+ GRPC::Core::StatusCodes::UNKNOWN
509
+ # TODO
510
+ # Server error, so retry via re-raising the error.
511
+ raise error
512
+ when GRPC::Core::StatusCodes::UNIMPLEMENTED,
513
+ GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED
514
+ # Most client errors indicate a problem with the request itself
515
+ # and should not be retried.
516
+ dropped = entries.length
517
+ @log.warn "Dropping #{dropped} log message(s)",
518
+ error: error.to_s, error_code: error.code.to_s
519
+ when GRPC::Core::StatusCodes::UNAUTHENTICATED
520
+ # Authorization error.
521
+ # These are usually solved via a `gcloud auth` call, or by
522
+ # modifying the permissions on the Google Cloud project.
523
+ dropped = entries.length
524
+ @log.warn "Dropping #{dropped} log message(s)",
525
+ error: error.to_s, error_code: error.code.to_s
526
+ else
527
+ # Assume this is a problem with the request itself
528
+ # and don't retry.
529
+ dropped = entries.length
530
+ @log.error "Unknown response code #{error.code} from the "\
531
+ "server, dropping #{dropped} log message(s)",
532
+ error: error.to_s, error_code: error.code.to_s
533
+ end
452
534
  end
535
+ else
536
+ begin
537
+ # Does the actual write to the cloud logging api.
538
+
539
+ client = api_client
540
+
541
+ # The URI of the write is constructed by the Google::Api request;
542
+ # it is equivalent to this URL:
543
+ # 'https://logging.googleapis.com/v1beta3/projects/' \
544
+ # "#{@project_id}/logs/#{log_name}/entries:write"
545
+ write_request = \
546
+ Google::Apis::LoggingV1beta3::WriteLogEntriesRequest.new(
547
+ common_labels: labels,
548
+ entries: entries)
549
+
550
+ # TODO: RequestOptions
551
+ client.write_log_entries(@project_id, log_name, write_request)
552
+
553
+ # Let the user explicitly know when the first call succeeded,
554
+ # to aid with verification and troubleshooting.
555
+ unless @successful_call
556
+ @successful_call = true
557
+ @log.info 'Successfully sent to Google Cloud Logging API.'
558
+ end
453
559
 
454
- rescue Google::Apis::ServerError => error
455
- # Server error, so retry via re-raising the error.
456
- raise error
457
-
458
- rescue Google::Apis::AuthorizationError => error
459
- # Authorization error.
460
- # These are usually solved via a `gcloud auth` call, or by modifying
461
- # the permissions on the Google Cloud project.
462
- dropped = entries.length
463
- @log.warn "Dropping #{dropped} log message(s)",
464
- error_class: error.class.to_s, error: error.to_s
465
-
466
- rescue Google::Apis::ClientError => error
467
- # Most ClientErrors indicate a problem with the request itself and
468
- # should not be retried.
469
- dropped = entries.length
470
- @log.warn "Dropping #{dropped} log message(s)",
471
- error_class: error.class.to_s, error: error.to_s
560
+ rescue Google::Apis::ServerError => error
561
+ # Server error, so retry via re-raising the error.
562
+ raise error
563
+
564
+ rescue Google::Apis::AuthorizationError => error
565
+ # Authorization error.
566
+ # These are usually solved via a `gcloud auth` call, or by modifying
567
+ # the permissions on the Google Cloud project.
568
+ dropped = entries.length
569
+ @log.warn "Dropping #{dropped} log message(s)",
570
+ error_class: error.class.to_s, error: error.to_s
571
+
572
+ rescue Google::Apis::ClientError => error
573
+ # Most ClientErrors indicate a problem with the request itself and
574
+ # should not be retried.
575
+ dropped = entries.length
576
+ @log.warn "Dropping #{dropped} log message(s)",
577
+ error_class: error.class.to_s, error: error.to_s
578
+ end
472
579
  end
473
580
  end
474
581
  end
475
582
 
583
+ # def old_write(chunk)
584
+ # # Group the entries since we have to make one call per tag.
585
+ # grouped_entries = {}
586
+ # chunk.msgpack_each do |tag, *arr|
587
+ # grouped_entries[tag] = [] unless grouped_entries.key?(tag)
588
+ # grouped_entries[tag].push(arr)
589
+ # end
590
+ #
591
+ # grouped_entries.each do |tag, arr|
592
+ # entries = []
593
+ # labels = @common_labels.clone
594
+ #
595
+ # if @running_cloudfunctions
596
+ # # If the current group of entries is coming from a Cloud Functions
597
+ # # function, the function name can be extracted from the tag.
598
+ # match_data = @cloudfunctions_tag_regexp.match(tag)
599
+ # if match_data
600
+ # # Service name is set to Cloud Functions only for logs actually
601
+ # # coming from a function.
602
+ # @service_name = CLOUDFUNCTIONS_SERVICE
603
+ # labels["#{CLOUDFUNCTIONS_SERVICE}/region"] = @gcf_region
604
+ # labels["#{CLOUDFUNCTIONS_SERVICE}/function_name"] =
605
+ # decode_cloudfunctions_function_name(
606
+ # match_data['encoded_function_name'])
607
+ # else
608
+ # # Other logs are considered as coming from the Container Engine
609
+ # # service.
610
+ # @service_name = CONTAINER_SERVICE
611
+ # end
612
+ # end
613
+ # if @service_name == CONTAINER_SERVICE && \
614
+ # @compiled_kubernetes_tag_regexp
615
+ # # Container logs in Kubernetes are tagged based on where they came
616
+ # # from, so we can extract useful metadata from the tag.
617
+ # # Do this here to avoid having to repeat it for each record.
618
+ # match_data = @compiled_kubernetes_tag_regexp.match(tag)
619
+ # if match_data
620
+ # %w(namespace_name pod_name container_name).each do |field|
621
+ # labels["#{CONTAINER_SERVICE}/#{field}"] = match_data[field]
622
+ # end
623
+ # end
624
+ # end
625
+ #
626
+ # if @use_grpc
627
+ # arr.each do |time, record|
628
+ # next unless record.is_a?(Hash)
629
+ #
630
+ # entry = Google::Logging::V1::LogEntry.new(
631
+ # metadata: Google::Logging::V1::LogEntryMetadata.new(
632
+ # service_name: @service_name.encode('utf-8'),
633
+ # project_id: @project_id.encode('utf-8'),
634
+ # zone: @zone.encode('utf-8'),
635
+ # labels: {}
636
+ # ))
637
+ #
638
+ # if @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
639
+ # @cloudfunctions_log_match =
640
+ # @cloudfunctions_log_regexp.match(record['log'])
641
+ # end
642
+ # if @service_name == CONTAINER_SERVICE
643
+ # # Move the stdout/stderr annotation from the record into a
644
+ # # label.
645
+ # field_to_label(record, 'stream', entry.metadata.labels,
646
+ # "#{CONTAINER_SERVICE}/stream")
647
+ # # If the record has been annotated by the
648
+ # # kubernetes_metadata_filter
649
+ # # plugin, then use that metadata. Otherwise, rely on
650
+ # # commonLabels
651
+ # # populated at the grouped_entries level from the group's tag.
652
+ # if record.key?('kubernetes')
653
+ # handle_container_metadata(record, entry)
654
+ # end
655
+ #
656
+ # # Save the timestamp if available, then clear it out to allow
657
+ # # for determining whether we should parse the log or message
658
+ # # field.
659
+ # timestamp = record.key?('time') ? record['time'] : nil
660
+ # record.delete('time')
661
+ # # If the log is json, we want to export it as a structured log
662
+ # # unless there is additional metadata that would be lost.
663
+ # is_json = false
664
+ # if record.length == 1 && record.key?('log')
665
+ # record_json = parse_json_or_nil(record['log'])
666
+ # end
667
+ # if record.length == 1 && record.key?('message')
668
+ # record_json = parse_json_or_nil(record['message'])
669
+ # end
670
+ # unless record_json.nil?
671
+ # record = record_json
672
+ # is_json = true
673
+ # end
674
+ # # Restore timestamp if necessary.
675
+ # unless record.key?('time') || timestamp.nil?
676
+ # record['time'] = timestamp
677
+ # end
678
+ # end
679
+ #
680
+ # ts_secs, ts_nanos = compute_timestamp(record, time)
681
+ # entry.metadata.timestamp = Google::Protobuf::Timestamp.new(
682
+ # seconds: ts_secs,
683
+ # nanos: ts_nanos
684
+ # )
685
+ #
686
+ # entry.metadata.severity =
687
+ # grpc_severity(compute_severity(record, entry))
688
+ #
689
+ # set_http_request_grpc(record, entry) # FIXME
690
+ #
691
+ # # If a field is present in the label_map, send its value as a
692
+ # # label
693
+ # # (mapping the field name to label name as specified in the
694
+ # # config)
695
+ # # and do not send that field as part of the payload.
696
+ # if @label_map
697
+ # @label_map.each do |field, label|
698
+ # field_to_label(record, field, entry.metadata.labels, label)
699
+ # end
700
+ # end
701
+ #
702
+ # if @service_name == CLOUDFUNCTIONS_SERVICE &&
703
+ # @cloudfunctions_log_match &&
704
+ # @cloudfunctions_log_match['execution_id']
705
+ # entry.metadata.labels['execution_id'] =
706
+ # @cloudfunctions_log_match['execution_id']
707
+ # end
708
+ #
709
+ # set_payload_grpc(record, entry, is_json)
710
+ #
711
+ # entries.push(entry)
712
+ # end
713
+ # # Don't send an empty request if we rejected all the entries.
714
+ # next if entries.empty?
715
+ #
716
+ # log_name = log_name(tag, labels)
717
+ #
718
+ # begin
719
+ # # Does the actual write to the cloud logging api.
720
+ #
721
+ # client = api_client
722
+ #
723
+ # labels_utf8_pairs = labels.map do |k, v|
724
+ # [k.encode('utf-8'), v.encode('utf-8')]
725
+ # end
726
+ #
727
+ # write_request = Google::Logging::V1::WriteLogEntriesRequest.new(
728
+ # log_name: log_name.encode('utf-8'),
729
+ # common_labels: Hash[labels_utf8_pairs],
730
+ # entries: entries
731
+ # )
732
+ #
733
+ # client.write_log_entries(write_request)
734
+ #
735
+ # # Let the user explicitly know when the first call succeeded,
736
+ # # to aid with verification and troubleshooting.
737
+ # unless @successful_call
738
+ # @successful_call = true
739
+ # @log.info 'Successfully sent gRPC to Google Cloud Logging API.'
740
+ # end
741
+ #
742
+ # rescue GRPC::Cancelled => error
743
+ # # RPC cancelled, so retry via re-raising the error.
744
+ # raise error
745
+ #
746
+ # rescue GRPC::BadStatus => error
747
+ # case error.code
748
+ # when GRPC::Core::StatusCodes::CANCELLED,
749
+ # GRPC::Core::StatusCodes::UNAVAILABLE,
750
+ # GRPC::Core::StatusCodes::DEADLINE_EXCEEDED,
751
+ # GRPC::Core::StatusCodes::INTERNAL,
752
+ # GRPC::Core::StatusCodes::UNKNOWN
753
+ # # TODO
754
+ # # Server error, so retry via re-raising the error.
755
+ # raise error
756
+ # when GRPC::Core::StatusCodes::UNIMPLEMENTED,
757
+ # GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED
758
+ # # Most client errors indicate a problem with the request itself
759
+ # # and should not be retried.
760
+ # dropped = entries.length
761
+ # @log.warn "Dropping #{dropped} log message(s)",
762
+ # error: error.to_s, error_code: error.code.to_s
763
+ # when GRPC::Core::StatusCodes::UNAUTHENTICATED
764
+ # # Authorization error.
765
+ # # These are usually solved via a `gcloud auth` call, or by
766
+ # # modifying the permissions on the Google Cloud project.
767
+ # dropped = entries.length
768
+ # @log.warn "Dropping #{dropped} log message(s)",
769
+ # error: error.to_s, error_code: error.code.to_s
770
+ # else
771
+ # @log.error "Unknown response code #{error.code} from the " \
772
+ # "server",
773
+ # error: error.to_s, error_code: error.code.to_s
774
+ # end
775
+ # end
776
+ # else
777
+ # arr.each do |time, record|
778
+ # next unless record.is_a?(Hash)
779
+ #
780
+ # entry = Google::Apis::LoggingV1beta3::LogEntry.new(
781
+ # metadata: Google::Apis::LoggingV1beta3::LogEntryMetadata.new(
782
+ # service_name: @service_name,
783
+ # project_id: @project_id,
784
+ # zone: @zone,
785
+ # labels: {}
786
+ # ))
787
+ #
788
+ # if @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
789
+ # @cloudfunctions_log_match =
790
+ # @cloudfunctions_log_regexp.match(record['log'])
791
+ # end
792
+ # if @service_name == CONTAINER_SERVICE
793
+ # # Move the stdout/stderr annotation from the record into a label
794
+ # field_to_label(record, 'stream', entry.metadata.labels,
795
+ # "#{CONTAINER_SERVICE}/stream")
796
+ # # If the record has been annotated by the
797
+ # # kubernetes_metadata_filter
798
+ # # plugin, then use that metadata. Otherwise, rely on
799
+ # # commonLabels
800
+ # # populated at the grouped_entries level from the group's tag.
801
+ # if record.key?('kubernetes')
802
+ # handle_container_metadata(record, entry)
803
+ # end
804
+ #
805
+ # # Save the timestamp if available, then clear it out to allow
806
+ # # for determining whether we should parse the log or message
807
+ # # field.
808
+ # timestamp = record.key?('time') ? record['time'] : nil
809
+ # record.delete('time')
810
+ # # If the log is json, we want to export it as a structured log
811
+ # # unless there is additional metadata that would be lost.
812
+ # is_json = false
813
+ # if record.length == 1 && record.key?('log')
814
+ # record_json = parse_json_or_nil(record['log'])
815
+ # end
816
+ # if record.length == 1 && record.key?('message')
817
+ # record_json = parse_json_or_nil(record['message'])
818
+ # end
819
+ # unless record_json.nil?
820
+ # record = record_json
821
+ # is_json = true
822
+ # end
823
+ # # Restore timestamp if necessary.
824
+ # unless record.key?('time') || timestamp.nil?
825
+ # record['time'] = timestamp
826
+ # end
827
+ # end
828
+ #
829
+ # ts_secs, ts_nanos = compute_timestamp(record, time)
830
+ # entry.metadata.timestamp = {
831
+ # seconds: ts_secs,
832
+ # nanos: ts_nanos
833
+ # }
834
+ #
835
+ # entry.metadata.severity = compute_severity(record, entry)
836
+ #
837
+ # set_http_request(record, entry)
838
+ #
839
+ # # If a field is present in the label_map, send its value as a
840
+ # # label
841
+ # # (mapping the field name to label name as specified in the
842
+ # # config)
843
+ # # and do not send that field as part of the payload.
844
+ # if @label_map
845
+ # @label_map.each do |field, label|
846
+ # field_to_label(record, field, entry.metadata.labels, label)
847
+ # end
848
+ # end
849
+ #
850
+ # if @service_name == CLOUDFUNCTIONS_SERVICE &&
851
+ # @cloudfunctions_log_match &&
852
+ # @cloudfunctions_log_match['execution_id']
853
+ # entry.metadata.labels['execution_id'] =
854
+ # @cloudfunctions_log_match['execution_id']
855
+ # end
856
+ #
857
+ # set_payload(record, entry, is_json)
858
+ # entry.metadata.labels = nil if entry.metadata.labels.empty?
859
+ #
860
+ # entries.push(entry)
861
+ # end
862
+ # # Don't send an empty request if we rejected all the entries.
863
+ # next if entries.empty?
864
+ #
865
+ # log_name = log_name(tag, labels)
866
+ #
867
+ # begin
868
+ # # Does the actual write to the cloud logging api.
869
+ # # The URI of the write is constructed by the Google::Api request;
870
+ # # it is equivalent to this URL:
871
+ # # 'https://logging.googleapis.com/v1beta3/projects/' \
872
+ # # "#{@project_id}/logs/#{log_name}/entries:write"
873
+ #
874
+ # client = api_client
875
+ #
876
+ # write_request = \
877
+ # Google::Apis::LoggingV1beta3::WriteLogEntriesRequest.new(
878
+ # common_labels: labels,
879
+ # entries: entries)
880
+ #
881
+ # # TODO: RequestOptions
882
+ # client.write_log_entries(@project_id, log_name, write_request)
883
+ #
884
+ # # Let the user explicitly know when the first call succeeded,
885
+ # # to aid with verification and troubleshooting.
886
+ # unless @successful_call
887
+ # @successful_call = true
888
+ # @log.info 'Successfully sent to Google Cloud Logging API.'
889
+ # end
890
+ #
891
+ # rescue Google::Apis::ServerError => error
892
+ # # Server error, so retry via re-raising the error.
893
+ # raise error
894
+ #
895
+ # rescue Google::Apis::AuthorizationError => error
896
+ # # Authorization error.
897
+ # # These are usually solved via a `gcloud auth` call, or by
898
+ # # modifying
899
+ # # the permissions on the Google Cloud project.
900
+ # dropped = entries.length
901
+ # @log.warn "Dropping #{dropped} log message(s)",
902
+ # error_class: error.class.to_s, error: error.to_s
903
+ #
904
+ # rescue Google::Apis::ClientError => error
905
+ # # Most ClientErrors indicate a problem with the request itself and
906
+ # # should not be retried.
907
+ # dropped = entries.length
908
+ # @log.warn "Dropping #{dropped} log message(s)",
909
+ # error_class: error.class.to_s, error: error.to_s
910
+ # end
911
+ # end
912
+ # end
913
+ # end
914
+
476
915
  private
477
916
 
478
917
  def parse_json_or_nil(input)
@@ -602,7 +1041,7 @@ module Fluent
602
1041
  instance_prefix
603
1042
  end
604
1043
 
605
- def set_timestamp(record, entry, time)
1044
+ def compute_timestamp(record, time)
606
1045
  if record.key?('timestamp') &&
607
1046
  record['timestamp'].is_a?(Hash) &&
608
1047
  record['timestamp'].key?('seconds') &&
@@ -645,41 +1084,36 @@ module Fluent
645
1084
  ts_secs = timestamp.tv_sec
646
1085
  ts_nanos = timestamp.tv_nsec
647
1086
  end
648
- entry.metadata.timestamp = {
649
- seconds: ts_secs,
650
- nanos: ts_nanos
651
- }
1087
+ [ts_secs, ts_nanos]
652
1088
  end
653
1089
 
654
- def set_severity(record, entry)
1090
+ def compute_severity(record, entry)
655
1091
  if @service_name == CLOUDFUNCTIONS_SERVICE
656
1092
  if @cloudfunctions_log_match && @cloudfunctions_log_match['severity']
657
- entry.metadata.severity =
658
- parse_severity(@cloudfunctions_log_match['severity'])
1093
+ return parse_severity(@cloudfunctions_log_match['severity'])
659
1094
  elsif record.key?('stream') && record['stream'] == 'stdout'
660
- entry.metadata.severity = 'INFO'
661
1095
  record.delete('stream')
1096
+ return 'INFO'
662
1097
  elsif record.key?('stream') && record['stream'] == 'stderr'
663
- entry.metadata.severity = 'ERROR'
664
1098
  record.delete('stream')
1099
+ return 'ERROR'
665
1100
  else
666
- entry.metadata.severity = 'DEFAULT'
1101
+ return 'DEFAULT'
667
1102
  end
668
1103
  elsif record.key?('severity')
669
- entry.metadata.severity = parse_severity(record['severity'])
670
- record.delete('severity')
1104
+ return parse_severity(record.delete('severity'))
671
1105
  elsif @service_name == CONTAINER_SERVICE && \
672
1106
  entry.metadata.labels.key?("#{CONTAINER_SERVICE}/stream")
673
1107
  stream = entry.metadata.labels["#{CONTAINER_SERVICE}/stream"]
674
1108
  if stream == 'stdout'
675
- entry.metadata.severity = 'INFO'
1109
+ return 'INFO'
676
1110
  elsif stream == 'stderr'
677
- entry.metadata.severity = 'ERROR'
1111
+ return 'ERROR'
678
1112
  else
679
- entry.metadata.severity = 'DEFAULT'
1113
+ return 'DEFAULT'
680
1114
  end
681
1115
  else
682
- entry.metadata.severity = 'DEFAULT'
1116
+ return 'DEFAULT'
683
1117
  end
684
1118
  end
685
1119
 
@@ -702,6 +1136,25 @@ module Fluent
702
1136
  entry.http_request = output
703
1137
  end
704
1138
 
1139
+ def set_http_request_grpc(record, entry)
1140
+ return nil unless record['httpRequest'].is_a?(Hash)
1141
+ input = record['httpRequest']
1142
+ output = Google::Logging::Type::HttpRequest.new
1143
+ output.request_method = input.delete('requestMethod')
1144
+ output.request_url = input.delete('requestUrl')
1145
+ output.request_size = input.delete('requestSize').to_i
1146
+ output.status = input.delete('status').to_i
1147
+ output.response_size = input.delete('responseSize').to_i
1148
+ output.user_agent = input.delete('userAgent')
1149
+ output.remote_ip = input.delete('remoteIp')
1150
+ output.referer = input.delete('referer')
1151
+ output.cache_hit = input.delete('cacheHit') == 'true'
1152
+ output.validated_with_origin_server = \
1153
+ input.delete('validatedWithOriginServer') == 'true'
1154
+ record.delete('httpRequest') if input.empty?
1155
+ entry.http_request = output
1156
+ end
1157
+
705
1158
  # Values permitted by the API for 'severity' (which is an enum).
706
1159
  VALID_SEVERITIES = Set.new(
707
1160
  %w(DEFAULT DEBUG INFO NOTICE WARNING ERROR CRITICAL ALERT EMERGENCY))
@@ -762,6 +1215,38 @@ module Fluent
762
1215
  'DEFAULT'
763
1216
  end
764
1217
 
1218
+ GRPC_SEVERITY_MAPPING = {
1219
+ 'DEFAULT' => Google::Logging::Type::LogSeverity::DEFAULT,
1220
+ 'DEBUG' => Google::Logging::Type::LogSeverity::DEBUG,
1221
+ 'INFO' => Google::Logging::Type::LogSeverity::INFO,
1222
+ 'NOTICE' => Google::Logging::Type::LogSeverity::NOTICE,
1223
+ 'WARNING' => Google::Logging::Type::LogSeverity::WARNING,
1224
+ 'ERROR' => Google::Logging::Type::LogSeverity::ERROR,
1225
+ 'CRITICAL' => Google::Logging::Type::LogSeverity::CRITICAL,
1226
+ 'ALERT' => Google::Logging::Type::LogSeverity::ALERT,
1227
+ 'EMERGENCY' => Google::Logging::Type::LogSeverity::EMERGENCY,
1228
+ 0 => Google::Logging::Type::LogSeverity::DEFAULT,
1229
+ 100 => Google::Logging::Type::LogSeverity::DEBUG,
1230
+ 200 => Google::Logging::Type::LogSeverity::INFO,
1231
+ 300 => Google::Logging::Type::LogSeverity::NOTICE,
1232
+ 400 => Google::Logging::Type::LogSeverity::WARNING,
1233
+ 500 => Google::Logging::Type::LogSeverity::ERROR,
1234
+ 600 => Google::Logging::Type::LogSeverity::CRITICAL,
1235
+ 700 => Google::Logging::Type::LogSeverity::ALERT,
1236
+ 800 => Google::Logging::Type::LogSeverity::EMERGENCY
1237
+ }
1238
+
1239
+ def grpc_severity(severity)
1240
+ # TODO: find out why this doesn't work.
1241
+ # if severity.is_a? String
1242
+ # return Google::Logging::Type::LogSeverity.resolve(severity)
1243
+ # end
1244
+ if GRPC_SEVERITY_MAPPING.key?(severity)
1245
+ return GRPC_SEVERITY_MAPPING[severity]
1246
+ end
1247
+ severity
1248
+ end
1249
+
765
1250
  def decode_cloudfunctions_function_name(function_name)
766
1251
  function_name.gsub(/c\.[a-z]/) { |s| s.upcase[-1] }
767
1252
  .gsub('u.u', '_').gsub('d.d', '$').gsub('a.a', '@').gsub('p.p', '.')
@@ -815,6 +1300,72 @@ module Fluent
815
1300
  end
816
1301
  end
817
1302
 
1303
+ def value_from_ruby(value)
1304
+ ret = Google::Protobuf::Value.new
1305
+ case value
1306
+ when NilClass
1307
+ ret.null_value = 0
1308
+ when Numeric
1309
+ ret.number_value = value
1310
+ when String
1311
+ ret.string_value = value.encode('utf-8')
1312
+ when TrueClass
1313
+ ret.bool_value = true
1314
+ when FalseClass
1315
+ ret.bool_value = false
1316
+ when Google::Protobuf::Struct
1317
+ ret.struct_value = value
1318
+ when Hash
1319
+ ret.struct_value = struct_from_ruby(value)
1320
+ when Google::Protobuf::ListValue
1321
+ ret.list_value = value
1322
+ when Array
1323
+ ret.list_value = list_from_ruby(value)
1324
+ else
1325
+ @log.error "Unknown type: #{value.class}"
1326
+ fail Google::Protobuf::Error, "Unknown type: #{value.class}"
1327
+ end
1328
+ ret
1329
+ end
1330
+
1331
+ def list_from_ruby(arr)
1332
+ ret = Google::Protobuf::ListValue.new
1333
+ arr.each do |v|
1334
+ ret.values << value_from_ruby(v)
1335
+ end
1336
+ ret
1337
+ end
1338
+
1339
+ def struct_from_ruby(hash)
1340
+ ret = Google::Protobuf::Struct.new
1341
+ hash.each do |k, v|
1342
+ ret.fields[k] ||= value_from_ruby(v)
1343
+ end
1344
+ ret
1345
+ end
1346
+
1347
+ def set_payload_grpc(record, entry, is_json)
1348
+ # If this is a Cloud Functions log that matched the expected regexp,
1349
+ # use text payload. Otherwise, use JSON if we found valid JSON, or text
1350
+ # payload in the following cases:
1351
+ # 1. This is a Cloud Functions log and the 'log' key is available
1352
+ # 2. This is an unstructured Container log and the 'log' key is available
1353
+ # 3. The only remaining key is 'message'
1354
+ if @service_name == CLOUDFUNCTIONS_SERVICE && @cloudfunctions_log_match
1355
+ entry.text_payload = @cloudfunctions_log_match['text']
1356
+ elsif @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
1357
+ entry.text_payload = record['log']
1358
+ elsif is_json
1359
+ entry.struct_payload = struct_from_ruby(record)
1360
+ elsif @service_name == CONTAINER_SERVICE && record.key?('log')
1361
+ entry.text_payload = record['log']
1362
+ elsif record.size == 1 && record.key?('message')
1363
+ entry.text_payload = record['message']
1364
+ else
1365
+ entry.struct_payload = struct_from_ruby(record)
1366
+ end
1367
+ end
1368
+
818
1369
  def log_name(tag, common_labels)
819
1370
  if @service_name == CLOUDFUNCTIONS_SERVICE
820
1371
  return 'cloud-functions'
@@ -833,6 +1384,7 @@ module Fluent
833
1384
  end
834
1385
 
835
1386
  def init_api_client
1387
+ return if @use_grpc
836
1388
  # TODO: Use a non-default ClientOptions object.
837
1389
  Google::Apis::ClientOptions.default.application_name = PLUGIN_NAME
838
1390
  Google::Apis::ClientOptions.default.application_version = PLUGIN_VERSION
@@ -842,14 +1394,23 @@ module Fluent
842
1394
  end
843
1395
 
844
1396
  def api_client
845
- unless @client.authorization.expired?
846
- begin
847
- @client.authorization.fetch_access_token!
848
- rescue MultiJson::ParseError
849
- # Workaround an issue in the API client; just re-raise a more
850
- # descriptive error for the user (which will still cause a retry).
851
- raise Google::APIClient::ClientError, 'Unable to fetch access ' \
852
- 'token (no scopes configured?)'
1397
+ if @use_grpc
1398
+ ssl_creds = GRPC::Core::ChannelCredentials.new
1399
+ authentication = Google::Auth.get_application_default
1400
+ creds = GRPC::Core::CallCredentials.new(authentication.updater_proc)
1401
+ creds = ssl_creds.compose(creds)
1402
+ @client = Google::Logging::V1::LoggingService::Stub.new(
1403
+ 'logging.googleapis.com', creds)
1404
+ else
1405
+ unless @client.authorization.expired?
1406
+ begin
1407
+ @client.authorization.fetch_access_token!
1408
+ rescue MultiJson::ParseError
1409
+ # Workaround an issue in the API client; just re-raise a more
1410
+ # descriptive error for the user (which will still cause a retry).
1411
+ raise Google::APIClient::ClientError, 'Unable to fetch access ' \
1412
+ 'token (no scopes configured?)'
1413
+ end
853
1414
  end
854
1415
  end
855
1416
  @client