elastic-apm 4.3.0 → 4.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/.jenkins_exclude.yml +23 -23
  3. data/.ci/.jenkins_framework.yml +2 -2
  4. data/.ci/{.jenkins_master_framework.yml → .jenkins_main_framework.yml} +0 -0
  5. data/.ci/Jenkinsfile +14 -13
  6. data/.ci/docker/jruby/11-jdk/Dockerfile +1 -1
  7. data/.ci/docker/jruby/12-jdk/Dockerfile +1 -1
  8. data/.ci/docker/jruby/13-jdk/Dockerfile +1 -1
  9. data/.ci/docker/jruby/7-jdk/Dockerfile +1 -1
  10. data/.ci/docker/jruby/8-jdk/Dockerfile +1 -1
  11. data/.ci/jobs/apm-agent-ruby-mbp.yml +1 -0
  12. data/.github/PULL_REQUEST_TEMPLATE.md +5 -5
  13. data/.pre-commit-config.yaml +2 -2
  14. data/CHANGELOG.asciidoc +42 -0
  15. data/CONTRIBUTING.md +2 -2
  16. data/Gemfile +11 -7
  17. data/README.md +1 -1
  18. data/Rakefile +2 -2
  19. data/docs/api.asciidoc +2 -2
  20. data/docs/configuration.asciidoc +27 -3
  21. data/docs/introduction.asciidoc +3 -3
  22. data/docs/log-correlation.asciidoc +1 -1
  23. data/docs/upgrading.asciidoc +1 -1
  24. data/elastic-apm.gemspec +8 -8
  25. data/lib/elastic_apm/agent.rb +2 -2
  26. data/lib/elastic_apm/config.rb +19 -0
  27. data/lib/elastic_apm/context/request/socket.rb +1 -2
  28. data/lib/elastic_apm/fields.rb +29 -19
  29. data/lib/elastic_apm/instrumenter.rb +21 -19
  30. data/lib/elastic_apm/metadata/service_info.rb +1 -1
  31. data/lib/elastic_apm/metadata/system_info.rb +2 -1
  32. data/lib/elastic_apm/metrics/cpu_mem_set.rb +4 -1
  33. data/lib/elastic_apm/metrics/metric.rb +2 -0
  34. data/lib/elastic_apm/metrics.rb +2 -5
  35. data/lib/elastic_apm/span/context/destination.rb +2 -2
  36. data/lib/elastic_apm/span.rb +8 -16
  37. data/lib/elastic_apm/spies/dynamo_db.rb +8 -3
  38. data/lib/elastic_apm/spies/elasticsearch.rb +11 -1
  39. data/lib/elastic_apm/spies/mongo.rb +7 -6
  40. data/lib/elastic_apm/spies/s3.rb +10 -1
  41. data/lib/elastic_apm/spies/sns.rb +1 -1
  42. data/lib/elastic_apm/transport/base.rb +1 -3
  43. data/lib/elastic_apm/transport/serializers/context_serializer.rb +1 -2
  44. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +6 -2
  45. data/lib/elastic_apm/transport/serializers/span_serializer.rb +4 -4
  46. data/lib/elastic_apm/transport/user_agent.rb +12 -6
  47. data/lib/elastic_apm/version.rb +1 -1
  48. data/lib/elastic_apm.rb +5 -2
  49. metadata +8 -8
@@ -25,7 +25,7 @@ module ElasticAPM
25
25
  # class MyThing
26
26
  # include Fields
27
27
  # field :name
28
- # field :address, optional: true
28
+ # field :address, default: 'There'
29
29
  # end
30
30
  #
31
31
  # MyThing.new(name: 'AJ').to_h
@@ -33,56 +33,66 @@ module ElasticAPM
33
33
  # MyThing.new().empty?
34
34
  # # => true
35
35
  module Fields
36
+ class Field
37
+ def initialize(key, default: nil)
38
+ @key = key
39
+ @default = default
40
+ end
41
+
42
+ attr_reader :key, :default
43
+ end
44
+
36
45
  module InstanceMethods
37
46
  def initialize(**attrs)
