elastic-apm 3.15.1 → 4.2.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/.jenkins_exclude.yml +13 -0
  3. data/.ci/.jenkins_ruby.yml +0 -1
  4. data/.ci/Jenkinsfile +1 -0
  5. data/.github/dependabot.yml +16 -0
  6. data/.rubocop.yml +0 -7
  7. data/CHANGELOG.asciidoc +77 -4
  8. data/Gemfile +4 -1
  9. data/README.md +12 -0
  10. data/SECURITY.md +7 -0
  11. data/bench/report.rb +1 -1
  12. data/bin/run-bdd +17 -0
  13. data/bin/run-tests +1 -1
  14. data/docker-compose.yml +1 -1
  15. data/docs/configuration.asciidoc +62 -147
  16. data/docs/release-notes.asciidoc +1 -0
  17. data/docs/upgrading.asciidoc +0 -27
  18. data/lib/elastic_apm.rb +12 -1
  19. data/lib/elastic_apm/agent.rb +7 -3
  20. data/lib/elastic_apm/central_config.rb +8 -7
  21. data/lib/elastic_apm/config.rb +2 -88
  22. data/lib/elastic_apm/config/regexp_list.rb +1 -1
  23. data/lib/elastic_apm/config/wildcard_pattern_list.rb +1 -1
  24. data/lib/elastic_apm/context/response.rb +1 -3
  25. data/lib/elastic_apm/fields.rb +88 -0
  26. data/lib/elastic_apm/grpc.rb +2 -4
  27. data/lib/elastic_apm/instrumenter.rb +1 -1
  28. data/lib/elastic_apm/metadata/cloud_info.rb +32 -5
  29. data/lib/elastic_apm/metadata/system_info.rb +14 -4
  30. data/lib/elastic_apm/metrics/cpu_mem_set.rb +1 -1
  31. data/lib/elastic_apm/normalizers.rb +2 -2
  32. data/lib/elastic_apm/normalizers/rails/active_record.rb +3 -3
  33. data/lib/elastic_apm/opentracing.rb +3 -2
  34. data/lib/elastic_apm/span.rb +26 -1
  35. data/lib/elastic_apm/span/context.rb +2 -1
  36. data/lib/elastic_apm/span/context/destination.rb +53 -40
  37. data/lib/elastic_apm/span_helpers.rb +6 -8
  38. data/lib/elastic_apm/spies.rb +20 -0
  39. data/lib/elastic_apm/spies/action_dispatch.rb +10 -9
  40. data/lib/elastic_apm/spies/azure_storage_table.rb +148 -0
  41. data/lib/elastic_apm/spies/dynamo_db.rb +12 -12
  42. data/lib/elastic_apm/spies/elasticsearch.rb +32 -29
  43. data/lib/elastic_apm/spies/faraday.rb +83 -63
  44. data/lib/elastic_apm/spies/http.rb +33 -34
  45. data/lib/elastic_apm/spies/mongo.rb +5 -3
  46. data/lib/elastic_apm/spies/net_http.rb +59 -56
  47. data/lib/elastic_apm/spies/rake.rb +28 -26
  48. data/lib/elastic_apm/spies/redis.rb +11 -10
  49. data/lib/elastic_apm/spies/resque.rb +18 -21
  50. data/lib/elastic_apm/spies/s3.rb +14 -15
  51. data/lib/elastic_apm/spies/sequel.rb +42 -48
  52. data/lib/elastic_apm/spies/sidekiq.rb +13 -15
  53. data/lib/elastic_apm/spies/sinatra.rb +20 -21
  54. data/lib/elastic_apm/spies/sns.rb +39 -44
  55. data/lib/elastic_apm/spies/sqs.rb +21 -31
  56. data/lib/elastic_apm/spies/tilt.rb +10 -9
  57. data/lib/elastic_apm/sql/tokenizer.rb +21 -5
  58. data/lib/elastic_apm/stacktrace_builder.rb +3 -1
  59. data/lib/elastic_apm/subscriber.rb +1 -0
  60. data/lib/elastic_apm/trace_context.rb +5 -13
  61. data/lib/elastic_apm/trace_context/traceparent.rb +5 -6
  62. data/lib/elastic_apm/transport/connection.rb +1 -1
  63. data/lib/elastic_apm/transport/connection/http.rb +4 -2
  64. data/lib/elastic_apm/transport/connection/proxy_pipe.rb +1 -2
  65. data/lib/elastic_apm/transport/filters/hash_sanitizer.rb +5 -23
  66. data/lib/elastic_apm/transport/serializers/metadata_serializer.rb +3 -2
  67. data/lib/elastic_apm/transport/serializers/metricset_serializer.rb +2 -2
  68. data/lib/elastic_apm/transport/serializers/span_serializer.rb +12 -12
  69. data/lib/elastic_apm/transport/user_agent.rb +3 -2
  70. data/lib/elastic_apm/transport/worker.rb +1 -1
  71. data/lib/elastic_apm/util/deep_dup.rb +1 -1
  72. data/lib/elastic_apm/version.rb +1 -1
  73. metadata +12 -9
  74. data/lib/elastic_apm/sql.rb +0 -36
  75. data/lib/elastic_apm/sql_summarizer.rb +0 -53
