fluent-plugin-google-cloud 0.6.3 → 0.6.4.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a0d7c7c98a69f3fa2f2c40ad70020260bf55e8be
4
- data.tar.gz: 539deff09173b3b13fe9c226eaee60f8bd3695c7
3
+ metadata.gz: f8b2b5d197dc8da098e69040d3c1e33cf1d13390
4
+ data.tar.gz: d01388ebf1188dcd27061bdc3466a65be62856e9
5
5
  SHA512:
6
- metadata.gz: 9bd48380bc63a1863ddc846beaf10d47053b3c17013e5d4044bc6474383d5958cd331ed9195092b8ffbf27e9b18c8f61e98f7e3eaff23ec2ed8b3fbfdb30ac11
7
- data.tar.gz: 7c0e134bbfa2c2b78b33727be8739c2e548f169f7b7c76a451bedea257babc82bf062c89e0020daf9426d8b47a1cf72542670e24ef73cc94782e8d9ecec86263
6
+ metadata.gz: b6720df5c417423746b423fd34cebbcb3d417cdbc1cdfe91307970841661cf8d18edd987e0f622680bcc51b4503b5e3458c3029774362396eea769f2a2378dc5
7
+ data.tar.gz: 0ebf298b06bc970370d4a7640096f2fd14a2646339c9db6e93f29dacbf640140e52d1016c89cc63811b8248c314e945e54f8e47088b58a4384cd6082da233d59
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluent-plugin-google-cloud (0.6.3)
4
+ fluent-plugin-google-cloud (0.6.4.pre.1)
5
+ excon (~> 0.56.0)
5
6
  fluentd (~> 0.10)
6
7
  google-api-client (~> 0.9.0)
7
8
  google-cloud-logging (~> 0.23.2)
@@ -21,9 +22,10 @@ GEM
21
22
  cool.io (1.5.0)
22
23
  crack (0.4.3)
23
24
  safe_yaml (~> 1.0.0)
25
+ excon (0.56.0)
24
26
  faraday (0.12.1)
25
27
  multipart-post (>= 1.2, < 3)
26
- fluentd (0.14.17)
28
+ fluentd (0.14.18)
27
29
  cool.io (>= 1.4.5, < 2.0.0)
28
30
  http_parser.rb (>= 0.5.1, < 0.7.0)
29
31
  msgpack (>= 0.7.0, < 2.0.0)
@@ -57,7 +59,7 @@ GEM
57
59
  googleauth (~> 0.5.1)
58
60
  grpc (~> 1.0)
59
61
  rly (~> 0.2.3)
60
- google-protobuf (3.3.0-universal-darwin)
62
+ google-protobuf (3.3.0)
61
63
  googleapis-common-protos (1.3.5)
62
64
  google-protobuf (~> 3.2)
63
65
  grpc (~> 1.0)
@@ -69,7 +71,7 @@ GEM
69
71
  multi_json (~> 1.11)
70
72
  os (~> 0.9)
71
73
  signet (~> 0.7)
72
- grpc (1.2.5-universal-darwin)
74
+ grpc (1.2.5)
73
75
  google-protobuf (~> 3.1)
74
76
  googleauth (~> 0.5.1)
75
77
  hashdiff (0.3.4)
@@ -82,7 +84,7 @@ GEM
82
84
  logging (2.2.2)
83
85
  little-plugger (~> 1.1)
84
86
  multi_json (~> 1.10)
85
- memoist (0.15.0)
87
+ memoist (0.16.0)
86
88
  metaclass (0.0.4)
87
89
  mime-types (3.1)
88
90
  mime-types-data (~> 3.2015)
@@ -152,4 +154,4 @@ DEPENDENCIES
152
154
  webmock (~> 1.17)
153
155
 
154
156
  BUNDLED WITH
155
- 1.15.0
157
+ 1.15.1
@@ -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.6.3'
13
+ gem.version = '0.6.4.pre.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')
@@ -19,6 +19,7 @@ eos
19
19
  gem.test_files = gem.files.grep(/^(test)/)
20
20
  gem.require_paths = ['lib']
21
21
 
22
+ gem.add_runtime_dependency 'excon', '~> 0.56.0'
22
23
  gem.add_runtime_dependency 'fluentd', '~> 0.10'
23
24
  gem.add_runtime_dependency 'googleapis-common-protos', '~> 1.3'
24
25
  gem.add_runtime_dependency 'google-api-client', '~> 0.9.0'
@@ -11,9 +11,11 @@
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 'excon'
14
15
  require 'grpc'
15
16
  require 'json'
16
17
  require 'open-uri'
18
+ require 'rubygems'
17
19
  require 'socket'
18
20
  require 'time'
19
21
  require 'yaml'
@@ -36,8 +38,10 @@ end
36
38
  module Fluent
37
39
  # fluentd output plugin for the Stackdriver Logging API
38
40
  class GoogleCloudOutput < BufferedOutput
39
- # Constants for service names and resource types.
41
+ # Constants.
40
42
  module Constants
43
+ # Service names and resource types.
44
+
41
45
  APPENGINE_CONSTANTS = {
42
46
  service: 'appengine.googleapis.com',
43
47
  resource_type: 'gae_app'
@@ -54,10 +58,18 @@ module Fluent
54
58
  service: 'container.googleapis.com',
55
59
  resource_type: 'container'
56
60
  }
61
+ DOCKER_CONSTANTS = {
62
+ service: 'dockercontainer.googleapis.com',
63
+ resource_type: 'docker_container'
64
+ }
57
65
  DATAFLOW_CONSTANTS = {
58
66
  service: 'dataflow.googleapis.com',
59
67
  resource_type: 'dataflow_step'
60
68
  }
69
+ DATAPROC_CONSTANTS = {
70
+ service: 'cluster.dataproc.googleapis.com',
71
+ resource_type: 'cloud_dataproc_cluster'
72
+ }
61
73
  EC2_CONSTANTS = {
62
74
  service: 'ec2.amazonaws.com',
63
75
  resource_type: 'aws_ec2_instance'
@@ -66,6 +78,15 @@ module Fluent
66
78
  service: 'ml.googleapis.com',
67
79
  resource_type: 'ml_job'
68
80
  }
81
+
82
+ # Metadata agent support.
83
+
84
+ # Use empty string as request path when locally-unique key of monitored
85
+ # resource can be implicitly inferred by Metadata Agent.
86
+ IMPLICIT_MONITORED_RESOURCE_UNIQUE_KEY = ''
87
+
88
+ # Docker container support.
89
+ DEFAULT_DOCKER_API_SOCKET_PATH = '/var/run/docker.sock'
69
90
  end
70
91
 
71
92
  include self::Constants
@@ -73,7 +94,7 @@ module Fluent
73
94
  Fluent::Plugin.register_output('google_cloud', self)
74
95
 
75
96
  PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
76
- PLUGIN_VERSION = '0.6.3'
97
+ PLUGIN_VERSION = '0.6.4.pre.1'
77
98
 
78
99
  # Name of the the Google cloud logging write scope.
79
100
  LOGGING_SCOPE = 'https://www.googleapis.com/auth/logging.write'
