elastic-apm 3.10.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6db74de4a0201f5b5dd7e9f96b7ec383ce3276a56d7f9d57e64617dd7cf1a28
4
- data.tar.gz: '0319a664e07b2411428134e24ed58f21049acf856cf163c6434fb66934e4cd1f'
3
+ metadata.gz: bae79ddc57bfa8955a50f062b2305db04ed9618fa40011ec78341f62083487a3
4
+ data.tar.gz: 176267b31455a0f491aaff4326e1274db3684aa878bf811b60b7f5195613a228
5
5
  SHA512:
6
- metadata.gz: 7bc1dfb374ea6107f07f690d764205f44c00a588eed767d7621b42640bf00f322d5e8b7c6a3d4958b0753b4755722836a05577fcee5b1a15207a355860a1f71b
7
- data.tar.gz: 780227ecdd4592d5f3aaa87b54824b4ed36dc9686052b51314815ebbf3152855dc5ff186c69deb88b86f2eab1d7aa9e4170bfd1454b2fdca3ab82d513eb0280a
6
+ metadata.gz: '089b563ba5cba6732bf75f6b29218e1b2cdf29f251c21c4c0abd577b4492fcea2c7ee24d5d616c6e8cec92f051cbbf551219e580846db48689fcf5c77c857b71'
7
+ data.tar.gz: 28c4160d29e5af26bd388dfda7feacdbb0d484f522080e57b5aafc10041ed07b8c62b6c799a8a6676bb59fca3db643a9319fcb7afbe661788c37558454bd924c
@@ -35,6 +35,28 @@ endif::[]
35
35
  [[release-notes-3.x]]
36
36
  === Ruby Agent version 3.x
37
37
 
