elastic-apm 3.10.1 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/labeler-config.yml +3 -0
  3. data/.github/workflows/labeler.yml +16 -0
  4. data/.rubocop.yml +33 -4
  5. data/CHANGELOG.asciidoc +67 -0
  6. data/Gemfile +5 -6
  7. data/Rakefile +10 -10
  8. data/docs/api.asciidoc +2 -1
  9. data/docs/configuration.asciidoc +4 -3
  10. data/lib/elastic_apm.rb +14 -2
  11. data/lib/elastic_apm/central_config.rb +7 -2
  12. data/lib/elastic_apm/config.rb +51 -19
  13. data/lib/elastic_apm/config/log_level_map.rb +47 -0
  14. data/lib/elastic_apm/config/options.rb +2 -1
  15. data/lib/elastic_apm/config/round_float.rb +31 -0
  16. data/lib/elastic_apm/config/wildcard_pattern_list.rb +2 -0
  17. data/lib/elastic_apm/context.rb +2 -8
  18. data/lib/elastic_apm/graphql.rb +2 -0
  19. data/lib/elastic_apm/grpc.rb +3 -3
  20. data/lib/elastic_apm/instrumenter.rb +11 -4
  21. data/lib/elastic_apm/metadata.rb +3 -1
  22. data/lib/elastic_apm/metadata/cloud_info.rb +130 -0
  23. data/lib/elastic_apm/metadata/service_info.rb +2 -2
  24. data/lib/elastic_apm/metadata/system_info/container_info.rb +16 -5
  25. data/lib/elastic_apm/metrics.rb +1 -0
  26. data/lib/elastic_apm/metrics/cpu_mem_set.rb +1 -0
  27. data/lib/elastic_apm/middleware.rb +8 -3
  28. data/lib/elastic_apm/opentracing.rb +2 -1
  29. data/lib/elastic_apm/span.rb +14 -0
  30. data/lib/elastic_apm/span/context/db.rb +1 -1
  31. data/lib/elastic_apm/spies/delayed_job.rb +8 -2
  32. data/lib/elastic_apm/spies/dynamo_db.rb +8 -1
  33. data/lib/elastic_apm/spies/elasticsearch.rb +10 -2
  34. data/lib/elastic_apm/spies/faraday.rb +5 -2
  35. data/lib/elastic_apm/spies/http.rb +1 -0
  36. data/lib/elastic_apm/spies/mongo.rb +6 -2
  37. data/lib/elastic_apm/spies/net_http.rb +3 -0
  38. data/lib/elastic_apm/spies/rake.rb +4 -2
  39. data/lib/elastic_apm/spies/resque.rb +4 -2
  40. data/lib/elastic_apm/spies/sequel.rb +12 -1
  41. data/lib/elastic_apm/spies/shoryuken.rb +2 -0
  42. data/lib/elastic_apm/spies/sidekiq.rb +2 -0
  43. data/lib/elastic_apm/spies/sneakers.rb +2 -0
  44. data/lib/elastic_apm/spies/sucker_punch.rb +2 -0
  45. data/lib/elastic_apm/sql/signature.rb +4 -2
  46. data/lib/elastic_apm/sql/tokenizer.rb +2 -2
  47. data/lib/elastic_apm/stacktrace/frame.rb +1 -0
  48. data/lib/elastic_apm/stacktrace_builder.rb +2 -4
  49. data/lib/elastic_apm/trace_context.rb +1 -3
  50. data/lib/elastic_apm/trace_context/traceparent.rb +2 -6
  51. data/lib/elastic_apm/trace_context/tracestate.rb +114 -9
  52. data/lib/elastic_apm/transaction.rb +41 -7
  53. data/lib/elastic_apm/transport/connection.rb +2 -1
  54. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +16 -16
  55. data/lib/elastic_apm/transport/filters/secrets_filter.rb +35 -12
  56. data/lib/elastic_apm/transport/serializers.rb +9 -6
  57. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +44 -4
  58. data/lib/elastic_apm/transport/serializers/span_serializer.rb +3 -3
  59. data/lib/elastic_apm/transport/serializers/transaction_serializer.rb +2 -0
  60. data/lib/elastic_apm/transport/user_agent.rb +3 -3
  61. data/lib/elastic_apm/transport/worker.rb +1 -0
  62. data/lib/elastic_apm/util.rb +2 -0
  63. data/lib/elastic_apm/util/deep_dup.rb +65 -0
  64. data/lib/elastic_apm/util/precision_validator.rb +46 -0
  65. data/lib/elastic_apm/version.rb +1 -1
  66. metadata +10 -3
@@ -0,0 +1,47 @@
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
+ class Config
22
+ # @api private
23
+ class LogLevelMap
24
+ LEVELS = {
25
+ debug: Logger::DEBUG,
26
+ info: Logger::INFO,
27
+ warn: Logger::WARN,
28
+ error: Logger::ERROR,
29
+ fatal: Logger::FATAL,
30
+ trace: Logger::DEBUG,
31
+ warning: Logger::WARN,
32
+ critical: Logger::FATAL,
33
+ off: Logger::FATAL
34
+ }.freeze
35
+
36
+ DEFAULT = Logger::INFO
37
+
38
+ def call(value)
39
+ if value.is_a?(Integer)
40
+ LEVELS.value?(value) ? value : DEFAULT
41
+ else
42
+ LEVELS.fetch(value.to_sym, DEFAULT)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ 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
@@ -36,14 +36,9 @@ module ElasticAPM
36
36
  Service = Struct.new(:framework)
