fluent-plugin-google-cloud 0.6.4 → 0.6.5.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: ab431f0283f1c6bc88669159be387f21bb1e908f
4
- data.tar.gz: 77d320fd8b6e4c5370542142fd296581fcd9186e
3
+ metadata.gz: 7b1a26eab783c87890f78500848a29048b4ab615
4
+ data.tar.gz: 81711bc2e00bcf222b7249bde80d78c6bab34957
5
5
  SHA512:
6
- metadata.gz: 15fc471919366c52a612cb2b1941d0e9ffdf5e1d5c94ad6f3395c1860ca2f03edd888959a0ed267375bf80133458251bc1555c7416ef81f8be4f4aa83fd5c405
7
- data.tar.gz: 4aed5e4e2686eb4776d32f3b44f61fc86849f4f02a64bd12399d0e0ac4a6cc1a8752489513fa5e482fdb7cac432d517ed5b7b26e71365e344e2dcf2ca8e96832
6
+ metadata.gz: 7d5c1ca8f704998cd36c5a040dc4fa10989d2cbb7717dea8fa67032c3c42b428a5f3ef37f04581caea41bf991232688d3245ebe285e7fb72f9f0b783712d399f
7
+ data.tar.gz: 8a95c809c56fcc6351f08bd81d3124cd31b6529c54afb3c72d109519c4aecd5764d26514b4dc8a99417510c3883451ae54189e65a1e10d81465143888bf68b11
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fluent-plugin-google-cloud (0.6.4)
4
+ fluent-plugin-google-cloud (0.6.5.pre.1)
5
+ excon (~> 0.57.1)
5
6
  fluentd (~> 0.10)
6
7
  google-api-client (~> 0.9.0)
7
- google-cloud-logging (~> 0.23.2)
8
+ google-cloud-logging (= 0.24.1)
8
9
  googleapis-common-protos (~> 1.3)
9
- googleauth (~> 0.4)
10
+ googleauth (~> 0.4, < 0.5.2)
10
11
  grpc (~> 1.0, < 1.3)
11
12
  json (~> 1.8)
12
13
 
@@ -18,15 +19,17 @@ GEM
18
19
  ast (2.3.0)
19
20
  astrolabe (1.3.1)
20
21
  parser (~> 2.2)
21
- cool.io (1.5.0)
22
+ cool.io (1.5.1)
22
23
  crack (0.4.3)
23
24
  safe_yaml (~> 1.0.0)
24
- faraday (0.12.1)
25
+ excon (0.57.1)
26
+ faraday (0.12.2)
25
27
  multipart-post (>= 1.2, < 3)
26
- fluentd (0.14.18)
28
+ fluentd (0.14.20)
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)
32
+ ruby_dig (~> 0.0.2)
30
33
  serverengine (>= 2.0.4, < 3.0.0)
31
34
  sigdump (~> 0.2.2)
32
35
  strptime (~> 0.1.7)
@@ -44,20 +47,17 @@ GEM
44
47
  retriable (~> 2.0)
45
48
  google-cloud-core (0.21.1)
46
49
  googleauth (~> 0.5.1)
47
- google-cloud-logging (0.23.2)
50
+ google-cloud-logging (0.24.1)
48
51
  google-cloud-core (~> 0.21.1)
49
- google-gax (~> 0.6.0)
50
- google-protobuf (~> 3.0)
51
- googleapis-common-protos (~> 1.3)
52
- grpc (~> 1.0)
53
- orderedhash (= 0.0.6)
52
+ google-gax (~> 0.8.0)
54
53
  stackdriver-core (~> 0.21.0)
55
- google-gax (0.6.0)
56
- googleapis-common-protos (~> 1.3.1)
54
+ google-gax (0.8.5)
55
+ google-protobuf (~> 3.2)
56
+ googleapis-common-protos (~> 1.3.5)
57
57
  googleauth (~> 0.5.1)
58
58
  grpc (~> 1.0)
59
59
  rly (~> 0.2.3)
60
- google-protobuf (3.3.0)
60
+ google-protobuf (3.3.0-x86_64-linux)
61
61
  googleapis-common-protos (1.3.5)
62
62
  google-protobuf (~> 3.2)
63
63
  grpc (~> 1.0)
@@ -69,10 +69,10 @@ GEM
69
69
  multi_json (~> 1.11)
70
70
  os (~> 0.9)
71
71
  signet (~> 0.7)
72
- grpc (1.2.5)
72
+ grpc (1.2.5-x86_64-linux)
73
73
  google-protobuf (~> 3.1)
74
74
  googleauth (~> 0.5.1)
75
- hashdiff (0.3.4)
75
+ hashdiff (0.3.5)
76
76
  http_parser.rb (0.6.0)
77
77
  httpclient (2.8.3)
78
78
  hurley (0.2)
@@ -92,7 +92,6 @@ GEM
92
92
  msgpack (1.1.0)
93
93
  multi_json (1.12.1)
94
94
  multipart-post (2.0.0)
95
- orderedhash (0.0.6)
96
95
  os (0.9.6)
97
96
  parser (2.4.0.0)
98
97
  ast (~> 2.2)
@@ -117,6 +116,7 @@ GEM
117
116
  ruby-progressbar (~> 1.7)
118
117
  tins (<= 1.6.0)
119
118
  ruby-progressbar (1.8.1)
119
+ ruby_dig (0.0.2)
120
120
  safe_yaml (1.0.4)
121
121
  serverengine (2.0.5)
122
122
  sigdump (~> 0.2.2)
@@ -137,7 +137,7 @@ GEM
137
137
  tzinfo-data (1.2017.2)
138
138
  tzinfo (>= 1.0.0)
139
139
  uber (0.0.15)
140
- webmock (1.24.6)
140
+ webmock (2.3.2)
141
141
  addressable (>= 2.3.6)
142
142
  crack (>= 0.3.2)
143
143
  hashdiff
@@ -153,7 +153,7 @@ DEPENDENCIES
153
153
  rake (~> 10.3)
154
154
  rubocop (~> 0.35.0)
155
155
  test-unit (~> 3.0)
156
- webmock (~> 1.17)
156
+ webmock (~> 2.3.1)
157
157
 
158
158
  BUNDLED WITH
159
- 1.15.0
159
+ 1.15.3
@@ -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.4'
13
+ gem.version = '0.6.5.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,18 +19,19 @@ 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.57.1'
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'
25
- gem.add_runtime_dependency 'google-cloud-logging', '~> 0.23.2'
26
- gem.add_runtime_dependency 'googleauth', '~> 0.4'
26
+ gem.add_runtime_dependency 'google-cloud-logging', '0.24.1'
27
+ gem.add_runtime_dependency 'googleauth', '~> 0.4', '< 0.5.2'
27
28
  gem.add_runtime_dependency 'grpc', '~> 1.0', '< 1.3'
28
29
  gem.add_runtime_dependency 'json', '~> 1.8'
29
30
 
30
31
  gem.add_development_dependency 'mocha', '~> 1.1'
31
32
  gem.add_development_dependency 'rake', '~> 10.3'
32
33
  gem.add_development_dependency 'rubocop', '~> 0.35.0'
33
- gem.add_development_dependency 'webmock', '~> 1.17'
34
+ gem.add_development_dependency 'webmock', '~> 2.3.1'
34
35
  gem.add_development_dependency 'test-unit', '~> 3.0'
35
36
  gem.add_development_dependency 'prometheus-client', '~> 0.7.1'
36
37
  end
@@ -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'
@@ -38,8 +40,10 @@ end
38
40
  module Fluent
39
41
  # fluentd output plugin for the Stackdriver Logging API
40
42
  class GoogleCloudOutput < BufferedOutput
41
- # Constants for service names and resource types.
43
+ # Constants.
42
44
  module Constants
45
+ # Service names and resource types.
46
+
43
47
  APPENGINE_CONSTANTS = {
44
48
  service: 'appengine.googleapis.com',
45
49
  resource_type: 'gae_app'
@@ -56,6 +60,10 @@ module Fluent
56
60
  service: 'container.googleapis.com',
57
61
  resource_type: 'container'
58
62
  }
63
+ DOCKER_CONSTANTS = {
64
+ service: 'dockercontainer.googleapis.com',
65
+ resource_type: 'docker_container'
66
+ }
59
67
  DATAFLOW_CONSTANTS = {
60
68
  service: 'dataflow.googleapis.com',
61
69
  resource_type: 'dataflow_step'
@@ -72,6 +80,23 @@ module Fluent
72
80
  service: 'ml.googleapis.com',
73
81
  resource_type: 'ml_job'
74
82
  }
