elastic-apm 3.3.0 → 3.4.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/.jenkins_exclude.yml +4 -4
  3. data/.ci/.jenkins_ruby.yml +1 -1
  4. data/.ci/Jenkinsfile +5 -3
  5. data/.ci/jobs/apm-agent-ruby-downstream.yml +1 -0
  6. data/.ci/jobs/apm-agent-ruby-linting-mbp.yml +1 -0
  7. data/.ci/jobs/apm-agent-ruby-mbp.yml +1 -0
  8. data/.ci/prepare-git-context.sh +5 -2
  9. data/.github/ISSUE_TEMPLATE/Bug_report.md +38 -0
  10. data/.github/ISSUE_TEMPLATE/Feature_request.md +17 -0
  11. data/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  12. data/.gitignore +3 -0
  13. data/.rubocop.yml +6 -0
  14. data/CHANGELOG.asciidoc +25 -1
  15. data/Gemfile +6 -2
  16. data/bench/sql.rb +49 -0
  17. data/bin/build_docs +1 -1
  18. data/codecov.yml +32 -0
  19. data/docs/api.asciidoc +37 -0
  20. data/docs/configuration.asciidoc +18 -1
  21. data/docs/supported-technologies.asciidoc +20 -1
  22. data/lib/elastic_apm.rb +29 -5
  23. data/lib/elastic_apm/agent.rb +6 -2
  24. data/lib/elastic_apm/child_durations.rb +9 -4
  25. data/lib/elastic_apm/config.rb +8 -1
  26. data/lib/elastic_apm/config/options.rb +3 -4
  27. data/lib/elastic_apm/context/response.rb +10 -2
  28. data/lib/elastic_apm/instrumenter.rb +20 -11
  29. data/lib/elastic_apm/normalizers/rails/active_record.rb +12 -5
  30. data/lib/elastic_apm/rails.rb +1 -10
  31. data/lib/elastic_apm/railtie.rb +1 -1
  32. data/lib/elastic_apm/span.rb +3 -2
  33. data/lib/elastic_apm/span/context.rb +26 -44
  34. data/lib/elastic_apm/span/context/db.rb +19 -0
  35. data/lib/elastic_apm/span/context/destination.rb +44 -0
  36. data/lib/elastic_apm/span/context/http.rb +26 -0
  37. data/lib/elastic_apm/spies/elasticsearch.rb +18 -5
  38. data/lib/elastic_apm/spies/faraday.rb +36 -18
  39. data/lib/elastic_apm/spies/http.rb +16 -2
  40. data/lib/elastic_apm/spies/mongo.rb +5 -0
  41. data/lib/elastic_apm/spies/net_http.rb +27 -7
  42. data/lib/elastic_apm/spies/sequel.rb +25 -15
  43. data/lib/elastic_apm/spies/shoryuken.rb +48 -0
  44. data/lib/elastic_apm/spies/sneakers.rb +57 -0
  45. data/lib/elastic_apm/sql.rb +19 -0
  46. data/lib/elastic_apm/sql/signature.rb +152 -0
  47. data/lib/elastic_apm/sql/tokenizer.rb +247 -0
  48. data/lib/elastic_apm/sql/tokens.rb +46 -0
  49. data/lib/elastic_apm/sql_summarizer.rb +1 -2
  50. data/lib/elastic_apm/transaction.rb +11 -11
  51. data/lib/elastic_apm/transport/connection/proxy_pipe.rb +2 -2
  52. data/lib/elastic_apm/transport/headers.rb +4 -0
  53. data/lib/elastic_apm/transport/serializers/span_serializer.rb +24 -7
  54. data/lib/elastic_apm/version.rb +1 -1
  55. metadata +16 -3
  56. data/.github/workflows/main.yml +0 -14
@@ -4,53 +4,35 @@ module ElasticAPM
4
4
  class Span
5
5
  # @api private
6
6
  class Context
7
- def initialize(db: nil, http: nil, labels: {})
8
- @sync = true
9
- @db = db && Db.new(db)
10
- @http = http && Http.new(http)
7
+ def initialize(
8
+ db: nil,
9
+ destination: nil,
10
+ http: nil,
11
+ labels: {},
12
+ sync: nil
13
+ )
14
+ @sync = sync
15
+ @db = db && Db.new(**db)
16
+ @http = http && Http.new(**http)
17
+ @destination =
18
+ case destination
19
+ when Destination then destination
20
+ when Hash then Destination.new(**destination)
21
+ end
11
22
  @labels = labels
12
23
  end
13
24
 