47
+ schema.each do |key, field|
48
+ send(:"#{key}=", field.default)
49
+ end
50
+
38
51
  attrs.each do |key, value|
39
- self.send(:"#{key}=", value)
52
+ send(:"#{key}=", value)
40
53
  end
41
54
 
42
55
  super()
43
56
  end
44
57
 
45
58
  def empty?
46
- self.class.fields.each do |key|
47
- next if send(key)
48
- next if optionals.include?(key)
49
-
50
- return true
59
+ self.class.schema.each do |key, field|
60
+ next if send(key).nil?
61
+ return false
51
62
  end
52
63
 
53
- false
64
+ true
54
65
  end
55
66
 
56
67
  def to_h
57
- self.class.fields.each_with_object({}) do |key, fields|
58
- fields[key] = send(key)
68
+ schema.each_with_object({}) do |(key, field), hsh|
69
+ hsh[key] = send(key)
59
70
  end
60
71
  end
61
72
 
62
73
  private
63
74
 
64
- def optionals
65
- self.class.optionals
75
+ def schema
76
+ self.class.schema
66
77
  end
67
78
  end
68
79
 
69
80
  module ClassMethods
70
- def field(key, optional: false)
71
- attr_accessor(key)
81
+ def field(key, default: nil)
82
+ field = Field.new(key, default: default)
83
+ schema[key] = field
72
84
 
73
- fields.push(key)
74
- optionals.push(key) if optional
85
+ attr_accessor(key)
75
86
  end
76
87
 
77
- attr_reader :fields, :optionals
88
+ attr_reader :schema
78
89
  end
79
90
 
80
91
  def self.included(cls)
81
92
  cls.extend(ClassMethods)
82
93
  cls.include(InstanceMethods)
83
94
 
84
- cls.instance_variable_set(:@fields, [])
85
- cls.instance_variable_set(:@optionals, [])
95
+ cls.instance_variable_set(:@schema, {})
86
96
  end
87
97
  end
88
98
  end
@@ -179,7 +179,8 @@ module ElasticAPM
179
179
  context: nil,
180
180
  trace_context: nil,
181
181
  parent: nil,
182
- sync: nil
182
+ sync: nil,
183
+ exit_span: nil
183
184
  )
184
185
 
185
186
  transaction =
@@ -197,6 +198,15 @@ module ElasticAPM
197
198
 
198
199
  parent ||= (current_span || current_transaction)
199
200
 
201
+ # To not mess with breakdown metric stats, exit spans MUST not add
202
+ # sub-spans unless they share the same type and subtype.
203
+ if parent && parent.is_a?(Span) && parent.exit_span?
204
+ if parent.type != type || parent.subtype != subtype
205
+ debug "Skipping new span '#{name}' as its parent is an exit_span"
206
+ return
207
+ end
208
+ end
209
+
200
210
  span = Span.new(
201
211
  name: name,
202
212
  subtype: subtype,
@@ -207,7 +217,8 @@ module ElasticAPM
207
217
  type: type,
208
218
  context: context,
209
219
  stacktrace_builder: stacktrace_builder,
210
- sync: sync
220
+ sync: sync,
221
+ exit_span: exit_span
211
222
  )
212
223
 
213
224
  if backtrace && transaction.span_frames_min_duration
@@ -222,8 +233,14 @@ module ElasticAPM
222
233
  # rubocop:enable Metrics/CyclomaticComplexity
223
234
  # rubocop:enable Metrics/PerceivedComplexity
224
235
 
225
- def end_span
226
- return unless (span = current_spans.pop)
236
+ def end_span(span = nil)
237
+ if span
238
+ current_spans.delete(span)
239
+ else
240
+ span = current_spans.pop
241
+ end
242
+
243
+ return unless span
227
244
 
228
245
  span.done
229
246
 
@@ -273,24 +290,9 @@ module ElasticAPM
273
290
  'transaction.type': transaction.type
274
291
  }
275
292
 
276
- @metrics.get(:transaction).timer(
277
- :'transaction.duration.sum.us',
278
- tags: tags, reset_on_collect: true
279
- ).update(transaction.duration)
280
-
281
- @metrics.get(:transaction).counter(
282
- :'transaction.duration.count',
283
- tags: tags, reset_on_collect: true
284
- ).inc!
285
-
286
293
  return unless transaction.sampled?