37
37
  Framework = Struct.new(:name, :version)
38
38
 
39
- attr_accessor :request
40
- attr_accessor :response
41
- attr_accessor :user
42
- attr_reader :custom
43
- attr_reader :labels
44
- attr_reader :service
39
+ attr_accessor :request, :response, :user
40
+ attr_reader :custom, :labels, :service
45
41
 
46
- # rubocop:disable Metrics/CyclomaticComplexity
47
42
  def empty?
48
43
  return false if labels.any?
49
44
  return false if custom.any?
@@ -53,7 +48,6 @@ module ElasticAPM
53
48
 
54
49
  true
55
50
  end
56
- # rubocop:enable Metrics/CyclomaticComplexity
57
51
 
58
52
  def set_service(framework_name: nil, framework_version: nil)
59
53
  @service = Service.new(
@@ -48,6 +48,7 @@ module ElasticAPM
48
48
  # "authorized" => "graphql.authorized",
49
49
  }.freeze
50
50
 
51
+ # rubocop:disable Style/ExplicitBlockArgument
51
52
  def self.trace(key, data)
52
53
  return yield unless KEYS_TO_NAME.key?(key)
53
54
  return yield unless (transaction = ElasticAPM.current_transaction)
@@ -69,6 +70,7 @@ module ElasticAPM
69
70
 
70
71
  results
71
72
  end
73
+ # rubocop:enable Style/ExplicitBlockArgument
72
74
 
73
75
  class << self
74
76
  private
@@ -25,7 +25,7 @@ module ElasticAPM
25
25
  TYPE = 'external'
26
26
  SUBTYPE = 'grpc'
27
27
 
28
- # rubocop:disable Lint/UnusedMethodArgument
28
+ # rubocop:disable Lint/UnusedMethodArgument, Style/ExplicitBlockArgument
29
29
  def request_response(request:, call:, method:, metadata:)
30
30
  return yield unless (transaction = ElasticAPM.current_transaction)
31
31
  if (trace_context = transaction.trace_context)
@@ -40,7 +40,7 @@ module ElasticAPM
40
40
  yield
41
41
  end
42
42
  end
43
- # rubocop:enable Lint/UnusedMethodArgument
43
+ # rubocop:enable Lint/UnusedMethodArgument, Style/ExplicitBlockArgument
44
44
 
45
45
  private
46
46
 
@@ -71,7 +71,7 @@ module ElasticAPM
71
71
  transaction.done 'success'
72
72
  rescue ::Exception => e
73
73
  ElasticAPM.report(e, handled: false)
74
- transaction.done 'error' if transaction
74
+ transaction&.done 'error'
75
75
  raise
76
76
  ensure
77
77
  ElasticAPM.end_transaction
@@ -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
 
@@ -232,7 +239,7 @@ module ElasticAPM
232
239
  def set_label(key, value)
233
240
  return unless current_transaction
234
241
 