14
- attr_accessor :sync, :db, :http, :labels
15
-
16
- # @api private
17
- class Db
18
- def initialize(instance: nil, statement: nil, type: nil, user: nil)
19
- @instance = instance
20
- @statement = statement
21
- @type = type
22
- @user = user
23
- end
24
-
25
- attr_accessor :instance, :statement, :type, :user
26
- end
27
-
28
- # @api private
29
- class Http
30
- def initialize(url: nil, status_code: nil, method: nil)
31
- @url = sanitize_url(url)
32
- @status_code = status_code
33
- @method = method
34
- end
35
-
36
- attr_accessor :url, :status_code, :method
37
-
38
- private
39
-
40
- def sanitize_url(url)
41
- uri = URI(url)
42
-
43
- return url unless uri.userinfo
44
-
45
- format(
46
- '%s://%s@%s%s',
47
- uri.scheme,
48
- uri.user,
49
- uri.hostname,
50
- uri.path
51
- )
52
- end
53
- end
25
+ attr_reader(
26
+ :db,
27
+ :destination,
28
+ :http,
29
+ :labels,
30
+ :sync
31
+ )
54
32
  end
55
33
  end
56
34
  end
35
+
36
+ require 'elastic_apm/span/context/db'
37
+ require 'elastic_apm/span/context/http'
38
+ require 'elastic_apm/span/context/destination'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Db
8
+ def initialize(instance: nil, statement: nil, type: nil, user: nil)
9
+ @instance = instance
10
+ @statement = statement
11
+ @type = type
12
+ @user = user
13
+ end
14
+
15
+ attr_accessor :instance, :statement, :type, :user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Destination
8
+ def initialize(name: nil, resource: nil, type: nil)
9
+ @name = name
10
+ @resource = resource
11
+ @type = type
12
+ end
13
+
14
+ attr_reader :name, :resource, :type
15
+
16
+ def self.from_uri(uri_or_str, type: 'external')
17
+ uri = normalize(uri_or_str)
18
+
19
+ new(
20
+ name: only_scheme_and_host(uri),
21
+ resource: "#{uri.host}:#{uri.port}",
22
+ type: type
23
+ )
24
+ end
25
+
26
+ def self.only_scheme_and_host(uri_or_str)
27
+ uri = normalize(uri_or_str)
28
+ uri.path = ''
29
+ uri.password = uri.query = uri.fragment = nil
30
+ uri.to_s
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def normalize(uri_or_str)
37
+ return uri_or_str.dup if uri_or_str.is_a?(URI)
38
+ URI(uri_or_str)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Span
5
+ class Context
6
+ # @api private
7
+ class Http
8
+ def initialize(url: nil, status_code: nil, method: nil)
9
+ @url = sanitize_url(url)
10
+ @status_code = status_code
11
+ @method = method
12
+ end
13
+
14
+ attr_accessor :url, :status_code, :method
15
+
16
+ private
17
+
18
+ def sanitize_url(uri_or_str)
19
+ uri = uri_or_str.is_a?(URI) ? uri_or_str.dup : URI(uri_or_str)
20
+ uri.password = nil
21
+ uri.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -6,7 +6,9 @@ module ElasticAPM
6
6
  # @api private
7
7
  class ElasticsearchSpy
8
8
  NAME_FORMAT = '%s %s'
9
- TYPE = 'db.elasticsearch'
9
+ TYPE = 'db'
10
+ SUBTYPE = 'elasticsearch'
11
+
10
12
  def install
11
13
  ::Elasticsearch::Transport::Client.class_eval do
12
14
  alias perform_request_without_apm perform_request
@@ -14,11 +16,22 @@ module ElasticAPM
14
16
  def perform_request(method, path, *args, &block)
15
17
  name = format(NAME_FORMAT, method, path)
16
18
  statement = args[0].is_a?(String) ? args[0] : args[0].to_json
17
- context = Span::Context.new(db: { statement: statement })
18
19
 
19
- ElasticAPM.with_span name, TYPE, context: context do
20
- perform_request_without_apm(method, path, *args, &block)
21
- end
20
+ context = Span::Context.new(
21
+ db: { statement: statement },
22
+ destination: {
23
+ name: SUBTYPE,
24
+ resource: SUBTYPE,
25
+ type: TYPE
26
+ }
27
+ )
28
+
29
+ ElasticAPM.with_span(
30
+ name,
31
+ TYPE,
32
+ subtype: SUBTYPE,
33
+ context: context
34
+ ) { perform_request_without_apm(method, path, *args, &block) }
22
35
  end
23
36
  end
24
37
  end
@@ -16,7 +16,6 @@ module ElasticAPM
16
16
  end
17
17
  end
18
18
 