287
294
  return unless transaction.breakdown_metrics
288
295
 
289
- @metrics.get(:breakdown).counter(
290
- :'transaction.breakdown.count',
291
- tags: tags, reset_on_collect: true
292
- ).inc!
293
-
294
296
  span_tags = tags.merge('span.type': 'app')
295
297
 
296
298
  @metrics.get(:breakdown).timer(
@@ -49,7 +49,7 @@ module ElasticAPM
49
49
  )
50
50
  @language = Language.new(name: 'ruby', version: RUBY_VERSION)
51
51
  @runtime = lookup_runtime
52
- @version = @config.service_version || Util.git_sha
52
+ @version = @config.service_version
53
53
  end
54
54
 
55
55
  attr_reader :name, :node_name, :environment, :agent, :framework,
@@ -50,7 +50,8 @@ module ElasticAPM
50
50
  private
51
51
 
52
52
  def detect_hostname
53
- `hostname`.chomp
53
+ Socket.gethostname.chomp
54
+ rescue
54
55
  end
55
56
  end
56
57
  end
@@ -77,7 +77,7 @@ module ElasticAPM
77
77
 
78
78
  def sampler_for_os(os)
79
79
  case os
80
- when :linux then Linux.new
80
+ when /^linux/ then Linux.new
81
81
  else
82
82
  warn "Disabling system metrics, unsupported host OS '#{os}'"
83
83
  disable!
@@ -116,6 +116,9 @@ module ElasticAPM
116
116
  process_cpu_usage =
117
117
  current.process_cpu_usage - previous.process_cpu_usage
118
118
 
119
+ # No change / avoid dividing by 0
120
+ return [0, 0] if system_cpu_total == 0
121
+
119
122
  cpu_usage_pct = system_cpu_usage.to_f / system_cpu_total
120
123
  cpu_process_pct = process_cpu_usage.to_f / system_cpu_total
121
124
 
@@ -57,6 +57,8 @@ module ElasticAPM
57
57
  @mutex.synchronize do
58
58
  collected = @value
59
59
 
60
+ return nil if collected.is_a?(Float) && !collected.finite?
61
+
60
62
  @value = initial_value if reset_on_collect?
61
63
 
62
64
  return nil if reset_on_collect? && collected == 0
@@ -29,15 +29,13 @@ module ElasticAPM
29
29
  end
30
30
 
31
31
  def self.os
32
- @platform ||= RbConfig::CONFIG.fetch('host_os', 'unknown').to_sym
32
+ @os ||= RbConfig::CONFIG.fetch('host_os', 'unknown').to_sym
33
33
  end
34
34
 
35
35
  # @api private
36
36
  class Registry
37
37
  include Logging
38
38
 
39
- TIMEOUT_INTERVAL = 5 # seconds
40
-
41
39
  def initialize(config, &block)
42
40
  @config = config
43
41
  @callback = block
@@ -76,8 +74,7 @@ module ElasticAPM
76
74
 
77
75
  @timer_task = Concurrent::TimerTask.execute(
78
76
  run_now: true,
79
- execution_interval: config.metrics_interval,
80
- timeout_interval: TIMEOUT_INTERVAL
77
+ execution_interval: config.metrics_interval
81
78
  ) do
82
79
  begin
83
80
  debug 'Collecting metrics'
@@ -33,8 +33,8 @@ module ElasticAPM
33
33
  class Service
34
34
  include Fields
35
35
 
36
- field :name
37
- field :type
36
+ field :name, default: ''
37
+ field :type, default: ''
38
38
  field :resource
39
39
  end
40
40
 
@@ -49,7 +49,8 @@ module ElasticAPM
49
49
  action: nil,
50
50
  context: nil,
51
51
  stacktrace_builder: nil,
52
- sync: nil
52
+ sync: nil,
53
+ exit_span: false
53
54
  )
54
55
  @name = name
55
56
 
@@ -68,6 +69,8 @@ module ElasticAPM
68
69
 