@@ -24,7 +24,8 @@ module ElasticAPM
24
24
  def initialize(config)
25
25
  @config = config
26
26
 
27
- @hostname = @config.hostname || self.class.system_hostname
27
+ @configured_hostname = @config.hostname
28
+ @detected_hostname = detect_hostname
28
29
  @architecture = gem_platform.cpu
29
30
  @platform = gem_platform.os
30
31
 
@@ -33,14 +34,23 @@ module ElasticAPM
33
34
  @kubernetes = container_info.kubernetes
34
35
  end
35
36
 
36
- attr_reader :hostname, :architecture, :platform, :container, :kubernetes
37
+ attr_reader(
38
+ :detected_hostname,
39
+ :configured_hostname,
40
+ :architecture,
41
+ :platform,
42
+ :container,
43
+ :kubernetes
44
+ )
37
45
 
38
46
  def gem_platform
39
47
  @gem_platform ||= Gem::Platform.local
40
48
  end
41
49
 
42
- def self.system_hostname
43
- @system_hostname ||= `hostname`.chomp
50
+ private
51
+
52
+ def detect_hostname
53
+ `hostname`.chomp
44
54
  end
45
55
  end
46
56
  end
@@ -79,7 +79,7 @@ module ElasticAPM
79
79
  case platform
80
80
  when :linux then Linux.new
81
81
  else
82
- warn "Unsupported platform '#{platform}' - Disabling system metrics"
82
+ warn "Disabling system metrics, unsupported platform '#{platform}'"
83
83
  disable!
84
84
  nil
85
85
  end
@@ -39,8 +39,8 @@ module ElasticAPM # :nodoc:
39
39
  end
40
40
 
41
41
  def self.build(config)
42
- normalizers = @registered.each_with_object({}) do |(name, klass), built|
43
- built[name] = klass.new(config)
42
+ normalizers = @registered.transform_values do |klass|
43
+ klass.new(config)
44
44
  end
45
45
 
46
46
  Collection.new(normalizers)
@@ -17,7 +17,7 @@
17
17
 
18
18
  # frozen_string_literal: true
19
19
 
20
- require 'elastic_apm/sql'
20
+ require 'elastic_apm/sql/signature'
21
21
 
22
22
  module ElasticAPM
23
23
  module Normalizers
@@ -34,7 +34,7 @@ module ElasticAPM
34
34
  def initialize(*args)
35
35
  super
36
36
 
37
- @summarizer = Sql.summarizer
37
+ @summarizer = Sql::Signature::Summarizer.new
38
38
 
39
39
  @adapters = {}
40
40
  end
@@ -48,7 +48,7 @@ module ElasticAPM
48
48
  context =
49
49
  Span::Context.new(
50
50
  db: { statement: payload[:sql], type: 'sql' },
51
- destination: { name: subtype, resource: subtype, type: TYPE }
51
+ destination: { service: { name: subtype, resource: subtype, type: TYPE } }
52
52
  )
53
53
 
54
54
  [name, TYPE, subtype, ACTION, context]
@@ -129,8 +129,9 @@ module ElasticAPM
129
129
  def self.from_header(header)
