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

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1184 @@
1
+ # Copyright 2014 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'grpc'
15
+ require 'json'
16
+ require 'open-uri'
17
+ require 'socket'
18
+ require 'time'
19
+ require 'yaml'
20
+ require 'google/apis'
21
+ require 'google/apis/logging_v1beta3'
22
+ require 'google/logging/v1/logging_pb'
23
+ require 'google/logging/v1/logging_services_pb'
24
+ require 'google/logging/v1/log_entry_pb'
25
+ require 'googleauth'
26
+
27
+ module Google
28
+ module Protobuf
29
+ # Alias the has_key? method to have the same interface as a regular map.
30
+ class Map
31
+ alias_method :key?, :has_key?
32
+ end
33
+ end
34
+ end
35
+
36
+ module Fluent
37
+ # fluentd output plugin for the Stackdriver Logging API
38
+ class GoogleCloudOutput < BufferedOutput
39
+ Fluent::Plugin.register_output('google_cloud', self)
40
+
41
+ PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
42
+ PLUGIN_VERSION = '0.5.5'
43
+
44
+ # Constants for service names.
45
+ APPENGINE_SERVICE = 'appengine.googleapis.com'
46
+ CLOUDFUNCTIONS_SERVICE = 'cloudfunctions.googleapis.com'
47
+ COMPUTE_SERVICE = 'compute.googleapis.com'
48
+ CONTAINER_SERVICE = 'container.googleapis.com'
49
+ EC2_SERVICE = 'ec2.amazonaws.com'
50
+
51
+ # Name of the the Google cloud logging write scope.
52
+ LOGGING_SCOPE = 'https://www.googleapis.com/auth/logging.write'
53
+
54
+ # Address of the metadata service.
55
+ METADATA_SERVICE_ADDR = '169.254.169.254'
56
+
57
+ # Disable this warning to conform to fluentd config_param conventions.
58
+ # rubocop:disable Style/HashSyntax
59
+
60
+ # Specify project/instance metadata.
61
+ #
62
+ # project_id, zone, and vm_id are required to have valid values, which
63
+ # can be obtained from the metadata service or set explicitly.
64
+ # Otherwise, the plugin will fail to initialize.
65
+ #
66
+ # Note that while 'project id' properly refers to the alphanumeric name
67
+ # of the project, the logging service will also accept the project number,
68
+ # so either one is acceptable in this context.
69
+ #
70
+ # Whether to attempt to obtain metadata from the local metadata service.
71
+ # It is safe to specify 'true' even on platforms with no metadata service.
72
+ config_param :use_metadata_service, :bool, :default => true
73
+ # These parameters override any values obtained from the metadata service.
74
+ config_param :project_id, :string, :default => nil
75
+ config_param :zone, :string, :default => nil
76
+ config_param :vm_id, :string, :default => nil
77
+ config_param :vm_name, :string, :default => nil
78
+
79
+ # Whether to try to detect if the VM is owned by a "subservice" such as App
80
+ # Engine of Kubernetes, rather than just associating the logs with the
81
+ # compute service of the platform. This currently only has any effect when
82
+ # running on GCE.
83
+ #
84
+ # The initial motivation for this is to separate out Kubernetes node
85
+ # component (Docker, Kubelet, etc.) logs from container logs.
86
+ config_param :detect_subservice, :bool, :default => true
87
+ # The subservice_name overrides the subservice detection, if provided.
88
+ config_param :subservice_name, :string, :default => nil
89
+
90
+ # Whether to reject log entries with invalid tags. If this option is set to
91
+ # false, tags will be made valid by converting any non-string tag to a
92
+ # string, and sanitizing any non-utf8 or other invalid characters.
93
+ config_param :require_valid_tags, :bool, :default => false
94
+
95
+ # The regular expression to use on Kubernetes logs to extract some basic
96
+ # information about the log source. The regex must contain capture groups
97
+ # for pod_name, namespace_name, and container_name.
98
+ config_param :kubernetes_tag_regexp, :string, :default =>
99
+ '\.(?<pod_name>[^_]+)_(?<namespace_name>[^_]+)_(?<container_name>.+)$'
100
+
101
+ # label_map (specified as a JSON object) is an unordered set of fluent
102
+ # field names whose values are sent as labels rather than as part of the
103
+ # struct payload.
104
+ #
105
+ # Each entry in the map is a {"field_name": "label_name"} pair. When
106
+ # the "field_name" (as parsed by the input plugin) is encountered, a label
107
+ # with the corresponding "label_name" is added to the log entry. The
108
+ # value of the field is used as the value of the label.
109
+ #
110
+ # The map gives the user additional flexibility in specifying label
111
+ # names, including the ability to use characters which would not be
112
+ # legal as part of fluent field names.
113
+ #
114
+ # Example:
115
+ # label_map {
116
+ # "field_name_1": "sent_label_name_1",
117
+ # "field_name_2": "some.prefix/sent_label_name_2"
118
+ # }
119
+ config_param :label_map, :hash, :default => nil
120
+
121
+ # labels (specified as a JSON object) is a set of custom labels
122
+ # provided at configuration time. It allows users to inject extra
123
+ # environmental information into every message or to customize
124
+ # labels otherwise detected automatically.
125
+ #
126
+ # Each entry in the map is a {"label_name": "label_value"} pair.
127
+ #
128
+ # Example:
129
+ # labels {
130
+ # "label_name_1": "label_value_1",
131
+ # "label_name_2": "label_value_2"
132
+ # }
133
+ config_param :labels, :hash, :default => nil
134
+
135
+ # Whether to use gRPC instead of REST/JSON to communicate to the
136
+ # Cloud Logging API.
137
+ config_param :use_grpc, :bool, :default => false
138
+
139
+ # Whether to allow non-UTF-8 characters in user logs. If set to true, any
140
+ # non-UTF-8 character would be replaced by the string specified by
141
+ # 'non_utf8_replacement_string'. If set to false, any non-UTF-8 character
142
+ # would trigger the plugin to error out.
143
+ config_param :coerce_to_utf8, :bool, :default => true
144
+
145
+ # If 'coerce_to_utf8' is set to true, any non-UTF-8 character would be
146
+ # replaced by the string specified here.
147
+ config_param :non_utf8_replacement_string, :string, :default => ' '
148
+
149
+ # DEPRECATED: The following parameters, if present in the config
150
+ # indicate that the plugin configuration must be updated.
151
+ config_param :auth_method, :string, :default => nil
152
+ config_param :private_key_email, :string, :default => nil
153
+ config_param :private_key_path, :string, :default => nil
154
+ config_param :private_key_passphrase, :string,
155
+ :default => nil,
156
+ :secret => true
157
+
158
+ # rubocop:enable Style/HashSyntax
159
+
160
+ # TODO: Add a log_name config option rather than just using the tag?
161
+
162
+ # Expose attr_readers to make testing of metadata more direct than only
163
+ # testing it indirectly through metadata sent with logs.
164
+ attr_reader :project_id
165
+ attr_reader :zone
166
+ attr_reader :vm_id
167
+ attr_reader :running_on_managed_vm
168
+ attr_reader :gae_backend_name
169
+ attr_reader :gae_backend_version
170
+ attr_reader :service_name
171
+ attr_reader :common_labels
172
+
173
+ def initialize
174
+ super
175
+ # use the global logger
176
+ @log = $log # rubocop:disable Style/GlobalVars
177
+ end
178
+
179
+ def configure(conf)
180
+ super
181
+
182
+ # Alert on old authentication configuration.
183
+ unless @auth_method.nil? && @private_key_email.nil? &&
184
+ @private_key_path.nil? && @private_key_passphrase.nil?
185
+ extra = []
186
+ extra << 'auth_method' unless @auth_method.nil?
187
+ extra << 'private_key_email' unless @private_key_email.nil?
188
+ extra << 'private_key_path' unless @private_key_path.nil?
189
+ extra << 'private_key_passphrase' unless @private_key_passphrase.nil?
190
+
191
+ fail Fluent::ConfigError,
192
+ "#{PLUGIN_NAME} no longer supports auth_method.\n" \
193
+ 'Please remove configuration parameters: ' +
194
+ extra.join(' ')
195
+ end
196
+
197
+ # TODO: Send instance tags as labels as well?
198
+ @common_labels = {}
199
+ @common_labels.merge!(@labels) if @labels
200
+
201
+ @compiled_kubernetes_tag_regexp = nil
202
+ if @kubernetes_tag_regexp
203
+ @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
204
+ end
205
+
206
+ @cloudfunctions_tag_regexp =
207
+ /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
208
+ @cloudfunctions_log_regexp = /^
209
+ (?:\[(?<severity>.)\])?
210
+ \[(?<timestamp>.{24})\]
211
+ (?:\[(?<execution_id>[^\]]+)\])?
212
+ [ ](?<text>.*)$/x
213
+
214
+ # set attributes from metadata (unless overriden by static config)
215
+ @vm_name = Socket.gethostname if @vm_name.nil?
216
+ @platform = detect_platform
217
+ case @platform
218
+ when Platform::GCE
219
+ if @project_id.nil?
220
+ @project_id = fetch_gce_metadata('project/project-id')
221
+ end
222
+ if @zone.nil?
223
+ # this returns "projects/<number>/zones/<zone>"; we only want
224
+ # the part after the final slash.
225
+ fully_qualified_zone = fetch_gce_metadata('instance/zone')
226
+ @zone = fully_qualified_zone.rpartition('/')[2]
227
+ end
228
+ @vm_id = fetch_gce_metadata('instance/id') if @vm_id.nil?
229
+ when Platform::EC2
230
+ metadata = fetch_ec2_metadata
231
+ if @zone.nil? && metadata.key?('availabilityZone')
232
+ @zone = 'aws:' + metadata['availabilityZone']
233
+ end
234
+ if @vm_id.nil? && metadata.key?('instanceId')
235
+ @vm_id = metadata['instanceId']
236
+ end
237
+ if metadata.key?('accountId')
238
+ common_labels["#{EC2_SERVICE}/account_id"] = metadata['accountId']
239
+ end
240
+ when Platform::OTHER
241
+ # do nothing
242
+ else
243
+ fail Fluent::ConfigError, 'Unknown platform ' + @platform
244
+ end
245
+
246
+ # If we still don't have a project ID, try to obtain it from the
247
+ # credentials.
248
+ if @project_id.nil?
249
+ @project_id = CredentialsInfo.project_id
250
+ @log.info 'Set Project ID from credentials: ', @project_id unless
251
+ @project_id.nil?
252
+ end
253
+
254
+ # all metadata parameters must now be set
255
+ unless @project_id && @zone && @vm_id
256
+ missing = []
257
+ missing << 'project_id' unless @project_id
258
+ missing << 'zone' unless @zone
259
+ missing << 'vm_id' unless @vm_id
260
+ fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
261
+ missing.join(' ')
262
+ end
263
+
264
+ # Default this to false; it is only overwritten if we detect Managed VM.
265
+ @running_on_managed_vm = false
266
+
267
+ # Default this to false; it is only overwritten if we detect Cloud
268
+ # Functions.
269
+ @running_cloudfunctions = false
270
+
271
+ # Set labels, etc. based on the config
272
+ case @platform
273
+ when Platform::GCE
274
+ @service_name = COMPUTE_SERVICE
275
+ if @subservice_name
276
+ @service_name = @subservice_name
277
+ elsif @detect_subservice
278
+ # Check for specialized GCE environments.
279
+ # TODO: Add config options for these to allow for running outside GCE?
280
+ attributes = fetch_gce_metadata('instance/attributes/').split
281
+ # Do nothing, just don't populate other service's labels.
282
+ if attributes.include?('gae_backend_name') &&
283
+ attributes.include?('gae_backend_version')
284
+ # Managed VM
285
+ @running_on_managed_vm = true
286
+ @gae_backend_name =
287
+ fetch_gce_metadata('instance/attributes/gae_backend_name')
288
+ @gae_backend_version =
289
+ fetch_gce_metadata('instance/attributes/gae_backend_version')
290
+ @service_name = APPENGINE_SERVICE
291
+ common_labels["#{APPENGINE_SERVICE}/module_id"] = @gae_backend_name
292
+ common_labels["#{APPENGINE_SERVICE}/version_id"] =
293
+ @gae_backend_version
294
+ elsif attributes.include?('kube-env')
295
+ # Kubernetes/Container Engine
296
+ @service_name = CONTAINER_SERVICE
297
+ common_labels["#{CONTAINER_SERVICE}/instance_id"] = @vm_id
298
+ @raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
299
+ @kube_env = YAML.load(@raw_kube_env)
300
+ common_labels["#{CONTAINER_SERVICE}/cluster_name"] =
301
+ cluster_name_from_kube_env(@kube_env)
302
+ detect_cloudfunctions(attributes)
303
+ end
304
+ end
305
+ common_labels["#{COMPUTE_SERVICE}/resource_type"] = 'instance'
306
+ common_labels["#{COMPUTE_SERVICE}/resource_id"] = @vm_id
307
+ common_labels["#{COMPUTE_SERVICE}/resource_name"] = @vm_name
308
+ when Platform::EC2
309
+ @service_name = EC2_SERVICE
310
+ common_labels["#{EC2_SERVICE}/resource_type"] = 'instance'
311
+ common_labels["#{EC2_SERVICE}/resource_id"] = @vm_id
312
+ common_labels["#{EC2_SERVICE}/resource_name"] = @vm_name
313
+ when Platform::OTHER
314
+ # Use COMPUTE_SERVICE as the default environment.
315
+ @service_name = COMPUTE_SERVICE
316
+ common_labels["#{COMPUTE_SERVICE}/resource_type"] = 'instance'
317
+ common_labels["#{COMPUTE_SERVICE}/resource_id"] = @vm_id
318
+ common_labels["#{COMPUTE_SERVICE}/resource_name"] = @vm_name
319
+ end
320
+
321
+ # Log an informational message containing the Logs viewer URL
322
+ @log.info 'Logs viewer address: ',
323
+ 'https://console.developers.google.com/project/', @project_id,
324
+ '/logs?service=', @service_name, '&key1=instance&key2=', @vm_id
325
+ end
326
+
327
+ def start
328
+ super
329
+ init_api_client
330
+ @successful_call = false
331
+ @timenanos_warning = false
332
+ end
333
+
334
+ def shutdown
335
+ super
336
+ end
337
+
338
+ def format(tag, time, record)
339
+ [tag, time, record].to_msgpack
340
+ end
341
+
342
+ # Given a tag, returns the corresponding valid tag if possible, or nil if
343
+ # the tag should be rejected. If 'require_valid_tags' is false, non-string
344
+ # tags are converted to strings, and invalid characters are sanitized;
345
+ # otherwise such tags are rejected.
346
+ def sanitize_tag(tag)
347
+ if @require_valid_tags &&
348
+ (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
349
+ return nil
350
+ end
351
+ tag = convert_to_utf8(tag.to_s)
352
+ tag = '_' if tag == ''
353
+ tag
354
+ end
355
+
356
+ def write(chunk)
357
+ # Group the entries since we have to make one call per tag.
358
+ grouped_entries = {}
359
+ chunk.msgpack_each do |tag, *arr|
360
+ sanitized_tag = sanitize_tag(tag)
361
+ if sanitized_tag.nil?
362
+ @log.warn "Dropping log entries with invalid tag: '#{tag}'. " \
363
+ 'A tag should be a string with utf8 characters.'
364
+ next
365
+ end
366
+ grouped_entries[sanitized_tag] ||= []
367
+ grouped_entries[sanitized_tag].push(arr)
368
+ end
369
+
370
+ grouped_entries.each do |tag, arr|
371
+ entries = []
372
+ labels = @common_labels.clone
373
+
374
+ if @running_cloudfunctions
375
+ # If the current group of entries is coming from a Cloud Functions
376
+ # function, the function name can be extracted from the tag.
377
+ match_data = @cloudfunctions_tag_regexp.match(tag)
378
+ if match_data
379
+ # Service name is set to Cloud Functions only for logs actually
380
+ # coming from a function.
381
+ @service_name = CLOUDFUNCTIONS_SERVICE
382
+ labels["#{CLOUDFUNCTIONS_SERVICE}/region"] = @gcf_region
383
+ labels["#{CLOUDFUNCTIONS_SERVICE}/function_name"] =
384
+ decode_cloudfunctions_function_name(
385
+ match_data['encoded_function_name'])
386
+ else
387
+ # Other logs are considered as coming from the Container Engine
388
+ # service.
389
+ @service_name = CONTAINER_SERVICE
390
+ end
391
+ end
392
+ if @service_name == CONTAINER_SERVICE && @compiled_kubernetes_tag_regexp
393
+ # Container logs in Kubernetes are tagged based on where they came
394
+ # from, so we can extract useful metadata from the tag.
395
+ # Do this here to avoid having to repeat it for each record.
396
+ match_data = @compiled_kubernetes_tag_regexp.match(tag)
397
+ if match_data
398
+ %w(namespace_name pod_name container_name).each do |field|
399
+ labels["#{CONTAINER_SERVICE}/#{field}"] = match_data[field]
400
+ end
401
+ end
402
+ end
403
+
404
+ arr.each do |time, record|
405
+ next unless record.is_a?(Hash)
406
+
407
+ if @use_grpc
408
+ entry = Google::Logging::V1::LogEntry.new(
409
+ metadata: Google::Logging::V1::LogEntryMetadata.new(
410
+ service_name: convert_to_utf8(@service_name),
411
+ project_id: convert_to_utf8(@project_id),
412
+ zone: convert_to_utf8(@zone),
413
+ labels: {}
414
+ ))
415
+ else
416
+ entry = Google::Apis::LoggingV1beta3::LogEntry.new(
417
+ metadata: Google::Apis::LoggingV1beta3::LogEntryMetadata.new(
418
+ service_name: @service_name,
419
+ project_id: @project_id,
420
+ zone: @zone,
421
+ labels: {}
422
+ ))
423
+ end
424
+
425
+ if @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
426
+ @cloudfunctions_log_match =
427
+ @cloudfunctions_log_regexp.match(record['log'])
428
+ end
429
+ if @service_name == CONTAINER_SERVICE
430
+ # Move the stdout/stderr annotation from the record into a label.
431
+ field_to_label(record, 'stream', entry.metadata.labels,
432
+ "#{CONTAINER_SERVICE}/stream")
433
+ # If the record has been annotated by the kubernetes_metadata_filter
434
+ # plugin, then use that metadata. Otherwise, rely on commonLabels
435
+ # populated at the grouped_entries level from the group's tag.
436
+ if record.key?('kubernetes')
437
+ handle_container_metadata(record, entry)
438
+ end
439
+
440
+ # Save the timestamp if available, then clear it out to allow for
441
+ # determining whether we should parse the log or message field.
442
+ timestamp = record.key?('time') ? record['time'] : nil
443
+ record.delete('time')
444
+ # If the log is json, we want to export it as a structured log
445
+ # unless there is additional metadata that would be lost.
446
+ is_json = false
447
+ if record.length == 1 && record.key?('log')
448
+ record_json = parse_json_or_nil(record['log'])
449
+ end
450
+ if record.length == 1 && record.key?('message')
451
+ record_json = parse_json_or_nil(record['message'])
452
+ end
453
+ unless record_json.nil?
454
+ record = record_json
455
+ is_json = true
456
+ end
457
+ # Restore timestamp if necessary.
458
+ unless record.key?('time') || timestamp.nil?
459
+ record['time'] = timestamp
460
+ end
461
+ end
462
+
463
+ ts_secs, ts_nanos = compute_timestamp(record, time)
464
+ if @use_grpc
465
+ # If "seconds" is null or not an integer, we will omit the timestamp
466
+ # field and defer the decision on how to handle it to the downstream
467
+ # Logging API. If "nanos" is null or not an integer, it will be set
468
+ # to 0.
469
+ if ts_secs.is_a?(Integer)
470
+ ts_nanos = 0 unless ts_nanos.is_a?(Integer)
471
+ entry.metadata.timestamp = Google::Protobuf::Timestamp.new(
472
+ seconds: ts_secs,
473
+ nanos: ts_nanos
474
+ )
475
+ end
476
+
477
+ entry.metadata.severity =
478
+ grpc_severity(compute_severity(record, entry))
479
+
480
+ set_http_request_grpc(record, entry) # FIXME
481
+ else
482
+ entry.metadata.timestamp = {
483
+ seconds: ts_secs,
484
+ nanos: ts_nanos
485
+ }
486
+
487
+ entry.metadata.severity =
488
+ compute_severity(record, entry)
489
+
490
+ set_http_request(record, entry)
491
+ end
492
+
493
+ # If a field is present in the label_map, send its value as a label
494
+ # (mapping the field name to label name as specified in the config)
495
+ # and do not send that field as part of the payload.
496
+ if @label_map
497
+ @label_map.each do |field, label|
498
+ field_to_label(record, field, entry.metadata.labels, label)
499
+ end
500
+ end
501
+
502
+ if @service_name == CLOUDFUNCTIONS_SERVICE &&
503
+ @cloudfunctions_log_match &&
504
+ @cloudfunctions_log_match['execution_id']
505
+ entry.metadata.labels['execution_id'] =
506
+ @cloudfunctions_log_match['execution_id']
507
+ end
508
+
509
+ if @use_grpc
510
+ set_payload_grpc(record, entry, is_json)
511
+ else
512
+ set_payload(record, entry, is_json)
513
+ entry.metadata.labels = nil if entry.metadata.labels.empty?
514
+ end
515
+
516
+ entries.push(entry)
517
+ end
518
+ # Don't send an empty request if we rejected all the entries.
519
+ next if entries.empty?
520
+
521
+ log_name = log_name(tag, labels)
522
+
523
+ if @use_grpc
524
+ begin
525
+ # Does the actual write to the cloud logging api.
526
+
527
+ client = api_client
528
+
529
+ labels_utf8_pairs = labels.map do |k, v|
530
+ [k.encode('utf-8'), convert_to_utf8(v)]
531
+ end
532
+
533
+ write_request = Google::Logging::V1::WriteLogEntriesRequest.new(
534
+ log_name: "projects/#{@project_id}/logs/#{log_name}",
535
+ common_labels: Hash[labels_utf8_pairs],
536
+ entries: entries
537
+ )
538
+
539
+ client.write_log_entries(write_request)
540
+
541
+ # Let the user explicitly know when the first call succeeded,
542
+ # to aid with verification and troubleshooting.
543
+ unless @successful_call
544
+ @successful_call = true
545
+ @log.info 'Successfully sent gRPC to Stackdriver Logging API.'
546
+ end
547
+
548
+ rescue GRPC::Cancelled => error
549
+ # RPC cancelled, so retry via re-raising the error.
550
+ raise error
551
+
552
+ rescue GRPC::BadStatus => error
553
+ case error.code
554
+ when GRPC::Core::StatusCodes::CANCELLED,
555
+ GRPC::Core::StatusCodes::UNAVAILABLE,
556
+ GRPC::Core::StatusCodes::DEADLINE_EXCEEDED,
557
+ GRPC::Core::StatusCodes::INTERNAL,
558
+ GRPC::Core::StatusCodes::UNKNOWN
559
+ # TODO
560
+ # Server error, so retry via re-raising the error.
561
+ raise error
562
+ when GRPC::Core::StatusCodes::UNIMPLEMENTED,
563
+ GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED
564
+ # Most client errors indicate a problem with the request itself
565
+ # and should not be retried.
566
+ dropped = entries.length
567
+ @log.warn "Dropping #{dropped} log message(s)",
568
+ error: error.to_s, error_code: error.code.to_s
569
+ when GRPC::Core::StatusCodes::UNAUTHENTICATED
570
+ # Authorization error.
571
+ # These are usually solved via a `gcloud auth` call, or by
572
+ # modifying the permissions on the Google Cloud project.
573
+ dropped = entries.length
574
+ @log.warn "Dropping #{dropped} log message(s)",
575
+ error: error.to_s, error_code: error.code.to_s
576
+ else
577
+ # Assume this is a problem with the request itself
578
+ # and don't retry.
579
+ dropped = entries.length
580
+ @log.error "Unknown response code #{error.code} from the "\
581
+ "server, dropping #{dropped} log message(s)",
582
+ error: error.to_s, error_code: error.code.to_s
583
+ end
584
+ end
585
+ else
586
+ begin
587
+ # Does the actual write to the cloud logging api.
588
+
589
+ client = api_client
590
+
591
+ # The URI of the write is constructed by the Google::Api request;
592
+ # it is equivalent to this URL:
593
+ # 'https://logging.googleapis.com/v1beta3/projects/' \
594
+ # "#{@project_id}/logs/#{log_name}/entries:write"
595
+ write_request = \
596
+ Google::Apis::LoggingV1beta3::WriteLogEntriesRequest.new(
597
+ common_labels: labels,
598
+ entries: entries)
599
+
600
+ # TODO: RequestOptions
601
+ client.write_log_entries(@project_id, log_name, write_request)
602
+
603
+ # Let the user explicitly know when the first call succeeded,
604
+ # to aid with verification and troubleshooting.
605
+ unless @successful_call
606
+ @successful_call = true
607
+ @log.info 'Successfully sent to Stackdriver Logging API.'
608
+ end
609
+
610
+ rescue Google::Apis::ServerError => error
611
+ # Server error, so retry via re-raising the error.
612
+ raise error
613
+
614
+ rescue Google::Apis::AuthorizationError => error
615
+ # Authorization error.
616
+ # These are usually solved via a `gcloud auth` call, or by modifying
617
+ # the permissions on the Google Cloud project.
618
+ dropped = entries.length
619
+ @log.warn "Dropping #{dropped} log message(s)",
620
+ error_class: error.class.to_s, error: error.to_s
621
+
622
+ rescue Google::Apis::ClientError => error
623
+ # Most ClientErrors indicate a problem with the request itself and
624
+ # should not be retried.
625
+ dropped = entries.length
626
+ @log.warn "Dropping #{dropped} log message(s)",
627
+ error_class: error.class.to_s, error: error.to_s
628
+ end
629
+ end
630
+ end
631
+ end
632
+
633
+ private
634
+
635
+ def parse_json_or_nil(input)
636
+ # Only here to please rubocop...
637
+ return nil if input.nil?
638
+
639
+ input.each_codepoint do |c|
640
+ if c == 123
641
+ # left curly bracket (U+007B)
642
+ begin
643
+ return JSON.parse(input)
644
+ rescue JSON::ParserError
645
+ return nil
646
+ end
647
+ else
648
+ # Break (and return nil) unless the current character is whitespace,
649
+ # in which case we continue to look for a left curly bracket.
650
+ # Whitespace as per the JSON spec are: tabulation (U+0009),
651
+ # line feed (U+000A), carriage return (U+000D), and space (U+0020).
652
+ break unless c == 9 || c == 10 || c == 13 || c == 32
653
+ end # case
654
+ end # do
655
+ nil
656
+ end
657
+
658
+ # "enum" of Platform values
659
+ module Platform
660
+ OTHER = 0 # Other/unkown platform
661
+ GCE = 1 # Google Compute Engine
662
+ EC2 = 2 # Amazon EC2
663
+ end
664
+
665
+ # Determine what platform we are running on by consulting the metadata
666
+ # service (unless the user has explicitly disabled using that).
667
+ def detect_platform
668
+ unless @use_metadata_service
669
+ @log.info 'use_metadata_service is false; not detecting platform'
670
+ return Platform::OTHER
671
+ end
672
+
673
+ begin
674
+ open('http://' + METADATA_SERVICE_ADDR) do |f|
675
+ if f.meta['metadata-flavor'] == 'Google'
676
+ @log.info 'Detected GCE platform'
677
+ return Platform::GCE
678
+ end
679
+ if f.meta['server'] == 'EC2ws'
680
+ @log.info 'Detected EC2 platform'
681
+ return Platform::EC2
682
+ end
683
+ end
684
+ rescue StandardError => e
685
+ @log.debug 'Failed to access metadata service: ', error: e
686
+ end
687
+
688
+ @log.info 'Unable to determine platform'
689
+ Platform::OTHER
690
+ end
691
+
692
+ def fetch_gce_metadata(metadata_path)
693
+ fail "Called fetch_gce_metadata with platform=#{@platform}" unless
694
+ @platform == Platform::GCE
695
+ # See https://cloud.google.com/compute/docs/metadata
696
+ open('http://' + METADATA_SERVICE_ADDR + '/computeMetadata/v1/' +
697
+ metadata_path, 'Metadata-Flavor' => 'Google', &:read)
698
+ end
699
+
700
+ def fetch_ec2_metadata
701
+ fail "Called fetch_ec2_metadata with platform=#{@platform}" unless
702
+ @platform == Platform::EC2
703
+ # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
704
+ open('http://' + METADATA_SERVICE_ADDR +
705
+ '/latest/dynamic/instance-identity/document') do |f|
706
+ contents = f.read
707
+ return JSON.parse(contents)
708
+ end
709
+ end
710
+
711
+ # TODO: This functionality should eventually be available in another
712
+ # library, but implement it ourselves for now.
713
+ module CredentialsInfo
714
+ # Determine the project ID from the credentials, if possible.
715
+ # Returns the project ID (as a string) on success, or nil on failure.
716
+ def self.project_id
717
+ creds = Google::Auth.get_application_default(LOGGING_SCOPE)
718
+ if creds.issuer
719
+ id = extract_project_id(creds.issuer)
720
+ return id unless id.nil?
721
+ end
722
+ if creds.client_id
723
+ id = extract_project_id(creds.client_id)
724
+ return id unless id.nil?
725
+ end
726
+ nil
727
+ end
728
+
729
+ # Extracts the project id (either name or number) from str and returns
730
+ # it (as a string) on success, or nil on failure.
731
+ #
732
+ # Recognizes IAM format (account@project-name.iam.gserviceaccount.com)
733
+ # as well as the legacy format with a project number at the front of the
734
+ # string, terminated by a dash (-) which is not part of the ID, i.e.:
735
+ # 270694816269-1l1r2hb813leuppurdeik0apglbs80sv.apps.googleusercontent.com
736
+ def self.extract_project_id(str)
737
+ [/^.*@(?<project_id>.+)\.iam\.gserviceaccount\.com/,
738
+ /^(?<project_id>\d+)-/].each do |exp|
739
+ match_data = exp.match(str)
740
+ return match_data['project_id'] unless match_data.nil?
741
+ end
742
+ nil
743
+ end
744
+ end
745
+
746
+ def detect_cloudfunctions(attributes)
747
+ return unless attributes.include?('gcf_region')
748
+ # Cloud Functions detected
749
+ @running_cloudfunctions = true
750
+ @gcf_region = fetch_gce_metadata('instance/attributes/gcf_region')
751
+ end
752
+
753
+ def cluster_name_from_kube_env(kube_env)
754
+ return kube_env['CLUSTER_NAME'] if kube_env.key?('CLUSTER_NAME')
755
+ instance_prefix = kube_env['INSTANCE_PREFIX']
756
+ gke_name_match = /^gke-(.+)-[0-9a-f]{8}$/.match(instance_prefix)
757
+ return gke_name_match.captures[0] if gke_name_match &&
758
+ !gke_name_match.captures.empty?
759
+ instance_prefix
760
+ end
761
+
762
+ def compute_timestamp(record, time)
763
+ if record.key?('timestamp') &&
764
+ record['timestamp'].is_a?(Hash) &&
765
+ record['timestamp'].key?('seconds') &&
766
+ record['timestamp'].key?('nanos')
767
+ ts_secs = record['timestamp']['seconds']
768
+ ts_nanos = record['timestamp']['nanos']
769
+ record.delete('timestamp')
770
+ elsif record.key?('timestampSeconds') &&
771
+ record.key?('timestampNanos')
772
+ ts_secs = record.delete('timestampSeconds')
773
+ ts_nanos = record.delete('timestampNanos')
774
+ elsif record.key?('timeNanos')
775
+ # This is deprecated since the precision is insufficient.
776
+ # Use timestampSeconds/timestampNanos instead
777
+ nanos = record.delete('timeNanos')
778
+ ts_secs = (nanos / 1_000_000_000).to_i
779
+ ts_nanos = nanos % 1_000_000_000
780
+ unless @timenanos_warning
781
+ # Warn the user this is deprecated, but only once to avoid spam.
782
+ @timenanos_warning = true
783
+ @log.warn 'timeNanos is deprecated - please use ' \
784
+ 'timestampSeconds and timestampNanos instead.'
785
+ end
786
+ elsif @service_name == CLOUDFUNCTIONS_SERVICE &&
787
+ @cloudfunctions_log_match
788
+ timestamp = DateTime.parse(@cloudfunctions_log_match['timestamp'])
789
+ ts_secs = timestamp.strftime('%s').to_i
790
+ ts_nanos = timestamp.strftime('%N').to_i
791
+ elsif record.key?('time')
792
+ # k8s ISO8601 timestamp
793
+ begin
794
+ timestamp = Time.iso8601(record.delete('time'))
795
+ rescue
796
+ timestamp = Time.at(time)
797
+ end
798
+ ts_secs = timestamp.tv_sec
799
+ ts_nanos = timestamp.tv_nsec
800
+ else
801
+ timestamp = Time.at(time)
802
+ ts_secs = timestamp.tv_sec
803
+ ts_nanos = timestamp.tv_nsec
804
+ end
805
+ [ts_secs, ts_nanos]
806
+ end
807
+
808
+ def compute_severity(record, entry)
809
+ if @service_name == CLOUDFUNCTIONS_SERVICE
810
+ if @cloudfunctions_log_match && @cloudfunctions_log_match['severity']
811
+ return parse_severity(@cloudfunctions_log_match['severity'])
812
+ elsif record.key?('stream') && record['stream'] == 'stdout'
813
+ record.delete('stream')
814
+ return 'INFO'
815
+ elsif record.key?('stream') && record['stream'] == 'stderr'
816
+ record.delete('stream')
817
+ return 'ERROR'
818
+ else
819
+ return 'DEFAULT'
820
+ end
821
+ elsif record.key?('severity')
822
+ return parse_severity(record.delete('severity'))
823
+ elsif @service_name == CONTAINER_SERVICE && \
824
+ entry.metadata.labels.key?("#{CONTAINER_SERVICE}/stream")
825
+ stream = entry.metadata.labels["#{CONTAINER_SERVICE}/stream"]
826
+ if stream == 'stdout'
827
+ return 'INFO'
828
+ elsif stream == 'stderr'
829
+ return 'ERROR'
830
+ else
831
+ return 'DEFAULT'
832
+ end
833
+ else
834
+ return 'DEFAULT'
835
+ end
836
+ end
837
+
838
+ def set_http_request(record, entry)
839
+ return nil unless record['httpRequest'].is_a?(Hash)
840
+ input = record['httpRequest']
841
+ output = Google::Apis::LoggingV1beta3::HttpRequest.new
842
+ output.request_method = input.delete('requestMethod')
843
+ output.request_url = input.delete('requestUrl')
844
+ output.request_size = input.delete('requestSize')
845
+ output.status = input.delete('status')
846
+ output.response_size = input.delete('responseSize')
847
+ output.user_agent = input.delete('userAgent')
848
+ output.remote_ip = input.delete('remoteIp')
849
+ output.referer = input.delete('referer')
850
+ output.cache_hit = input.delete('cacheHit')
851
+ output.validated_with_origin_server = \
852
+ input.delete('validatedWithOriginServer')
853
+ record.delete('httpRequest') if input.empty?
854
+ entry.http_request = output
855
+ end
856
+
857
+ def set_http_request_grpc(record, entry)
858
+ return nil unless record['httpRequest'].is_a?(Hash)
859
+ input = record['httpRequest']
860
+ output = Google::Logging::Type::HttpRequest.new
861
+ # We need to delete each field from 'httpRequest' even if its value is
862
+ # nil. However we do not want to assign this nil value to proto fields
863
+ # defined as strings / integers.
864
+ request_method = input.delete('requestMethod')
865
+ output.request_method = request_method unless request_method.nil?
866
+ request_url = input.delete('requestUrl')
867
+ output.request_url = request_url unless request_url.nil?
868
+ request_size = input.delete('requestSize')
869
+ output.request_size = request_size.to_i unless request_size.nil?
870
+ status = input.delete('status')
871
+ output.status = status.to_i unless status.nil?
872
+ response_size = input.delete('responseSize')
873
+ output.response_size = response_size.to_i unless response_size.nil?
874
+ user_agent = input.delete('userAgent')
875
+ output.user_agent = user_agent unless user_agent.nil?
876
+ remote_ip = input.delete('remoteIp')
877
+ output.remote_ip = remote_ip unless remote_ip.nil?
878
+ referer = input.delete('referer')
879
+ output.referer = referer unless referer.nil?
880
+ cache_hit = input.delete('cacheHit')
881
+ output.cache_hit = cache_hit unless cache_hit.nil?
882
+ cache_validated_with_origin_server = \
883
+ input.delete('cacheValidatedWithOriginServer')
884
+ output.cache_validated_with_origin_server = \
885
+ cache_validated_with_origin_server \
886
+ unless cache_validated_with_origin_server.nil?
887
+ record.delete('httpRequest') if input.empty?
888
+ entry.http_request = output
889
+ end
890
+
891
+ # Values permitted by the API for 'severity' (which is an enum).
892
+ VALID_SEVERITIES = Set.new(
893
+ %w(DEFAULT DEBUG INFO NOTICE WARNING ERROR CRITICAL ALERT EMERGENCY))
894
+
895
+ # Translates other severity strings to one of the valid values above.
896
+ SEVERITY_TRANSLATIONS = {
897
+ # log4j levels (both current and obsolete).
898
+ 'WARN' => 'WARNING',
899
+ 'FATAL' => 'CRITICAL',
900
+ 'TRACE' => 'DEBUG',
901
+ 'TRACE_INT' => 'DEBUG',
902
+ 'FINE' => 'DEBUG',
903
+ 'FINER' => 'DEBUG',
904
+ 'FINEST' => 'DEBUG',
905
+ # nginx levels (only missing ones from above listed).
906
+ 'CRIT' => 'CRITICAL',
907
+ 'EMERG' => 'EMERGENCY',
908
+ # single-letter levels. Note E->ERROR and D->DEBUG.
909
+ 'D' => 'DEBUG',
910
+ 'I' => 'INFO',
911
+ 'N' => 'NOTICE',
912
+ 'W' => 'WARNING',
913
+ 'E' => 'ERROR',
914
+ 'C' => 'CRITICAL',
915
+ 'A' => 'ALERT',
916
+ # other misc. translations.
917
+ 'ERR' => 'ERROR',
918
+ 'F' => 'CRITICAL'
919
+ }
920
+
921
+ def parse_severity(severity_str)
922
+ # The API is case insensitive, but uppercase to make things simpler.
923
+ severity = severity_str.upcase.strip
924
+
925
+ # If the severity is already valid, just return it.
926
+ return severity if VALID_SEVERITIES.include?(severity)
927
+
928
+ # If the severity is an integer (string) return it as an integer,
929
+ # truncated to the closest valid value (multiples of 100 between 0-800).
930
+ if /\A\d+\z/.match(severity)
931
+ begin
932
+ numeric_severity = (severity.to_i / 100) * 100
933
+ if numeric_severity < 0
934
+ return 0
935
+ elsif numeric_severity > 800
936
+ return 800
937
+ else
938
+ return numeric_severity
939
+ end
940
+ rescue
941
+ return 'DEFAULT'
942
+ end
943
+ end
944
+
945
+ # Try to translate the severity.
946
+ if SEVERITY_TRANSLATIONS.key?(severity)
947
+ return SEVERITY_TRANSLATIONS[severity]
948
+ end
949
+
950
+ # If all else fails, use 'DEFAULT'.
951
+ 'DEFAULT'
952
+ end
953
+
954
+ GRPC_SEVERITY_MAPPING = {
955
+ 'DEFAULT' => Google::Logging::Type::LogSeverity::DEFAULT,
956
+ 'DEBUG' => Google::Logging::Type::LogSeverity::DEBUG,
957
+ 'INFO' => Google::Logging::Type::LogSeverity::INFO,
958
+ 'NOTICE' => Google::Logging::Type::LogSeverity::NOTICE,
959
+ 'WARNING' => Google::Logging::Type::LogSeverity::WARNING,
960
+ 'ERROR' => Google::Logging::Type::LogSeverity::ERROR,
961
+ 'CRITICAL' => Google::Logging::Type::LogSeverity::CRITICAL,
962
+ 'ALERT' => Google::Logging::Type::LogSeverity::ALERT,
963
+ 'EMERGENCY' => Google::Logging::Type::LogSeverity::EMERGENCY,
964
+ 0 => Google::Logging::Type::LogSeverity::DEFAULT,
965
+ 100 => Google::Logging::Type::LogSeverity::DEBUG,
966
+ 200 => Google::Logging::Type::LogSeverity::INFO,
967
+ 300 => Google::Logging::Type::LogSeverity::NOTICE,
968
+ 400 => Google::Logging::Type::LogSeverity::WARNING,
969
+ 500 => Google::Logging::Type::LogSeverity::ERROR,
970
+ 600 => Google::Logging::Type::LogSeverity::CRITICAL,
971
+ 700 => Google::Logging::Type::LogSeverity::ALERT,
972
+ 800 => Google::Logging::Type::LogSeverity::EMERGENCY
973
+ }
974
+
975
+ def grpc_severity(severity)
976
+ # TODO: find out why this doesn't work.
977
+ # if severity.is_a? String
978
+ # return Google::Logging::Type::LogSeverity.resolve(severity)
979
+ # end
980
+ if GRPC_SEVERITY_MAPPING.key?(severity)
981
+ return GRPC_SEVERITY_MAPPING[severity]
982
+ end
983
+ severity
984
+ end
985
+
986
+ def decode_cloudfunctions_function_name(function_name)
987
+ function_name.gsub(/c\.[a-z]/) { |s| s.upcase[-1] }
988
+ .gsub('u.u', '_').gsub('d.d', '$').gsub('a.a', '@').gsub('p.p', '.')
989
+ end
990
+
991
+ # Requires that record has a 'kubernetes' field.
992
+ def handle_container_metadata(record, entry)
993
+ fields = %w(namespace_id namespace_name pod_id pod_name container_name)
994
+ fields.each do |field|
995
+ field_to_label(record['kubernetes'], field, entry.metadata.labels,
996
+ "#{CONTAINER_SERVICE}/#{field}")
997
+ end
998
+ # Prepend label/ to all user-defined labels' keys.
999
+ if record['kubernetes'].key?('labels')
1000
+ record['kubernetes']['labels'].each do |key, value|
1001
+ entry.metadata.labels["label/#{key}"] = value
1002
+ end
1003
+ end
1004
+ # We've explicitly consumed all the fields we care about -- don't litter
1005
+ # the log entries with the remaining fields that the kubernetes metadata
1006
+ # filter plugin includes (or an empty 'kubernetes' field).
1007
+ record.delete('kubernetes')
1008
+ record.delete('docker')
1009
+ end
1010
+
1011
+ def field_to_label(record, field, labels, label)
1012
+ return unless record.key?(field)
1013
+ labels[label] = convert_to_utf8(record[field].to_s)
1014
+ record.delete(field)
1015
+ end
1016
+
1017
+ def set_payload(record, entry, is_json)
1018
+ # If this is a Cloud Functions log that matched the expected regexp,
1019
+ # use text payload. Otherwise, use JSON if we found valid JSON, or text
1020
+ # payload in the following cases:
1021
+ # 1. This is a Cloud Functions log and the 'log' key is available
1022
+ # 2. This is an unstructured Container log and the 'log' key is available
1023
+ # 3. The only remaining key is 'message'
1024
+ if @service_name == CLOUDFUNCTIONS_SERVICE && @cloudfunctions_log_match
1025
+ entry.text_payload = @cloudfunctions_log_match['text']
1026
+ elsif @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
1027
+ entry.text_payload = record['log']
1028
+ elsif is_json
1029
+ entry.struct_payload = record
1030
+ elsif @service_name == CONTAINER_SERVICE && record.key?('log')
1031
+ entry.text_payload = record['log']
1032
+ elsif record.size == 1 && record.key?('message')
1033
+ entry.text_payload = record['message']
1034
+ else
1035
+ entry.struct_payload = record
1036
+ end
1037
+ end
1038
+
1039
+ def value_from_ruby(value)
1040
+ ret = Google::Protobuf::Value.new
1041
+ case value
1042
+ when NilClass
1043
+ ret.null_value = 0
1044
+ when Numeric
1045
+ ret.number_value = value
1046
+ when String
1047
+ ret.string_value = convert_to_utf8(value)
1048
+ when TrueClass
1049
+ ret.bool_value = true
1050
+ when FalseClass
1051
+ ret.bool_value = false
1052
+ when Google::Protobuf::Struct
1053
+ ret.struct_value = value
1054
+ when Hash
1055
+ ret.struct_value = struct_from_ruby(value)
1056
+ when Google::Protobuf::ListValue
1057
+ ret.list_value = value
1058
+ when Array
1059
+ ret.list_value = list_from_ruby(value)
1060
+ else
1061
+ @log.error "Unknown type: #{value.class}"
1062
+ fail Google::Protobuf::Error, "Unknown type: #{value.class}"
1063
+ end
1064
+ ret
1065
+ end
1066
+
1067
+ def list_from_ruby(arr)
1068
+ ret = Google::Protobuf::ListValue.new
1069
+ arr.each do |v|
1070
+ ret.values << value_from_ruby(v)
1071
+ end
1072
+ ret
1073
+ end
1074
+
1075
+ def struct_from_ruby(hash)
1076
+ ret = Google::Protobuf::Struct.new
1077
+ hash.each do |k, v|
1078
+ ret.fields[convert_to_utf8(k.to_s)] ||= value_from_ruby(v)
1079
+ end
1080
+ ret
1081
+ end
1082
+
1083
+ def set_payload_grpc(record, entry, is_json)
1084
+ # If this is a Cloud Functions log that matched the expected regexp,
1085
+ # use text payload. Otherwise, use JSON if we found valid JSON, or text
1086
+ # payload in the following cases:
1087
+ # 1. This is a Cloud Functions log and the 'log' key is available
1088
+ # 2. This is an unstructured Container log and the 'log' key is available
1089
+ # 3. The only remaining key is 'message'
1090
+ if @service_name == CLOUDFUNCTIONS_SERVICE && @cloudfunctions_log_match
1091
+ entry.text_payload = convert_to_utf8(
1092
+ @cloudfunctions_log_match['text'])
1093
+ elsif @service_name == CLOUDFUNCTIONS_SERVICE && record.key?('log')
1094
+ entry.text_payload = convert_to_utf8(record['log'])
1095
+ elsif is_json
1096
+ entry.struct_payload = struct_from_ruby(record)
1097
+ elsif @service_name == CONTAINER_SERVICE && record.key?('log')
1098
+ entry.text_payload = convert_to_utf8(record['log'])
1099
+ elsif record.size == 1 && record.key?('message')
1100
+ entry.text_payload = convert_to_utf8(record['message'])
1101
+ else
1102
+ entry.struct_payload = struct_from_ruby(record)
1103
+ end
1104
+ end
1105
+
1106
+ def log_name(tag, common_labels)
1107
+ if @service_name == CLOUDFUNCTIONS_SERVICE
1108
+ tag = 'cloud-functions'
1109
+ elsif @running_on_managed_vm
1110
+ # Add a prefix to Managed VM logs to prevent namespace collisions.
1111
+ tag = "#{APPENGINE_SERVICE}/#{tag}"
1112
+ elsif @service_name == CONTAINER_SERVICE
1113
+ # For Kubernetes logs, use just the container name as the log name
1114
+ # if we have it.
1115
+ container_name_key = "#{CONTAINER_SERVICE}/container_name"
1116
+ if common_labels && common_labels.key?(container_name_key)
1117
+ sanitized_log_name = sanitize_tag(common_labels[container_name_key])
1118
+ tag = sanitized_log_name unless sanitized_log_name.nil?
1119
+ end
1120
+ end
1121
+ # Only encode the log name for the grpc path, since the non-grpc client
1122
+ # lib already handles encoding.
1123
+ tag = ERB::Util.url_encode(tag) if @use_grpc
1124
+ tag
1125
+ end
1126
+
1127
+ def init_api_client
1128
+ return if @use_grpc
1129
+ # TODO: Use a non-default ClientOptions object.
1130
+ Google::Apis::ClientOptions.default.application_name = PLUGIN_NAME
1131
+ Google::Apis::ClientOptions.default.application_version = PLUGIN_VERSION
1132
+ @client = Google::Apis::LoggingV1beta3::LoggingService.new
1133
+ @client.authorization = Google::Auth.get_application_default(
1134
+ LOGGING_SCOPE)
1135
+ end
1136
+
1137
+ def api_client
1138
+ if @use_grpc
1139
+ ssl_creds = GRPC::Core::ChannelCredentials.new
1140
+ authentication = Google::Auth.get_application_default
1141
+ creds = GRPC::Core::CallCredentials.new(authentication.updater_proc)
1142
+ creds = ssl_creds.compose(creds)
1143
+ @client = Google::Logging::V1::LoggingService::Stub.new(
1144
+ 'logging.googleapis.com', creds)
1145
+ else
1146
+ unless @client.authorization.expired?
1147
+ begin
1148
+ @client.authorization.fetch_access_token!
1149
+ rescue MultiJson::ParseError
1150
+ # Workaround an issue in the API client; just re-raise a more
1151
+ # descriptive error for the user (which will still cause a retry).
1152
+ raise Google::APIClient::ClientError, 'Unable to fetch access ' \
1153
+ 'token (no scopes configured?)'
1154
+ end
1155
+ end
1156
+ end
1157
+ @client
1158
+ end
1159
+
1160
+ # Encode as UTF-8. If 'coerce_to_utf8' is set to true in the config, any
1161
+ # non-UTF-8 character would be replaced by the string specified by
1162
+ # 'non_utf8_replacement_string'. If 'coerce_to_utf8' is set to false, any
1163
+ # non-UTF-8 character would trigger the plugin to error out.
1164
+ def convert_to_utf8(input)
1165
+ if @coerce_to_utf8
1166
+ input.encode(
1167
+ 'utf-8',
1168
+ invalid: :replace,
1169
+ undef: :replace,
1170
+ replace: @non_utf8_replacement_string)
1171
+ else
1172
+ begin
1173
+ input.encode('utf-8')
1174
+ rescue EncodingError
1175
+ @log.error 'Encountered encoding issues potentially due to non ' \
1176
+ 'UTF-8 characters. To allow non-UTF-8 characters and ' \
1177
+ 'replace them with spaces, please set "coerce_to_utf8" ' \
1178
+ 'to true.'
1179
+ raise
1180
+ end
1181
+ end
1182
+ end
1183
+ end
1184
+ end