83
+
84
+ # Default value for trace_key config param to set "trace" LogEntry field.
85
+ DEFAULT_TRACE_KEY = 'logging.googleapis.com/trace'
86
+
87
+ # Metadata Agent support.
88
+
89
+ # Use empty string as request path when locally-unique key of monitored
90
+ # resource can be implicitly inferred by Metadata Agent.
91
+ IMPLICIT_MONITORED_RESOURCE_UNIQUE_KEY = ''
92
+
93
+ # The label name of locally unique id in the json payload. When a record
94
+ # has this field in the payload, we will use the value to retrieve
95
+ # monitored resource from Stackdriver Metadata agent.
96
+ LOCALLY_UNIQUE_ID_LABEL_NAME = 'logging.googleapis.com/locally_unique_id'
97
+
98
+ # Docker container support.
99
+ DEFAULT_DOCKER_API_SOCKET_PATH = '/var/run/docker.sock'
75
100
  end
76
101
 
77
102
  include self::Constants
@@ -79,7 +104,7 @@ module Fluent
79
104
  Fluent::Plugin.register_output('google_cloud', self)
80
105
 
81
106
  PLUGIN_NAME = 'Fluentd Google Cloud Logging plugin'
82
- PLUGIN_VERSION = '0.6.3'
107
+ PLUGIN_VERSION = '0.6.5.pre.1'
83
108
 
84
109
  # Name of the the Google cloud logging write scope.
85
110
  LOGGING_SCOPE = 'https://www.googleapis.com/auth/logging.write'
@@ -109,6 +134,11 @@ module Fluent
109
134
  config_param :vm_id, :string, :default => nil
110
135
  config_param :vm_name, :string, :default => nil
111
136
 
137
+ # Set values from JSON payload with this key to the "trace" LogEntry field.
138
+ config_param :trace_key, :string, :default => DEFAULT_TRACE_KEY
139
+ # Whether to also keep the trace key/value in the payload.
140
+ config_param :keep_trace_key, :bool, :default => false
141
+
112
142
  # Whether to try to detect if the VM is owned by a "subservice" such as App
113
143
  # Engine of Kubernetes, rather than just associating the logs with the
114
144
  # compute service of the platform. This currently only has any effect when
@@ -126,7 +156,7 @@ module Fluent
126
156
  config_param :require_valid_tags, :bool, :default => false
127
157
 
128
158
  # The regular expression to use on Kubernetes logs to extract some basic
129
- # information about the log source. The regex must contain capture groups
159
+ # information about the log source. The regexp must contain capture groups
130
160
  # for pod_name, namespace_name, and container_name.
131
161
  config_param :kubernetes_tag_regexp, :string, :default =>
132
162
  '\.(?<pod_name>[^_]+)_(?<namespace_name>[^_]+)_(?<container_name>.+)$'
@@ -201,6 +231,18 @@ module Fluent
201
231
  config_param :monitoring_type, :string,
202
232
  :default => Monitoring::PrometheusMonitoringRegistry.name
203
233
 
234
+ # Whether to call metadata agent to retrieve monitored resource.
235
+ config_param :enable_metadata_agent, :bool, :default => false
236
+ config_param :metadata_agent_url, :string,
237
+ :default => 'http://local-metadata-agent.stackdriver.com:8000'
238
+
239
+ # Whether to call Docker Remote API locally when Metadata Agent is not
240
+ # enabled or if the request fails.
241
+ config_param :call_docker_api_locally, :bool, :default => true
242
+ # Docker Remote API unix socket path.
243
+ config_param :docker_remote_api_socket_path, :string,
244
+ :default => DEFAULT_DOCKER_API_SOCKET_PATH
245
+
204
246
  # rubocop:enable Style/HashSyntax
205
247
 
206
248
  # TODO: Add a log_name config option rather than just using the tag?
@@ -210,9 +252,6 @@ module Fluent
210
252
  attr_reader :project_id
211
253
  attr_reader :zone
212
254
  attr_reader :vm_id
213
- attr_reader :running_on_managed_vm
214
- attr_reader :gae_backend_name
215
- attr_reader :gae_backend_version
216
255
  attr_reader :resource
217
256
  attr_reader :common_labels
218
257
 
@@ -222,6 +261,35 @@ module Fluent
222
261
  @log = $log # rubocop:disable Style/GlobalVars
223
262
  end
224
263
 
264
+ # Set up regex patterns used to parse tags and logs.
265
+ def setup_regex_patterns
266
+ @compiled_kubernetes_tag_regexp = nil
267
+ if @kubernetes_tag_regexp
268
+ @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
269
+ end
270
+
271
+ @cloudfunctions_tag_regexp =
272
+ /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
273
+ @cloudfunctions_log_regexp = /^
274
+ (?:\[(?<severity>.)\])?
275
+ \[(?<timestamp>.{24})\]
276
+ (?:\[(?<execution_id>[^\]]+)\])?
277
+ [ ](?<text>.*)$/x
278
+
279
+ # Docker container tag format:
280
+ # "container.<container_id>.<container_name>".
281
+ @dockercontainer_tag_regexp =
282
+ /^container\.(?<container_id>[a-zA-Z0-9]+)\.
283
+ (?<container_name>[a-zA-Z0-9_.-]+)$/x
284
+ # Docker container with application tag format:
285
+ # "application-container.<container_name>.<additional_tag>".
286
+ @dockercontainer_tag_with_application_regexp =
287
+ /^application-container\.(?<container_name>[a-zA-Z0-9_.-]+)\.
288
+ (?<additional_tag>.+)$/x
289
+
290
+ @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
291
+ end
292
+
225
293
  def configure(conf)
226
294
  super
227
295
 
@@ -259,162 +327,54 @@ module Fluent
259
327
  extra.join(' ')
260
328
  end
261
329
 
262
- # TODO: Send instance tags as labels as well?
263
- @common_labels = {}
264
- @common_labels.merge!(@labels) if @labels
265
-
266
- # TODO: Construct Google::Api::MonitoredResource when @use_grpc is
267
- # true after the protobuf map corruption issue is fixed.
268
- @resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
269
- labels: {})
270
-
271
- @compiled_kubernetes_tag_regexp = nil
272
- if @kubernetes_tag_regexp
273
- @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
274
- end
330
+ setup_regex_patterns
275
331
 
276
- @cloudfunctions_tag_regexp =
277
- /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
278
- @cloudfunctions_log_regexp = /^
279
- (?:\[(?<severity>.)\])?
280
- \[(?<timestamp>.{24})\]
281
- (?:\[(?<execution_id>[^\]]+)\])?
282
- [ ](?<text>.*)$/x
283
-
284
- @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
285
-
286
- # set attributes from metadata (unless overriden by static config)
287
- @vm_name = Socket.gethostname if @vm_name.nil?
288
332
  @platform = detect_platform
289
- case @platform
290
- when Platform::GCE
291
- if @project_id.nil?
292
- @project_id = fetch_gce_metadata('project/project-id')
293
- end
294
- if @zone.nil?
295
- # this returns "projects/<number>/zones/<zone>"; we only want
296
- # the part after the final slash.
297
- fully_qualified_zone = fetch_gce_metadata('instance/zone')
298
- @zone = fully_qualified_zone.rpartition('/')[2]
299
- end
300
- @vm_id = fetch_gce_metadata('instance/id') if @vm_id.nil?
301
- when Platform::EC2
302
- metadata = fetch_ec2_metadata
303
- if @zone.nil? && metadata.key?('availabilityZone')
304
- @zone = 'aws:' + metadata['availabilityZone']
305
- end
306
- if @vm_id.nil? && metadata.key?('instanceId')
307
- @vm_id = metadata['instanceId']
308
- end
309
- if metadata.key?('accountId')
310
- @resource.labels['aws_account'] = metadata['accountId']
311
- end
312
- when Platform::OTHER
313
- # do nothing
314
- else
315
- fail Fluent::ConfigError, 'Unknown platform ' + @platform
316
- end
317
333
 
318
- # If we still don't have a project ID, try to obtain it from the
319
- # credentials.
320
- if @project_id.nil?
321
- @project_id = CredentialsInfo.project_id
322
- @log.info 'Set Project ID from credentials: ', @project_id unless
323
- @project_id.nil?
334
+ # Set agent-level monitored resource. This monitored resource is initiated
335
+ # as the logging agent starts up. It will be inherited by all log entries
336
+ # processed by this agent. First try to retrieve it via Metadata Agent.
337
+ if @enable_metadata_agent
338
+ # The locally-unique key for this should be the instance id. Since this
339
+ # can be implicitly inferred by Metadata Agent, we do not need to
340
+ # explicitly send the key.
341
+ @resource = call_metadata_agent_for_monitored_resource(
342
+ IMPLICIT_MONITORED_RESOURCE_UNIQUE_KEY)
324
343
  end
325
344
 