@@ -120,7 +141,7 @@ module Fluent
120
141
  config_param :require_valid_tags, :bool, :default => false
121
142
 
122
143
  # The regular expression to use on Kubernetes logs to extract some basic
123
- # information about the log source. The regex must contain capture groups
144
+ # information about the log source. The regexp must contain capture groups
124
145
  # for pod_name, namespace_name, and container_name.
125
146
  config_param :kubernetes_tag_regexp, :string, :default =>
126
147
  '\.(?<pod_name>[^_]+)_(?<namespace_name>[^_]+)_(?<container_name>.+)$'
@@ -182,6 +203,18 @@ module Fluent
182
203
  :default => nil,
183
204
  :secret => true
184
205
 
206
+ # Whether to call metadata agent to retrieve monitored resource.
207
+ config_param :enable_metadata_agent, :bool, :default => false
208
+ config_param :metadata_agent_url, :string,
209
+ :default => 'http://local-metadata-agent.stackdriver.com:8000'
210
+
211
+ # Whether to call Docker Remote API locally when Metadata Agent is not
212
+ # enabled or if the request fails.
213
+ config_param :call_docker_api_locally, :bool, :default => true
214
+ # Docker Remote API unix socket path.
215
+ config_param :docker_remote_api_socket_path, :string,
216
+ :default => '/var/run/docker.sock'
217
+
185
218
  # rubocop:enable Style/HashSyntax
186
219
 
187
220
  # TODO: Add a log_name config option rather than just using the tag?
@@ -191,9 +224,6 @@ module Fluent
191
224
  attr_reader :project_id
192
225
  attr_reader :zone
193
226
  attr_reader :vm_id
194
- attr_reader :running_on_managed_vm
195
- attr_reader :gae_backend_name
196
- attr_reader :gae_backend_version
197
227
  attr_reader :resource
198
228
  attr_reader :common_labels
199
229
 
@@ -203,6 +233,35 @@ module Fluent
203
233
  @log = $log # rubocop:disable Style/GlobalVars
204
234
  end
205
235
 
236
+ # Set up regex patterns used to parse tags and logs.
237
+ def setup_regex_patterns
238
+ @compiled_kubernetes_tag_regexp = nil
239
+ if @kubernetes_tag_regexp
240
+ @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
241
+ end
242
+
243
+ @cloudfunctions_tag_regexp =
244
+ /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
245
+ @cloudfunctions_log_regexp = /^
246
+ (?:\[(?<severity>.)\])?
247
+ \[(?<timestamp>.{24})\]
248
+ (?:\[(?<execution_id>[^\]]+)\])?
249
+ [ ](?<text>.*)$/x
250
+
251
+ # Docker container tag format:
252
+ # "container.<container_id>.<container_name>".
253
+ @dockercontainer_tag_regexp =
254
+ /^container\.(?<container_id>[a-zA-Z0-9]+)\.
255
+ (?<container_name>[a-zA-Z0-9_.-]+)$/x
256
+ # Docker container with application tag format:
257
+ # "application-container.<container_name>.<additional_tag>".
258
+ @dockercontainer_tag_with_application_regexp =
259
+ /^application-container\.(?<container_name>[a-zA-Z0-9_.-]+)\.
260
+ (?<additional_tag>.+)$/x
261
+
262
+ @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
263
+ end
264
+
206
265
  def configure(conf)
207
266
  super
208
267
 
@@ -221,150 +280,57 @@ module Fluent
221
280
  extra.join(' ')
222
281
  end
223
282
 
224
- # TODO: Send instance tags as labels as well?
225
- @common_labels = {}
226
- @common_labels.merge!(@labels) if @labels
227
-
228
- # TODO: Construct Google::Api::MonitoredResource when @use_grpc is
229
- # true after the protobuf map corruption issue is fixed.
230
- @resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
231
- labels: {})
232
-
233
- @compiled_kubernetes_tag_regexp = nil
234
- if @kubernetes_tag_regexp
235
- @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
236
- end
237
-
238
- @cloudfunctions_tag_regexp =
239
- /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
240
- @cloudfunctions_log_regexp = /^
241
- (?:\[(?<severity>.)\])?
242
- \[(?<timestamp>.{24})\]
243
- (?:\[(?<execution_id>[^\]]+)\])?
244
- [ ](?<text>.*)$/x
245
-
246
- @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
283
+ setup_regex_patterns
247
284
 
248
- # set attributes from metadata (unless overriden by static config)
249
- @vm_name = Socket.gethostname if @vm_name.nil?
250
285
  @platform = detect_platform
251
- case @platform
252
- when Platform::GCE
253
- if @project_id.nil?
254
- @project_id = fetch_gce_metadata('project/project-id')
255
- end
256
- if @zone.nil?
257
- # this returns "projects/<number>/zones/<zone>"; we only want
258
- # the part after the final slash.
259
- fully_qualified_zone = fetch_gce_metadata('instance/zone')
260
- @zone = fully_qualified_zone.rpartition('/')[2]
261
- end
262
- @vm_id = fetch_gce_metadata('instance/id') if @vm_id.nil?
263
- when Platform::EC2
264
- metadata = fetch_ec2_metadata
265
- if @zone.nil? && metadata.key?('availabilityZone')
266
- @zone = 'aws:' + metadata['availabilityZone']
267
- end
268
- if @vm_id.nil? && metadata.key?('instanceId')
269
- @vm_id = metadata['instanceId']
270
- end
271
- if metadata.key?('accountId')
272
- @resource.labels['aws_account'] = metadata['accountId']
273
- end
274
- when Platform::OTHER
275
- # do nothing
276
- else
277
- fail Fluent::ConfigError, 'Unknown platform ' + @platform
278
- end
279
286
 
280
- # If we still don't have a project ID, try to obtain it from the
281
- # credentials.
282
- if @project_id.nil?
283
- @project_id = CredentialsInfo.project_id
284
- @log.info 'Set Project ID from credentials: ', @project_id unless
285
- @project_id.nil?
287
+ # Set agent-level monitored resource. This monitored resource is initiated
288
+ # as the logging agent starts up. It will be inherited by all log entries
289
+ # processed by this agent. First try to retrieve it via Metadata Agent.
290
+ if @enable_metadata_agent
291
+ # The locally-unique key for this should be the instance id. Since this
292
+ # can be implicitly inferred by Metadata Agent, we do not need to
293
+ # explicitly send the key.
294
+ @resource = call_metadata_agent_for_monitored_resource(
295
+ IMPLICIT_MONITORED_RESOURCE_UNIQUE_KEY)
286
296
  end
287
297
 
