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

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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