326
- # all metadata parameters must now be set
327
- unless @project_id && @zone && @vm_id
328
- missing = []
329
- missing << 'project_id' unless @project_id
330
- missing << 'zone' unless @zone
331
- missing << 'vm_id' unless @vm_id
332
- fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
333
- missing.join(' ')
334
- end
335
-
336
- # Default this to false; it is only overwritten if we detect Managed VM.
337
- @running_on_managed_vm = false
338
-
339
- # Default this to false; it is only overwritten if we detect Cloud
340
- # Functions.
345
+ # Set required variables: @project_id, @vm_id, @vm_name and @zone.
346
+ # If any info above is included in the response from Metadata Agent, make
347
+ # use of that. Otherwise make some additional requests to metadata server.
348
+ #
349
+ # Note: Once we support metadata injection on the Logging API side, we
350
+ # might no longer need to require all these metadata in logging agent. But
351
+ # for now, they are still required.
352
+ set_required_metadata_variables
353
+
354
+ # Fail over to retrieve monitored resource via the legacy path if we fail
355
+ # to get it from Metadata Agent.
356
+ @resource ||= determine_agent_level_monitored_resource_via_legacy
357
+
358
+ # Set variables specific to CLoud Functions. This has to be called after
359
+ # we have determined the resource type. The purpose is to avoid repeated
360
+ # calls to metadata server.
341
361
  @running_cloudfunctions = false
342
-
343
- # Set up the MonitoredResource, labels, etc. based on the config.
344
- case @platform
345
- when Platform::GCE
346
- @resource.type = COMPUTE_CONSTANTS[:resource_type]
347
- # TODO: introduce a new MonitoredResource-centric configuration and
348
- # deprecate subservice-name; for now, translate known uses.
349
- if @subservice_name
350
- # TODO: what should we do if we encounter an unknown value?
351
- if @subservice_name == DATAFLOW_CONSTANTS[:service]
352
- @resource.type = DATAFLOW_CONSTANTS[:resource_type]
353
- elsif @subservice_name == ML_CONSTANTS[:service]
354
- @resource.type = ML_CONSTANTS[:resource_type]
355
- end
356
- elsif @detect_subservice
357
- # Check for specialized GCE environments.
358
- # TODO: Add config options for these to allow for running outside GCE?
359
- attributes = fetch_gce_metadata('instance/attributes/').split
360
- # Do nothing, just don't populate other service's labels.
361
- if attributes.include?('gae_backend_name') &&
362
- attributes.include?('gae_backend_version')
363
- # Managed VM
364
- @running_on_managed_vm = true
365
- @gae_backend_name =
366
- fetch_gce_metadata('instance/attributes/gae_backend_name')
367
- @gae_backend_version =
368
- fetch_gce_metadata('instance/attributes/gae_backend_version')
369
- @resource.type = APPENGINE_CONSTANTS[:resource_type]
370
- @resource.labels['module_id'] = @gae_backend_name
371
- @resource.labels['version_id'] = @gae_backend_version
372
- elsif attributes.include?('kube-env')
373
- # Kubernetes/Container Engine
374
- @resource.type = CONTAINER_CONSTANTS[:resource_type]
375
- @raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
376
- @kube_env = YAML.load(@raw_kube_env)
377
- @resource.labels['cluster_name'] =
378
- cluster_name_from_kube_env(@kube_env)
379
- detect_cloudfunctions(attributes)
380
- elsif attributes.include?('dataproc-cluster-uuid') &&
381
- attributes.include?('dataproc-cluster-name')
382
- # Dataproc
383
- @resource.type = DATAPROC_CONSTANTS[:resource_type]
384
- @resource.labels['cluster_uuid'] =
385
- fetch_gce_metadata('instance/attributes/dataproc-cluster-uuid')
386
- @resource.labels['cluster_name'] =
387
- fetch_gce_metadata('instance/attributes/dataproc-cluster-name')
388
- @resource.labels['region'] =
389
- fetch_gce_metadata('instance/attributes/dataproc-region')
390
- end
391
- end
392
- # Some services have the GCE instance_id and zone as MonitoredResource
393
- # labels; for other services we send them as entry labels.
394
- if @resource.type == COMPUTE_CONSTANTS[:resource_type] ||
395
- @resource.type == CONTAINER_CONSTANTS[:resource_type]
396
- @resource.labels['instance_id'] = @vm_id
397
- @resource.labels['zone'] = @zone
398
- else
399
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
400
- common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
401
- end
402
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
403
- when Platform::EC2
404
- @resource.type = EC2_CONSTANTS[:resource_type]
405
- @resource.labels['instance_id'] = @vm_id
406
- @resource.labels['region'] = @zone
407
- # the aws_account label is populated above.
408
- common_labels["#{EC2_CONSTANTS[:service]}/resource_name"] = @vm_name
409
- when Platform::OTHER
410
- # Use GCE as the default environment.
411
- @resource.type = COMPUTE_CONSTANTS[:resource_type]
412
- @resource.labels['instance_id'] = @vm_id
413
- @resource.labels['zone'] = @zone
414
- common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
362
+ # We only support Cloud Functions logs for GKE right now.
363
+ if @resource.type == CONTAINER_CONSTANTS[:resource_type] &&
364
+ fetch_gce_metadata('instance/attributes/').split.include?('gcf_region')
365
+ # We are not setting resource type as Cloud Functions here because
366
+ # whether a log entry is truly coming from a Cloud Functions function
367
+ # depends on the log tag. Only when @running_cloudfunctions is true will
368
+ # we try to match log tags against Cloud Functions tag regexp when
369
+ # processing log entries.
370
+ @running_cloudfunctions = true
371
+ # Fetch this info and store it to avoid recurring metadata server calls.
372
+ @gcf_region = fetch_gce_metadata('instance/attributes/gcf_region')
415
373
  end
416
- @resource.labels.merge!(
417
- extract_resource_labels(@resource.type, common_labels))
374
+
375
+ # Determine the common labels that should be added to all log entries
376
+ # processed by this logging agent.
377
+ @common_labels = determine_agent_level_common_labels
418
378
 
419
379
  # The resource and labels are now set up; ensure they can't be modified
420
380
  # without first duping them.
@@ -424,7 +384,7 @@ module Fluent
424
384
 
425
385
  # Log an informational message containing the Logs viewer URL
426
386
  @log.info 'Logs viewer address: https://console.cloud.google.com/logs/',
427
- "viewer?project=#{@project_id}&resource=#{@resource_type}/",
387
+ "viewer?project=#{@project_id}&resource=#{@resource.type}/",
428
388
  "instance_id/#{@vm_id}"
429
389
  end
430
390
 
@@ -439,130 +399,6 @@ module Fluent
439
399
  super
440
400
  end
441
401
 
442
- def format(tag, time, record)
443
- [tag, time, record].to_msgpack
444
- end
445
-
446
- # Given a tag, returns the corresponding valid tag if possible, or nil if
447
- # the tag should be rejected. If 'require_valid_tags' is false, non-string
448
- # tags are converted to strings, and invalid characters are sanitized;
449
- # otherwise such tags are rejected.
450
- def sanitize_tag(tag)
451
- if @require_valid_tags &&
452
- (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
453
- return nil
454
- end
455
- tag = convert_to_utf8(tag.to_s)
456
- tag = '_' if tag == ''
457
- tag
458
- end
459
-
460
- # Compute the monitored resource and common labels shared by a collection of
461
- # entries.
462
- def compute_group_resource_and_labels(tag)
463
- # Note that we assume that labels added to group_common_labels below are
464
- # not 'service' labels (i.e. we do not call extract_resource_labels
465
- # again).
466
- group_resource = @resource.dup
467
- group_common_labels = @common_labels.dup
468
-
469
- if @running_cloudfunctions
470
- # If the current group of entries is coming from a Cloud Functions
471
- # function, the function name can be extracted from the tag.
472
- match_data = @cloudfunctions_tag_regexp.match(tag)
473
- if match_data
474
- # Resource type is set to Cloud Functions only for logs actually
475
- # coming from a function, otherwise we leave it as Container.
476
- group_resource.type = CLOUDFUNCTIONS_CONSTANTS[:resource_type]
477
- group_resource.labels['region'] = @gcf_region
478
- group_resource.labels['function_name'] =
479
- decode_cloudfunctions_function_name(
480
- match_data['encoded_function_name'])
481
- # Move GKE container labels from the MonitoredResource to the
482
- # LogEntry.
483
- instance_id = group_resource.labels.delete('instance_id')
484
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/cluster_name"] =
485
- group_resource.labels.delete('cluster_name')
486
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/instance_id"] =
487
- instance_id
488
- group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
489
- instance_id
490
- group_common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] =
491
- group_resource.labels.delete('zone')
492
- end
493
- end
494
- if group_resource.type == CONTAINER_CONSTANTS[:resource_type] &&
495
- @compiled_kubernetes_tag_regexp
496
- # Container logs in Kubernetes are tagged based on where they came
497
- # from, so we can extract useful metadata from the tag.
498
- # Do this here to avoid having to repeat it for each record.
499
- match_data = @compiled_kubernetes_tag_regexp.match(tag)
500
- if match_data
501
- group_resource.labels['container_name'] = match_data['container_name']
502
- group_resource.labels['namespace_id'] = match_data['namespace_name']
503
- group_resource.labels['pod_id'] = match_data['pod_name']
504
- %w(namespace_name pod_name).each do |field|
505
- group_common_labels["#{CONTAINER_CONSTANTS[:service]}/#{field}"] =
506
- match_data[field]
507
- end
508
- end
509
- end
510
-
511
- # Freeze the per-request state. Any further changes must be made on a
512
- # per-entry basis.
513
- group_resource.freeze
514
- group_resource.labels.freeze
515
- group_common_labels.freeze
516
-
517
- [group_resource, group_common_labels]
518
- end
519
-
520
- # Extract entry resource and common labels that should be applied to
521
- # individual entries from the group resource.
522
- def extract_entry_labels(group_resource, record)
523
- resource_labels = {}
524
- common_labels = {}
525
-
526
- if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
527
- record.key?('log')
528
- @cloudfunctions_log_match =
529
- @cloudfunctions_log_regexp.match(record['log'])
530
- end
531
-
532
- if group_resource.type == CONTAINER_CONSTANTS[:resource_type]
533
- # Move the stdout/stderr annotation from the record into a label
534
- common_labels.merge!(
535
- fields_to_labels(
536
- record, 'stream' => "#{CONTAINER_CONSTANTS[:service]}/stream"))
537
-
538
- # If the record has been annotated by the kubernetes_metadata_filter
539
- # plugin, then use that metadata. Otherwise, rely on commonLabels
540
- # populated at the grouped_entries level from the group's tag.
541
- if record.key?('kubernetes')
542
- extracted_resource_labels, extracted_common_labels = \
543
- extract_container_metadata(record)
544
- resource_labels.merge!(extracted_resource_labels)
545
- common_labels.merge!(extracted_common_labels)
546
- end
547
- end
548
-
549
- # If a field is present in the label_map, send its value as a label
550
- # (mapping the field name to label name as specified in the config)
551
- # and do not send that field as part of the payload.
552
- common_labels.merge!(fields_to_labels(record, @label_map))
553
-
554
- if group_resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
555
- @cloudfunctions_log_match &&
556
- @cloudfunctions_log_match['execution_id']
557
- common_labels['execution_id'] =
558
- @cloudfunctions_log_match['execution_id']
559
- end
560
- resource_labels.merge!(
561
- extract_resource_labels(group_resource.type, common_labels))
562
-
563
- [resource_labels, common_labels]
564
- end
565
-
566
402
  def write(chunk)