69
70
  @context = context || Span::Context.new(sync: sync)
70
71
  @stacktrace_builder = stacktrace_builder
72
+
73
+ @exit_span = exit_span
71
74
  end
72
75
  # rubocop:enable Metrics/ParameterLists
73
76
 
@@ -75,6 +78,7 @@ module ElasticAPM
75
78
 
76
79
  attr_accessor(
77
80
  :action,
81
+ :exit_span,
78
82
  :name,
79
83
  :original_backtrace,
80
84
  :outcome,
@@ -93,6 +97,8 @@ module ElasticAPM
93
97
  :transaction
94
98
  )
95
99
 
100
+ alias :exit_span? :exit_span
101
+
96
102
  # life cycle
97
103
 
98
104
  def start(clock_start = Util.monotonic_micros)
@@ -107,17 +113,6 @@ module ElasticAPM
107
113
  @parent.child_stopped
108
114
  @self_time = @duration - child_durations.duration
109
115
 
110
- if exit_span?
111
- context.destination ||= Context::Destination.new
112
- context.destination.service ||= Context::Destination::Service.new
113
- context.destination.service.resource ||= (subtype || type)
114
-
115
- # Deprecated fields but required by some versions of APM Server, so
116
- # we auto-infer them from existing fields
117
- context.destination.service.name ||= (subtype || type)
118
- context.destination.service.type ||= type
119
- end
120
-
121
116
  self
122
117
  end
123
118
 
@@ -158,6 +153,7 @@ module ElasticAPM
158
153
  " type:#{type.inspect}" \
159
154
  " subtype:#{subtype.inspect}" \
160
155
  " action:#{action.inspect}" \
156
+ " exit_span:#{exit_span.inspect}" \
161
157
  '>'
162
158
  end
163
159
 
@@ -180,9 +176,5 @@ module ElasticAPM
180
176
 
181
177
  duration >= min_duration
182
178
  end
183
-
184
- def exit_span?
185
- context.destination || context.db || context.message || context.http
186
- end
187
179
  end
188
180
  end
@@ -22,9 +22,9 @@ module ElasticAPM
22
22
  module Spies
23
23
  # @api private
24
24
  class DynamoDBSpy
25
- NAME = 'dynamodb'
26
25
  TYPE = 'db'
27
26
  SUBTYPE = 'dynamodb'
27
+ ACTION = 'query'
28
28
 
29
29
  @@formatted_op_names = Concurrent::Map.new
30
30
 
@@ -63,7 +63,12 @@ module ElasticAPM
63
63
  statement: params[:key_condition_expression]
64
64
  },
65
65
  destination: {
66
- service: { resource: "#{SUBTYPE}/#{config.region}" },
66
+ address: config.endpoint.host,
67
+ port: config.endpoint.port,
68
+ service: {
69
+ name: SUBTYPE,
70
+ type: TYPE,
71
+ resource: SUBTYPE },
67
72
  cloud: { region: config.region }
68
73
  }
69
74
  )
@@ -72,7 +77,7 @@ module ElasticAPM
72
77
  ElasticAPM::Spies::DynamoDBSpy.span_name(operation_name, params),
73
78
  TYPE,
74
79
  subtype: SUBTYPE,
75
- action: operation_name,
80
+ action: ACTION,
76
81
  context: context
77
82
  ) do
78
83
  ElasticAPM::Spies::DynamoDBSpy.without_net_http do
@@ -79,7 +79,11 @@ module ElasticAPM
79
79
  end
80
80
 
81
81
  def install
82
- ::Elasticsearch::Transport::Client.prepend(Ext)
82
+ if defined?(::Elastic::Transport::Client)
83
+ ::Elastic::Transport::Client.prepend(Ext)
84
+ elsif defined?(::Elasticsearch::Transport::Client)
85
+ ::Elasticsearch::Transport::Client.prepend(Ext)
86
+ end
83
87
  end
84
88
  end
85
89
 
@@ -88,5 +92,11 @@ module ElasticAPM
88
92
  'elasticsearch-transport',
89
93
  ElasticsearchSpy.new
90
94
  )
