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 +4 -4
- data/Gemfile.lock +9 -1
- data/Rakefile +17 -7
- data/fluent-plugin-google-cloud.gemspec +3 -1
- data/lib/fluent/plugin/out_google_cloud.rb +640 -79
- data/lib/google/logging/type/http_request.rb +30 -0
- data/lib/google/logging/type/log_severity.rb +26 -0
- data/lib/google/logging/v1/log_entry.rb +52 -0
- data/lib/google/logging/v1/logging.rb +84 -0
- data/lib/google/logging/v1/logging_services.rb +150 -0
- data/test/plugin/test_out_google_cloud.rb +14 -0
- metadata +37 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4289702253d986d7711b6e0a97712b13702901c3
|
4
|
+
data.tar.gz: aac7a196ff97e4c70234772ad5755861d80549aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4eb63be9e1eb8492ff627270ba13ab5b2d1cbe4dc1c01a0e2b7b14cc3192084d3d477ee9338c25c3af3ab8875e2203ee8e0d7fac2ddbda52fbbc6c5983959a16
|
7
|
+
data.tar.gz: 8fd692a696fec2163b7c1935cf68e488d3eb4d780ebe90bd5e22f53d4e06cab5d2dbc8dedca419a8218019ce13eacf9d15eebe76a71af40ad02514dec80887dc
|
data/Gemfile.lock
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fluent-plugin-google-cloud (0.5.
|
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 '
|
22
|
-
task :
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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, :
|
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.
|
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.
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
421
|
-
|
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
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
@
|
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
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
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
|
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
|
-
|
649
|
-
seconds: ts_secs,
|
650
|
-
nanos: ts_nanos
|
651
|
-
}
|
1087
|
+
[ts_secs, ts_nanos]
|
652
1088
|
end
|
653
1089
|
|
654
|
-
def
|
1090
|
+
def compute_severity(record, entry)
|
655
1091
|
if @service_name == CLOUDFUNCTIONS_SERVICE
|
656
1092
|
if @cloudfunctions_log_match && @cloudfunctions_log_match['severity']
|
657
|
-
|
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
|
-
|
1101
|
+
return 'DEFAULT'
|
667
1102
|
end
|
668
1103
|
elsif record.key?('severity')
|
669
|
-
|
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
|
-
|
1109
|
+
return 'INFO'
|
676
1110
|
elsif stream == 'stderr'
|
677
|
-
|
1111
|
+
return 'ERROR'
|
678
1112
|
else
|
679
|
-
|
1113
|
+
return 'DEFAULT'
|
680
1114
|
end
|
681
1115
|
else
|
682
|
-
|
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
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
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
|