288
- # all metadata parameters must now be set
289
- unless @project_id && @zone && @vm_id
290
- missing = []
291
- missing << 'project_id' unless @project_id
292
- missing << 'zone' unless @zone
293
- missing << 'vm_id' unless @vm_id
294
- fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
295
- missing.join(' ')
298
+ # Set required variables: @project_id, @vm_id, @vm_name and @zone.
299
+ # If any info above is included in the response from Metadata Agent, make
300
+ # use of that. Otherwise make some additional requests to metadata server.
301
+ #
302
+ # Note: Once we support metadata injection on the Logging API side, we
303
+ # might no longer need to require all these metadata in logging agent. But
304
+ # for now, they are still required.
305
+ set_required_metadata_variables
306
+
307
+ # Fail over to retrieve monitored resource via the legacy path if we fail
308
+ # to get it from Metadata Agent.
309
+ @resource ||= determine_agent_level_monitored_resource_via_legacy
310
+
311
+ # Set variables specific to CLoud Functions. This has to be called after
312
+ # we have determined the resource type. The purpose is to avoid repeated
313
+ # calls to metadata server.
314
+ @running_cloudfunctions = false
315
+ # We only support Cloud Functions logs for GKE right now.
316
+ if @resource.type == CONTAINER_CONSTANTS[:resource_type] &&
317
+ fetch_gce_metadata('instance/attributes/').split.include?('gcf_region')
318
+ # We are not setting resource type as Cloud Functions here because
319
+ # whether a log entry is truly coming from a Cloud Functions function
320
+ # depends on the log tag. Only when @running_cloudfunctions is true will
321
+ # we try to match log tags against Cloud Functions tag regexp when
322
+ # processing log entries.
323
+ @running_cloudfunctions = true
324
+ # Fetch this info and store it to avoid recurring metadata server calls.
325
+ @gcf_region = fetch_gce_metadata('instance/attributes/gcf_region')
296
326
  end
297
327
 
298
- # Default this to false; it is only overwritten if we detect Managed VM.
299
- @running_on_managed_vm = false
300
-
301
- # Default this to false; it is only overwritten if we detect Cloud
302
- # Functions.
303
- @running_cloudfunctions = false
328
+ # Determine the common labels that should be added to all log entries
329
+ # processed by this logging agent.
330
+ @common_labels = determine_agent_level_common_labels
304
331
 
305
- # Set up the MonitoredResource, labels, etc. based on the config.
306
- case @platform
307
- when Platform::GCE
308
- @resource.type = COMPUTE_CONSTANTS[:resource_type]
309
- # TODO: introduce a new MonitoredResource-centric configuration and
310
- # deprecate subservice-name; for now, translate known uses.
311
- if @subservice_name
312
- # TODO: what should we do if we encounter an unknown value?
313
- if @subservice_name == DATAFLOW_CONSTANTS[:service]
314
- @resource.type = DATAFLOW_CONSTANTS[:resource_type]
315
- elsif @subservice_name == ML_CONSTANTS[:service]
316
- @resource.type = ML_CONSTANTS[:resource_type]
317
- end
318
- elsif @detect_subservice
319
- # Check for specialized GCE environments.
320
- # TODO: Add config options for these to allow for running outside GCE?
321
- attributes = fetch_gce_metadata('instance/attributes/').split
322
- # Do nothing, just don't populate other service's labels.
323
- if attributes.include?('gae_backend_name') &&
324
- attributes.include?('gae_backend_version')
325
- # Managed VM
326
- @running_on_managed_vm = true
327
- @gae_backend_name =
328
- fetch_gce_metadata('instance/attributes/gae_backend_name')
329
- @gae_backend_version =
330
- fetch_gce_metadata('instance/attributes/gae_backend_version')
331
- @resource.type = APPENGINE_CONSTANTS[:resource_type]
332
- @resource.labels['module_id'] = @gae_backend_name
333
- @resource.labels['version_id'] = @gae_backend_version
334
- elsif attributes.include?('kube-env')
335
- # Kubernetes/Container Engine
336
- @resource.type = CONTAINER_CONSTANTS[:resource_type]
337
- @raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
338
- @kube_env = YAML.load(@raw_kube_env)
339
- @resource.labels['cluster_name'] =
340
- cluster_name_from_kube_env(@kube_env)
341
- detect_cloudfunctions(attributes)
342
- end
343
- end
344
- # Some services have the GCE instance_id and zone as MonitoredResource
345
- # labels; for other services we send them as entry labels.
346
- if @resource.type == COMPUTE_CONSTANTS[:resource_type] ||
347
- @resource.type == CONTAINER_CONSTANTS[:resource_type]
348
- @resource.labels['instance_id'] = @vm_id
349
- @resource.labels['zone'] = @zone
350
- else
351
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
352
- common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
353
- end
354
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
355
- when Platform::EC2
356
- @resource.type = EC2_CONSTANTS[:resource_type]
357
- @resource.labels['instance_id'] = @vm_id
358
- @resource.labels['region'] = @zone
359
- # the aws_account label is populated above.
360
- common_labels["#{EC2_CONSTANTS[:service]}/resource_name"] = @vm_name
361
- when Platform::OTHER
362
- # Use GCE as the default environment.
363
- @resource.type = COMPUTE_CONSTANTS[:resource_type]
364
- @resource.labels['instance_id'] = @vm_id
365
- @resource.labels['zone'] = @zone
366
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
367
- end
332
+ # For each resource type, there is a list of labels that we want to report
333
+ # as monitored resource instead of metadata labels. Move them if present.
368
334
  @resource.labels.merge!(
369
335
  extract_resource_labels(@resource.type, common_labels))
370
336
 
@@ -376,7 +342,7 @@ module Fluent
376
342
 
377
343
  # Log an informational message containing the Logs viewer URL
378
344
  @log.info 'Logs viewer address: https://console.cloud.google.com/logs/',
379
- "viewer?project=#{@project_id}&resource=#{@resource_type}/",
345
+ "viewer?project=#{@project_id}&resource=#{@resource.type}/",
380
346
  "instance_id/#{@vm_id}"
381
347
  end
382
348
 
@@ -391,130 +357,6 @@ module Fluent
391
357
  super
392
358
  end
393
359
 