567
403
  # Group the entries since we have to make one call per tag.
568
404
  grouped_entries = {}
@@ -579,20 +415,22 @@ module Fluent
579
415
 
580
416
  grouped_entries.each do |tag, arr|
581
417
  entries = []
582
- group_resource, group_common_labels = compute_group_resource_and_labels(
583
- tag)
418
+ group_resource, group_common_labels =
419
+ determine_group_level_monitored_resource_and_labels(tag)
584
420
 
585
421
  arr.each do |time, record|
586
422
  next unless record.is_a?(Hash)
587
423
 
588
- extracted_resource_labels, extracted_common_labels = \
589
- extract_entry_labels(group_resource, record)
424
+ resource_type, extracted_resource_labels, extracted_common_labels = \
425
+ determine_entry_level_labels(group_resource, record)
590
426
  entry_resource = group_resource.dup
427
+ entry_resource.type = resource_type
591
428
  entry_resource.labels.merge!(extracted_resource_labels)
592
429
  entry_common_labels = \
593
430
  group_common_labels.merge(extracted_common_labels)
594
431
 
595
- if entry_resource.type == CONTAINER_CONSTANTS[:resource_type]
432
+ if [CONTAINER_CONSTANTS[:resource_type],
433
+ DOCKER_CONSTANTS[:resource_type]].include?(entry_resource.type)
596
434
  # Save the timestamp if available, then clear it out to allow for
597
435
  # determining whether we should parse the log or message field.
598
436
  timestamp = record.key?('time') ? record['time'] : nil
@@ -621,6 +459,13 @@ module Fluent
621
459
  severity = compute_severity(
622
460
  entry_resource.type, record, entry_common_labels)
623
461
 
462
+ # Get fully-qualified trace id for LogEntry "trace" field per config.
463
+ fq_trace_id = if @keep_trace_key
464
+ record[@trace_key]
465
+ else
466
+ record.delete(@trace_key)
467
+ end
468
+
624
469
  if @use_grpc
625
470
  entry = Google::Logging::V2::LogEntry.new(
626
471
  labels: entry_common_labels,
@@ -630,6 +475,7 @@ module Fluent
630
475
  ),
631
476
  severity: grpc_severity(severity)
632
477
  )
478
+ entry.trace = fq_trace_id if fq_trace_id
633
479
  # If "seconds" is null or not an integer, we will omit the timestamp
634
480
  # field and defer the decision on how to handle it to the downstream
635
481
  # Logging API. If "nanos" is null or not an integer, it will be set
@@ -655,6 +501,7 @@ module Fluent
655
501
  nanos: ts_nanos
656
502
  }
657
503
  )
504
+ entry.trace = fq_trace_id if fq_trace_id
658
505
  set_http_request(record, entry)
659
506
  set_payload(entry_resource.type, record, entry, is_json)
660
507
  end
@@ -841,7 +688,7 @@ module Fluent
841
688
  end
842
689
  end
843
690
  rescue StandardError => e
844
- @log.debug 'Failed to access metadata service: ', error: e
691
+ @log.error 'Failed to access metadata service: ', error: e
845
692
  end
846
693
 
847
694
  @log.info 'Unable to determine platform'
@@ -856,15 +703,630 @@ module Fluent
856
703
  metadata_path, 'Metadata-Flavor' => 'Google', &:read)
857
704
  end
858
705
 
859
- def fetch_ec2_metadata
860
- fail "Called fetch_ec2_metadata with platform=#{@platform}" unless
706
+ # EC2 Metadata server returns everything in one call. Store it after the
707
+ # first fetch to avoid making multiple calls.
708
+ def ec2_metadata
709
+ fail "Called ec2_metadata with platform=#{@platform}" unless
861
710
  @platform == Platform::EC2
862
- # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
863
- open('http://' + METADATA_SERVICE_ADDR +
864
- '/latest/dynamic/instance-identity/document') do |f|
865
- contents = f.read
866
- return JSON.parse(contents)
711
+ unless @ec2_metadata
712
+ # See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
713
+ open('http://' + METADATA_SERVICE_ADDR +
714
+ '/latest/dynamic/instance-identity/document') do |f|
715
+ contents = f.read
716
+ @ec2_metadata = JSON.parse(contents)
717
+ end
718
+ end
719
+
720
+ @ec2_metadata
721
+ end
722
+
723
+ # Set regexp patterns to parse tags and logs.
724
+ def set_regexp_patterns
725
+ @compiled_kubernetes_tag_regexp = nil
726
+ if @kubernetes_tag_regexp
727
+ @compiled_kubernetes_tag_regexp = Regexp.new(@kubernetes_tag_regexp)
867
728
  end