130
130
  return unless header
131
131
 
132
- trace_context = ElasticAPM::TraceContext.parse(header)
133
- return unless trace_context
132
+ trace_context = TraceContext.new(
133
+ traceparent: TraceContext::Traceparent.parse(header)
134
+ )
134
135
 
135
136
  trace_context.traceparent.id = trace_context.parent_id
136
137
  trace_context.traceparent.parent_id = nil
@@ -106,6 +106,18 @@ module ElasticAPM
106
106
  @duration ||= (clock_end - @clock_start)
107
107
  @parent.child_stopped
108
108
  @self_time = @duration - child_durations.duration
109
+
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
+
109
121
  self
110
122
  end
111
123
 
@@ -131,12 +143,21 @@ module ElasticAPM
131
143
  started? && !stopped?
132
144
  end
133
145
 
134
- # relations
146
+ def set_destination(address: nil, port: nil, service: nil, cloud: nil)
147
+ context.destination = Span::Context::Destination.new(
148
+ address: address,
149
+ port: port,
150
+ service: service,
151
+ cloud: cloud
152
+ )
153
+ end
135
154
 
136
155
  def inspect
137
156
  "<ElasticAPM::Span id:#{trace_context&.id}" \
138
157
  " name:#{name.inspect}" \
139
158
  " type:#{type.inspect}" \
159
+ " subtype:#{subtype.inspect}" \
160
+ " action:#{action.inspect}" \
140
161
  '>'
141
162
  end
142
163
 
@@ -159,5 +180,9 @@ module ElasticAPM
159
180
 
160
181
  duration >= min_duration
161
182
  end
183
+
184
+ def exit_span?
185
+ context.destination || context.db || context.message || context.http
186
+ end
162
187
  end
163
188
  end
@@ -47,12 +47,13 @@ module ElasticAPM
47
47
 
48
48
  attr_reader(
49
49
  :db,
50
- :destination,
51
50
  :http,
52
51
  :labels,
53
52
  :sync,
54
53
  :message
55
54
  )
55
+
56
+ attr_accessor :destination
56
57
  end
57
58
  end
58
59
  end
@@ -14,7 +14,7 @@
14
14
  # KIND, either express or implied. See the License for the
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
-
17
+ #
18
18
  # frozen_string_literal: true
19
19
 
20
20
  module ElasticAPM
@@ -22,60 +22,53 @@ module ElasticAPM
22
22
  class Context
23
23
  # @api private
24
24
  class Destination
25
+ include Fields
26
+
27
+ field :address
28
+ field :port
29
+ field :service
30
+ field :cloud
25
31
 
26
32
  # @api private
27
- class Cloud
28
- def initialize(region: nil)
29
- @region = region
30
- end
33
+ class Service
34
+ include Fields
31
35
 
32
- attr_accessor :region
36
+ field :name
37
+ field :type
38
+ field :resource
33
39
  end
34
40
 
35
- def initialize(
36
- name: nil,
37
- resource: nil,
38
- type: nil,
39
- address: nil,
40
- port: nil,
41
- cloud: nil
42
- )
43
- @name = name
44
- @resource = resource
45
- @type = type
46
- @address = address
47
- @port = port
48
- @cloud = cloud
41
+ # @api private
42
+ class Cloud
43
+ include Fields
44
+
45
+ field :region
49
46
  end
50
47
 
51
- attr_reader(
52
- :name,
53
- :resource,
54
- :type,
55
- :address,
56
- :port,
57
- :cloud
58
- )
48
+ def initialize(service: nil, cloud: nil, **attrs)
49
+ super(**attrs)
50
+
51
+ self.service = build_service(service)
52
+ self.cloud = build_cloud(cloud)
53
+ end
59
54
 
60
- def self.from_uri(uri_or_str, type: 'external', port: nil)
55
+ def self.from_uri(uri_or_str, type: nil, **attrs)
61
56
  uri = normalize(uri_or_str)
62
57
 
58
+ service =
59
+ case type
60
+ when 'http' then http_service(uri)
61
+ else nil
62
+ end
63
+
63
64
  new(
64
- name: only_scheme_and_host(uri),
65
- resource: "#{uri.host}:#{uri.port}",
66
- type: type,
67
65
  address: uri.hostname,
68
- port: port || uri.port
66
+ port: uri.port,
67
+ service: service,
68
+ **attrs
69
69
  )