394
- def format(tag, time, record)
395
- [tag, time, record].to_msgpack
396
- end
397
-
398
- # Given a tag, returns the corresponding valid tag if possible, or nil if
399
- # the tag should be rejected. If 'require_valid_tags' is false, non-string
400
- # tags are converted to strings, and invalid characters are sanitized;
401
- # otherwise such tags are rejected.
402
- def sanitize_tag(tag)
403
- if @require_valid_tags &&
404
- (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
405
- return nil
406
- end
407
- tag = convert_to_utf8(tag.to_s)
408
- tag = '_' if tag == ''
409
- tag
410
- end
411
-
412
- # Compute the monitored resource and common labels shared by a collection of
413
- # entries.
414
- def compute_group_resource_and_labels(tag)
415
- # Note that we assume that labels added to group_common_labels below are
416
- # not 'service' labels (i.e. we do not call extract_resource_labels
417
- # again).
418
- group_resource = @resource.dup
419
- group_common_labels = @common_labels.dup
420
-
421
- if @running_cloudfunctions
422
- # If the current group of entries is coming from a Cloud Functions
423
- # function, the function name can be extracted from the tag.
424
- match_data = @cloudfunctions_tag_regexp.match(tag)
425
- if match_data
426
- # Resource type is set to Cloud Functions only for logs actually
427
- # coming from a function, otherwise we leave it as Container.
428
- group_resource.type = CLOUDFUNCTIONS_CONSTANTS[:resource_type]
429
- group_resource.labels['region'] = @gcf_region
430
- group_resource.labels['function_name'] =
431
- decode_cloudfunctions_function_name(
432
- match_data['encoded_function_name'])
433
- # Move GKE container labels from the MonitoredResource to the
434
- # LogEntry.
435
- instance_id = group_resource.labels.delete('instance_id')
436
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/cluster_name"] =
437
- group_resource.labels.delete('cluster_name')
438
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/instance_id"] =
439
- instance_id
440
- group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
441
- instance_id
442
- group_common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] =
443
- group_resource.labels.delete('zone')
444
- end
445
- end
446
- if group_resource.type == CONTAINER_CONSTANTS[:resource_type] &&
447
- @compiled_kubernetes_tag_regexp
448
- # Container logs in Kubernetes are tagged based on where they came
449
- # from, so we can extract useful metadata from the tag.
450
- # Do this here to avoid having to repeat it for each record.
451
- match_data = @compiled_kubernetes_tag_regexp.match(tag)
452
- if match_data
453
- group_resource.labels['container_name'] = match_data['container_name']
454
- group_resource.labels['namespace_id'] = match_data['namespace_name']
455
- group_resource.labels['pod_id'] = match_data['pod_name']
456
- %w(namespace_name pod_name).each do |field|
457
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/#{field}"] =
458
- match_data[field]
459
- end
460
- end
461
- end
462
-
463
- # Freeze the per-request state. Any further changes must be made on a
464
- # per-entry basis.
465
- group_resource.freeze
466
- group_resource.labels.freeze
467
- group_common_labels.freeze
468
-
469
- [group_resource, group_common_labels]
470
- end
471
-
472
- # Extract entry resource and common labels that should be applied to
473
- # individual entries from the group resource.
474
- def extract_entry_labels(group_resource, record)
475
- resource_labels = {}
476
- common_labels = {}
477
-
478
- if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
479
- record.key?('log')
480
- @cloudfunctions_log_match =
481
- @cloudfunctions_log_regexp.match(record['log'])
482
- end
483
-
484
- if group_resource.type == CONTAINER_CONSTANTS[:resource_type]
485
- # Move the stdout/stderr annotation from the record into a label
486
- common_labels.merge!(
487
- fields_to_labels(
488
- record, 'stream' => "#{CONTAINER_CONSTANTS[:service]}/stream"))
489
-
490
- # If the record has been annotated by the kubernetes_metadata_filter
491
- # plugin, then use that metadata. Otherwise, rely on commonLabels
492
- # populated at the grouped_entries level from the group's tag.
493
- if record.key?('kubernetes')
494
- extracted_resource_labels, extracted_common_labels = \
495
- extract_container_metadata(record)
496
- resource_labels.merge!(extracted_resource_labels)
497
- common_labels.merge!(extracted_common_labels)
498
- end
499
- end
500
-
501
- # If a field is present in the label_map, send its value as a label
502
- # (mapping the field name to label name as specified in the config)
503
- # and do not send that field as part of the payload.
504
- common_labels.merge!(fields_to_labels(record, @label_map))
505
-
506
- if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
507
- @cloudfunctions_log_match &&
508
- @cloudfunctions_log_match['execution_id']
509
- common_labels['execution_id'] =
510
- @cloudfunctions_log_match['execution_id']
511
- end
512
- resource_labels.merge!(
513
- extract_resource_labels(group_resource.type, common_labels))
514
-
515
- [resource_labels, common_labels]
516
- end
517
-
518
360
  def write(chunk)
519
361
  # Group the entries since we have to make one call per tag.
520
362
  grouped_entries = {}
@@ -531,20 +373,21 @@ module Fluent
531
373
 
532
374
  grouped_entries.each do |tag, arr|
533
375
  entries = []
534
- group_resource, group_common_labels = compute_group_resource_and_labels(
535
- tag)
376
+ group_resource, group_common_labels =
377
+ determine_group_level_monitored_resource_and_labels(tag)
536
378
 
537
379
  arr.each do |time, record|
538
380
  next unless record.is_a?(Hash)
539
381
 
540
382
  extracted_resource_labels, extracted_common_labels = \
541
- extract_entry_labels(group_resource, record)
383
+ determine_entry_level_labels(group_resource, record)
542
384
  entry_resource = group_resource.dup
543
385
  entry_resource.labels.merge!(extracted_resource_labels)
544
386
  entry_common_labels = \
545
387
  group_common_labels.merge(extracted_common_labels)
546
388
 
547
- if entry_resource.type == CONTAINER_CONSTANTS[:resource_type]
389
+ if [CONTAINER_CONSTANTS[:resource_type],
390
+ DOCKER_CONSTANTS[:resource_type]].include?(entry_resource.type)
548
391
  # Save the timestamp if available, then clear it out to allow for
549
392
  # determining whether we should parse the log or message field.
550
393
  timestamp = record.key?('time') ? record['time'] : nil
@@ -777,7 +620,7 @@ module Fluent
777
620
  end
778
621
  end
779
622
  rescue StandardError => e
780
- @log.debug 'Failed to access metadata service: ', error: e
623
+ @log.error 'Failed to access metadata service: ', error: e
781
624
  end
782
625
 
783
626
  @log.info 'Unable to determine platform'
@@ -792,15 +635,570 @@ module Fluent
792
635
  metadata_path, 'Metadata-Flavor' => 'Google', &:read)
793
636
  end
794
637
 