729
+
730
+ @cloudfunctions_tag_regexp =
731
+ /\.(?<encoded_function_name>.+)\.\d+-[^-]+_default_worker$/
732
+ @cloudfunctions_log_regexp = /^
733
+ (?:\[(?<severity>.)\])?
734
+ \[(?<timestamp>.{24})\]
735
+ (?:\[(?<execution_id>[^\]]+)\])?
736
+ [ ](?<text>.*)$/x
737
+
738
+ @http_latency_regexp = /^\s*(?<seconds>\d+)(?<decimal>\.\d+)?\s*s\s*$/
739
+ end
740
+
741
+ # Set required variables like @project_id, @vm_id, @vm_name and @zone.
742
+ def set_required_metadata_variables
743
+ set_project_id
744
+ set_vm_id
745
+ set_vm_name
746
+ set_location
747
+
748
+ # All metadata parameters must now be set.
749
+ return if @project_id && @zone && @vm_id
750
+ missing = []
751
+ missing << 'project_id' unless @project_id
752
+ missing << 'zone' unless @zone
753
+ missing << 'vm_id' unless @vm_id
754
+ fail Fluent::ConfigError, 'Unable to obtain metadata parameters: ' +
755
+ missing.join(' ')
756
+ end
757
+
758
+ # 1. Return the value if it is explicitly set in the config already.
759
+ # 2. If not, try to retrieve it by calling metadata server directly.
760
+ # 3. If still not set, try to obtain it from the credentials.
761
+ def set_project_id
762
+ @project_id ||= fetch_gce_metadata('project/project-id') if
763
+ @platform == Platform::GCE
764
+ @project_id ||= CredentialsInfo.project_id
765
+ rescue StandardError => e
766
+ @log.error 'Failed to obtain project id: ', error: e
767
+ end
768
+
769
+ # 1. Return the value if it is explicitly set in the config already.
770
+ # 2. If not, check if the response from Metadata Agent includes this info.
771
+ # 3. If not, try to retrieve it by calling metadata servers directly.
772
+ def set_vm_id
773
+ @vm_id ||= @resource.labels['instance_id'] if
774
+ !@resource.nil? && @resource.labels.key?('instance_id')
775
+ @vm_id ||= fetch_gce_metadata('instance/id') if @platform == Platform::GCE
776
+ @vm_id ||= ec2_metadata['instanceId'] if @platform == Platform::EC2
777
+ rescue StandardError => e
778
+ @log.error 'Failed to obtain vm_id: ', error: e
779
+ end
780
+
781
+ # 1. Return the value if it is explicitly set in the config already.
782
+ # 2. If not, check if the response from Metadata Agent includes this info.
783
+ # 3. If not, try to retrieve it locally.
784
+ def set_vm_name
785
+ @vm_name ||= @resource.labels['instance_name'] if
786
+ !@resource.nil? && @resource.labels.key?('instance_name')
787
+ @vm_name ||= Socket.gethostname
788
+ rescue StandardError => e
789
+ @log.error 'Failed to obtain vm name: ', error: e
790
+ end
791
+
792
+ # 1. Return the value if it is explicitly set in the config already.
793
+ # 2. If not, check if the response from Metadata Agent includes this info.
794
+ # 3. If not, try to retrieve it locally.
795
+ def set_location
796
+ unless @resource.nil?
797
+ @zone ||= @resource.labels['location'] if
798
+ @resource.type == DOCKER_CONSTANTS[:resource_type] &&
799
+ @resource.labels.key?('location')
800
+ @zone ||= @resource.labels['zone'] if
801
+ @platform == Platform::GCE && @resource.labels.key?('zone')
802
+ @zone ||= @resource.labels['region'] if
803
+ @platform == Platform::EC2 && @resource.labels.key?('region')
804
+ end
805
+ # Response format: "projects/<number>/zones/<zone>"
806
+ @zone ||= fetch_gce_metadata('instance/zone').rpartition('/')[2] if
807
+ @platform == Platform::GCE
808
+ @zone ||= 'aws:' + ec2_metadata['availabilityZone'] if
809
+ @platform == Platform::EC2 && ec2_metadata.key?('availabilityZone')
810
+ rescue StandardError => e
811
+ @log.error 'Failed to obtain location: ', error: e
812
+ end
813
+
814
+ # Retrieve monitored resource via the legacy way.
815
+ #
816
+ # Note: This is just a failover plan if we fail to get metadata from
817
+ # Metadata Agent. Thus it should be equivalent to what Metadata Agent
818
+ # returns.
819
+ def determine_agent_level_monitored_resource_via_legacy
820
+ resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
821
+ labels: {})
822
+ resource.type = determine_agent_level_monitored_resource_type
823
+ resource.labels = determine_agent_level_monitored_resource_labels(
824
+ resource.type)
825
+ resource
826
+ end
827
+
828
+ # Determine agent level monitored resource type.
829
+ def determine_agent_level_monitored_resource_type
830
+ # EC2 instance.
831
+ return EC2_CONSTANTS[:resource_type] if
832
+ @platform == Platform::EC2
833
+
834
+ # Unknown platform will be defaulted to GCE instance..
835
+ return COMPUTE_CONSTANTS[:resource_type] if
836
+ @platform == Platform::OTHER
837
+
838
+ # Resource types determined by @subservice_name config.
839
+ # Cloud Dataflow.
840
+ return DATAFLOW_CONSTANTS[:resource_type] if
841
+ @subservice_name == DATAFLOW_CONSTANTS[:service]
842
+ # Cloud ML.
843
+ return ML_CONSTANTS[:resource_type] if
844
+ @subservice_name == ML_CONSTANTS[:service]
845
+ # Default back to GCE if invalid value is detected.
846
+ return COMPUTE_CONSTANTS[:resource_type] if
847
+ @subservice_name
848
+
849
+ # Resource types determined by @detect_subservice config.
850
+ if @detect_subservice
851
+ begin
852
+ attributes = fetch_gce_metadata('instance/attributes/').split
853
+ rescue StandardError => e
854
+ @log.error 'Failed to detect subservice: ', error: e
855
+ end
856
+ # GAE app.
857
+ return APPENGINE_CONSTANTS[:resource_type] if
858
+ attributes.include?('gae_backend_name') &&
859
+ attributes.include?('gae_backend_version')
860
+ # GKE container.
861
+ return CONTAINER_CONSTANTS[:resource_type] if
862
+ attributes.include?('kube-env')
863
+ # Cloud Dataproc.
864
+ return DATAPROC_CONSTANTS[:resource_type] if
865
+ attributes.include?('dataproc-cluster-uuid') &&
866
+ attributes.include?('dataproc-cluster-name')
867
+ end
868
+ # GCE instance.
869
+ COMPUTE_CONSTANTS[:resource_type]
870
+ end
871
+
872
+ # Determine agent level monitored resource labels based on the resource
873
+ # type. Each resource type has its own labels that need to be filled in.
874
+ def determine_agent_level_monitored_resource_labels(type)
875
+ labels = {}
876
+
877
+ case type
878
+
879
+ # GAE app.
880
+ when APPENGINE_CONSTANTS[:resource_type]
881
+ begin
882
+ labels['module_id'] = fetch_gce_metadata(
883
+ 'instance/attributes/gae_backend_name')
884
+ labels['version_id'] = fetch_gce_metadata(
885
+ 'instance/attributes/gae_backend_version')
886
+ rescue StandardError => e
887
+ @log.error 'Failed to set monitored resource labels for GAE: ',
888
+ error: e
889
+ end
890
+
891
+ # GCE.
892
+ when COMPUTE_CONSTANTS[:resource_type]
893
+ labels['instance_id'] = @vm_id
894
+ labels['zone'] = @zone
895
+
896
+ # GKE container.
897
+ when CONTAINER_CONSTANTS[:resource_type]
898
+ labels['instance_id'] = @vm_id
899
+ labels['zone'] = @zone
900
+ begin
901
+ raw_kube_env = fetch_gce_metadata('instance/attributes/kube-env')
902
+ kube_env = YAML.load(raw_kube_env)
903
+ labels['cluster_name'] =
904
+ cluster_name_from_kube_env(kube_env)
905
+ rescue StandardError => e
906
+ @log.error 'Failed to set monitored resource labels for GKE: ',
907
+ error: e
908
+ end
909
+
910
+ # Cloud Dataproc.
911
+ when DATAPROC_CONSTANTS[:resource_type]
912
+ begin
913
+ labels['cluster_uuid'] =
914
+ fetch_gce_metadata('instance/attributes/dataproc-cluster-uuid')
915
+ labels['cluster_name'] =
916
+ fetch_gce_metadata('instance/attributes/dataproc-cluster-name')
917
+ labels['region'] =
918
+ fetch_gce_metadata('instance/attributes/dataproc-region')
919
+ rescue StandardError => e
920
+ @log.error 'Failed to set monitored resource labels for Cloud ' \
921
+ 'Dataproc: ', error: e
922
+ end
923
+
924
+ # EC2.
925
+ when EC2_CONSTANTS[:resource_type]
926
+ labels['instance_id'] = @vm_id
927
+ labels['region'] = @zone
928
+ labels['aws_account'] = ec2_metadata['accountId'] if
929
+ ec2_metadata.key?('accountId')
930
+ end
931
+ labels
932
+ end
933
+
934
+ # Determine the common labels that should be added to all log entries
935
+ # processed by this logging agent.
936
+ def determine_agent_level_common_labels
937
+ labels = {}
938
+ # User can specify labels via config. We want to capture those as well.
939
+ # TODO: Send instance tags as labels as well?
940
+ labels.merge!(@labels) if @labels
941
+
942
+ case @resource.type
943
+
944
+ # GAE app.
945
+ when APPENGINE_CONSTANTS[:resource_type]
946
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
947
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
948
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
949
+
950
+ # GCE and GKE container.
951
+ when COMPUTE_CONSTANTS[:resource_type],
952
+ CONTAINER_CONSTANTS[:resource_type]
953
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
954
+
955
+ # Cloud Dataflow and Cloud Dataproc.
956
+ when DATAFLOW_CONSTANTS[:resource_type],
957
+ DATAPROC_CONSTANTS[:resource_type]
958
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
959
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
960
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
961
+
962
+ # EC2.
963
+ when EC2_CONSTANTS[:resource_type]
964
+ labels["#{EC2_CONSTANTS[:service]}/resource_name"] = @vm_name
965
+
966
+ # Cloud ML.
967
+ when ML_CONSTANTS[:resource_type]
968
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] = @vm_id
969
+ labels["#{COMPUTE_CONSTANTS[:service]}/resource_name"] = @vm_name
970
+ labels["#{COMPUTE_CONSTANTS[:service]}/zone"] = @zone
971
+ end
972
+ labels
973
+ end
974
+
975
+ # Determine the group level monitored resource and common labels shared by a
976
+ # collection of entries.
977
+ def determine_group_level_monitored_resource_and_labels(tag)
978
+ # Determine group level monitored resource type. For certain types,
979
+ # extract useful info from the tag and store those in
980
+ # matched_regexp_group.
981
+ group_resource_type, matched_regexp_group =
982
+ determine_group_level_monitored_resource_type(tag)
983
+
984
+ # Determine group level monitored resource labels and common labels.
985
+ group_resource_type, group_resource_labels, group_common_labels = \
986
+ determine_group_level_labels_and_adjust_type(
987
+ group_resource_type, matched_regexp_group)
988
+
989
+ group_resource = Google::Apis::LoggingV2beta1::MonitoredResource.new(
990
+ type: group_resource_type,
991
+ labels: group_resource_labels.to_h
992
+ )
993
+
994
+ # Freeze the per-request state. Any further changes must be made on a
995
+ # per-entry basis.
996
+ group_resource.freeze
997
+ group_resource.labels.freeze
998
+ group_common_labels.freeze
999
+
1000
+ [group_resource, group_common_labels]
1001
+ end
1002
+
1003
+ # Determine group level monitored resource type shared by a collection of
1004
+ # entries.
1005
+ # Returns the resource type and tag regexp matched groups. The matched
1006
+ # groups only apply to some resource types. Return nil if not applicable or
1007
+ # if there is no match.
1008
+ def determine_group_level_monitored_resource_type(tag)
1009
+ # Match tag against Cloud Functions format.
1010
+ if @running_cloudfunctions
1011
+ matched_regexp_group = @cloudfunctions_tag_regexp.match(tag)
1012
+ return [CLOUDFUNCTIONS_CONSTANTS[:resource_type],
1013
+ matched_regexp_group] if matched_regexp_group
1014
+ end
1015
+
1016
+ # Match tag against Docker container stderr / stdout log format and
1017
+ # Docker container application log format.
1018
+ matched_regexp_group =
1019
+ # Format: "container.<container_id>.<container_name>"
1020
+ @dockercontainer_tag_regexp.match(tag) ||
1021
+ # Format: "application-container.<container_name>.<additional_tag>"
1022
+ @dockercontainer_tag_with_application_regexp.match(tag)
1023
+ return [DOCKER_CONSTANTS[:resource_type], matched_regexp_group] if
1024
+ matched_regexp_group
1025
+
1026
+ # Match tag against GKE Container format.
1027
+ if @resource.type == CONTAINER_CONSTANTS[:resource_type] &&
1028
+ @compiled_kubernetes_tag_regexp
1029
+ # Container logs in Kubernetes are tagged based on where they came from,
1030
+ # so we can extract useful metadata from the tag. Do this here to avoid
1031
+ # having to repeat it for each record.
1032
+ matched_regexp_group = @compiled_kubernetes_tag_regexp.match(tag)
1033
+ return [@resource.type, matched_regexp_group] if matched_regexp_group
1034
+ end
1035
+
1036
+ # Otherwise, return the original type.
1037
+ [@resource.type, nil]
1038
+ end
1039
+
1040
+ # Determine group level monitored resource labels and common labels. These
1041
+ # labels will be shared by a collection of entries. In certain cases, we
1042
+ # might also adjust the resource type.
1043
+ def determine_group_level_labels_and_adjust_type(group_resource_type,
1044
+ matched_regexp_group)
1045
+ group_resource_labels = @resource.labels.dup
1046
+ group_common_labels = @common_labels.dup
1047
+
1048
+ case group_resource_type
1049
+
1050
+ # Cloud Functions.
1051
+ when CLOUDFUNCTIONS_CONSTANTS[:resource_type]
1052
+ group_resource_labels['region'] = @gcf_region
1053
+ group_resource_labels['function_name'] =
1054
+ decode_cloudfunctions_function_name(
1055
+ matched_regexp_group['encoded_function_name'])
1056
+ instance_id = group_resource_labels.delete('instance_id')
1057
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/cluster_name"] =
1058
+ group_resource_labels.delete('cluster_name')
1059
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/instance_id"] =
1060
+ instance_id
1061
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
1062
+ instance_id
1063
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/zone"] =
1064
+ group_resource_labels.delete('zone')
1065
+
1066
+ # GKE container.
1067
+ when CONTAINER_CONSTANTS[:resource_type]
1068
+ if matched_regexp_group
1069
+ group_resource_labels['container_name'] =
1070
+ matched_regexp_group['container_name']
1071
+ # The kubernetes_tag_regexp is poorly named. 'namespace_name' is in
1072
+ # fact 'namespace_id'. 'pod_name' is in fact 'pod_id'.
1073
+ group_resource_labels['namespace_id'] =
1074
+ matched_regexp_group['namespace_name']
1075
+ group_resource_labels['pod_id'] =
1076
+ matched_regexp_group['pod_name']
1077
+ %w(namespace_name pod_name).each do |field|
1078
+ group_common_labels["#{CONTAINER_CONSTANTS[:service]}/#{field}"] =
1079
+ matched_regexp_group[field]
1080
+ end
1081
+ end
1082
+
1083
+ # Docker container.
1084
+ when DOCKER_CONSTANTS[:resource_type]
1085
+ # For Docker container stderr / stdout logs generated by Docker Fluentd
1086
+ # Logging Driver, tags are in the format of "container.<container_id>.
1087
+ # <container_name>", thus they include 'container_id' info.
1088
+ # For logs generated by applications running in Docker containers,
1089
+ # tags are in the format of "application-container.<container_name>.
1090
+ # <additional_tag>", thus 'container_id' info is unknown yet.
1091
+ # 'container_name' info on the other hand is always available.
1092
+ container_id = matched_regexp_group['container_id'] if
1093
+ matched_regexp_group.names.include? 'container_id'
1094
+ container_name = matched_regexp_group['container_name']
1095
+
1096
+ if @enable_metadata_agent
1097
+ # Call Metadata Agent with "container.<container_id>" or
1098
+ # "application-container.<container_name>" as the locally-unique key
1099
+ # to retrieve monitored resource. This should be different from the
1100
+ # original @resource value that got initiated when the agent starts up
1101
+ # because that one is always at the VM level.
1102
+ if container_id
1103
+ locally_unique_id = "container.#{container_id}"
1104
+ else
1105
+ locally_unique_id = "containerName.#{container_name}"
1106
+ end
1107
+ retrieved_resource = call_metadata_agent_for_monitored_resource(
1108
+ locally_unique_id)
1109
+ end
1110
+
1111
+ if !retrieved_resource.nil?
1112
+ # If we successfully get a monitored resource from Metadata Agent,
1113
+ # use this one instead of the original instance monitored resource.
1114
+ group_resource_labels = retrieved_resource.labels.dup
1115
+ @log.debug 'Retrieved monitored resource from Metadata Agent: ' \
1116
+ "#{retrieved_resource.inspect}."
1117
+ else
1118
+ # If Metadata Agent is not enabled, or we failed to get a monitored
1119
+ # resource, we need to have some backup plan.
1120
+ @log.debug 'Metadata Agent not enabled or failed to retrieve ' \
1121
+ 'docker container monitored resource from Metadata ' \
1122
+ 'Agent.'
1123
+
1124
+ # 1. Check if 'container_id' is set already. It should be available
1125
+ # for stdout / stderr). If so, use that.
1126
+ # 2. If not, call Docker Remote API to retrieve the container ID from
1127
+ # container name, but only if @call_docker_api_locally is true.
1128
+ container_id ||= retrieve_container_id_by_name_locally(
1129
+ container_name) if @call_docker_api_locally
1130
+ unless container_id
1131
+ @log.debug 'No docker container id retrieved. Falling back to
1132
+ instance monitored resource.'
1133
+ # If a container id is not available, fall back to the instance
1134
+ # monitored resource.
1135
+ return [COMPUTE_CONSTANTS[:resource_type], group_resource_labels,
1136
+ group_common_labels]
1137
+ end
1138
+ group_resource_labels['container_id'] = container_id
1139
+ # 'zone' for GCP and 'region' for EC2 must have been set at this
1140
+ # point. Rename them to 'location'.
1141
+ group_resource_labels['location'] = @zone
1142
+ if @platform == Platform::EC2
1143
+ group_resource_labels.delete('region')
1144
+ else
1145
+ group_resource_labels.delete('zone')
1146
+ end
1147
+ # vm id info should be reported as a metadata label instead.
1148
+ group_resource_labels.delete('instance_id')
1149
+
1150
+ end
1151
+ # Set metadata labels.
1152
+ group_common_labels["#{DOCKER_CONSTANTS[:service]}/container_name"] =
1153
+ matched_regexp_group['container_name']
1154
+ group_common_labels["#{COMPUTE_CONSTANTS[:service]}/resource_id"] =
1155
+ @vm_id
1156
+ end
1157
+
1158
+ [group_resource_type, group_resource_labels, group_common_labels]
1159
+ end
1160
+
1161
+ # Extract entry resource and common labels that should be applied to
1162
+ # individual entries from the group resource.
1163
+ def determine_entry_level_labels(group_resource, record)
1164
+ resource_type = group_resource.type
1165
+ resource_labels = {}
1166
+ common_labels = {}
1167
+
1168
+ # The format of the locally unique key varies by monitored resource.
1169
+ #
1170
+ # Docker container:
1171
+ # "container.<container_id>"
1172
+ # "containerName.<container_name>"
1173
+ # GKE container:
1174
+ # "gke_containerName.<namespace_id>.<pod_name>.<container_name>"
1175
+ if @enable_metadata_agent && record.key?(LOCALLY_UNIQUE_ID_LABEL_NAME)
1176
+ locally_unique_id = record.delete(LOCALLY_UNIQUE_ID_LABEL_NAME)
1177
+ @log.debug 'Calling metadata agent with locally unique id: ' \
1178
+ "#{locally_unique_id}."
1179
+ retrieved_resource = call_metadata_agent_for_monitored_resource(
1180
+ locally_unique_id)
1181
+ @log.debug 'Retrieved monitored resource from metadata agent: ' \
1182
+ "#{retrieved_resource.inspect}."
1183
+ unless retrieved_resource.nil?
1184
+ resource_type = retrieved_resource.type
1185
+ # Temporarily renaming 'gke_container' to 'container'.
1186
+ resource_type = 'container' if resource_type == 'gke_container'
1187
+ # If we successfully get a monitored resource from Metadata Agent,
1188
+ # use this one instead of the original VM-level monitored resource.
1189
+ resource_labels = retrieved_resource.labels.dup
1190
+ @log.debug 'Retrieved gke_container monitored resource from' \
1191
+ 'Stackdriver Metadata agent: ' \
1192
+ "#{retrieved_resource.inspect}."
1193
+ end
1194
+ end
1195
+
1196
+ # Cloud Functions.
1197
+ if resource_type == CLOUDFUNCTIONS_CONSTANTS[:resource_type] &&
1198
+ record.key?('log')
1199
+ @cloudfunctions_log_match =
1200
+ @cloudfunctions_log_regexp.match(record['log'])
1201
+ common_labels['execution_id'] =
1202
+ @cloudfunctions_log_match['execution_id'] if \
1203
+ @cloudfunctions_log_match &&
1204
+ @cloudfunctions_log_match['execution_id']
1205
+ end
1206
+
1207
+ # GKE containers.
1208
+ if resource_type == CONTAINER_CONSTANTS[:resource_type]
1209
+ # Move the stdout/stderr annotation from the record into a label.
1210
+ common_labels.merge!(
1211
+ delete_and_extract_labels(
1212
+ record, 'stream' => "#{CONTAINER_CONSTANTS[:service]}/stream"))
1213
+
1214
+ # If the record has been annotated by the kubernetes_metadata_filter
1215
+ # plugin, then use that metadata. Otherwise, rely on commonLabels
1216
+ # populated at the grouped_entries level from the group's tag.
1217
+ if record.key?('kubernetes')
1218
+ resource_labels.merge!(
1219
+ delete_and_extract_labels(
1220
+ record['kubernetes'], %w(namespace_id pod_id container_name)
1221
+ .map { |l| [l, l] }.to_h))
1222
+ common_labels.merge!(
1223
+ delete_and_extract_labels(
1224
+ record['kubernetes'], %w(namespace_name pod_name)
1225
+ .map { |l| [l, "#{CONTAINER_CONSTANTS[:service]}/#{l}"] }.to_h))
1226
+ # Prepend label/ to all user-defined labels' keys.
1227
+ if record['kubernetes'].key?('labels')
1228
+ record['kubernetes']['labels'].each do |key, value|
1229
+ common_labels["label/#{key}"] = value
1230
+ end
1231
+ end
1232
+ # We've explicitly consumed all the fields we care about -- don't
1233
+ # litter the log entries with the remaining fields that the kubernetes
1234
+ # metadata filter plugin includes (or an empty 'kubernetes' field).
1235
+ record.delete('kubernetes')
1236
+ record.delete('docker')
1237
+ end
1238
+ end
1239
+
1240
+ # Docker containers.
1241
+ if resource_type == DOCKER_CONSTANTS[:resource_type]
1242
+ # For logs coming from Docker Fluentd Logging Driver, the log record
1243
+ # has 4 fields: 'container_id', 'container_name', 'source' and 'log'.
1244
+ # Extract 'container_id', 'container_name' and 'source' from json
1245
+ # record, set corresponding labels, and remove these fields from record.
1246
+ {
1247
+ 'container_name' => 'container_name',
1248
+ 'source' => 'stream'
1249
+ }.each do |field_name, label_name|
1250
+ common_labels.merge!(
1251
+ delete_and_extract_labels(
1252
+ record,
1253
+ field_name => "#{DOCKER_CONSTANTS[:service]}/#{label_name}"
1254
+ )
1255
+ )
1256
+ end
1257
+ resource_labels.merge!(
1258
+ delete_and_extract_labels(record, 'container_id' => 'container_id'))
1259
+ end
1260
+
1261
+ # If the name of a field in the record is present in the @label_map
1262
+ # configured by users, report its value as a label and do not send that
1263
+ # field as part of the payload.
1264
+ common_labels.merge!(delete_and_extract_labels(record, @label_map))
1265
+
1266
+ # Cloud Dataflow.
1267
+ # These labels can be set via configuring 'labels' or 'label_map'.
1268
+ # Report them as monitored resource labels instead of common labels.
1269
+ if group_resource.type == DATAFLOW_CONSTANTS[:resource_type]
1270
+ resource_labels.merge!(
1271
+ delete_and_extract_labels(
1272
+ common_labels, %w(region job_name job_id step_id)
1273
+ .map { |l| ["#{DATAFLOW_CONSTANTS[:service]}/#{l}", l] }.to_h))
1274
+ end
1275
+
1276
+ # Cloud ML.
1277
+ # These labels can be set via configuring 'labels' or 'label_map'.
1278
+ # Report them as monitored resource labels instead of common labels.
1279
+ if group_resource.type == ML_CONSTANTS[:resource_type]
1280
+ resource_labels.merge!(
1281
+ delete_and_extract_labels(
1282
+ common_labels, %w(job_id task_name)
1283
+ .map { |l| ["#{ML_CONSTANTS[:service]}/#{l}", l] }.to_h))
1284
+ end
1285
+
1286
+ [resource_type, resource_labels, common_labels]
1287
+ end
1288
+
1289
+ # Call Metadata Agent to get monitored resource information and parse
1290
+ # response to Google::Api::MonitoredResource.
1291
+ def call_metadata_agent_for_monitored_resource(unique_key)
1292
+ response = call_metadata_agent("monitoredResource/#{unique_key}")
1293
+ return nil if response.nil?
1294
+ begin
1295
+ resource = Google::Api::MonitoredResource.decode_json(response.to_json)
1296
+ rescue Google::Protobuf::ParseError, ArgumentError => e
1297
+ @log.error 'Error paring monitored resource from Metadata Agent. ' \
1298
+ "response: #{response.inspect}", error: e
1299
+ return nil
1300
+ end
1301
+
1302
+ # TODO(qingling128): Use Google::Api::MonitoredResource directly after we
1303
+ # upgrade gRPC version to include the fix for the protobuf map
1304
+ # corruption issue.
1305
+ Google::Apis::LoggingV2beta1::MonitoredResource.new(
1306
+ type: resource.type,
1307
+ labels: resource.labels.to_h
1308
+ )
1309
+ end
1310
+
1311
+ # Call Metadata Agent and parse response to json. Return nil in case of any
1312
+ # error / failure.
1313
+ def call_metadata_agent(path)
1314
+ url = "#{@metadata_agent_url}/#{path}"
1315
+ @log.debug("Calling Metadata Agent: #{url}")
1316
+ open(url) do |f|
1317
+ response = f.read
1318
+ parsed_hash = parse_json_or_nil(response)
1319
+ if parsed_hash.nil?
1320
+ @log.error 'Response from Metadata Agent is not in valid json ' \
1321
+ "format: '#{response.inspect}'."
1322
+ return nil
1323
+ end
1324
+ @log.debug "Response from Metadata Agent: #{parsed_hash}"
1325
+ return parsed_hash
1326
+ end
1327
+ rescue StandardError => e
1328
+ @log.error 'Error calling Metadata Agent.', error: e
1329
+ return nil
868
1330
  end