19
- # rubocop:disable Metrics/PerceivedComplexity
20
19
  # rubocop:disable Metrics/CyclomaticComplexity
21
20
  def install
22
21
  ::Faraday::Connection.class_eval do
@@ -27,40 +26,59 @@ module ElasticAPM
27
26
  return run_request_without_apm(method, url, body, headers, &block)
28
27
  end
29
28
 
30
- host = if url_prefix.is_a?(URI) && url_prefix.host
31
- url_prefix.host
32
- elsif url.nil?
33
- tmp_request = build_request(method) do |req|
34
- yield(req) if block_given?
35
- end
36
- URI(tmp_request.path).host
37
- else
38
- URI(url).host
39
- end
29
+ uri = URI(build_url(url))
40
30
 
41
- name = "#{method.upcase} #{host}"
31
+ # If url is set inside block it isn't available until yield,
32
+ # so we temporarily build the request to yield. This could be a
33
+ # problem if the block has side effects as it will be yielded twice
34
+ # ~mikker
35
+ unless uri.host
36
+ tmp_request = build_request(method) do |req|
37
+ yield(req) if block_given?
38
+ end
39
+ uri = URI(tmp_request.path)
40
+ end
41
+
42
+ host = uri.host
43
+
44
+ upcased_method = method.to_s.upcase
45
+
46
+ destination = ElasticAPM::Span::Context::Destination.from_uri(uri)
47
+
48
+ context =
49
+ ElasticAPM::Span::Context.new(
50
+ http: { url: uri, method: upcased_method },
51
+ destination: destination
52
+ )
42
53
 
43
54
  ElasticAPM.with_span(
44
- name,
55
+ "#{upcased_method} #{host}",
45
56
  TYPE,
46
57
  subtype: SUBTYPE,
47
- action: method.to_s
58
+ action: upcased_method,
59
+ context: context
48
60
  ) do |span|
49
61
  ElasticAPM::Spies::FaradaySpy.without_net_http do
50
62
  trace_context = span&.trace_context || transaction.trace_context
51
63
 
52
- run_request_without_apm(method, url, body, headers) do |req|
53
- req['Elastic-Apm-Traceparent'] = trace_context.to_header
64
+ result =
65
+ run_request_without_apm(method, url, body, headers) do |req|
66
+ req['Elastic-Apm-Traceparent'] = trace_context.to_header
54
67
 
55
- yield req if block_given?
68
+ yield req if block_given?
69
+ end
70
+
71
+ if (http = span&.context&.http)
72
+ http.status_code = result.status.to_s
56
73
  end
74
+
75
+ result
57
76
  end
58
77
  end
59
78
  end
60
79
  end
61
80
  end
62
81
  # rubocop:enable Metrics/CyclomaticComplexity
63
- # rubocop:enable Metrics/PerceivedComplexity
64
82
  end
65
83
 
66
84
  register 'Faraday', 'faraday', FaradaySpy.new
@@ -19,17 +19,31 @@ module ElasticAPM
19
19
  method = req.verb.to_s.upcase
20
20
  host = req.uri.host
21
21
 
22
+ destination =
23
+ ElasticAPM::Span::Context::Destination.from_uri(req.uri)
24
+ context = ElasticAPM::Span::Context.new(
25
+ http: { url: req.uri, method: method },
26
+ destination: destination
27
+ )
28
+
22
29
  name = "#{method} #{host}"
23
30
 
24
31
  ElasticAPM.with_span(
25
32
  name,
26
33
  TYPE,
27
34
  subtype: SUBTYPE,
28
- action: method
35
+ action: method,
36
+ context: context
29
37
  ) do |span|
30
38
  trace_context = span&.trace_context || transaction.trace_context
31
39
  req['Elastic-Apm-Traceparent'] = trace_context.to_header
32
- perform_without_apm(req, options)
40
+ result = perform_without_apm(req, options)
41
+
42
+ if (http = span&.context&.http)
43
+ http.status_code = result.status.to_s
44
+ end
45
+
46
+ result
33
47
  end
34
48
  end
35
49
  end
@@ -81,6 +81,11 @@ module ElasticAPM
81
81
  statement: event.command.to_s,
82
82
  type: 'mongodb',
83
83
  user: nil
84
+ },
85
+ destination: {
86
+ name: SUBTYPE,
87
+ resource: SUBTYPE,
88
+ type: TYPE
84
89
  }
85
90
  )
86
91
  end
@@ -8,6 +8,7 @@ module ElasticAPM
8
8
  KEY = :__elastic_apm_net_http_disabled
9
9
  TYPE = 'ext'
10
10
  SUBTYPE = 'net_http'
11
+
11
12
  class << self
12
13
  def disabled=(disabled)