795
- def fetch_ec2_metadata
796
- fail "Called fetch_ec2_metadata with platform=#{@platform}" unless
638
+ # EC2 Metadata server returns everything in one call. Store it after the
639
+ # first fetch to avoid making multiple calls.
640
+ def ec2_metadata
641
+ fail "Called ec2_metadata with platform=#{@platform}" unless
642
+ @platform == Platform::EC2
643
+ unless @ec2_metadata
644
+ # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
645
+ open('http://' + METADATA_SERVICE_ADDR +
646
+ '/latest/dynamic/instance-identity/document') do |f|
647
+ contents = f.read
648
+ @ec2_metadata = JSON.parse(contents)
649
+ end
650
+ end
651
+
652
+ @ec2_metadata
653
+ end
654
+
655
+ # Set regexp patterns to parse tags and logs.
656
+ def set_regexp_patterns
657
+ @compiled_kubernetes_tag_regexp = nil
658
+ if @kubernetes_tag_regexp
659
+ @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
660
+ end
661
+
662
+ @cloudfunctions_tag_regexp =
663
+ /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
664
+ @cloudfunctions_log_regexp = /^
665
+ (?:\[(?<severity>.)\])?
666
+ \[(?<timestamp>.{24})\]
667
+ (?:\[(?<execution_id>[^\]]+)\])?
668
+ [ ](?<text>.*)$/x
669
+
670
+ @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
671
+ end
672
+
673
+ # Set required variables like @project_id, @vm_id, @vm_name and @zone.
674
+ def set_required_metadata_variables
675
+ set_project_id
676
+ set_vm_id
677
+ set_vm_name
678
+ set_location
679
+
680
+ # All metadata parameters must now be set.
681
+ return if @project_id && @zone && @vm_id
682
+ missing = []
683
+ missing << 'project_id' unless @project_id
684
+ missing << 'zone' unless @zone
685
+ missing << 'vm_id' unless @vm_id
686
+ fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
687
+ missing.join(' ')
688
+ end
689
+
690
+ # 1. Return the value if it is explicitly set in the config already.
691
+ # 2. If not, try to retrieve it by calling metadata server directly.
692
+ # 3. If still not set, try to obtain it from the credentials.
693
+ def set_project_id
694
+ @project_id ||= fetch_gce_metadata('project/project-id') if
695
+ @platform == Platform::GCE
696
+ @project_id ||= CredentialsInfo.project_id
697
+ rescue StandardError => e
698
+ @log.error 'Failed to obtain project id: ', error: e
699
+ end
700
+
701
+ # 1. Return the value if it is explicitly set in the config already.
702
+ # 2. If not, check if the response from Metadata Agent includes this info.
703
+ # 3. If not, try to retrieve it by calling metadata servers directly.
704
+ def set_vm_id
705
+ @vm_id ||= @resource.labels['instance_id'] if
706
+ !@resource.nil? && @resource.labels.key?('instance_id')
707
+ @vm_id ||= fetch_gce_metadata('instance/id') if @platform == Platform::GCE
708
+ @vm_id ||= ec2_metadata['instanceId'] if @platform == Platform::EC2
709
+ rescue StandardError => e
710
+ @log.error 'Failed to obtain vm_id: ', error: e
711
+ end
712
+
713
+ # 1. Return the value if it is explicitly set in the config already.
714
+ # 2. If not, check if the response from Metadata Agent includes this info.
715
+ # 3. If not, try to retrieve it locally.
716
+ def set_vm_name
717
+ @vm_name ||= @resource.labels['instance_name'] if
718
+ !@resource.nil? && @resource.labels.key?('instance_name')
719
+ @vm_name ||= Socket.gethostname
720
+ rescue StandardError => e
721
+ @log.error 'Failed to obtain vm name: ', error: e
722
+ end
723
+
724
+ # 1. Return the value if it is explicitly set in the config already.
725
+ # 2. If not, check if the response from Metadata Agent includes this info.
726
+ # 3. If not, try to retrieve it locally.
727
+ def set_location
728
+ unless @resource.nil?
729
+ @zone ||= @resource.labels['location'] if
730
+ @resource.type == DOCKER_CONSTANTS[:resource_type] &&
731
+ @resource.labels.key?('location')
732
+ @zone ||= @resource.labels['zone'] if
733
+ @platform == Platform::GCE && @resource.labels.key?('zone')
734
+ @zone ||= @resource.labels['region'] if
735
+ @platform == Platform::EC2 && @resource.labels.key?('region')
736
+ end
737
+ # Response format: "projects/<number>/zones/<zone>"
738
+ @zone ||= fetch_gce_metadata('instance/zone').rpartition('/')[2] if
739
+ @platform == Platform::GCE
740
+ @zone ||= 'aws:' + ec2_metadata['availabilityZone'] if
741
+ @platform == Platform::EC2 && ec2_metadata.key?('availabilityZone')
742
+ rescue StandardError => e
743
+ @log.error 'Failed to obtain location: ', error: e
744
+ end
745
+
746
+ # Retrieve monitored resource via the legacy way.
747
+ #
748
+ # Note: This is just a failover plan if we fail to get metadata from
749
+ # Metadata agent. Thus it should be equivalent to what Metadata Agent
750
+ # returns.
751
+ def determine_agent_level_monitored_resource_via_legacy
752
+ resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
753
+ labels: {})
754
+ resource.type = determine_agent_level_monitored_resource_type
755
+ resource.labels = determine_agent_level_monitored_resource_labels(
756
+ resource.type)
757
+ resource
758
+ end
759
+
760
+ # Determine agent level monitored resource type.
761
+ def determine_agent_level_monitored_resource_type
762
+ # EC2 instance.
763
+ return EC2_CONSTANTS[:resource_type] if
797
764
  @platform == Platform::EC2