70
70
  end
71
71
 
72
- def self.only_scheme_and_host(uri_or_str)
73
- uri = normalize(uri_or_str)
74
- uri.path = ''
75
- uri.password = uri.query = uri.fragment = nil
76
- uri.to_s
77
- end
78
-
79
72
  class << self
80
73
  private
81
74
 
@@ -83,6 +76,26 @@ module ElasticAPM
83
76
  return uri_or_str.dup if uri_or_str.is_a?(URI)
84
77
  URI(uri_or_str)
85
78
  end
79
+
80
+ def http_service(uri)
81
+ Service.new(resource: "#{uri.host}:#{uri.port}")
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def build_cloud(cloud = nil)
88
+ return Cloud.new unless cloud
89
+ return cloud if cloud.is_a?(Cloud)
90
+
91
+ Cloud.new(**cloud)
92
+ end
93
+
94
+ def build_service(service = nil)
95
+ return Service.new unless service
96
+ return service if service.is_a?(Service)
97
+
98
+ Service.new(**service)
86
99
  end
87
100
  end
88
101
  end
@@ -36,19 +36,17 @@ module ElasticAPM
36
36
  name ||= method.to_s
37
37
  type ||= Span::DEFAULT_TYPE
38
38
 
39
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
- alias :"__without_apm_#{method}" :"#{method}"
41
-
42
- def #{method}(*args, &block)
39
+ klass.prepend(Module.new do
40
+ define_method(method) do |*args, &block|
43
41
  unless ElasticAPM.current_transaction
44
- return __without_apm_#{method}(*args, &block)
42
+ return super(*args, &block)
45
43
  end
46
44
 
47
- ElasticAPM.with_span "#{name}", "#{type}" do
48
- __without_apm_#{method}(*args, &block)
45
+ ElasticAPM.with_span name.to_s, type.to_s do
46
+ super(*args, &block)
49
47
  end
50
48
  end
51
- RUBY
49
+ end)
52
50
  end
53
51
  end
54
52
 
@@ -56,6 +56,26 @@ module ElasticAPM
56
56
  end
57
57
  end
58
58
 
59
+ def self.without_faraday
60
+ return yield unless defined?(FaradaySpy)
61
+
62
+ # rubocop:disable Style/ExplicitBlockArgument
63
+ ElasticAPM::Spies::FaradaySpy.disable_in do
64
+ yield
65
+ end
66
+ # rubocop:enable Style/ExplicitBlockArgument
67
+ end
68
+
69
+ def self.without_net_http
70
+ return yield unless defined?(NetHTTPSpy)
71
+
72
+ # rubocop:disable Style/ExplicitBlockArgument
73
+ ElasticAPM::Spies::NetHTTPSpy.disable_in do
74
+ yield
75
+ end
76
+ # rubocop:enable Style/ExplicitBlockArgument
77
+ end
78
+
59
79
  def self.register_require_hook(registration)
60
80
  registration.require_paths.each do |path|
61
81
  require_hooks[path] = registration
@@ -22,18 +22,19 @@ module ElasticAPM
22
22
  module Spies
23
23
  # @api private
24
24
  class ActionDispatchSpy
25
- def install
26
- ::ActionDispatch::ShowExceptions.class_eval do
27
- alias render_exception_without_apm render_exception
28
-
29
- def render_exception(env, exception)
30
- context = ElasticAPM.build_context(rack_env: env, for_type: :error)
31
- ElasticAPM.report(exception, context: context, handled: false)
25
+ # @api private
26
+ module Ext
27
+ def render_exception(env, exception)
28
+ context = ElasticAPM.build_context(rack_env: env, for_type: :error)
29
+ ElasticAPM.report(exception, context: context, handled: false)
32
30
 
33
- render_exception_without_apm env, exception
34
- end
31
+ super(env, exception)
35
32
  end
36
33
  end
34
+
35
+ def install
36
+ ::ActionDispatch::ShowExceptions.prepend(Ext)
37
+ end
37
38
  end