235
- key = key.to_s.gsub(/[\."\*]/, '_').to_sym
242
+ key = key.to_s.gsub(/[."*]/, '_').to_sym
236
243
  current_transaction.context.labels[key] = value
237
244
  end
238
245
 
@@ -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,130 @@
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(connect: 0.1, read: 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
+ # rubocop:disable Metrics/CyclomaticComplexity
53
+ def fetch!
54
+ case config.cloud_provider
55
+ when "aws"
56
+ fetch_aws
57
+ when "gcp"
58
+ fetch_gcp
59
+ when "azure"
60
+ fetch_azure
61
+ when "auto"
62
+ fetch_aws || fetch_gcp || fetch_azure
63
+ when "none"
64
+ nil
65
+ else
66
+ error("Unknown setting for cloud_provider '#{config.cloud_provider}'")
67
+ end
68
+
69
+ self
70
+ end
71
+ # rubocop:enable Metrics/CyclomaticComplexity
72
+
73
+ private
74
+
75
+ def fetch_aws
76
+ resp = @client.get(AWS_URI)
77
+
78
+ return unless resp.status == 200
79
+ return unless (metadata = JSON.parse(resp.body))
80
+
81
+ self.provider = "aws"
82
+ self.account_id = metadata["accountId"]
83
+ self.instance_id = metadata["instanceId"]
84
+ self.availability_zone = metadata["availabilityZone"]
85
+ self.machine_type = metadata["instanceType"]
86
+ self.region = metadata["region"]
87
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
88
+ nil
89
+ end
90
+
91
+ def fetch_gcp
92
+ resp = @client.headers("Metadata-Flavor" => "Google").get(GCP_URI)
93
+
94
+ return unless resp.status == 200
95
+ return unless (metadata = JSON.parse(resp.body))
96
+
97
+ zone = metadata["instance"]["zone"]&.split("/")&.at(-1)
98
+
99
+ self.provider = "gcp"
100
+ self.instance_id = metadata["instance"]["id"]
101
+ self.instance_name = metadata["instance"]["name"]
102
+ self.project_id = metadata["project"]["numericProjectId"]
103
+ self.project_name = metadata["project"]["projectId"]
104
+ self.availability_zone = zone
105
+ self.region = zone.split("-")[0..-2].join("-")
106
+ self.machine_type = metadata["instance"]["machineType"]
107
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
108
+ nil
109
+ end
110
+
111
+ def fetch_azure
112
+ resp = @client.headers("Metadata" => "true").get(AZURE_URI)
113
+
114
+ return unless resp.status == 200
115
+ return unless (metadata = JSON.parse(resp.body))
116
+
117
+ self.provider = 'azure'
118
+ self.account_id = metadata["subscriptionId"]
119
+ self.instance_id = metadata["vmId"]
120
+ self.instance_name = metadata["name"]
121
+ self.project_name = metadata["resourceGroupName"]
122
+ self.availability_zone = metadata["zone"]
123
+ self.machine_type = metadata["vmSize"]
124
+ self.region = metadata["location"]
125
+ rescue HTTP::TimeoutError, HTTP::ConnectionError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
@@ -52,8 +52,8 @@ module ElasticAPM
52
52
  @version = @config.service_version || Util.git_sha
53
53
  end
54
54
 
55
- attr_reader :name, :node_name, :environment, :agent, :framework, :language,
56
- :runtime, :version
55
+ attr_reader :name, :node_name, :environment, :agent, :framework,
56
+ :language, :runtime, :version
57
57
 
58
58
  private
59
59
 
@@ -81,15 +81,23 @@ module ElasticAPM
81
81
  ENV.fetch('KUBERNETES_POD_UID', kubernetes_pod_uid)
82
82
  end
83
83
 
84
+ # rubocop:disable Style/RegexpLiteral
84
85
  CONTAINER_ID_REGEXES = [
85
86
  %r{^[[:xdigit:]]{64}$},
86
- %r{^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4,}$}
87
- ]
87
+ %r{
88
+ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]
89
+ {4}-[[:xdigit:]]{4}-[[:xdigit:]]{4,}$
90
+ }x
91
+ ].freeze
88
92
  KUBEPODS_REGEXES = [
89
93
  %r{(?:^/kubepods[^\s]*/pod([^/]+)$)},
90
- %r{(?:^/kubepods\.slice/kubepods-[^/]+\.slice/kubepods-[^/]+-pod([^/]+)\.slice$)}
91
- ]
94
+ %r{
95
+ (?:^/kubepods\.slice/(kubepods-[^/]+\.slice/)?
96
+ kubepods[^/]*-pod([^/]+)\.slice$)
97
+ }x
98
+ ].freeze
92
99
  SYSTEMD_SCOPE_SUFFIX = '.scope'
100
+ # rubocop:enable Style/RegexpLiteral
93
101
 
94
102
  # rubocop:disable Metrics/PerceivedComplexity
95
103
  # rubocop:disable Metrics/CyclomaticComplexity
@@ -125,7 +133,10 @@ module ElasticAPM
125
133
  end
126
134
 
127
135
  if (kubepods_match = match_kubepods(directory))
128
- pod_id = kubepods_match[1] || kubepods_match[2]
136
+ unless (pod_id = kubepods_match[1])
137
+ pod_id = kubepods_match[2]
138
+ pod_id&.tr!('_', '-')
139
+ end
129
140
 
130
141
  self.container_id = container_id
131
142
  self.kubernetes_pod_uid = pod_id
@@ -40,6 +40,7 @@ module ElasticAPM
40
40
  end
41
41
 
42
42
  attr_reader :config, :sets, :callback
43
+
43
44
  def start
44
45
  unless config.collect_metrics?
45
46
  debug 'Skipping metrics'
@@ -214,6 +214,7 @@ module ElasticAPM
214
214
  # @api private
215
215
  class Meminfo
216
216
  attr_reader :total, :available, :page_size
217
+
217
218
  # rubocop:disable Metrics/PerceivedComplexity
218
219
  # rubocop:disable Metrics/CyclomaticComplexity
219
220
  def read!
@@ -41,9 +41,14 @@ module ElasticAPM
41
41
  ElasticAPM.report(e, context: context, handled: false)
42
42
  raise
43
43
  ensure
44
- if resp && transaction
45
- status, headers, _body = resp
46
- transaction.add_response(status, headers: headers.dup)
44
+ if transaction
45
+ if resp
46
+ status, headers, _body = resp
47
+ transaction.add_response(status, headers: headers.dup)
48
+ transaction&.outcome = Transaction::Outcome.from_http_status(status)
49
+ else
50
+ transaction&.outcome = Transaction::Outcome::FAILURE
51
+ end
47
52
  end
48
53
 
49
54
  ElasticAPM.end_transaction http_result(status)