798
- # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
799
- open('http://' + METADATA_SERVICE_ADDR +
800
- '/latest/dynamic/instance-identity/document') do |f|
801
- contents = f.read
802
- return JSON.parse(contents)
765
+
766
+ # Unknown platform will be defaulted to GCE instance..
767
+ return COMPUTE_CONSTANTS[:resource_type] if
768
+ @platform == Platform::OTHER
769
+
770
+ # Resource types determined by @subservice_name config.
771
+ # Cloud Dataflow.
772
+ return DATAFLOW_CONSTANTS[:resource_type] if
773
+ @subservice_name == DATAFLOW_CONSTANTS[:service]
774
+ # Cloud ML.
775
+ return ML_CONSTANTS[:resource_type] if
776
+ @subservice_name == ML_CONSTANTS[:service]
777
+ # Default back to GCE if invalid value is detected.
778
+ return COMPUTE_CONSTANTS[:resource_type] if
779
+ @subservice_name
780
+
781
+ # Resource types determined by @detect_subservice config.
782
+ if @detect_subservice
783
+ begin
784
+ attributes = fetch_gce_metadata('instance/attributes/').split
785
+ rescue StandardError => e
786
+ @log.error 'Failed to detect subservice: ', error: e
787
+ end
788
+ # GAE app.
789
+ return APPENGINE_CONSTANTS[:resource_type] if
790
+ attributes.include?('gae_backend_name') &&
791
+ attributes.include?('gae_backend_version')
792
+ # GKE container.
793
+ return CONTAINER_CONSTANTS[:resource_type] if
794
+ attributes.include?('kube-env')
795
+ # Cloud Dataproc.
796
+ return DATAPROC_CONSTANTS[:resource_type] if
797
+ attributes.include?('dataproc-cluster-uuid') &&
798
+ attributes.include?('dataproc-cluster-name')
799
+ end
800
+ # GCE instance.
801
+ COMPUTE_CONSTANTS[:resource_type]
802
+ end
803
+
804
+ # Determine agent level monitored resource labels based on the resource
805
+ # type. Each resource type has its own labels that need to be filled in.
806
+ def determine_agent_level_monitored_resource_labels(type)
807
+ labels = {}
808
+
809
+ case type
810
+
811
+ # GAE app.
812
+ when APPENGINE_CONSTANTS[:resource_type]
813
+ begin
814
+ labels['module_id'] = fetch_gce_metadata(
815
+ 'instance/attributes/gae_backend_name')
816
+ labels['version_id'] = fetch_gce_metadata(
817
+ 'instance/attributes/gae_backend_version')
818
+ rescue StandardError => e
819
+ @log.error 'Failed to set monitored resource labels for GAE: ',
820
+ error: e
821
+ end
822
+
823
+ # GCE.
824
+ when COMPUTE_CONSTANTS[:resource_type]
825
+ labels['instance_id'] = @vm_id
826
+ labels['zone'] = @zone
827
+
828
+ # GKE container.
829
+ when CONTAINER_CONSTANTS[:resource_type]
830
+ labels['instance_id'] = @vm_id
831
+ labels['zone'] = @zone
832
+ begin
833
+ raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
834
+ kube_env = YAML.load(raw_kube_env)
835
+ labels['cluster_name'] =
836
+ cluster_name_from_kube_env(kube_env)
837
+ rescue StandardError => e
838
+ @log.error 'Failed to set monitored resource labels for GKE: ',
839
+ error: e
840
+ end
841
+
842
+ # Cloud Dataproc.
843
+ when DATAPROC_CONSTANTS[:resource_type]
844
+ begin
845
+ labels['cluster_uuid'] =
846
+ fetch_gce_metadata('instance/attributes/dataproc-cluster-uuid')
847
+ labels['cluster_name'] =
848
+ fetch_gce_metadata('instance/attributes/dataproc-cluster-name')
849
+ labels['region'] =
850
+ fetch_gce_metadata('instance/attributes/dataproc-region')
851
+ rescue StandardError => e
852
+ @log.error 'Failed to set monitored resource labels for Cloud ' \
853
+ 'Dataproc: ', error: e
854
+ end
855
+
856
+ # EC2.
857
+ when EC2_CONSTANTS[:resource_type]
858
+ labels['instance_id'] = @vm_id
859
+ labels['region'] = @zone
860
+ labels['aws_account'] = ec2_metadata['accountId'] if
861
+ ec2_metadata.key?('accountId')
862
+ end
863
+ labels
864
+ end
865
+
866
+ # Determine the common labels that should be added to all log entries
867
+ # processed by this logging agent.
868
+ def determine_agent_level_common_labels
869
+ labels = {}
870
+ # User can specify labels via config. We want to capture those as well.
871
+ # TODO: Send instance tags as labels as well?
872
+ labels.merge!(@labels) if @labels
873
+
874
+ case @resource.type
875
+
876
+ # GAE app.
877
+ when APPENGINE_CONSTANTS[:resource_type]
878
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
879
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
880
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
881
+
882
+ # GCE.
883
+ when COMPUTE_CONSTANTS[:resource_type]
884
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
885
+
886
+ # GKE container.
887
+ when CONTAINER_CONSTANTS[:resource_type]
888
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
889
+
890
+ # Cloud Dataflow and Cloud Dataproc.
891
+ when DATAFLOW_CONSTANTS[:resource_type],
892
+ DATAPROC_CONSTANTS[:resource_type]
893
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
894
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
895
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
896
+
897
+ # EC2.
898
+ when EC2_CONSTANTS[:resource_type]
899
+ labels["#{EC2_CONSTANTS[:service]}/resource_name"] = @vm_name
900
+
901
+ # Cloud ML.
902
+ when ML_CONSTANTS[:resource_type]
903
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
904
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
905
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
906
+ end
907
+ labels
908
+ end
909
+
910
+ # Determine the group level monitored resource and common labels shared by a
911
+ # collection of entries.
912
+ def determine_group_level_monitored_resource_and_labels(tag)
913
+ # Determine group level monitored resource type. For certain types,
914
+ # extract useful info from the tag and store those in
915
+ # matched_regexp_group.
916
+ group_resource_type, matched_regexp_group =
917
+ determine_group_level_monitored_resource_type(tag)
918
+
919
+ # Determine group level monitored resource labels and common labels.
920
+ group_resource_type, group_resource_labels, group_common_labels = \
921
+ determine_group_level_labels_and_adjust_type(
922
+ group_resource_type, matched_regexp_group)
923
+
924
+ group_resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
925
+ type: group_resource_type,
926
+ labels: group_resource_labels.to_h
927
+ )
928
+
929
+ # Freeze the per-request state. Any further changes must be made on a
930
+ # per-entry basis.
931
+ group_resource.freeze
932
+ group_resource.labels.freeze
933
+ group_common_labels.freeze
934
+
935
+ [group_resource, group_common_labels]
936
+ end
937
+
938
+ # Determine group level monitored resource type shared by a collection of
939
+ # entries.
940
+ # Returns the resource type and tag regexp matched groups. The matched
941
+ # groups only apply to some resource types. Return nil if not applicable or
942
+ # if there is no match.
943
+ def determine_group_level_monitored_resource_type(tag)
944
+ # Match tag against Cloud Functions format.
945
+ if @running_cloudfunctions
946
+ matched_regexp_group = @cloudfunctions_tag_regexp.match(tag)
947
+ return [CLOUDFUNCTIONS_CONSTANTS[:resource_type],
948
+ matched_regexp_group] if matched_regexp_group
949
+ end
950
+
951
+ # Match tag against Docker container stderr / stdout log format and
952
+ # Docker container application log format.
953
+ matched_regexp_group =
954
+ # Format: "container.<container_id>.<container_name>"
955
+ @dockercontainer_tag_regexp.match(tag) ||
956
+ # Format: "application-container.<container_name>.<additional_tag>"
957
+ @dockercontainer_tag_with_application_regexp.match(tag)
958
+ return [DOCKER_CONSTANTS[:resource_type], matched_regexp_group] if
959
+ matched_regexp_group
960
+
961
+ # Match tag against GKE Container format.
962
+ if @resource.type == CONTAINER_CONSTANTS[:resource_type] &&
963
+ @compiled_kubernetes_tag_regexp
964
+ # Container logs in Kubernetes are tagged based on where they came from,
965
+ # so we can extract useful metadata from the tag. Do this here to avoid
966
+ # having to repeat it for each record.
967
+ matched_regexp_group = @compiled_kubernetes_tag_regexp.match(tag)
968
+ return [@resource.type, matched_regexp_group] if matched_regexp_group
969
+ end
970
+
971
+ # Otherwise, return the original type.
972
+ [@resource.type, nil]
973
+ end
974
+
975
+ # Determine group level monitored resource labels and common labels. These
976
+ # labels will be shared by a collection of entries. In certain cases, we
977
+ # might also adjust the resource type.
978
+ def determine_group_level_labels_and_adjust_type(group_resource_type,
979
+ matched_regexp_group)
980
+ group_resource_labels = @resource.labels.dup
981
+ group_common_labels = @common_labels.dup
982
+
983
+ case group_resource_type
984
+
985
+ # Cloud Functions.
986
+ when CLOUDFUNCTIONS_CONSTANTS[:resource_type]
987
+ group_resource_labels['region'] = @gcf_region
988
+ group_resource_labels['function_name'] =
989
+ decode_cloudfunctions_function_name(
990
+ matched_regexp_group['encoded_function_name'])
991
+ instance_id = group_resource_labels.delete('instance_id')
992
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/cluster_name"] =
993
+ group_resource_labels.delete('cluster_name')
994
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/instance_id"] =
995
+ instance_id
996
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
997
+ instance_id
998
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] =
999
+ group_resource_labels.delete('zone')
1000
+
1001
+ # GKE container.
1002
+ when CONTAINER_CONSTANTS[:resource_type]
1003
+ if matched_regexp_group
1004
+ group_resource_labels['container_name'] =
1005
+ matched_regexp_group['container_name']
1006
+ group_resource_labels['namespace_id'] =
1007
+ matched_regexp_group['namespace_name']
1008
+ group_resource_labels['pod_id'] =
1009
+ matched_regexp_group['pod_name']
1010
+ %w(namespace_name pod_name).each do |field|
1011
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/#{field}"] =
1012
+ matched_regexp_group[field]
1013
+ end
1014
+ end
1015
+
1016
+ # Docker container.
1017
+ when DOCKER_CONSTANTS[:resource_type]
1018
+ # For Docker container stderr / stdout logs generated by Docker Fluentd
1019
+ # Logging Driver, tags are in the format of "container.<container_id>.
1020
+ # <container_name>", thus they include 'container_id' info.
1021
+ # For logs generated by applications running in Docker containers,
1022
+ # tags are in the format of "application-container.<container_name>.
1023
+ # <additional_tag>", thus 'container_id' info is unknown yet.
1024
+ # 'container_name' info on the other hand is always available.
1025
+ container_id = matched_regexp_group['container_id'] if
1026
+ matched_regexp_group.names.include? 'container_id'
1027
+ container_name = matched_regexp_group['container_name']
1028
+
1029
+ if @enable_metadata_agent
1030
+ # Call Metadata Agent with "container.<container_id>" or
1031
+ # "application-container.<container_name>" as the locally-unique key
1032
+ # to retrieve monitored resource. This should be different from the
1033
+ # original @resource value that got initiated when the agent starts up
1034
+ # because that one is always at the VM level.
1035
+ if container_id
1036
+ locally_unique_id = "container.#{container_id}"
1037
+ else
1038
+ locally_unique_id = "containerName.#{container_name}"
1039
+ end
1040
+ retrieved_resource = call_metadata_agent_for_monitored_resource(
1041
+ locally_unique_id)
1042
+ end
1043
+
1044
+ if @enable_metadata_agent && !retrieved_resource.nil?
1045
+ # If we successfully get a monitored resource from Metadata Agent,
1046
+ # use this one instead of the original VM-level monitored resource.
1047
+ group_resource_labels = retrieved_resource.labels.dup
1048
+ @log.debug 'Retrieved monitored resource from Metadata Agent: ' \
1049
+ "#{retrieved_resource.inspect}."
1050
+ else
1051
+ # If Metadata Agent is not enabled, or we failed to get a monitored
1052
+ # resource, we need to have some backup plan.
1053
+ @log.debug 'Metadata agent not enabled or failed to retrieve ' \
1054
+ 'docker container monitored resource from Metadata ' \
1055
+ 'Agent.'
1056
+
1057
+ # 1. Check if 'container_id' is set already. It should be available
1058
+ # for stdout / stderr). If so, use that.
1059
+ # 2. If not, call Docker Remote API to calculate container id from
1060
+ # container name, but only if @call_docker_api_locally is true.
1061
+ container_id ||= retrieve_container_id_by_name_locally(
1062
+ container_name) if @call_docker_api_locally
1063
+ unless container_id
1064
+ @log.debug 'No docker container id retrieved. Falling back to
1065
+ instance monitored resource.'
1066
+ # If a container id is not available, fall back to the instance
1067
+ # monitored resource.
1068
+ return [COMPUTE_CONSTANTS[:resource_type], group_resource_labels,
1069
+ group_common_labels]
1070
+ end
1071
+ group_resource_labels['container_id'] = container_id
1072
+ # 'zone' for GCP and 'region' for EC2 must have been set at this
1073
+ # point. Rename them to 'location'.
1074
+ group_resource_labels['location'] = @zone
1075
+ if @platform == Platform::EC2
1076
+ group_resource_labels.delete('region')
1077
+ else
1078
+ group_resource_labels.delete('zone')
1079
+ end
1080
+ # vm id info should be reported as a metadata label instead.
1081
+ group_resource_labels.delete('instance_id')
1082
+
1083
+ end
1084
+ # Set metadata labels.
1085
+ group_common_labels["#{DOCKER_CONSTANTS[:service]}/container_name"] =
1086
+ matched_regexp_group['container_name']
1087
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
1088
+ @vm_id
1089
+ end
1090
+
1091
+ [group_resource_type, group_resource_labels, group_common_labels]
1092
+ end
1093
+
1094
+ # Extract entry resource and common labels that should be applied to
1095
+ # individual entries from the group resource.
1096
+ def determine_entry_level_labels(group_resource, record)
1097
+ resource_labels = {}
1098
+ common_labels = {}
1099
+
1100
+ # Cloud Functions.
1101
+ if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1102
+ record.key?('log')
1103
+ @cloudfunctions_log_match =
1104
+ @cloudfunctions_log_regexp.match(record['log'])
1105
+ common_labels['execution_id'] =
1106
+ @cloudfunctions_log_match['execution_id'] if \
1107
+ @cloudfunctions_log_match &&
1108
+ @cloudfunctions_log_match['execution_id']
1109
+ end
1110
+
1111
+ # GKE containers.
1112
+ if group_resource.type == CONTAINER_CONSTANTS[:resource_type]
1113
+ # Move the stdout/stderr annotation from the record into a label.
1114
+ common_labels.merge!(
1115
+ fields_to_labels(
1116
+ record, 'stream' => "#{CONTAINER_CONSTANTS[:service]}/stream"))
1117
+
1118
+ # If the record has been annotated by the kubernetes_metadata_filter
1119
+ # plugin, then use that metadata. Otherwise, rely on commonLabels
1120
+ # populated at the grouped_entries level from the group's tag.
1121
+ if record.key?('kubernetes')
1122
+ extracted_resource_labels, extracted_common_labels = \
1123
+ extract_container_metadata(record)
1124
+ resource_labels.merge!(extracted_resource_labels)
1125
+ common_labels.merge!(extracted_common_labels)
1126
+ end
1127
+ end
1128
+
1129
+ # Docker containers.
1130
+ if group_resource.type == DOCKER_CONSTANTS[:resource_type]
1131
+ # For logs coming from Docker Fluentd Logging Driver, the log record
1132
+ # has 4 fields: 'container_id', 'container_name', 'source' and 'log'.
1133
+ # Extract 'container_id', 'container_name' and 'source' from json
1134
+ # record, set corresponding labels, and remove these fields from record.
1135
+ {
1136
+ 'container_name' => 'container_name',
1137
+ 'source' => 'stream'
1138
+ }.each do |field_name, label_name|
1139
+ common_labels.merge!(
1140
+ fields_to_labels(
1141
+ record,
1142
+ field_name => "#{DOCKER_CONSTANTS[:service]}/#{label_name}"
1143
+ )
1144
+ )
1145
+ end
1146
+ resource_labels.merge!(
1147
+ fields_to_labels(record, 'container_id' => 'container_id'))
1148
+ end
1149
+
1150
+ # If the name of a field in the record is present in the @label_map
1151
+ # configured by users, report its value as a label and do not send that
1152
+ # field as part of the payload.
1153
+ common_labels.merge!(fields_to_labels(record, @label_map))
1154
+
1155
+ resource_labels.merge!(
1156
+ extract_resource_labels(group_resource.type, common_labels))
1157
+
1158
+ [resource_labels, common_labels]
1159
+ end
1160
+
1161
+ # Call Metadata Agent to get monitored resource information and parse
1162
+ # response to Google::Api::MonitoredResource.
1163
+ def call_metadata_agent_for_monitored_resource(unique_key)
1164
+ response = call_metadata_agent("monitoredResource/#{unique_key}")
1165
+ return nil if response.nil?
1166
+ begin
1167
+ resource = Google::Api::MonitoredResource.decode_json(response.to_json)
1168
+ rescue Google::Protobuf::ParseError, ArgumentError => e
1169
+ @log.error 'Error paring monitored resource from Metadata Agent. ' \
1170
+ "response: #{response.inspect}", error: e
1171
+ return nil
1172
+ end
1173
+
1174
+ # TODO(lingshi): Use Google::Api::MonitoredResource directly after we
1175
+ # upgrade gRPC version to include the fix for the protobuf map
1176
+ # corruption issue.
1177
+ Google::Apis::LoggingV2beta1::MonitoredResource.new(
1178
+ type: resource.type,
1179
+ labels: resource.labels.to_h
1180
+ )
1181
+ end
1182
+
1183
+ # Call Metadata Agent and parse response to json. Return nil in case of any
1184
+ # error / failure.
1185
+ def call_metadata_agent(path)
1186
+ url = "#{@metadata_agent_url}/#{path}"
1187
+ @log.debug("Calling Metadata Agent: #{url}")
1188
+ open(url) do |f|
1189
+ response = f.read
1190
+ parsed_hash = parse_json_or_nil(response)
1191
+ if parsed_hash.nil?
1192
+ @log.error 'Response from Metadata Agent is not in valid json ' \
1193
+ "format: '#{response.inspect}'."
1194
+ return nil
1195
+ end
1196
+ @log.debug "Response from Metadata Agent: #{parsed_hash}"
1197
+ return parsed_hash
803
1198
  end