38
+ [[release-notes-3.11.0]]
39
+ ==== 3.11.0 (2020-10-27)
40
+
41
+ [float]
42
+ ===== Added
43
+
44
+ - Add and read sampling info from Tracestate headers {pull}858[#858]
45
+ - Add information about cloud hosting environment if available {pull}871[#871]
46
+
47
+ [float]
48
+ ===== Changed
49
+
50
+ - Align the default value of `sanitize_field_names` with other agents {pull}867[#867]
51
+ - Ensure max 4 digits of precision for `sample_rate` as per agent spec {pull}880[#880]
52
+
53
+ [float]
54
+ ===== Fixed
55
+
56
+ - Fix Delayed::Job class names when used through ActiveJob {pull}869[#869]
57
+ - Fix Delayed::Job when run without the agent running {pull}874[#874]
58
+ - Fix Kubernetes related metadata {pull}882[#882]
59
+
38
60
  [[release-notes-3.10.1]]
39
61
  ==== 3.10.1 (2020-08-26)
40
62
 
@@ -42,6 +64,7 @@ endif::[]
42
64
  ===== Fixed
43
65
 
44
66
  - Remove secrets from cookies in errors {pull}863[#863]
67
+ - Silence deprecation warning when setting `ignore_url_patterns` to default {pull}865[#865]
45
68
 
46
69
  [[release-notes-3.10.0]]
47
70
  ==== 3.10.0 (2020-08-26)
data/Gemfile CHANGED
@@ -28,8 +28,6 @@ gem 'pry'
28
28
  gem 'rack-test'
29
29
  gem 'rspec', '~> 3'
30
30
  gem 'rspec-its'
31
- gem 'rubocop', require: nil
32
- gem 'rubocop-performance', require: nil
33
31
  gem 'timecop'
34
32
  gem 'webmock'
35
33
 
@@ -853,6 +853,7 @@ To reduce overhead and storage requirements, you can set the sample rate to a va
853
853
  between `0.0` and `1.0`.
854
854
  We still record overall time and the result for unsampled transactions, but no
855
855
  context information, tags, or spans.
856
+ Note that the sample rate will be rounded to 4 digits of precision.
856
857
 
857
858
  [float]
858
859
  [[config-use-experimental-sql-parser]]
@@ -17,9 +17,10 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- require 'elastic_apm/config/options'
21
- require 'elastic_apm/config/duration'
22
20
  require 'elastic_apm/config/bytes'
21
+ require 'elastic_apm/config/duration'
22
+ require 'elastic_apm/config/options'
23
+ require 'elastic_apm/config/round_float'
23
24
  require 'elastic_apm/config/regexp_list'
24
25
  require 'elastic_apm/config/wildcard_pattern_list'
25
26
 
@@ -30,6 +31,11 @@ module ElasticAPM
30
31
 
31
32
  DEPRECATED_OPTIONS = %i[].freeze
32
33
 
34
+ # DEPRECATED: To align with other agents, change on next major bump to:
35
+ # "password, passwd, pwd, secret, *key, *token*, *session*, *credit*, *card*, authorization, set-cookie"
36
+ SANITIZE_FIELD_NAMES_DEFAULT =
37
+ %w[*password* *passwd* *pwd* *secret* *key* *token* *session* *credit* *card* *authorization* *set-cookie*]
38
+
33
39
  # rubocop:disable Metrics/LineLength, Layout/ExtraSpacing
34
40
  option :config_file, type: :string, default: 'config/elastic_apm.yml'
35
41
  option :server_url, type: :url, default: 'http://localhost:8200'
@@ -45,6 +51,7 @@ module ElasticAPM
45
51
  option :capture_elasticsearch_queries, type: :bool, default: false
46
52
  option :capture_env, type: :bool, default: true
47
53
  option :central_config, type: :bool, default: true
54
+ option :cloud_provider, type: :string, default: 'auto'
48
55
  option :current_user_email_method, type: :string, default: 'email'
49
56
  option :current_user_id_method, type: :string, default: 'id'
50
57
  option :current_user_username_method, type: :string, default: 'username'
@@ -76,7 +83,8 @@ module ElasticAPM
76
83
  option :proxy_port, type: :int
77
84
  option :proxy_username, type: :string
78
85
  option :recording, type: :bool, default: true
79
- option :sanitize_field_names, type: :list, default: [], converter: WildcardPatternList.new
86
+ option :sanitize_field_names, type: :list,
87
+ default: SANITIZE_FIELD_NAMES_DEFAULT, converter: WildcardPatternList.new
80
88
  option :server_ca_cert, type: :string
81
89
  option :service_name, type: :string
82
90
  option :service_node_name, type: :string
@@ -89,7 +97,7 @@ module ElasticAPM
89
97
  option :stack_trace_limit, type: :int, default: 999_999
90
98
  option :transaction_ignore_urls, type: :list, default: [], converter: WildcardPatternList.new
91
99
  option :transaction_max_spans, type: :int, default: 500
92
- option :transaction_sample_rate, type: :float, default: 1.0
100
+ option :transaction_sample_rate, type: :float, default: 1.0, converter: RoundFloat.new
93
101
  option :use_elastic_traceparent_header, type: :bool, default: true
94
102
  option :use_legacy_sql_parser, type: :bool, default: false
95
103
  option :verify_server_cert, type: :bool, default: true
@@ -189,6 +197,14 @@ module ElasticAPM
189
197
  metrics_interval > 0
190
198
  end
191
199
 
200
+ # DEPRECATED: Remove this in next major version
201
+ def sanitize_field_names=(value)
202
+ list = WildcardPatternList.new.call(value)
203
+ defaults = WildcardPatternList.new.call(SANITIZE_FIELD_NAMES_DEFAULT)
204
+ get(:sanitize_field_names).value =
205
+ defaults.concat(list).uniq(&:pattern) # use regex pattern for comparisons
206
+ end
207
+
192
208
  def span_frames_min_duration?
193
209
  span_frames_min_duration != 0
194
210
  end
@@ -236,8 +252,10 @@ module ElasticAPM
236
252
  end
237
253
 
238
254
  def ignore_url_patterns=(value)
239
- warn '[DEPRECATED] The option ignore_url_patterns is being removed. ' \
240
- 'Consider using transaction_ignore_urls instead.'
255
+ unless value == self.class.schema[:ignore_url_patterns][:default]
256
+ warn '[DEPRECATED] The option ignore_url_patterns is being removed. ' \
257
+ 'Consider using transaction_ignore_urls instead.'
258
+ end
241
259
 
242
260
  set(:ignore_url_patterns, value)
243
261
  end
@@ -38,7 +38,8 @@ module ElasticAPM
38
38
  set(value || default)
39
39
  end
40
40
 
41
- attr_reader :key, :value, :default, :type
41
+ attr_reader :key, :default, :type, :converter
42
+ attr_accessor :value
42
43
 
43
44
  def set(value)
44
45
  @value = normalize(value)
@@ -0,0 +1,31 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ require 'elastic_apm/util/precision_validator'
21
+
22
+ module ElasticAPM
23
+ class Config
24
+ # @api private
25
+ class RoundFloat
26
+ def call(value)
27
+ Util::PrecisionValidator.validate(value, precision: 4, minimum: 0.0001)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -27,6 +27,8 @@ module ElasticAPM
27
27
  @pattern = convert(str)
28
28
  end
29
29
 
30
+ attr_reader :pattern
31
+
30
32
  def match?(other)
31
33
  !!@pattern.match(other)
32
34
  end
@@ -119,7 +119,13 @@ module ElasticAPM
119
119
  "Already inside #{transaction.inspect}"
120
120
  end
121
121
 
122
- sampled = trace_context ? trace_context.recorded? : random_sample?(config)
122
+ if trace_context
123
+ sampled = trace_context.recorded?
124
+ sample_rate = trace_context.tracestate.sample_rate
125
+ else
126
+ sampled = random_sample?(config)
127
+ sample_rate = config.transaction_sample_rate
128
+ end
123
129
 
124
130
  transaction =
125
131
  Transaction.new(
@@ -128,6 +134,7 @@ module ElasticAPM
128
134
  context: context,
129
135
  trace_context: trace_context,
130
136
  sampled: sampled,
137
+ sample_rate: sample_rate,
131
138
  config: config
132
139
  )
133
140
 
@@ -259,7 +266,7 @@ module ElasticAPM
259
266
  end
260
267
 
261
268
  def update_transaction_metrics(transaction)
262
- return unless transaction.collect_metrics
269
+ return unless transaction.collect_metrics?
263
270
 
264
271
  tags = {
265
272
  'transaction.name': transaction.name,
@@ -298,7 +305,7 @@ module ElasticAPM
298
305
  end
299
306
 
300
307
  def update_span_metrics(span)
301
- return unless span.transaction.breakdown_metrics
308
+ return unless span.transaction.collect_metrics?
302
309
 
303
310
  tags = {
304
311
  'span.type': span.type,
@@ -25,12 +25,14 @@ module ElasticAPM
25
25
  @process = ProcessInfo.new(config)
26
26
  @system = SystemInfo.new(config)
27
27
  @labels = config.global_labels
28
+ @cloud = CloudInfo.new(config).fetch!
28
29
  end
29
30
 
30
- attr_reader :service, :process, :system, :labels
31
+ attr_reader :service, :process, :system, :cloud, :labels
31
32
  end
32
33
  end
33
34
 
34
35
  require 'elastic_apm/metadata/service_info'
35
36
  require 'elastic_apm/metadata/system_info'
36
37
  require 'elastic_apm/metadata/process_info'
38
+ require 'elastic_apm/metadata/cloud_info'
@@ -0,0 +1,128 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ require "http"
21
+
22
+ module ElasticAPM
23
+ class Metadata
24
+ # @api private
25
+ class CloudInfo
26
+ include Logging
27
+
28
+ AWS_URI = "http://169.254.169.254/latest/dynamic/instance-identity/document"
29
+ GCP_URI = "http://metadata.google.internal/computeMetadata/v1/?recursive=true"
30
+ AZURE_URI = "http://169.254.169.254/metadata/instance/compute?api-version=2019-08-15"
31
+
32
+ def initialize(config)
33
+ @config = config
34
+ @client = HTTP.timeout(0.1)
35
+ end
36
+
37
+ attr_reader :config
38
+
39
+ attr_accessor(
40
+ :account_id,
41
+ :account_name,
42
+ :instance_id,
43
+ :instance_name,
44
+ :machine_type,
45
+ :project_id,
46
+ :project_name,
47
+ :availability_zone,
48
+ :provider,
49
+ :region
50
+ )
51
+
52
+ def fetch!
53
+ case config.cloud_provider
54
+ when "aws"
55
+ fetch_aws
56
+ when "gcp"
57
+ fetch_gcp
58
+ when "azure"
59
+ fetch_azure
60
+ when "auto"
61
+ fetch_aws || fetch_gcp || fetch_azure
62
+ when "none"
63
+ nil
64
+ else
65
+ error("Unknown setting for cloud_provider '#{config.cloud_provider}'")
66
+ end
67
+
68
+ self
69
+ end
70
+
71
+ private
72
+
73
+ def fetch_aws
74
+ resp = @client.get(AWS_URI)
75
+
76
+ return unless resp.status === 200
77
+ return unless (metadata = JSON.parse(resp.body))
78
+
79
+ self.provider = "aws"
80
+ self.account_id = metadata["accountId"]
81
+ self.instance_id = metadata["instanceId"]
82
+ self.availability_zone = metadata["availabilityZone"]
83
+ self.machine_type = metadata["instanceType"]
84
+ self.region = metadata["region"]
85
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
86
+ nil
87
+ end
88
+
89
+ def fetch_gcp
90
+ resp = @client.headers("Metadata-Flavor" => "Google").get(GCP_URI)
91
+
92
+ return unless resp.status === 200
93
+ return unless (metadata = JSON.parse(resp.body))
94
+
95
+ zone = metadata["instance"]["zone"]&.split("/")&.at(-1)
96
+
97
+ self.provider = "gcp"
98
+ self.instance_id = metadata["instance"]["id"]
99
+ self.instance_name = metadata["instance"]["name"]
100
+ self.project_id = metadata["project"]["numericProjectId"]
101
+ self.project_name = metadata["project"]["projectId"]
102
+ self.availability_zone = zone
103
+ self.region = zone.split("-")[0..-2].join("-")
104
+ self.machine_type = metadata["instance"]["machineType"]
105
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
106
+ nil
107
+ end
108
+
109
+ def fetch_azure
110
+ resp = @client.headers("Metadata" => "true").get(AZURE_URI)
111
+
112
+ return unless resp.status === 200
113
+ return unless (metadata = JSON.parse(resp.body))
114
+
115
+ self.provider = 'azure'
116
+ self.account_id = metadata["subscriptionId"]
117
+ self.instance_id = metadata["vmId"]
118
+ self.instance_name = metadata["name"]
119
+ self.project_name = metadata["resourceGroupName"]
120
+ self.availability_zone = metadata["zone"]
121
+ self.machine_type = metadata["vmSize"]
122
+ self.region = metadata["location"]
123
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
124
+ nil
125
+ end
126
+ end
127
+ end
128
+ end
@@ -38,7 +38,8 @@ module ElasticAPM
38
38
  action: nil,
39
39
  context: nil,
40
40
  stacktrace_builder: nil,
41
- sync: nil
41
+ sync: nil,
42
+ sample_rate: nil
42
43
  )
43
44
  @name = name
44
45
 
@@ -53,6 +54,7 @@ module ElasticAPM
53
54
  @transaction = transaction
54
55
  @parent = parent
55
56
  @trace_context = trace_context || parent.trace_context.child
57
+ @sample_rate = transaction.sample_rate
56
58
 
57
59
  @context = context || Span::Context.new(sync: sync)
58
60
  @stacktrace_builder = stacktrace_builder
@@ -73,6 +75,7 @@ module ElasticAPM
73
75
  :context,
74
76
  :duration,
75
77
  :parent,
78
+ :sample_rate,
76
79
  :self_time,
77
80
  :stacktrace,
78
81
  :timestamp,
@@ -41,10 +41,10 @@ module ElasticAPM
41
41
  job_name = name_from_payload(job.payload_object)
42
42
  transaction = ElasticAPM.start_transaction(job_name, TYPE)
43
43
  job.invoke_job_without_apm(*args, &block)
44
- transaction.done 'success'
44
+ transaction&.done 'success'
45
45
  rescue ::Exception => e
46
46
  ElasticAPM.report(e, handled: false)
47
- transaction.done 'error'
47
+ transaction&.done 'error'
48
48
  raise
49
49
  ensure
50
50
  ElasticAPM.end_transaction
@@ -53,6 +53,8 @@ module ElasticAPM
53
53
  def self.name_from_payload(payload_object)
54
54
  if payload_object.is_a?(::Delayed::PerformableMethod)
55
55
  performable_method_name(payload_object)
56
+ elsif payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'
57
+ payload_object.job_data['job_class']
56
58
  else
57
59
  payload_object.class.name
58
60
  end
@@ -27,7 +27,13 @@ module ElasticAPM
27
27
  SUBTYPE = 'elasticsearch'
28
28
 
29
29
  def self.sanitizer
30
- @sanitizer ||= ElasticAPM::Transport::Filters::HashSanitizer.new
30
+ @sanitizer ||=
31
+ begin
32
+ config = ElasticAPM.agent.config
33
+ ElasticAPM::Transport::Filters::HashSanitizer.new(
34
+ key_patterns: config.custom_key_filters + config.sanitize_field_names
35
+ )
36
+ end
31
37
  end
32
38
 
33
39
  def install
@@ -33,7 +33,7 @@ module ElasticAPM
33
33
  **legacy_traceparent_attrs
34
34
  )
35
35
  @traceparent = traceparent || Traceparent.new(**legacy_traceparent_attrs)
36
- @tracestate = tracestate
36
+ @tracestate = tracestate || Tracestate.new
37
37
  end
38
38
 
39
39
  attr_accessor :traceparent, :tracestate
@@ -33,8 +33,7 @@ module ElasticAPM
33
33
  trace_id: nil,
34
34
  span_id: nil,
35
35
  id: nil,
36
- recorded: true,
37
- tracestate: nil
36
+ recorded: true
38
37
  )
39
38
  @version = version
40
39
  @trace_id = trace_id || hex(TRACE_ID_LENGTH)
@@ -42,11 +41,10 @@ module ElasticAPM
42
41
  @parent_id = span_id
43
42
  @id = id || hex(ID_LENGTH)
44
43
  @recorded = recorded
45
- @tracestate = tracestate
46
44
  end
47
45
  # rubocop:enable Metrics/ParameterLists
48
46
 
49
- attr_accessor :version, :id, :trace_id, :parent_id, :recorded, :tracestate
47
+ attr_accessor :version, :id, :trace_id, :parent_id, :recorded
50
48
 
51
49
  alias :recorded? :recorded
52
50
 
@@ -17,26 +17,129 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
+ require 'elastic_apm/util/precision_validator'
21
+
20
22
  module ElasticAPM
21
23
  class TraceContext
22
24
  # @api private
23
25
  class Tracestate
24
- def initialize(values = [])
25
- @values = values
26
+ # @api private
27
+ class Entry
28
+ def initialize(key, value)
29
+ @key = key
30
+ @value = value
31
+ end
32
+
33
+ attr_reader :key, :value
34
+
35
+ def to_s
36
+ "#{key}=#{value}"
37
+ end
26
38
  end
27
39
 
28
- attr_accessor :values
40
+ class EsEntry
41
+ ASSIGN = ':'
42
+ SPLIT = ';'
43
+
44
+ SHORT_TO_LONG = { 's' => 'sample_rate' }
45
+ LONG_TO_SHORT = { 'sample_rate' => 's' }
46
+
47
+ def initialize(values = nil)
48
+ parse(values)
49
+ end
50
+
51
+ attr_reader :sample_rate
52
+
53
+ def key
54
+ 'es'
55
+ end
56
+
57
+ def value
58
+ LONG_TO_SHORT.map do |l, s|
59
+ "#{s}#{ASSIGN}#{send(l)}"
60
+ end.join(SPLIT)
61
+ end
62
+
63
+ def empty?
64
+ !sample_rate
65
+ end
66
+
67
+ def sample_rate=(val)
68
+ @sample_rate = Util::PrecisionValidator.validate(
69
+ val, precision: 4, minimum: 0.0001
70
+ )
71
+ end
72
+
73
+ def to_s
74
+ return nil if empty?
75
+
76
+ "es=#{value}"
77
+ end
78
+
79
+ private
80
+
81
+ def parse(values)
82
+ return unless values
83
+
84
+ values.split(SPLIT).map do |kv|
85
+ k, v = kv.split(ASSIGN)
86
+ next unless SHORT_TO_LONG.keys.include?(k)
87
+ send("#{SHORT_TO_LONG[k]}=", v)
88
+ end
89
+ end
90
+ end
91
+
92
+ extend Forwardable
93
+
94
+ def initialize(entries: {}, sample_rate: nil)
95
+ @entries = entries
96
+
97
+ self.sample_rate = sample_rate if sample_rate
98
+ end
99
+
100
+ attr_accessor :entries
101
+
102
+ def_delegators :es_entry, :sample_rate, :sample_rate=
29
103
 
30
104
  def self.parse(header)
31
- # HTTP allows multiple headers with the same name, eg. multiple
32
- # Set-Cookie headers per response.
33
- # Rack handles this by joining the headers under the same key, separated
34
- # by newlines, see https://www.rubydoc.info/github/rack/rack/file/SPEC
35
- new(String(header).split("\n"))
105
+ entries =
106
+ split_by_nl_and_comma(header)
107
+ .each_with_object({}) do |entry, hsh|
108
+ k, v = entry.split('=')
109
+
110
+ hsh[k] =
111
+ case k
112
+ when 'es' then EsEntry.new(v)
113
+ else Entry.new(k, v)
114
+ end
115
+ end
116
+
117
+ new(entries: entries)
36
118
  end
37
119
 
38
120
  def to_header
39
- values.join(',')
121
+ return "" unless entries.any?
122
+
123
+ entries.values.map(&:to_s).join(',')
124
+ end
125
+
126
+ private
127
+
128
+ def es_entry
129
+ # lazy generate this so we only add it if necessary
130
+ entries['es'] ||= EsEntry.new
131
+ end
132
+
133
+ class << self
134
+ private
135
+
136
+ def split_by_nl_and_comma(str)
137
+ # HTTP allows multiple headers with the same name, eg. multiple
138
+ # Set-Cookie headers per response.
139
+ # Rack handles this by joining the headers under the same key, separated
140
+ # by newlines, see https://www.rubydoc.info/github/rack/rack/file/SPEC
141
+ String(str).split("\n").map { |s| s.split(',') }.flatten
142
+ end
40
143
  end
41
144
  end
42
145
  end
@@ -34,6 +34,7 @@ module ElasticAPM
34
34
  name = nil,
35
35
  type = nil,
36
36
  sampled: true,
37
+ sample_rate: 1,
37
38
  context: nil,
38
39
  config:,
39
40
  trace_context: nil
@@ -52,13 +53,19 @@ module ElasticAPM
52
53
  @default_labels = config.default_labels
53
54
 
54
55
  @sampled = sampled
56
+ @sample_rate = sample_rate
55
57
 
56
58
  @context = context || Context.new # TODO: Lazy generate this?
57
59
  if @default_labels
58
60
  Util.reverse_merge!(@context.labels, @default_labels)
59
61
  end
60
62
 
61
- @trace_context = trace_context || TraceContext.new(recorded: sampled)
63
+ unless (@trace_context = trace_context)
64
+ @trace_context = TraceContext.new(
65
+ traceparent: TraceContext::Traceparent.new(recorded: sampled),
66
+ tracestate: TraceContext::Tracestate.new(sample_rate: sampled ? sample_rate : 0)
67
+ )
68
+ end
62
69
 
63
70
  @started_spans = 0
64
71
  @dropped_spans = 0
@@ -69,10 +76,24 @@ module ElasticAPM
69
76
 
70
77
  attr_accessor :name, :type, :result
71
78
 
72
- attr_reader :context, :duration, :started_spans, :dropped_spans,
73
- :timestamp, :trace_context, :notifications, :self_time,
74
- :span_frames_min_duration, :collect_metrics, :breakdown_metrics,
75
- :framework_name, :transaction_max_spans
79
+ attr_reader(
80
+ :breakdown_metrics,
81
+ :collect_metrics,
82
+ :context,
83
+ :dropped_spans,
84
+ :duration,
85
+ :framework_name,
86
+ :notifications,
87
+ :self_time,
88
+ :sample_rate,
89
+ :span_frames_min_duration,
90
+ :started_spans,
91
+ :timestamp,
92
+ :trace_context,
93
+ :transaction_max_spans
94
+ )
95
+
96
+ alias :collect_metrics? :collect_metrics
76
97
 
77
98
  def sampled?
78
99
  @sampled
@@ -47,6 +47,7 @@ module ElasticAPM
47
47
  end
48
48
 
49
49
  attr_reader :http
50
+
50
51
  def write(str)
51
52
  return false if @config.disable_send
52
53
 
@@ -23,29 +23,22 @@ module ElasticAPM
23
23
  class HashSanitizer
24
24
  FILTERED = '[FILTERED]'
25
25
 
26
- KEY_FILTERS = [
27
- /passw(or)?d/i,
28
- /auth/i,
29
- /^pw$/,
30
- /secret/i,
31
- /token/i,
32
- /api[-._]?key/i,
33
- /session[-._]?id/i,
34
- /(set[-_])?cookie/i
35
- ].freeze
26
+ # DEPRECATED: Remove these additions in next major version
27
+ LEGACY_KEY_FILTERS = [/cookie/i, /auth/i].freeze
36
28
 
29
+ # DEPRECATED: Remove this check in next major version
37
30
  VALUE_FILTERS = [
38
31
  # (probably) credit card number
39
32
  /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
40
33
  ].freeze
41
34
 
42
- attr_accessor :key_filters
43
-
44
- def initialize
45
- @key_filters = KEY_FILTERS
35
+ def initialize(key_patterns:)
36
+ @key_patterns = key_patterns + LEGACY_KEY_FILTERS
46
37
  end
47
38
 
48
- def strip_from!(obj, key_filters = KEY_FILTERS)
39
+ attr_accessor :key_patterns
40
+
41
+ def strip_from!(obj)
49
42
  return unless obj&.is_a?(Hash)
50
43
 
51
44
  obj.each do |k, v|
@@ -65,7 +58,7 @@ module ElasticAPM
65
58
  end
66
59
 
67
60
  def filter_key?(key)
68
- @key_filters.any? { |regex| regex.match(key) }
61
+ @key_patterns.any? { |regex| regex.match(key) }
69
62
  end
70
63
 
71
64
  def filter_value?(value)
@@ -26,20 +26,21 @@ module ElasticAPM
26
26
  class SecretsFilter
27
27
  def initialize(config)
28
28
  @config = config
29
- @sanitizer = HashSanitizer.new
30
- @sanitizer.key_filters += config.custom_key_filters +
31
- config.sanitize_field_names
29
+ @sanitizer =
30
+ HashSanitizer.new(
31
+ key_patterns: config.custom_key_filters + config.sanitize_field_names
32
+ )
32
33
  end
33
34
 
34
35
  def call(payload)
35
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
36
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
36
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
37
37
  @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :cookies)
38
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :env)
39
+ @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :headers)
38
40
  @sanitizer.strip_from! payload.dig(:transaction, :context, :response, :headers)
39
- @sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
40
41
  @sanitizer.strip_from! payload.dig(:error, :context, :request, :cookies)
42
+ @sanitizer.strip_from! payload.dig(:error, :context, :request, :headers)
41
43
  @sanitizer.strip_from! payload.dig(:error, :context, :response, :headers)
42
- @sanitizer.strip_from! payload.dig(:transaction, :context, :request, :body)
43
44
 
44
45
  payload
45
46
  end
@@ -45,18 +45,20 @@ module ElasticAPM
45
45
  def keyword_object(hash)
46
46
  return unless hash
47
47
 
48
- hash.tap do |h|
49
- h.each { |k, v| hash[k] = keyword_field(v) }
48
+ hash.each do |k, v|
49
+ hash[k] =
50
+ case v
51
+ when Hash then keyword_object(v)
52
+ else keyword_field(v)
53
+ end
50
54
  end
51
55
  end
52
56
 
53
57
  def mixed_object(hash)
54
58
  return unless hash
55
59
 
56
- hash.tap do |h|
57
- h.each do |k, v|
58
- hash[k] = v.is_a?(String) ? keyword_field(v) : v
59
- end
60
+ hash.each do |k, v|
61
+ hash[k] = v.is_a?(String) ? keyword_field(v) : v
60
62
  end
61
63
  end
62
64
  end
@@ -23,14 +23,19 @@ module ElasticAPM
23
23
  # @api private
24
24
  class MetadataSerializer < Serializer
25
25
  def build(metadata)
26
- {
27
- metadata: {
26
+ base =
27
+ {
28
28
  service: build_service(metadata.service),
29
29
  process: build_process(metadata.process),
30
30
  system: build_system(metadata.system),
31
31
  labels: build_labels(metadata.labels)
32
32
  }
33
- }
33
+
34
+ if (metadata.cloud.provider)
35
+ base[:cloud] = build_cloud(metadata.cloud)
36
+ end
37
+
38
+ { metadata: base }
34
39
  end
35
40
 
36
41
  private
@@ -83,6 +88,27 @@ module ElasticAPM
83
88
  }
84
89
  end
85
90
 
91
+ def build_cloud(cloud)
92
+ {
93
+ provider: cloud.provider,
94
+ account: {
95
+ id: keyword_field(cloud.account_id),
96
+ name: keyword_field(cloud.account_name),
97
+ },
98
+ availability_zone: keyword_field(cloud.availability_zone),
99
+ instance: {
100
+ id: keyword_field(cloud.instance_id),
101
+ name: keyword_field(cloud.instance_name),
102
+ },
103
+ machine: { type: keyword_field(cloud.machine_type) },
104
+ project: {
105
+ id: keyword_field(cloud.project_id),
106
+ name: keyword_field(cloud.project_name),
107
+ },
108
+ region: keyword_field(cloud.region)
109
+ }
110
+ end
111
+
86
112
  def build_labels(labels)
87
113
  keyword_object(labels)
88
114
  end
@@ -42,7 +42,8 @@ module ElasticAPM
42
42
  context: context_serializer.build(span.context),
43
43
  stacktrace: span.stacktrace.to_a,
44
44
  timestamp: span.timestamp,
45
- trace_id: span.trace_id
45
+ trace_id: span.trace_id,
46
+ sample_rate: span.sample_rate
46
47
  }
47
48
  }
48
49
  end
@@ -38,6 +38,7 @@ module ElasticAPM
38
38
  duration: ms(transaction.duration),
39
39
  timestamp: transaction.timestamp,
40
40
  sampled: transaction.sampled?,
41
+ sample_rate: transaction.sample_rate,
41
42
  context: context_serializer.build(transaction.context),
42
43
  span_count: {
43
44
  started: transaction.started_spans,
@@ -32,14 +32,14 @@ module ElasticAPM
32
32
  private
33
33
 
34
34
  def build(config)
35
- metadata = Metadata.new(config)
35
+ service = Metadata::ServiceInfo.new(config)
36
36
 
37
37
  [
38
38
  "elastic-apm-ruby/#{VERSION}",
39
39
  HTTP::Request::USER_AGENT,
40
40
  [
41
- metadata.service.runtime.name,
42
- metadata.service.runtime.version
41
+ service.runtime.name,
42
+ service.runtime.version
43
43
  ].join('/')
44
44
  ].join(' ')
45
45
  end
@@ -53,6 +53,7 @@ module ElasticAPM
53
53
  end
54
54
 
55
55
  attr_reader :queue, :filters, :name, :connection, :serializers
56
+
56
57
  def work_forever
57
58
  while (msg = queue.pop)
58
59
  case msg
@@ -46,6 +46,8 @@ module ElasticAPM
46
46
 
47
47
  def self.truncate(value, max_length: 1024)
48
48
  return unless value
49
+
50
+ value = String(value)
49
51
  return value if value.length <= max_length
50
52
 
51
53
  value[0...(max_length - 1)] + '…'
@@ -0,0 +1,46 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ # frozen_string_literal: true
19
+
20
+ module ElasticAPM
21
+ module Util
22
+ # @api private
23
+ # Rounds half away from zero.
24
+ # If `minimum` is provided, and the value rounds to 0 (but was not zero to
25
+ # begin with), use the minimum instead.
26
+ module PrecisionValidator
27
+ extend self
28
+
29
+ def validate(value, precision: 0, minimum: nil)
30
+ float = Float(value)
31
+ return nil unless (0.0..1.0).cover?(float)
32
+ return float if float == 0
33
+
34
+ multiplier = Float(10**precision)
35
+ rounded = (float * multiplier + 0.5).floor / multiplier
36
+ if rounded == 0 && minimum
37
+ minimum
38
+ else
39
+ rounded
40
+ end
41
+ rescue ArgumentError
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
@@ -18,5 +18,5 @@
18
18
  # frozen_string_literal: true
19
19
 
20
20
  module ElasticAPM
21
- VERSION = '3.10.1'
21
+ VERSION = '3.11.0'
22
22
  end
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: 3.10.1
4
+ version: 3.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-26 00:00:00.000000000 Z
11
+ date: 2020-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -128,6 +128,7 @@ files:
128
128
  - lib/elastic_apm/config/duration.rb
129
129
  - lib/elastic_apm/config/options.rb
130
130
  - lib/elastic_apm/config/regexp_list.rb
131
+ - lib/elastic_apm/config/round_float.rb
131
132
  - lib/elastic_apm/config/wildcard_pattern_list.rb
132
133
  - lib/elastic_apm/context.rb
133
134
  - lib/elastic_apm/context/request.rb
@@ -148,6 +149,7 @@ files:
148
149
  - lib/elastic_apm/internal_error.rb
149
150
  - lib/elastic_apm/logging.rb
150
151
  - lib/elastic_apm/metadata.rb
152
+ - lib/elastic_apm/metadata/cloud_info.rb
151
153
  - lib/elastic_apm/metadata/process_info.rb
152
154
  - lib/elastic_apm/metadata/service_info.rb
153
155
  - lib/elastic_apm/metadata/system_info.rb
@@ -235,6 +237,7 @@ files:
235
237
  - lib/elastic_apm/util.rb
236
238
  - lib/elastic_apm/util/inflector.rb
237
239
  - lib/elastic_apm/util/lru_cache.rb
240
+ - lib/elastic_apm/util/precision_validator.rb
238
241
  - lib/elastic_apm/util/throttle.rb
239
242
  - lib/elastic_apm/version.rb
240
243
  homepage: https://github.com/elastic/apm-agent-ruby
@@ -257,7 +260,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
260
  - !ruby/object:Gem::Version
258
261
  version: '0'
259
262
  requirements: []
260
- rubygems_version: 3.1.2
263
+ rubygems_version: 3.1.4
261
264
  signing_key:
262
265
  specification_version: 4
263
266
  summary: The official Elastic APM agent for Ruby