869
1331
 
870
1332
  # TODO: This functionality should eventually be available in another
@@ -902,11 +1364,28 @@ module Fluent
902
1364
  end
903
1365
  end
904
1366
 
905
- def detect_cloudfunctions(attributes)
906
- return unless attributes.include?('gcf_region')
907
- # Cloud Functions detected
908
- @running_cloudfunctions = true
909
- @gcf_region = fetch_gce_metadata('instance/attributes/gcf_region')
1367
+ # Calling Docker Remote API to get container id by name.
1368
+ def retrieve_container_id_by_name_locally(container_name)
1369
+ response = Excon.get(
1370
+ "unix:///containers/#{container_name}/json",
1371
+ socket: @docker_remote_api_socket_path)
1372
+ @log.debug "Response from Docker API with name '#{container_name}': " \
1373
+ "#{response.inspect}."
1374
+ return parse_container_id_from_docker_api_response(response)
1375
+ rescue StandardError => e
1376
+ @log.error 'Error calling Docker API to get container id.', error: e
1377
+ return nil
1378
+ end
1379
+
1380
+ # Parse the container id from Docker Remote API response.
1381
+ # TODO(qingling128) Add a config for Docker API version to support parsing
1382
+ # different versions of Docker Remote API when the format varies.
1383
+ def parse_container_id_from_docker_api_response(response)
1384
+ JSON.parse(response.data[:body])['Id']
1385
+ rescue StandardError => e
1386
+ @log.error 'Error parsing Docker API response to get container id.',
1387
+ error: e
1388
+ return nil
910
1389
  end
