fluent-plugin-google-cloud 0.5.2 → 0.5.3.grpc.alpha.1

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