1199
+ rescue StandardError => e
1200
+ @log.error 'Error calling Metadata Agent.', error: e
1201
+ return nil
804
1202
  end
805
1203
 
806
1204
  # TODO: This functionality should eventually be available in another
@@ -838,11 +1236,28 @@ module Fluent
838
1236
  end
839
1237
  end
840
1238
 
841
- def detect_cloudfunctions(attributes)
842
- return unless attributes.include?('gcf_region')
843
- # Cloud Functions detected
844
- @running_cloudfunctions = true
845
- @gcf_region = fetch_gce_metadata('instance/attributes/gcf_region')
1239
+ # Calling Docker Remote API to get container id by name.
1240
+ def retrieve_container_id_by_name_locally(container_name)
1241
+ response = Excon.get(
1242
+ "unix:///containers/#{container_name}/json",
1243
+ socket: @docker_remote_api_socket_path)
1244
+ @log.debug "Response from Docker API with name '#{container_name}': " \
1245
+ "#{response.inspect}."
1246
+ return parse_container_id_from_docker_api_response(response)
1247
+ rescue StandardError => e
1248
+ @log.error 'Error calling Docker API to get container id.', error: e
1249
+ return nil
1250
+ end
1251
+
1252
+ # Parse the container id from Docker Remote API response.
1253
+ # TODO(qingling128) Add a config for Docker API version to support parsing
1254
+ # different versions of Docker Remote API when the format varies.
1255
+ def parse_container_id_from_docker_api_response(response)
1256
+ JSON.parse(response.data[:body])['Id']
1257
+ rescue StandardError => e
1258
+ @log.error 'Error parsing Docker API response to get container id.',
1259
+ error: e
1260
+ return nil
846
1261
  end