13
14
  Thread.current[KEY] = disabled
@@ -28,6 +29,7 @@ module ElasticAPM
28
29
  end
29
30
  end
30
31
 
32
+ # rubocop:disable Metrics/CyclomaticComplexity
31
33
  def install
32
34
  Net::HTTP.class_eval do
33
35
  alias request_without_apm request
@@ -36,30 +38,48 @@ module ElasticAPM
36
38
  unless (transaction = ElasticAPM.current_transaction)
37
39
  return request_without_apm(req, body, &block)
38
40
  end
41
+
39
42
  if ElasticAPM::Spies::NetHTTPSpy.disabled?
40
43
  return request_without_apm(req, body, &block)
41
44
  end
42
45
 
43
- host, = req['host']&.split(':')
44
- method = req.method
46
+ host = req['host']&.split(':')&.first || address
47
+ method = req.method.to_s.upcase
48
+ path, query = req.path.split('?')
49
+
50
+ cls = use_ssl? ? URI::HTTPS : URI::HTTP
51
+ uri = cls.build([nil, host, port, path, query, nil])
45
52
 
46
- host ||= address
53
+ destination =
54
+ ElasticAPM::Span::Context::Destination.from_uri(uri)
47
55
 
48
- name = "#{method} #{host}"
56
+ context =
57
+ ElasticAPM::Span::Context.new(
58
+ http: { url: uri, method: method },
59
+ destination: destination
60
+ )
49
61
 
50
62
  ElasticAPM.with_span(
51
- name,
63
+ "#{method} #{host}",
52
64
  TYPE,
53
65
  subtype: SUBTYPE,
54
- action: method.to_s
66
+ action: method,
67
+ context: context
55
68
  ) do |span|
56
69
  trace_context = span&.trace_context || transaction.trace_context
57
70
  req['Elastic-Apm-Traceparent'] = trace_context.to_header
58
- request_without_apm(req, body, &block)
71
+ result = request_without_apm(req, body, &block)
72
+
73
+ if (http = span&.context&.http)
74
+ http.status_code = result.code
75
+ end
76
+
77
+ result
59
78
  end
60
79
  end
61
80
  end
62
81
  end
82
+ # rubocop:enable Metrics/CyclomaticComplexity
63
83
  end
64
84
 
65
85
  register 'Net::HTTP', 'net/http', NetHTTPSpy.new
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'elastic_apm/sql_summarizer'
3
+ require 'elastic_apm/sql'
4
4
 
5
5
  module ElasticAPM
6
6
  # @api private
7
7
  module Spies
8
8
  # @api private
9
9
  class SequelSpy
10
- TYPE = 'db.sequel.sql'
10
+ TYPE = 'db'
11
+ ACTION = 'query'
11
12
 
12
13
  def self.summarizer
13
- @summarizer ||= SqlSummarizer.new
14
- end
15
-
16
- def self.build_context(sql, opts)
17
- Span::Context.new(
18
- db: { statement: sql, type: 'sql', user: opts[:user] }
19
- )
14
+ @summarizer = Sql.summarizer
20
15
  end
21
16
 
22
17
  def install
@@ -25,16 +20,31 @@ module ElasticAPM
25
20
  ::Sequel::Database.class_eval do
26
21
  alias log_connection_yield_without_apm log_connection_yield
27
22
 
28
- def log_connection_yield(sql, *args, &block)
23
+ def log_connection_yield(sql, connection, args = nil, &block)
29
24
  unless ElasticAPM.current_transaction
30
- return log_connection_yield_without_apm(sql, *args, &block)
25
+ return log_connection_yield_without_apm(
26
+ sql, connection, args, &block
27
+ )
31
28
  end
32
29
 
33
- summarizer = ElasticAPM::Spies::SequelSpy.summarizer
34
- name = summarizer.summarize sql
35
- context = ElasticAPM::Spies::SequelSpy.build_context(sql, opts)
30
+ subtype = database_type.to_s
31
+
32
+ name =
33
+ ElasticAPM::Spies::SequelSpy.summarizer.summarize sql
34
+
35
+ context = ElasticAPM::Span::Context.new(
36
+ db: { statement: sql, type: 'sql', user: opts[:user] },
37
+ destination: { name: subtype, resource: subtype, type: TYPE }
38
+ )
36
39
 
37
- ElasticAPM.with_span(name, TYPE, context: context, &block)
40
+ ElasticAPM.with_span(
41
+ name,
42
+ TYPE,
43
+ subtype: subtype,
44
+ action: ACTION,
45
+ context: context,
46
+ &block
47
+ )
38
48
  end
39
49
  end
40
50
  end