elastic-apm 3.15.1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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