95
+
96
+ register(
97
+ 'Elastic::Transport::Client',
98
+ 'elastic-transport',
99
+ ElasticsearchSpy.new
100
+ )
91
101
  end
92
102
  end
@@ -35,8 +35,10 @@ module ElasticAPM
35
35
  SUBTYPE = 'mongodb'
36
36
  ACTION = 'query'
37
37
 
38
- def initialize
39
- @events = {}
38
+ EVENT_KEY = :__elastic_instrumenter_mongo_events_key
39
+
40
+ def events
41
+ Thread.current[EVENT_KEY] ||= []
40
42
  end
41
43
 
42
44
  def started(event)
@@ -70,7 +72,7 @@ module ElasticAPM
70
72
  # and the collection name is at the key `collection`
71
73
  collection =
72
74
  if event.command[event.command_name] == 1 ||
73
- event.command[event.command_name].is_a?(BSON::Int64)
75
+ event.command[event.command_name].is_a?(BSON::Int64)
74
76
  event.command[:collection]
75
77
  else
76
78
  event.command[event.command_name]
@@ -89,14 +91,13 @@ module ElasticAPM
89
91
  context: build_context(event)
90
92
  )
91
93
 
92
- @events[event.operation_id] = span
94
+ events << span
93
95
  end
94
96
 
95
97
  def pop_event(event)
96
- span = @events.delete(event.operation_id)
97
98
  return unless (curr = ElasticAPM.current_span)
98
99
 
99
- curr == span && ElasticAPM.end_span
100
+ curr == events[-1] && ElasticAPM.end_span(events.pop)
100
101
  end
101
102
 
102
103
  def build_context(event)
@@ -87,8 +87,17 @@ module ElasticAPM
87
87
 
88
88
  resource = "#{SUBTYPE}/#{bucket_name || 'unknown-bucket'}"
89
89
  context = ElasticAPM::Span::Context.new(
90
+ db: {
91
+ instance: config.region,
92
+ type: SUBTYPE
93
+ },
90
94
  destination: {
91
- service: { resource: resource },
95
+ address: config.endpoint.host,
96
+ port: config.endpoint.port,
97
+ service: {
98
+ name: SUBTYPE,
99
+ type: TYPE,
100
+ resource: resource },
92
101
  cloud: { region: region }
93
102
  }
94
103
  )
@@ -38,7 +38,7 @@ module ElasticAPM
38
38
  end
39
39
 
40
40
  def self.get_topic(params)
41
- return '<PHONE_NUMBER>' if params[:phone_number]
41
+ return '[PHONENUMBER]' if params[:phone_number]
42
42
 