911
1390
 
912
1391
  def cluster_name_from_kube_env(kube_env)
@@ -979,9 +1458,14 @@ module Fluent
979
1458
  end
980
1459
  elsif record.key?('severity')
981
1460
  return parse_severity(record.delete('severity'))
982
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
983
- entry_common_labels.key?("#{CONTAINER_CONSTANTS[:service]}/stream")
984
- stream = entry_common_labels["#{CONTAINER_CONSTANTS[:service]}/stream"]
1461
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1462
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type)
1463
+ stream = entry_common_labels[
1464
+ "#{CONTAINER_CONSTANTS[:service]}/stream"] if
1465
+ resource_type == CONTAINER_CONSTANTS[:resource_type]
1466
+ stream = entry_common_labels[
1467
+ "#{DOCKER_CONSTANTS[:service]}/stream"] if
1468
+ resource_type == DOCKER_CONSTANTS[:resource_type]
985
1469
  if stream == 'stdout'
986
1470
  return 'INFO'
987
1471
  elsif stream == 'stderr'
@@ -1161,43 +1645,35 @@ module Fluent
1161
1645
  .gsub('u.u', '_').gsub('d.d', '$').gsub('a.a', '@').gsub('p.p', '.')
1162
1646
  end
1163
1647
 
1164
- # Requires that record has a 'kubernetes' field.
1165
- def extract_container_metadata(record)
1166
- resource_labels = {}
1167
- common_labels = {}
1168
- %w(namespace_id pod_id container_name).each do |field|
1169
- resource_labels.merge!(
1170
- fields_to_labels(record['kubernetes'], field => field))
1171
- end
1172
- %w(namespace_name pod_name).each do |field|
1173
- common_labels.merge!(
1174
- fields_to_labels(
1175
- record['kubernetes'],
1176
- field => "#{CONTAINER_CONSTANTS[:service]}/#{field}"))
1177
- end
1178
- # Prepend label/ to all user-defined labels' keys.
1179
- if record['kubernetes'].key?('labels')
1180
- record['kubernetes']['labels'].each do |key, value|
1181
- common_labels["label/#{key}"] = value
1182
- end
1648
+ def format(tag, time, record)
1649
+ [tag, time, record].to_msgpack
1650
+ end
1651
+
1652
+ # Given a tag, returns the corresponding valid tag if possible, or nil if
1653
+ # the tag should be rejected. If 'require_valid_tags' is false, non-string
1654
+ # tags are converted to strings, and invalid characters are sanitized;
1655
+ # otherwise such tags are rejected.
1656
+ def sanitize_tag(tag)
1657
+ if @require_valid_tags &&
1658
+ (!tag.is_a?(String) || tag == '' || convert_to_utf8(tag) != tag)
1659
+ return nil
1183
1660
  end