38
39
 
39
40
  register(
@@ -0,0 +1,148 @@
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
+ # @api private
22
+ module Spies
23
+ # @api private
24
+ class AzureStorageTableSpy
25
+ TYPE = "storage"
26
+ SUBTYPE = "azuretable"
27
+
28
+ module Helpers
29
+ class << self
30
+ def instrument(operation_name, table_name = nil, service:)
31
+ span_name = span_name(operation_name, table_name)
32
+ action = formatted_op_name(operation_name)
33
+ account_name = account_name_from_storage_table_host(service.storage_service_host[:primary])
34
+
35
+ destination = ElasticAPM::Span::Context::Destination.from_uri(service.storage_service_host[:primary])
36
+ destination.service.resource = "#{SUBTYPE}/#{account_name}"
37
+
38
+ context = ElasticAPM::Span::Context.new(destination: destination)
39
+
40
+ ElasticAPM.with_span(span_name, TYPE, subtype: SUBTYPE, action: action, context: context) do
41
+ ElasticAPM::Spies.without_faraday do
42
+ ElasticAPM::Spies.without_net_http do
43
+ yield
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ DEFAULT_OP_NAMES = {
52
+ "create_table" => "Create",
53
+ "delete_table" => "Delete",
54
+ "get_table_acl" => "GetAcl",
55
+ "set_table_acl" => "SetAcl",
56
+ "insert_entity" => "Insert",
57
+ "query_entities" => "Query",
58
+ "update_entity" => "Update",
59
+ "merge_entity" => "Merge",
60
+ "delete_entity" => "Delete"
61
+ }.freeze
62
+
63
+ def formatted_op_names
64
+ @formatted_op_names ||= Concurrent::Map.new
65
+ end
66
+
67
+ def account_names
68
+ @account_names ||= Concurrent::Map.new
69
+ end
70
+
71
+ def span_name(operation_name, table_name = nil)
72
+ base = "AzureTable #{formatted_op_name(operation_name)}"
73
+
74
+ return base unless table_name
75
+
76
+ "#{base} #{table_name}"
77
+ end
78
+
79
+ def formatted_op_name(operation_name)
80
+ formatted_op_names.compute_if_absent(operation_name) do
81
+ DEFAULT_OP_NAMES.fetch(operation_name) do
82
+ operation_name.to_s.split("_").collect(&:capitalize).join
83
+ end
84
+ end
85
+ end
86
+
87
+ def account_name_from_storage_table_host(host)
88
+ account_names.compute_if_absent(host) do
89
+ URI(host).host.split(".").first || "unknown"
90
+ end
91
+ rescue Exception
92
+ "unknown"
93
+ end
94
+ end
95
+ end
96
+
97
+ # @api private
98
+ module Ext
99
+ # Methods with table_name as first parameter
100
+ %i[
101
+ create_table
102
+ delete_table
103
+ get_table
104
+ get_table_acl
105
+ set_table_acl
106
+ insert_entity
107
+ query_entities
108
+ update_entity
109
+ merge_entity
110
+ delete_entity
111
+ ].each do |method_name|
112
+ define_method(method_name) do |table_name, *args|
113
+ unless (transaction = ElasticAPM.current_transaction)
114
+ return super(table_name, *args)
115
+ end
116
+
117
+ ElasticAPM::Spies::AzureStorageTableSpy::Helpers.instrument(
118
+ method_name.to_s, table_name, service: self
119
+ ) do
120
+ super(table_name, *args)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Methods WITHOUT table_name as first parameter
126
+ def query_tables(*args)
127
+ unless (transaction = ElasticAPM.current_transaction)
128
+ return super(*args)
129
+ end
130
+
131
+ ElasticAPM::Spies::AzureStorageTableSpy::Helpers.instrument("query_tables", service: self) do
132
+ super(*args)
133
+ end
134
+ end
135
+ end
136
+
137
+ def install
138
+ ::Azure::Storage::Table::TableService.prepend(Ext)
139
+ end
140
+ end
141
+
142
+ register(
143
+ "Azure::Storage::Table::TableService",
144
+ "azure/storage/table",
145
+ AzureStorageTableSpy.new
146
+ )
147
+ end
148
+ end