43
43
  last_after_slash_or_colon(
44
44
  params[:topic_arn] || params[:target_arn]
@@ -35,7 +35,6 @@ module ElasticAPM
35
35
  include Logging
36
36
 
37
37
  WATCHER_EXECUTION_INTERVAL = 5
38
- WATCHER_TIMEOUT_INTERVAL = 4
39
38
  WORKER_JOIN_TIMEOUT = 5
40
39
 
41
40
  def initialize(config)
@@ -112,8 +111,7 @@ module ElasticAPM
112
111
 
113
112
  def create_watcher
114
113
  @watcher = Concurrent::TimerTask.execute(
115
- execution_interval: WATCHER_EXECUTION_INTERVAL,
116
- timeout_interval: WATCHER_TIMEOUT_INTERVAL
114
+ execution_interval: WATCHER_EXECUTION_INTERVAL
117
115
  ) { ensure_worker_count }
118
116
  end
119
117
 
@@ -77,8 +77,7 @@ module ElasticAPM
77
77
  return unless socket
78
78
 
79
79
  {
80
- remote_addr: socket.remote_addr,
81
- encrypted: socket.encrypted
80
+ remote_addr: socket.remote_addr
82
81
  }
83
82
  end
84
83
 
@@ -80,14 +80,18 @@ module ElasticAPM
80
80
  end
81
81
 
82
82
  def build_system(system)
83
- {
84
- detected_hostname: keyword_field(system.detected_hostname),
83
+ base = {
85
84
  configured_hostname: keyword_field(system.configured_hostname),
86
85
  architecture: keyword_field(system.architecture),
87
86
  platform: keyword_field(system.platform),
88
87
  kubernetes: keyword_object(system.kubernetes),
89
88
  container: keyword_object(system.container)
90
89
  }
90
+ if system.detected_hostname
91
+ base[:detected_hostname] = keyword_field(system.detected_hostname)
92
+ end
93
+
94
+ base
91
95
  end
92
96
 
93
97
  def build_cloud(cloud)
@@ -100,12 +100,12 @@ module ElasticAPM
100
100
  port: destination.port
101
101
  }
102
102
 
103
- unless destination.service&.empty?
104
- base[:service] = destination.service.to_h
103
+ if (service = destination.service) && !service.empty?
104
+ base[:service] = service.to_h
105
105
  end
106
106
 
107
- unless destination.cloud&.empty?
108
- base[:cloud] = destination.cloud.to_h
107
+ if (cloud = destination.cloud) && !cloud.empty?
108
+ base[:cloud] = cloud.to_h
109
109
  end
110
110
 
111
111
  base
@@ -37,12 +37,18 @@ module ElasticAPM
37
37
 
38
38
  [
39
39
  "elastic-apm-ruby/#{@version}",
40
- HTTP::Request::USER_AGENT,
41
- [
42
- service.runtime.name,
43
- service.runtime.version
44
- ].join('/')
45
- ].join(' ')
40
+ formatted_service_info(service)
41
+ ].compact.join(' ')
42
+ end
43
+
44
+ def formatted_service_info(service)
45
+ if service.name
46
+ "(#{[
47
+ service.name,
48
+ service.version
49
+ ].compact.join(' ')
50
+ })"
51
+ end
46
52
  end
47
53
  end
48
54
  end
@@ -18,5 +18,5 @@
18
18
  # frozen_string_literal: true
19
19
 
20
20
  module ElasticAPM
21
- VERSION = '4.3.0'
21
+ VERSION = '4.5.1'
22
22
  end
data/lib/elastic_apm.rb CHANGED
@@ -243,9 +243,12 @@ module ElasticAPM
243
243
 
244
244
  # Ends the current span
245
245
  #
246
+ # @param span [Span] Optional span to be ended instead of the last span
247
+ # created, useful for asynchronous environments where multiple spans are created in parallel
248
+ #
246
249
  # @return [Span]
247
- def end_span
248
- agent&.end_span
250
+ def end_span(span = nil)
251
+ agent&.end_span(span)
249
252
  end
250
253
 
251
254
  # rubocop:disable Metrics/ParameterLists
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic-apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-26 00:00:00.000000000 Z
11
+ date: 2022-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.0'
41
- description:
41
+ description:
42
42
  email:
43
43
  - mikkel@elastic.co
44
44
  executables: []
@@ -48,7 +48,7 @@ files:
48
48
  - ".ci/.jenkins_codecov.yml"
49
49
  - ".ci/.jenkins_exclude.yml"
50
50
  - ".ci/.jenkins_framework.yml"
51
- - ".ci/.jenkins_master_framework.yml"
51
+ - ".ci/.jenkins_main_framework.yml"
52
52
  - ".ci/.jenkins_ruby.yml"
53
53
  - ".ci/Jenkinsfile"
54
54
  - ".ci/docker/jruby/11-jdk/Dockerfile"
@@ -258,7 +258,7 @@ licenses:
258
258
  - Apache-2.0
259
259
  metadata:
260
260
  source_code_uri: https://github.com/elastic/apm-agent-ruby
261
- post_install_message:
261
+ post_install_message:
262
262
  rdoc_options: []
263
263
  require_paths:
264
264
  - lib
@@ -273,8 +273,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
273
273
  - !ruby/object:Gem::Version
274
274
  version: '0'
275
275
  requirements: []
276
- rubygems_version: 3.0.6
277
- signing_key:
276
+ rubygems_version: 3.0.3.1
277
+ signing_key:
278
278
  specification_version: 4
279
279
  summary: The official Elastic APM agent for Ruby
280
280
  test_files: []