1184
- # We've explicitly consumed all the fields we care about -- don't litter
1185
- # the log entries with the remaining fields that the kubernetes metadata
1186
- # filter plugin includes (or an empty 'kubernetes' field).
1187
- record.delete('kubernetes')
1188
- record.delete('docker')
1189
- [resource_labels, common_labels]
1661
+ tag = convert_to_utf8(tag.to_s)
1662
+ tag = '_' if tag == ''
1663
+ tag
1190
1664
  end
1191
1665
 
1192
1666
  # For every original_label => new_label pair in the label_map, delete the
1193
- # original_label from the record if it exists, and extract the value to form
1194
- # a map with the new_label as the key.
1195
- def fields_to_labels(record, label_map)
1196
- return {} if label_map.nil? || !label_map.is_a?(Hash)
1667
+ # original_label from the original_resource map if it exists, and extract
1668
+ # the value to form a map with the new_label as the key.
1669
+ def delete_and_extract_labels(original_resource, label_map)
1670
+ return {} if label_map.nil? || !label_map.is_a?(Hash) ||
1671
+ original_resource.nil? || !original_resource.is_a?(Hash)
1197
1672
  label_map.each_with_object({}) \
1198
1673
  do |(original_label, new_label), extracted_labels|
1199
1674
  extracted_labels[new_label] = convert_to_utf8(
1200
- record.delete(original_label).to_s) if record.key?(original_label)
1675
+ original_resource.delete(original_label).to_s) if
1676
+ original_resource.key?(original_label)
1201
1677
  end
1202
1678
  end
1203
1679
 
@@ -1216,7 +1692,8 @@ module Fluent
1216
1692
  entry.text_payload = record['log']
1217
1693
  elsif is_json
1218
1694
  entry.json_payload = record
1219
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1695
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1696
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type) &&
1220
1697
  record.key?('log')
1221
1698
  entry.text_payload = record['log']
1222
1699
  elsif record.size == 1 && record.key?('message')
@@ -1286,7 +1763,8 @@ module Fluent
1286
1763
  entry.text_payload = convert_to_utf8(record['log'])
1287
1764
  elsif is_json
1288
1765
  entry.json_payload = struct_from_ruby(record)
1289
- elsif resource_type == CONTAINER_CONSTANTS[:resource_type] &&
1766
+ elsif [CONTAINER_CONSTANTS[:resource_type],
1767
+ DOCKER_CONSTANTS[:resource_type]].include?(resource_type) &&
1290
1768
  record.key?('log')
1291
1769
  entry.text_payload = convert_to_utf8(record['log'])
1292
1770
  elsif record.size == 1 && record.key?('message')
@@ -1299,7 +1777,7 @@ module Fluent
1299
1777
  def log_name(tag, resource)
1300
1778
  if resource.type == CLOUDFUNCTIONS_CONSTANTS[:resource_type]
1301
1779
  tag = 'cloud-functions'
1302
- elsif @running_on_managed_vm
1780
+ elsif resource.type == APPENGINE_CONSTANTS[:resource_type]
1303
1781
  # Add a prefix to Managed VM logs to prevent namespace collisions.
1304
1782
  tag = "#{APPENGINE_CONSTANTS[:service]}/#{tag}"
1305
1783
  elsif resource.type == CONTAINER_CONSTANTS[:resource_type]
@@ -1314,32 +1792,6 @@ module Fluent
1314
1792
  tag
1315
1793
  end
1316
1794
 
1317
- # Some services set labels (via configuring 'labels' or 'label_map') which
1318
- # are now MonitoredResource labels in v2.
1319
- # For these services, remove resource labels from 'labels' and return a
1320
- # Hash of labels to be merged into the MonitoredResource labels.
1321
- # Otherwise, return an empty hash and leave 'labels' unmodified.
1322
- def extract_resource_labels(resource_type, labels)
1323
- extracted_labels = {}
1324
- return extracted_labels if labels.nil? || !labels.is_a?(Hash)
1325
-
1326
- if resource_type == DATAFLOW_CONSTANTS[:resource_type]
1327
- label_prefix = DATAFLOW_CONSTANTS[:service]
1328
- labels_to_extract = %w(region job_name job_id step_id)
1329
- elsif resource_type == ML_CONSTANTS[:resource_type]
1330
- label_prefix = ML_CONSTANTS[:service]
1331
- labels_to_extract = %w(job_id task_name)
1332
- else
1333
- return extracted_labels
1334
- end
1335
-
1336
- labels_to_extract.each do |label|
1337
- extracted_labels[label] = labels.delete("#{label_prefix}/#{label}") if
1338
- labels.key?("#{label_prefix}/#{label}")
1339
- end
1340
- extracted_labels
1341
- end
1342
-
1343
1795
  def init_api_client
1344
1796
  return if @use_grpc
1345
1797
  # TODO: Use a non-default ClientOptions object.