847
1262
 
848
1263
  def cluster_name_from_kube_env(kube_env)
@@ -915,9 +1330,14 @@ module Fluent
915
1330
  end
916
1331
  elsif record.key?('severity')
917
1332
  return parse_severity(record.delete('severity'))
918
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
919
- entry_common_labels.key?("#{CONTAINER_CONSTANTS[:service]}/stream")
920
- stream = entry_common_labels["#{CONTAINER_CONSTANTS[:service]}/stream"]
1333
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1334
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type)
1335
+ stream = entry_common_labels[
1336
+ "#{CONTAINER_CONSTANTS[:service]}/stream"] if
1337
+ resource_type == CONTAINER_CONSTANTS[:resource_type]
1338
+ stream = entry_common_labels[
1339
+ "#{DOCKER_CONSTANTS[:service]}/stream"] if
1340
+ resource_type == DOCKER_CONSTANTS[:resource_type]
921
1341
  if stream == 'stdout'
922
1342
  return 'INFO'
923
1343
  elsif stream == 'stderr'
@@ -1125,6 +1545,24 @@ module Fluent
1125
1545
  [resource_labels, common_labels]
1126
1546
  end
1127
1547
 
1548
+ def format(tag, time, record)
1549
+ [tag, time, record].to_msgpack
1550
+ end
1551
+
1552
+ # Given a tag, returns the corresponding valid tag if possible, or nil if
1553
+ # the tag should be rejected. If 'require_valid_tags' is false, non-string
1554
+ # tags are converted to strings, and invalid characters are sanitized;
1555
+ # otherwise such tags are rejected.
1556
+ def sanitize_tag(tag)
1557
+ if @require_valid_tags &&
1558
+ (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
1559
+ return nil
1560
+ end
1561
+ tag = convert_to_utf8(tag.to_s)
1562
+ tag = '_' if tag == ''
1563
+ tag
1564
+ end
1565
+
1128
1566
  # For every original_label => new_label pair in the label_map, delete the
1129
1567
  # original_label from the record if it exists, and extract the value to form
1130
1568
  # a map with the new_label as the key.
@@ -1152,7 +1590,8 @@ module Fluent
1152
1590
  entry.text_payload = record['log']
1153
1591
  elsif is_json
1154
1592
  entry.json_payload = record
1155
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1593
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1594
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type) &&
1156
1595
  record.key?('log')
1157
1596
  entry.text_payload = record['log']
1158
1597
  elsif record.size == 1 && record.key?('message')
@@ -1222,7 +1661,8 @@ module Fluent
1222
1661
  entry.text_payload = convert_to_utf8(record['log'])
1223
1662
  elsif is_json
1224
1663
  entry.json_payload = struct_from_ruby(record)
1225
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1664
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1665
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type) &&
1226
1666
  record.key?('log')
1227
1667
  entry.text_payload = convert_to_utf8(record['log'])
1228
1668
  elsif record.size == 1 && record.key?('message')
@@ -1235,7 +1675,7 @@ module Fluent
1235
1675
  def log_name(tag, resource)
1236
1676
  if resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type]
1237
1677
  tag = 'cloud-functions'
1238
- elsif @running_on_managed_vm
1678
+ elsif resource.type == APPENGINE_CONSTANTS[:resource_type]
1239
1679
  # Add a prefix to Managed VM logs to prevent namespace collisions.
1240
1680
  tag = "#{APPENGINE_CONSTANTS[:service]}/#{tag}"
1241
1681
  elsif resource.type == CONTAINER_CONSTANTS[:resource_type]
@@ -1265,6 +1705,9 @@ module Fluent
1265
1705
  elsif resource_type == ML_CONSTANTS[:resource_type]
1266
1706
  label_prefix = ML_CONSTANTS[:service]
1267
1707
  labels_to_extract = %w(job_id task_name)
1708
+ elsif resource_type == DOCKER_CONSTANTS[:resource_type]
1709
+ label_prefix = DOCKER_CONSTANTS[:service]
1710
+ labels_to_extract = %w(container_id)
1268
1711
  else
1269
1712
  return extracted_labels
1270
1713
  end